@plazmodium/odin 0.3.2-beta → 0.3.4-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -11
- package/builtin/ODIN.md +1045 -0
- package/builtin/agent-definitions/README.md +170 -0
- package/builtin/agent-definitions/_shared-context.md +377 -0
- package/builtin/agent-definitions/architect.md +627 -0
- package/builtin/agent-definitions/builder.md +716 -0
- package/builtin/agent-definitions/discovery.md +293 -0
- package/builtin/agent-definitions/documenter.md +238 -0
- package/builtin/agent-definitions/guardian.md +1049 -0
- package/builtin/agent-definitions/integrator.md +363 -0
- package/builtin/agent-definitions/planning.md +236 -0
- package/builtin/agent-definitions/product.md +405 -0
- package/builtin/agent-definitions/release.md +430 -0
- package/builtin/agent-definitions/reviewer.md +447 -0
- package/builtin/agent-definitions/watcher.md +402 -0
- package/builtin/skills/api/graphql/SKILL.md +548 -0
- package/builtin/skills/api/grpc/SKILL.md +554 -0
- package/builtin/skills/api/rest-api/SKILL.md +469 -0
- package/builtin/skills/api/trpc/SKILL.md +503 -0
- package/builtin/skills/architecture/clean-architecture/SKILL.md +141 -0
- package/builtin/skills/architecture/domain-driven-design/SKILL.md +129 -0
- package/builtin/skills/architecture/event-driven/SKILL.md +145 -0
- package/builtin/skills/architecture/microservices/SKILL.md +143 -0
- package/builtin/skills/architecture/tla-precheck/SKILL.md +171 -0
- package/builtin/skills/backend/golang-gin/SKILL.md +141 -0
- package/builtin/skills/backend/nodejs-express/SKILL.md +277 -0
- package/builtin/skills/backend/nodejs-fastify/SKILL.md +152 -0
- package/builtin/skills/backend/python-django/SKILL.md +128 -0
- package/builtin/skills/backend/python-fastapi/SKILL.md +140 -0
- package/builtin/skills/database/mongodb/SKILL.md +132 -0
- package/builtin/skills/database/postgresql/SKILL.md +120 -0
- package/builtin/skills/database/prisma-orm/SKILL.md +366 -0
- package/builtin/skills/database/redis/SKILL.md +140 -0
- package/builtin/skills/database/supabase/SKILL.md +416 -0
- package/builtin/skills/devops/aws/SKILL.md +382 -0
- package/builtin/skills/devops/docker/SKILL.md +359 -0
- package/builtin/skills/devops/github-actions/SKILL.md +435 -0
- package/builtin/skills/devops/kubernetes/SKILL.md +459 -0
- package/builtin/skills/devops/terraform/SKILL.md +453 -0
- package/builtin/skills/frontend/alpine-dev/SKILL.md +27 -0
- package/builtin/skills/frontend/angular-dev/SKILL.md +28 -0
- package/builtin/skills/frontend/astro-dev/SKILL.md +28 -0
- package/builtin/skills/frontend/htmx-dev/SKILL.md +28 -0
- package/builtin/skills/frontend/nextjs-dev/SKILL.md +470 -0
- package/builtin/skills/frontend/react-patterns/SKILL.md +166 -0
- package/builtin/skills/frontend/svelte-dev/SKILL.md +28 -0
- package/builtin/skills/frontend/tailwindcss/SKILL.md +131 -0
- package/builtin/skills/frontend/vuejs-dev/SKILL.md +28 -0
- package/builtin/skills/generic-dev/SKILL.md +307 -0
- package/builtin/skills/testing/cypress/SKILL.md +372 -0
- package/builtin/skills/testing/jest/SKILL.md +176 -0
- package/builtin/skills/testing/playwright/SKILL.md +341 -0
- package/builtin/skills/testing/unit-tests-eval-sdd/SKILL.md +73 -0
- package/builtin/skills/testing/unit-tests-sdd/SKILL.md +83 -0
- package/builtin/skills/testing/vitest/SKILL.md +249 -0
- package/dist/adapters/skills/filesystem.d.ts.map +1 -1
- package/dist/adapters/skills/filesystem.js +2 -18
- package/dist/adapters/skills/filesystem.js.map +1 -1
- package/dist/builtin-assets.d.ts +8 -0
- package/dist/builtin-assets.d.ts.map +1 -0
- package/dist/builtin-assets.js +90 -0
- package/dist/builtin-assets.js.map +1 -0
- package/dist/init.js +69 -11
- package/dist/init.js.map +1 -1
- package/dist/schemas.d.ts +1 -1
- package/dist/server.js +1 -1
- package/dist/server.js.map +1 -1
- package/dist/tools/prepare-phase-context.d.ts.map +1 -1
- package/dist/tools/prepare-phase-context.js +5 -0
- package/dist/tools/prepare-phase-context.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -3
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: graphql
|
|
3
|
+
description: GraphQL API design and implementation expertise. Covers schema design, resolvers, queries, mutations, subscriptions, and client integration with Apollo or urql.
|
|
4
|
+
category: api
|
|
5
|
+
compatible_with:
|
|
6
|
+
- nodejs-express
|
|
7
|
+
- nodejs-fastify
|
|
8
|
+
- nextjs-dev
|
|
9
|
+
- react-patterns
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# GraphQL API Design
|
|
13
|
+
|
|
14
|
+
## Instructions
|
|
15
|
+
|
|
16
|
+
1. **Assess the API need**: Flexible queries, real-time data, or complex relationships.
|
|
17
|
+
2. **Follow GraphQL conventions**:
|
|
18
|
+
- Schema-first or code-first approach
|
|
19
|
+
- Proper type definitions
|
|
20
|
+
- N+1 query prevention with DataLoader
|
|
21
|
+
- Input validation
|
|
22
|
+
3. **Provide complete examples**: Include schema, resolvers, and client queries.
|
|
23
|
+
4. **Guide on best practices**: Pagination, error handling, authentication.
|
|
24
|
+
|
|
25
|
+
## Schema Design
|
|
26
|
+
|
|
27
|
+
### Basic Types
|
|
28
|
+
|
|
29
|
+
```graphql
|
|
30
|
+
type User {
|
|
31
|
+
id: ID!
|
|
32
|
+
email: String!
|
|
33
|
+
name: String!
|
|
34
|
+
avatar: String
|
|
35
|
+
role: Role!
|
|
36
|
+
posts: [Post!]!
|
|
37
|
+
createdAt: DateTime!
|
|
38
|
+
updatedAt: DateTime!
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type Post {
|
|
42
|
+
id: ID!
|
|
43
|
+
title: String!
|
|
44
|
+
content: String!
|
|
45
|
+
published: Boolean!
|
|
46
|
+
author: User!
|
|
47
|
+
comments: [Comment!]!
|
|
48
|
+
tags: [Tag!]!
|
|
49
|
+
createdAt: DateTime!
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
enum Role {
|
|
53
|
+
USER
|
|
54
|
+
ADMIN
|
|
55
|
+
MODERATOR
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
scalar DateTime
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Input Types
|
|
62
|
+
|
|
63
|
+
```graphql
|
|
64
|
+
input CreateUserInput {
|
|
65
|
+
email: String!
|
|
66
|
+
name: String!
|
|
67
|
+
password: String!
|
|
68
|
+
role: Role = USER
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
input UpdateUserInput {
|
|
72
|
+
email: String
|
|
73
|
+
name: String
|
|
74
|
+
avatar: String
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
input PostFilters {
|
|
78
|
+
published: Boolean
|
|
79
|
+
authorId: ID
|
|
80
|
+
tags: [String!]
|
|
81
|
+
search: String
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Queries and Mutations
|
|
86
|
+
|
|
87
|
+
```graphql
|
|
88
|
+
type Query {
|
|
89
|
+
# Single resource
|
|
90
|
+
user(id: ID!): User
|
|
91
|
+
post(id: ID!): Post
|
|
92
|
+
|
|
93
|
+
# Collections with pagination
|
|
94
|
+
users(
|
|
95
|
+
first: Int
|
|
96
|
+
after: String
|
|
97
|
+
filter: UserFilter
|
|
98
|
+
): UserConnection!
|
|
99
|
+
|
|
100
|
+
posts(
|
|
101
|
+
first: Int
|
|
102
|
+
after: String
|
|
103
|
+
filter: PostFilters
|
|
104
|
+
orderBy: PostOrderBy
|
|
105
|
+
): PostConnection!
|
|
106
|
+
|
|
107
|
+
# Current user
|
|
108
|
+
me: User
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
type Mutation {
|
|
112
|
+
# User mutations
|
|
113
|
+
createUser(input: CreateUserInput!): CreateUserPayload!
|
|
114
|
+
updateUser(id: ID!, input: UpdateUserInput!): UpdateUserPayload!
|
|
115
|
+
deleteUser(id: ID!): DeleteUserPayload!
|
|
116
|
+
|
|
117
|
+
# Auth mutations
|
|
118
|
+
login(email: String!, password: String!): AuthPayload!
|
|
119
|
+
logout: Boolean!
|
|
120
|
+
|
|
121
|
+
# Post mutations
|
|
122
|
+
createPost(input: CreatePostInput!): Post!
|
|
123
|
+
updatePost(id: ID!, input: UpdatePostInput!): Post!
|
|
124
|
+
publishPost(id: ID!): Post!
|
|
125
|
+
deletePost(id: ID!): Boolean!
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
type Subscription {
|
|
129
|
+
postCreated: Post!
|
|
130
|
+
commentAdded(postId: ID!): Comment!
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Relay-style Pagination
|
|
135
|
+
|
|
136
|
+
```graphql
|
|
137
|
+
type UserConnection {
|
|
138
|
+
edges: [UserEdge!]!
|
|
139
|
+
pageInfo: PageInfo!
|
|
140
|
+
totalCount: Int!
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
type UserEdge {
|
|
144
|
+
cursor: String!
|
|
145
|
+
node: User!
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
type PageInfo {
|
|
149
|
+
hasNextPage: Boolean!
|
|
150
|
+
hasPreviousPage: Boolean!
|
|
151
|
+
startCursor: String
|
|
152
|
+
endCursor: String
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Payload Types (for mutations)
|
|
157
|
+
|
|
158
|
+
```graphql
|
|
159
|
+
type CreateUserPayload {
|
|
160
|
+
user: User
|
|
161
|
+
errors: [Error!]
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
type Error {
|
|
165
|
+
field: String
|
|
166
|
+
message: String!
|
|
167
|
+
code: String!
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Resolvers
|
|
172
|
+
|
|
173
|
+
### Basic Resolvers (Node.js)
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
import { Resolvers } from './generated/graphql';
|
|
177
|
+
|
|
178
|
+
const resolvers: Resolvers = {
|
|
179
|
+
Query: {
|
|
180
|
+
user: async (_, { id }, { dataSources }) => {
|
|
181
|
+
return dataSources.users.findById(id);
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
users: async (_, { first = 20, after, filter }, { dataSources }) => {
|
|
185
|
+
return dataSources.users.findMany({ first, after, filter });
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
me: async (_, __, { currentUser }) => {
|
|
189
|
+
return currentUser;
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
Mutation: {
|
|
194
|
+
createUser: async (_, { input }, { dataSources }) => {
|
|
195
|
+
try {
|
|
196
|
+
const user = await dataSources.users.create(input);
|
|
197
|
+
return { user, errors: null };
|
|
198
|
+
} catch (error) {
|
|
199
|
+
return {
|
|
200
|
+
user: null,
|
|
201
|
+
errors: [{ message: error.message, code: 'CREATE_FAILED' }]
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
updateUser: async (_, { id, input }, { dataSources, currentUser }) => {
|
|
207
|
+
if (currentUser.id !== id && currentUser.role !== 'ADMIN') {
|
|
208
|
+
throw new ForbiddenError('Not authorized');
|
|
209
|
+
}
|
|
210
|
+
return dataSources.users.update(id, input);
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
// Field resolvers
|
|
215
|
+
User: {
|
|
216
|
+
posts: async (user, _, { dataSources }) => {
|
|
217
|
+
return dataSources.posts.findByAuthorId(user.id);
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
Post: {
|
|
222
|
+
author: async (post, _, { dataSources }) => {
|
|
223
|
+
return dataSources.users.findById(post.authorId);
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### DataLoader (N+1 Prevention)
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
import DataLoader from 'dataloader';
|
|
233
|
+
|
|
234
|
+
// Create loaders
|
|
235
|
+
const createLoaders = (db) => ({
|
|
236
|
+
userLoader: new DataLoader(async (ids: string[]) => {
|
|
237
|
+
const users = await db.users.findMany({
|
|
238
|
+
where: { id: { in: ids } }
|
|
239
|
+
});
|
|
240
|
+
// Return in same order as requested ids
|
|
241
|
+
return ids.map(id => users.find(u => u.id === id));
|
|
242
|
+
}),
|
|
243
|
+
|
|
244
|
+
postsByAuthorLoader: new DataLoader(async (authorIds: string[]) => {
|
|
245
|
+
const posts = await db.posts.findMany({
|
|
246
|
+
where: { authorId: { in: authorIds } }
|
|
247
|
+
});
|
|
248
|
+
// Group by author
|
|
249
|
+
return authorIds.map(authorId =>
|
|
250
|
+
posts.filter(p => p.authorId === authorId)
|
|
251
|
+
);
|
|
252
|
+
}),
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Use in resolvers
|
|
256
|
+
const resolvers = {
|
|
257
|
+
Post: {
|
|
258
|
+
author: (post, _, { loaders }) => {
|
|
259
|
+
return loaders.userLoader.load(post.authorId);
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
User: {
|
|
263
|
+
posts: (user, _, { loaders }) => {
|
|
264
|
+
return loaders.postsByAuthorLoader.load(user.id);
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## Server Setup
|
|
271
|
+
|
|
272
|
+
### Apollo Server
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
import { ApolloServer } from '@apollo/server';
|
|
276
|
+
import { expressMiddleware } from '@apollo/server/express4';
|
|
277
|
+
import { makeExecutableSchema } from '@graphql-tools/schema';
|
|
278
|
+
|
|
279
|
+
const schema = makeExecutableSchema({
|
|
280
|
+
typeDefs,
|
|
281
|
+
resolvers,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const server = new ApolloServer({
|
|
285
|
+
schema,
|
|
286
|
+
plugins: [
|
|
287
|
+
ApolloServerPluginDrainHttpServer({ httpServer }),
|
|
288
|
+
],
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
await server.start();
|
|
292
|
+
|
|
293
|
+
app.use(
|
|
294
|
+
'/graphql',
|
|
295
|
+
cors(),
|
|
296
|
+
express.json(),
|
|
297
|
+
expressMiddleware(server, {
|
|
298
|
+
context: async ({ req }) => {
|
|
299
|
+
const token = req.headers.authorization?.replace('Bearer ', '');
|
|
300
|
+
const currentUser = token ? await verifyToken(token) : null;
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
currentUser,
|
|
304
|
+
dataSources: createDataSources(db),
|
|
305
|
+
loaders: createLoaders(db),
|
|
306
|
+
};
|
|
307
|
+
},
|
|
308
|
+
})
|
|
309
|
+
);
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
## Client Queries
|
|
313
|
+
|
|
314
|
+
### Apollo Client
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
import { gql, useQuery, useMutation } from '@apollo/client';
|
|
318
|
+
|
|
319
|
+
// Query
|
|
320
|
+
const GET_USERS = gql`
|
|
321
|
+
query GetUsers($first: Int, $after: String) {
|
|
322
|
+
users(first: $first, after: $after) {
|
|
323
|
+
edges {
|
|
324
|
+
cursor
|
|
325
|
+
node {
|
|
326
|
+
id
|
|
327
|
+
name
|
|
328
|
+
email
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
pageInfo {
|
|
332
|
+
hasNextPage
|
|
333
|
+
endCursor
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
`;
|
|
338
|
+
|
|
339
|
+
function UserList() {
|
|
340
|
+
const { data, loading, error, fetchMore } = useQuery(GET_USERS, {
|
|
341
|
+
variables: { first: 20 },
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
if (loading) return <Loading />;
|
|
345
|
+
if (error) return <Error error={error} />;
|
|
346
|
+
|
|
347
|
+
return (
|
|
348
|
+
<>
|
|
349
|
+
{data.users.edges.map(({ node }) => (
|
|
350
|
+
<UserCard key={node.id} user={node} />
|
|
351
|
+
))}
|
|
352
|
+
{data.users.pageInfo.hasNextPage && (
|
|
353
|
+
<button onClick={() => fetchMore({
|
|
354
|
+
variables: { after: data.users.pageInfo.endCursor }
|
|
355
|
+
})}>
|
|
356
|
+
Load More
|
|
357
|
+
</button>
|
|
358
|
+
)}
|
|
359
|
+
</>
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Mutation
|
|
364
|
+
const CREATE_USER = gql`
|
|
365
|
+
mutation CreateUser($input: CreateUserInput!) {
|
|
366
|
+
createUser(input: $input) {
|
|
367
|
+
user {
|
|
368
|
+
id
|
|
369
|
+
name
|
|
370
|
+
email
|
|
371
|
+
}
|
|
372
|
+
errors {
|
|
373
|
+
field
|
|
374
|
+
message
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
`;
|
|
379
|
+
|
|
380
|
+
function CreateUserForm() {
|
|
381
|
+
const [createUser, { loading }] = useMutation(CREATE_USER, {
|
|
382
|
+
update(cache, { data }) {
|
|
383
|
+
if (data.createUser.user) {
|
|
384
|
+
cache.modify({
|
|
385
|
+
fields: {
|
|
386
|
+
users(existing = { edges: [] }) {
|
|
387
|
+
const newEdge = {
|
|
388
|
+
__typename: 'UserEdge',
|
|
389
|
+
cursor: data.createUser.user.id,
|
|
390
|
+
node: data.createUser.user,
|
|
391
|
+
};
|
|
392
|
+
return {
|
|
393
|
+
...existing,
|
|
394
|
+
edges: [newEdge, ...existing.edges],
|
|
395
|
+
};
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
},
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
const handleSubmit = async (values) => {
|
|
404
|
+
const { data } = await createUser({ variables: { input: values } });
|
|
405
|
+
if (data.createUser.errors) {
|
|
406
|
+
// Handle errors
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### Fragments
|
|
413
|
+
|
|
414
|
+
```graphql
|
|
415
|
+
fragment UserFields on User {
|
|
416
|
+
id
|
|
417
|
+
name
|
|
418
|
+
email
|
|
419
|
+
avatar
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
fragment PostWithAuthor on Post {
|
|
423
|
+
id
|
|
424
|
+
title
|
|
425
|
+
content
|
|
426
|
+
author {
|
|
427
|
+
...UserFields
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
query GetPost($id: ID!) {
|
|
432
|
+
post(id: $id) {
|
|
433
|
+
...PostWithAuthor
|
|
434
|
+
comments {
|
|
435
|
+
id
|
|
436
|
+
content
|
|
437
|
+
author {
|
|
438
|
+
...UserFields
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
## Subscriptions
|
|
446
|
+
|
|
447
|
+
### Server
|
|
448
|
+
|
|
449
|
+
```typescript
|
|
450
|
+
import { createServer } from 'http';
|
|
451
|
+
import { WebSocketServer } from 'ws';
|
|
452
|
+
import { useServer } from 'graphql-ws/lib/use/ws';
|
|
453
|
+
|
|
454
|
+
const httpServer = createServer(app);
|
|
455
|
+
const wsServer = new WebSocketServer({
|
|
456
|
+
server: httpServer,
|
|
457
|
+
path: '/graphql',
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
useServer(
|
|
461
|
+
{
|
|
462
|
+
schema,
|
|
463
|
+
context: async (ctx) => {
|
|
464
|
+
const token = ctx.connectionParams?.authToken;
|
|
465
|
+
return { currentUser: await verifyToken(token) };
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
wsServer
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
// Resolver
|
|
472
|
+
const resolvers = {
|
|
473
|
+
Subscription: {
|
|
474
|
+
postCreated: {
|
|
475
|
+
subscribe: () => pubsub.asyncIterator(['POST_CREATED']),
|
|
476
|
+
},
|
|
477
|
+
commentAdded: {
|
|
478
|
+
subscribe: (_, { postId }) => {
|
|
479
|
+
return pubsub.asyncIterator([`COMMENT_ADDED_${postId}`]);
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
},
|
|
483
|
+
Mutation: {
|
|
484
|
+
createPost: async (_, { input }, { dataSources, pubsub }) => {
|
|
485
|
+
const post = await dataSources.posts.create(input);
|
|
486
|
+
pubsub.publish('POST_CREATED', { postCreated: post });
|
|
487
|
+
return post;
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
};
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
### Client
|
|
494
|
+
|
|
495
|
+
```typescript
|
|
496
|
+
const POST_SUBSCRIPTION = gql`
|
|
497
|
+
subscription OnPostCreated {
|
|
498
|
+
postCreated {
|
|
499
|
+
id
|
|
500
|
+
title
|
|
501
|
+
author {
|
|
502
|
+
name
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
`;
|
|
507
|
+
|
|
508
|
+
function PostFeed() {
|
|
509
|
+
const { data, subscribeToMore } = useQuery(GET_POSTS);
|
|
510
|
+
|
|
511
|
+
useEffect(() => {
|
|
512
|
+
return subscribeToMore({
|
|
513
|
+
document: POST_SUBSCRIPTION,
|
|
514
|
+
updateQuery: (prev, { subscriptionData }) => {
|
|
515
|
+
if (!subscriptionData.data) return prev;
|
|
516
|
+
const newPost = subscriptionData.data.postCreated;
|
|
517
|
+
return {
|
|
518
|
+
...prev,
|
|
519
|
+
posts: {
|
|
520
|
+
...prev.posts,
|
|
521
|
+
edges: [
|
|
522
|
+
{ node: newPost, cursor: newPost.id },
|
|
523
|
+
...prev.posts.edges,
|
|
524
|
+
],
|
|
525
|
+
},
|
|
526
|
+
};
|
|
527
|
+
},
|
|
528
|
+
});
|
|
529
|
+
}, [subscribeToMore]);
|
|
530
|
+
}
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
## Best Practices
|
|
534
|
+
|
|
535
|
+
- **Use DataLoader** - Prevent N+1 queries
|
|
536
|
+
- **Implement pagination** - Relay cursor-based for large datasets
|
|
537
|
+
- **Validate inputs** - Use custom scalars or directives
|
|
538
|
+
- **Handle errors gracefully** - Return errors in payload, not exceptions
|
|
539
|
+
- **Depth limiting** - Prevent deeply nested queries
|
|
540
|
+
- **Query complexity** - Limit expensive queries
|
|
541
|
+
- **Persisted queries** - For production security
|
|
542
|
+
- **Schema stitching/federation** - For microservices
|
|
543
|
+
|
|
544
|
+
## References
|
|
545
|
+
|
|
546
|
+
- GraphQL Specification: https://spec.graphql.org/
|
|
547
|
+
- Apollo Documentation: https://www.apollographql.com/docs/
|
|
548
|
+
- GraphQL Best Practices: https://graphql.org/learn/best-practices/
|