autoworkflow 3.1.4 → 3.5.0
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/.claude/commands/analyze.md +19 -0
- package/.claude/commands/audit.md +174 -11
- package/.claude/commands/build.md +39 -0
- package/.claude/commands/commit.md +25 -0
- package/.claude/commands/fix.md +23 -0
- package/.claude/commands/plan.md +18 -0
- package/.claude/commands/suggest.md +23 -0
- package/.claude/commands/verify.md +18 -0
- package/.claude/hooks/post-bash-router.sh +20 -0
- package/.claude/hooks/post-commit.sh +140 -0
- package/.claude/hooks/pre-edit.sh +129 -0
- package/.claude/hooks/session-check.sh +79 -0
- package/.claude/settings.json +40 -6
- package/.claude/settings.local.json +3 -1
- package/.claude/skills/actix.md +337 -0
- package/.claude/skills/alembic.md +504 -0
- package/.claude/skills/angular.md +237 -0
- package/.claude/skills/api-design.md +187 -0
- package/.claude/skills/aspnet-core.md +377 -0
- package/.claude/skills/astro.md +245 -0
- package/.claude/skills/auth-clerk.md +327 -0
- package/.claude/skills/auth-firebase.md +367 -0
- package/.claude/skills/auth-nextauth.md +359 -0
- package/.claude/skills/auth-supabase.md +368 -0
- package/.claude/skills/axum.md +386 -0
- package/.claude/skills/blazor.md +456 -0
- package/.claude/skills/chi.md +348 -0
- package/.claude/skills/code-review.md +133 -0
- package/.claude/skills/csharp.md +296 -0
- package/.claude/skills/css-modules.md +325 -0
- package/.claude/skills/cypress.md +343 -0
- package/.claude/skills/debugging.md +133 -0
- package/.claude/skills/diesel.md +392 -0
- package/.claude/skills/django.md +301 -0
- package/.claude/skills/docker.md +319 -0
- package/.claude/skills/doctrine.md +473 -0
- package/.claude/skills/documentation.md +182 -0
- package/.claude/skills/dotnet.md +409 -0
- package/.claude/skills/drizzle.md +293 -0
- package/.claude/skills/echo.md +321 -0
- package/.claude/skills/eloquent.md +256 -0
- package/.claude/skills/emotion.md +426 -0
- package/.claude/skills/entity-framework.md +370 -0
- package/.claude/skills/express.md +316 -0
- package/.claude/skills/fastapi.md +329 -0
- package/.claude/skills/fastify.md +299 -0
- package/.claude/skills/fiber.md +315 -0
- package/.claude/skills/flask.md +322 -0
- package/.claude/skills/gin.md +342 -0
- package/.claude/skills/git.md +116 -0
- package/.claude/skills/github-actions.md +353 -0
- package/.claude/skills/go.md +377 -0
- package/.claude/skills/gorm.md +409 -0
- package/.claude/skills/graphql.md +478 -0
- package/.claude/skills/hibernate.md +379 -0
- package/.claude/skills/hono.md +306 -0
- package/.claude/skills/java.md +400 -0
- package/.claude/skills/jest.md +313 -0
- package/.claude/skills/jpa.md +282 -0
- package/.claude/skills/kotlin.md +347 -0
- package/.claude/skills/kubernetes.md +363 -0
- package/.claude/skills/laravel.md +414 -0
- package/.claude/skills/mcp-browser.md +320 -0
- package/.claude/skills/mcp-database.md +219 -0
- package/.claude/skills/mcp-fetch.md +241 -0
- package/.claude/skills/mcp-filesystem.md +204 -0
- package/.claude/skills/mcp-github.md +217 -0
- package/.claude/skills/mcp-memory.md +240 -0
- package/.claude/skills/mcp-search.md +218 -0
- package/.claude/skills/mcp-slack.md +262 -0
- package/.claude/skills/micronaut.md +388 -0
- package/.claude/skills/mongodb.md +319 -0
- package/.claude/skills/mongoose.md +355 -0
- package/.claude/skills/mysql.md +281 -0
- package/.claude/skills/nestjs.md +335 -0
- package/.claude/skills/nextjs-app-router.md +260 -0
- package/.claude/skills/nextjs-pages.md +172 -0
- package/.claude/skills/nuxt.md +202 -0
- package/.claude/skills/openapi.md +489 -0
- package/.claude/skills/performance.md +199 -0
- package/.claude/skills/php.md +398 -0
- package/.claude/skills/playwright.md +371 -0
- package/.claude/skills/postgresql.md +257 -0
- package/.claude/skills/prisma.md +293 -0
- package/.claude/skills/pydantic.md +304 -0
- package/.claude/skills/pytest.md +313 -0
- package/.claude/skills/python.md +272 -0
- package/.claude/skills/quarkus.md +377 -0
- package/.claude/skills/react.md +230 -0
- package/.claude/skills/redis.md +391 -0
- package/.claude/skills/refactoring.md +143 -0
- package/.claude/skills/remix.md +246 -0
- package/.claude/skills/rest-api.md +490 -0
- package/.claude/skills/rocket.md +366 -0
- package/.claude/skills/rust.md +341 -0
- package/.claude/skills/sass.md +380 -0
- package/.claude/skills/sea-orm.md +382 -0
- package/.claude/skills/security.md +167 -0
- package/.claude/skills/sequelize.md +395 -0
- package/.claude/skills/spring-boot.md +416 -0
- package/.claude/skills/sqlalchemy.md +269 -0
- package/.claude/skills/sqlx-rust.md +408 -0
- package/.claude/skills/state-jotai.md +346 -0
- package/.claude/skills/state-mobx.md +353 -0
- package/.claude/skills/state-pinia.md +431 -0
- package/.claude/skills/state-redux.md +337 -0
- package/.claude/skills/state-tanstack-query.md +434 -0
- package/.claude/skills/state-zustand.md +340 -0
- package/.claude/skills/styled-components.md +403 -0
- package/.claude/skills/svelte.md +238 -0
- package/.claude/skills/sveltekit.md +207 -0
- package/.claude/skills/symfony.md +437 -0
- package/.claude/skills/tailwind.md +279 -0
- package/.claude/skills/terraform.md +394 -0
- package/.claude/skills/testing-library.md +371 -0
- package/.claude/skills/trpc.md +426 -0
- package/.claude/skills/typeorm.md +368 -0
- package/.claude/skills/vitest.md +330 -0
- package/.claude/skills/vue.md +202 -0
- package/.claude/skills/warp.md +365 -0
- package/README.md +135 -52
- package/package.json +1 -1
- package/system/triggers.md +152 -11
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
# GraphQL Skill
|
|
2
|
+
|
|
3
|
+
## Schema Design
|
|
4
|
+
\`\`\`graphql
|
|
5
|
+
# schema.graphql
|
|
6
|
+
|
|
7
|
+
# Scalars
|
|
8
|
+
scalar DateTime
|
|
9
|
+
scalar JSON
|
|
10
|
+
|
|
11
|
+
# Enums
|
|
12
|
+
enum Role {
|
|
13
|
+
USER
|
|
14
|
+
ADMIN
|
|
15
|
+
MODERATOR
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
enum PostStatus {
|
|
19
|
+
DRAFT
|
|
20
|
+
PUBLISHED
|
|
21
|
+
ARCHIVED
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# Types
|
|
25
|
+
type User {
|
|
26
|
+
id: ID!
|
|
27
|
+
email: String!
|
|
28
|
+
name: String
|
|
29
|
+
role: Role!
|
|
30
|
+
posts(first: Int, after: String): PostConnection!
|
|
31
|
+
createdAt: DateTime!
|
|
32
|
+
updatedAt: DateTime!
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type Post {
|
|
36
|
+
id: ID!
|
|
37
|
+
title: String!
|
|
38
|
+
content: String!
|
|
39
|
+
status: PostStatus!
|
|
40
|
+
author: User!
|
|
41
|
+
comments(first: Int, after: String): CommentConnection!
|
|
42
|
+
tags: [String!]!
|
|
43
|
+
publishedAt: DateTime
|
|
44
|
+
createdAt: DateTime!
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type Comment {
|
|
48
|
+
id: ID!
|
|
49
|
+
content: String!
|
|
50
|
+
author: User!
|
|
51
|
+
post: Post!
|
|
52
|
+
createdAt: DateTime!
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Pagination (Relay-style)
|
|
56
|
+
type PageInfo {
|
|
57
|
+
hasNextPage: Boolean!
|
|
58
|
+
hasPreviousPage: Boolean!
|
|
59
|
+
startCursor: String
|
|
60
|
+
endCursor: String
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type PostEdge {
|
|
64
|
+
node: Post!
|
|
65
|
+
cursor: String!
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
type PostConnection {
|
|
69
|
+
edges: [PostEdge!]!
|
|
70
|
+
pageInfo: PageInfo!
|
|
71
|
+
totalCount: Int!
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# Inputs
|
|
75
|
+
input CreateUserInput {
|
|
76
|
+
email: String!
|
|
77
|
+
name: String!
|
|
78
|
+
password: String!
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
input CreatePostInput {
|
|
82
|
+
title: String!
|
|
83
|
+
content: String!
|
|
84
|
+
status: PostStatus = DRAFT
|
|
85
|
+
tags: [String!]
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
input UpdatePostInput {
|
|
89
|
+
title: String
|
|
90
|
+
content: String
|
|
91
|
+
status: PostStatus
|
|
92
|
+
tags: [String!]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
input PostFilterInput {
|
|
96
|
+
status: PostStatus
|
|
97
|
+
authorId: ID
|
|
98
|
+
search: String
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# Queries
|
|
102
|
+
type Query {
|
|
103
|
+
# Single resources
|
|
104
|
+
user(id: ID!): User
|
|
105
|
+
post(id: ID!): Post
|
|
106
|
+
me: User
|
|
107
|
+
|
|
108
|
+
# Collections with filtering & pagination
|
|
109
|
+
users(first: Int, after: String): UserConnection!
|
|
110
|
+
posts(
|
|
111
|
+
first: Int
|
|
112
|
+
after: String
|
|
113
|
+
filter: PostFilterInput
|
|
114
|
+
): PostConnection!
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# Mutations
|
|
118
|
+
type Mutation {
|
|
119
|
+
# User mutations
|
|
120
|
+
createUser(input: CreateUserInput!): CreateUserPayload!
|
|
121
|
+
updateUser(id: ID!, input: UpdateUserInput!): UpdateUserPayload!
|
|
122
|
+
deleteUser(id: ID!): DeleteUserPayload!
|
|
123
|
+
|
|
124
|
+
# Post mutations
|
|
125
|
+
createPost(input: CreatePostInput!): CreatePostPayload!
|
|
126
|
+
updatePost(id: ID!, input: UpdatePostInput!): UpdatePostPayload!
|
|
127
|
+
deletePost(id: ID!): DeletePostPayload!
|
|
128
|
+
publishPost(id: ID!): PublishPostPayload!
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# Mutation Payloads
|
|
132
|
+
type CreatePostPayload {
|
|
133
|
+
post: Post
|
|
134
|
+
errors: [Error!]
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
type Error {
|
|
138
|
+
field: String
|
|
139
|
+
message: String!
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
# Subscriptions
|
|
143
|
+
type Subscription {
|
|
144
|
+
postCreated: Post!
|
|
145
|
+
postUpdated(id: ID!): Post!
|
|
146
|
+
commentAdded(postId: ID!): Comment!
|
|
147
|
+
}
|
|
148
|
+
\`\`\`
|
|
149
|
+
|
|
150
|
+
## Resolvers (Apollo Server)
|
|
151
|
+
\`\`\`typescript
|
|
152
|
+
// resolvers.ts
|
|
153
|
+
import { Resolvers } from './generated/graphql';
|
|
154
|
+
import DataLoader from 'dataloader';
|
|
155
|
+
|
|
156
|
+
export const resolvers: Resolvers = {
|
|
157
|
+
Query: {
|
|
158
|
+
user: async (_, { id }, { prisma }) => {
|
|
159
|
+
return prisma.user.findUnique({ where: { id } });
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
me: async (_, __, { user }) => {
|
|
163
|
+
if (!user) throw new AuthenticationError('Not authenticated');
|
|
164
|
+
return user;
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
posts: async (_, { first = 10, after, filter }, { prisma }) => {
|
|
168
|
+
const where = {
|
|
169
|
+
...(filter?.status && { status: filter.status }),
|
|
170
|
+
...(filter?.authorId && { authorId: filter.authorId }),
|
|
171
|
+
...(filter?.search && {
|
|
172
|
+
OR: [
|
|
173
|
+
{ title: { contains: filter.search } },
|
|
174
|
+
{ content: { contains: filter.search } },
|
|
175
|
+
],
|
|
176
|
+
}),
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const posts = await prisma.post.findMany({
|
|
180
|
+
where,
|
|
181
|
+
take: first + 1,
|
|
182
|
+
...(after && { cursor: { id: after }, skip: 1 }),
|
|
183
|
+
orderBy: { createdAt: 'desc' },
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const hasNextPage = posts.length > first;
|
|
187
|
+
const edges = posts.slice(0, first).map((post) => ({
|
|
188
|
+
node: post,
|
|
189
|
+
cursor: post.id,
|
|
190
|
+
}));
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
edges,
|
|
194
|
+
pageInfo: {
|
|
195
|
+
hasNextPage,
|
|
196
|
+
hasPreviousPage: !!after,
|
|
197
|
+
startCursor: edges[0]?.cursor,
|
|
198
|
+
endCursor: edges[edges.length - 1]?.cursor,
|
|
199
|
+
},
|
|
200
|
+
totalCount: await prisma.post.count({ where }),
|
|
201
|
+
};
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
Mutation: {
|
|
206
|
+
createPost: async (_, { input }, { user, prisma }) => {
|
|
207
|
+
if (!user) {
|
|
208
|
+
return { post: null, errors: [{ message: 'Not authenticated' }] };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
const post = await prisma.post.create({
|
|
213
|
+
data: { ...input, authorId: user.id },
|
|
214
|
+
});
|
|
215
|
+
return { post, errors: null };
|
|
216
|
+
} catch (error) {
|
|
217
|
+
return { post: null, errors: [{ message: 'Failed to create post' }] };
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
publishPost: async (_, { id }, { user, prisma }) => {
|
|
222
|
+
const post = await prisma.post.findUnique({ where: { id } });
|
|
223
|
+
|
|
224
|
+
if (!post) {
|
|
225
|
+
return { post: null, errors: [{ message: 'Post not found' }] };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (post.authorId !== user.id) {
|
|
229
|
+
return { post: null, errors: [{ message: 'Not authorized' }] };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const updated = await prisma.post.update({
|
|
233
|
+
where: { id },
|
|
234
|
+
data: { status: 'PUBLISHED', publishedAt: new Date() },
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
return { post: updated, errors: null };
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
// Field resolvers
|
|
242
|
+
User: {
|
|
243
|
+
posts: async (user, { first, after }, { prisma }) => {
|
|
244
|
+
// Delegate to Query.posts with filter
|
|
245
|
+
return resolvers.Query.posts(
|
|
246
|
+
null,
|
|
247
|
+
{ first, after, filter: { authorId: user.id } },
|
|
248
|
+
{ prisma }
|
|
249
|
+
);
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
Post: {
|
|
254
|
+
author: async (post, _, { loaders }) => {
|
|
255
|
+
// Use DataLoader to batch & cache
|
|
256
|
+
return loaders.userLoader.load(post.authorId);
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
Subscription: {
|
|
261
|
+
postCreated: {
|
|
262
|
+
subscribe: (_, __, { pubsub }) => {
|
|
263
|
+
return pubsub.asyncIterator(['POST_CREATED']);
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
\`\`\`
|
|
269
|
+
|
|
270
|
+
## DataLoader (N+1 Prevention)
|
|
271
|
+
\`\`\`typescript
|
|
272
|
+
// loaders.ts
|
|
273
|
+
import DataLoader from 'dataloader';
|
|
274
|
+
import { PrismaClient, User } from '@prisma/client';
|
|
275
|
+
|
|
276
|
+
export function createLoaders(prisma: PrismaClient) {
|
|
277
|
+
return {
|
|
278
|
+
userLoader: new DataLoader<string, User>(async (ids) => {
|
|
279
|
+
const users = await prisma.user.findMany({
|
|
280
|
+
where: { id: { in: [...ids] } },
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const userMap = new Map(users.map((u) => [u.id, u]));
|
|
284
|
+
return ids.map((id) => userMap.get(id) || null);
|
|
285
|
+
}),
|
|
286
|
+
|
|
287
|
+
postsByAuthorLoader: new DataLoader<string, Post[]>(async (authorIds) => {
|
|
288
|
+
const posts = await prisma.post.findMany({
|
|
289
|
+
where: { authorId: { in: [...authorIds] } },
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const postsByAuthor = new Map<string, Post[]>();
|
|
293
|
+
posts.forEach((post) => {
|
|
294
|
+
const existing = postsByAuthor.get(post.authorId) || [];
|
|
295
|
+
postsByAuthor.set(post.authorId, [...existing, post]);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
return authorIds.map((id) => postsByAuthor.get(id) || []);
|
|
299
|
+
}),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Context setup
|
|
304
|
+
const server = new ApolloServer({
|
|
305
|
+
typeDefs,
|
|
306
|
+
resolvers,
|
|
307
|
+
context: ({ req }) => ({
|
|
308
|
+
prisma,
|
|
309
|
+
user: req.user,
|
|
310
|
+
loaders: createLoaders(prisma),
|
|
311
|
+
}),
|
|
312
|
+
});
|
|
313
|
+
\`\`\`
|
|
314
|
+
|
|
315
|
+
## Client Usage (Apollo Client)
|
|
316
|
+
\`\`\`typescript
|
|
317
|
+
// apollo-client.ts
|
|
318
|
+
import { ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client';
|
|
319
|
+
import { setContext } from '@apollo/client/link/context';
|
|
320
|
+
|
|
321
|
+
const httpLink = createHttpLink({
|
|
322
|
+
uri: '/api/graphql',
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const authLink = setContext((_, { headers }) => {
|
|
326
|
+
const token = localStorage.getItem('token');
|
|
327
|
+
return {
|
|
328
|
+
headers: {
|
|
329
|
+
...headers,
|
|
330
|
+
authorization: token ? \`Bearer \${token}\` : '',
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
export const client = new ApolloClient({
|
|
336
|
+
link: authLink.concat(httpLink),
|
|
337
|
+
cache: new InMemoryCache({
|
|
338
|
+
typePolicies: {
|
|
339
|
+
Query: {
|
|
340
|
+
fields: {
|
|
341
|
+
posts: {
|
|
342
|
+
keyArgs: ['filter'],
|
|
343
|
+
merge(existing, incoming, { args }) {
|
|
344
|
+
if (!args?.after) return incoming;
|
|
345
|
+
return {
|
|
346
|
+
...incoming,
|
|
347
|
+
edges: [...(existing?.edges || []), ...incoming.edges],
|
|
348
|
+
};
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
}),
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// hooks/usePosts.ts
|
|
358
|
+
import { gql, useQuery } from '@apollo/client';
|
|
359
|
+
|
|
360
|
+
const GET_POSTS = gql\`
|
|
361
|
+
query GetPosts($first: Int, $after: String, $filter: PostFilterInput) {
|
|
362
|
+
posts(first: $first, after: $after, filter: $filter) {
|
|
363
|
+
edges {
|
|
364
|
+
node {
|
|
365
|
+
id
|
|
366
|
+
title
|
|
367
|
+
status
|
|
368
|
+
author {
|
|
369
|
+
id
|
|
370
|
+
name
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
cursor
|
|
374
|
+
}
|
|
375
|
+
pageInfo {
|
|
376
|
+
hasNextPage
|
|
377
|
+
endCursor
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
\`;
|
|
382
|
+
|
|
383
|
+
export function usePosts(filter?: PostFilterInput) {
|
|
384
|
+
const { data, loading, error, fetchMore } = useQuery(GET_POSTS, {
|
|
385
|
+
variables: { first: 10, filter },
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
const loadMore = () => {
|
|
389
|
+
if (!data?.posts.pageInfo.hasNextPage) return;
|
|
390
|
+
|
|
391
|
+
fetchMore({
|
|
392
|
+
variables: {
|
|
393
|
+
after: data.posts.pageInfo.endCursor,
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
return { posts: data?.posts.edges, loading, error, loadMore };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Mutation hook
|
|
402
|
+
const CREATE_POST = gql\`
|
|
403
|
+
mutation CreatePost($input: CreatePostInput!) {
|
|
404
|
+
createPost(input: $input) {
|
|
405
|
+
post {
|
|
406
|
+
id
|
|
407
|
+
title
|
|
408
|
+
}
|
|
409
|
+
errors {
|
|
410
|
+
field
|
|
411
|
+
message
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
\`;
|
|
416
|
+
|
|
417
|
+
export function useCreatePost() {
|
|
418
|
+
const [createPost, { loading }] = useMutation(CREATE_POST, {
|
|
419
|
+
update(cache, { data }) {
|
|
420
|
+
if (data?.createPost.post) {
|
|
421
|
+
cache.modify({
|
|
422
|
+
fields: {
|
|
423
|
+
posts(existing = { edges: [] }) {
|
|
424
|
+
const newEdge = {
|
|
425
|
+
__typename: 'PostEdge',
|
|
426
|
+
node: data.createPost.post,
|
|
427
|
+
cursor: data.createPost.post.id,
|
|
428
|
+
};
|
|
429
|
+
return {
|
|
430
|
+
...existing,
|
|
431
|
+
edges: [newEdge, ...existing.edges],
|
|
432
|
+
};
|
|
433
|
+
},
|
|
434
|
+
},
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
},
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
return { createPost, loading };
|
|
441
|
+
}
|
|
442
|
+
\`\`\`
|
|
443
|
+
|
|
444
|
+
## Error Handling
|
|
445
|
+
\`\`\`typescript
|
|
446
|
+
// Custom errors
|
|
447
|
+
import { GraphQLError } from 'graphql';
|
|
448
|
+
|
|
449
|
+
throw new GraphQLError('Not authenticated', {
|
|
450
|
+
extensions: {
|
|
451
|
+
code: 'UNAUTHENTICATED',
|
|
452
|
+
http: { status: 401 },
|
|
453
|
+
},
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
throw new GraphQLError('Resource not found', {
|
|
457
|
+
extensions: {
|
|
458
|
+
code: 'NOT_FOUND',
|
|
459
|
+
http: { status: 404 },
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
\`\`\`
|
|
463
|
+
|
|
464
|
+
## ❌ DON'T
|
|
465
|
+
- Return null without proper error handling
|
|
466
|
+
- Skip DataLoader for relationship fields
|
|
467
|
+
- Expose sensitive fields without authorization
|
|
468
|
+
- Use deeply nested queries without limits
|
|
469
|
+
- Forget input validation
|
|
470
|
+
|
|
471
|
+
## ✅ DO
|
|
472
|
+
- Use Relay-style pagination for lists
|
|
473
|
+
- Implement DataLoader for all relationships
|
|
474
|
+
- Return error payloads from mutations
|
|
475
|
+
- Add query complexity limits
|
|
476
|
+
- Use code generation for types
|
|
477
|
+
- Implement field-level authorization
|
|
478
|
+
- Cache and batch with DataLoader
|