Skip to content

Commit a4bea69

Browse files
committed
Stories and comments (demo)
1 parent 9e788b9 commit a4bea69

File tree

11 files changed

+595
-2
lines changed

11 files changed

+595
-2
lines changed

api/schema.graphql

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ type Root {
1919
me: User
2020
user(username: String!): User
2121
users(after: String, first: Int): UserConnection
22+
story(slug: String!): Story
23+
stories: [Story]
2224
}
2325

2426
# An object with an ID
@@ -114,6 +116,32 @@ type UserEdge {
114116
cursor: String!
115117
}
116118

119+
type Story implements Node {
120+
# The ID of an object
121+
id: ID!
122+
author: User!
123+
slug: String!
124+
title: String!
125+
text(truncate: Int): String!
126+
isURL: Boolean!
127+
comments: [Comment]
128+
pointsCount: Int!
129+
pointGiven: Boolean!
130+
commentsCount: Int!
131+
createdAt(format: String): String
132+
updatedAt(format: String): String
133+
}
134+
135+
type Comment implements Node {
136+
# The ID of an object
137+
id: ID!
138+
parent: Comment
139+
author: User!
140+
text: String
141+
createdAt(format: String): String
142+
updatedAt(format: String): String
143+
}
144+
117145
type Mutation {
118146
# Authenticates user with an ID token or email and password.
119147
signIn(idToken: String, email: String, password: String): SignInPayload
@@ -123,6 +151,12 @@ type Mutation {
123151

124152
# Updates a user.
125153
updateUser(input: UpdateUserInput!): UpdateUserPayload
154+
155+
# Creates or updates a story.
156+
upsertStory(input: UpsertStoryInput!): UpsertStoryPayload
157+
158+
# Marks the story as "liked".
159+
likeStory(input: LikeStoryInput!): LikeStoryPayload
126160
}
127161

128162
type SignInPayload {
@@ -146,3 +180,28 @@ input UpdateUserInput {
146180
validateOnly: Boolean
147181
clientMutationId: String
148182
}
183+
184+
type UpsertStoryPayload {
185+
story: Story
186+
errors: [[String!]!]
187+
clientMutationId: String
188+
}
189+
190+
input UpsertStoryInput {
191+
id: ID
192+
title: String
193+
text: String
194+
approved: Boolean
195+
validateOnly: Boolean
196+
clientMutationId: String
197+
}
198+
199+
type LikeStoryPayload {
200+
story: Story
201+
clientMutationId: String
202+
}
203+
204+
input LikeStoryInput {
205+
id: ID!
206+
clientMutationId: String
207+
}

api/src/context.ts

Lines changed: 105 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
import DataLoader from "dataloader";
88
import { Request } from "express";
99

10-
import db, { User, Identity } from "./db";
11-
import { mapTo, mapToMany } from "./utils";
10+
import db, { User, Identity, Story, Comment } from "./db";
11+
import { mapTo, mapToMany, mapToValues } from "./utils";
1212
import { UnauthorizedError, ForbiddenError } from "./error";
1313

1414
export class Context {
@@ -89,4 +89,107 @@ export class Context {
8989
.select()
9090
.then((rows) => mapToMany(rows, keys, (x) => x.user_id)),
9191
);
92+
93+
storyById = new DataLoader<string, Story | null>((keys) =>
94+
db
95+
.table<Story>("stories")
96+
.whereIn("id", keys)
97+
.select()
98+
.then((rows) => {
99+
rows.forEach((x) => this.storyBySlug.prime(x.slug, x));
100+
return rows;
101+
})
102+
.then((rows) => mapTo(rows, keys, (x) => x.id)),
103+
);
104+
105+
storyBySlug = new DataLoader<string, Story | null>((keys) =>
106+
db
107+
.table<Story>("stories")
108+
.whereIn("slug", keys)
109+
.select()
110+
.then((rows) => {
111+
rows.forEach((x) => this.storyById.prime(x.id, x));
112+
return rows;
113+
})
114+
.then((rows) => mapTo(rows, keys, (x) => x.slug)),
115+
);
116+
117+
storyCommentsCount = new DataLoader<string, number>((keys) =>
118+
db
119+
.table<Comment>("comments")
120+
.whereIn("story_id", keys)
121+
.groupBy("story_id")
122+
.select<{ story_id: string; count: string }[]>(
123+
"story_id",
124+
db.raw("count(story_id)"),
125+
)
126+
.then((rows) =>
127+
mapToValues(
128+
rows,
129+
keys,
130+
(x) => x.story_id,
131+
(x) => (x ? Number(x.count) : 0),
132+
),
133+
),
134+
);
135+
136+
storyPointsCount = new DataLoader<string, number>((keys) =>
137+
db
138+
.table("stories")
139+
.leftJoin("story_points", "story_points.story_id", "stories.id")
140+
.whereIn("stories.id", keys)
141+
.groupBy("stories.id")
142+
.select("stories.id", db.raw("count(story_points.user_id)::int"))
143+
.then((rows) =>
144+
mapToValues(
145+
rows,
146+
keys,
147+
(x) => x.id,
148+
(x) => (x ? parseInt(x.count, 10) : 0),
149+
),
150+
),
151+
);
152+
153+
storyPointGiven = new DataLoader<string, boolean>((keys) => {
154+
const currentUser = this.user;
155+
const userId = currentUser ? currentUser.id : "";
156+
157+
return db
158+
.table("stories")
159+
.leftJoin("story_points", function join() {
160+
this.on("story_points.story_id", "stories.id").andOn(
161+
"story_points.user_id",
162+
db.raw("?", [userId]),
163+
);
164+
})
165+
.whereIn("stories.id", keys)
166+
.select<{ id: string; given: boolean }[]>(
167+
"stories.id",
168+
db.raw("(story_points.user_id IS NOT NULL) AS given"),
169+
)
170+
.then((rows) =>
171+
mapToValues(
172+
rows,
173+
keys,
174+
(x) => x.id,
175+
(x) => x?.given || false,
176+
),
177+
);
178+
});
179+
180+
commentById = new DataLoader<string, Comment | null>((keys) =>
181+
db
182+
.table<Comment>("comments")
183+
.whereIn("id", keys)
184+
.select()
185+
.then((rows) => mapTo(rows, keys, (x) => x.id)),
186+
);
187+
188+
commentsByStoryId = new DataLoader<string, Comment[]>((keys) =>
189+
db
190+
.table<Comment>("comments")
191+
.whereIn("story_id", keys)
192+
.select()
193+
.then((rows) => mapToMany(rows, keys, (x) => x.story_id)),
194+
);
92195
}

api/src/db.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,21 @@ export enum IdentityProvider {
4949
playgames = "playgames",
5050
}
5151

52+
export type CommentPoint = {
53+
comment_id: string;
54+
user_id: string;
55+
};
56+
57+
export type Comment = {
58+
id: string;
59+
story_id: string;
60+
parent_id: string | null;
61+
author_id: string;
62+
text: string | null;
63+
created_at: Date;
64+
updated_at: Date;
65+
};
66+
5267
export type Identity = {
5368
provider: IdentityProvider;
5469
id: string;
@@ -71,6 +86,23 @@ export type Identity = {
7186
expires_at: Date | null;
7287
};
7388

89+
export type Story = {
90+
id: string;
91+
author_id: string;
92+
slug: string;
93+
title: string;
94+
text: string | null;
95+
is_url: boolean;
96+
approved: boolean;
97+
created_at: Date;
98+
updated_at: Date;
99+
};
100+
101+
export type StoryPoint = {
102+
story_id: string;
103+
user_id: string;
104+
};
105+
74106
export type User = {
75107
id: string;
76108
username: string;

api/src/mutations/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@
66

77
export * from "./auth";
88
export * from "./user";
9+
export * from "./story";

0 commit comments

Comments
 (0)