@girardmedia/bootspring 1.2.0 → 2.0.3
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 +107 -14
- package/bin/bootspring.js +166 -27
- package/cli/agent.js +189 -17
- package/cli/analyze.js +499 -0
- package/cli/audit.js +557 -0
- package/cli/auth.js +495 -38
- package/cli/billing.js +302 -0
- package/cli/build.js +695 -0
- package/cli/business.js +109 -26
- package/cli/checkpoint-utils.js +168 -0
- package/cli/checkpoint.js +639 -0
- package/cli/cloud-sync.js +447 -0
- package/cli/content.js +198 -0
- package/cli/context.js +1 -1
- package/cli/deploy.js +543 -0
- package/cli/fundraise.js +112 -50
- package/cli/github-cmd.js +435 -0
- package/cli/health.js +477 -0
- package/cli/init.js +84 -13
- package/cli/legal.js +107 -95
- package/cli/log.js +2 -2
- package/cli/loop.js +976 -73
- package/cli/manager.js +711 -0
- package/cli/metrics.js +480 -0
- package/cli/monitor.js +812 -0
- package/cli/onboard.js +521 -0
- package/cli/orchestrator.js +12 -24
- package/cli/prd.js +594 -0
- package/cli/preseed-start.js +1483 -0
- package/cli/preseed.js +2302 -0
- package/cli/project.js +436 -0
- package/cli/quality.js +233 -0
- package/cli/security.js +913 -0
- package/cli/seed.js +1441 -5
- package/cli/skill.js +273 -211
- package/cli/suggest.js +989 -0
- package/cli/switch.js +453 -0
- package/cli/visualize.js +527 -0
- package/cli/watch.js +769 -0
- package/cli/workspace.js +607 -0
- package/core/analyze-workflow.js +1134 -0
- package/core/api-client.js +535 -22
- package/core/audit-workflow.js +1350 -0
- package/core/build-orchestrator.js +480 -0
- package/core/build-state.js +577 -0
- package/core/checkpoint-engine.js +408 -0
- package/core/config.js +1109 -26
- package/core/context-loader.js +21 -1
- package/core/deploy-workflow.js +836 -0
- package/core/entitlements.js +93 -22
- package/core/github-sync.js +610 -0
- package/core/index.js +8 -1
- package/core/ingest.js +1111 -0
- package/core/metrics-engine.js +768 -0
- package/core/onboard-workflow.js +1007 -0
- package/core/preseed-workflow.js +934 -0
- package/core/preseed.js +1617 -0
- package/core/project-context.js +325 -0
- package/core/project-state.js +694 -0
- package/core/r2-sync.js +583 -0
- package/core/scaffold.js +525 -7
- package/core/session.js +258 -0
- package/core/task-extractor.js +758 -0
- package/core/telemetry.js +28 -6
- package/core/tier-enforcement.js +737 -0
- package/core/utils.js +38 -14
- package/generators/questionnaire.js +15 -12
- package/generators/sections/ai.js +7 -7
- package/generators/sections/content.js +300 -0
- package/generators/sections/index.js +3 -0
- package/generators/sections/plugins.js +7 -6
- package/generators/templates/build-planning.template.js +596 -0
- package/generators/templates/content.template.js +819 -0
- package/generators/templates/index.js +2 -1
- package/hooks/git-autopilot.js +1250 -0
- package/hooks/index.js +9 -0
- package/intelligence/agent-collab.js +2057 -0
- package/intelligence/auto-suggest.js +634 -0
- package/intelligence/content-gen.js +1589 -0
- package/intelligence/cross-project.js +1647 -0
- package/intelligence/index.js +184 -0
- package/intelligence/learning/insights.json +517 -7
- package/intelligence/learning/pattern-learner.js +1008 -14
- package/intelligence/memory/decision-tracker.js +1431 -31
- package/intelligence/memory/decisions.jsonl +0 -0
- package/intelligence/orchestrator.js +2896 -1
- package/intelligence/prd.js +92 -1
- package/intelligence/recommendation-weights.json +14 -2
- package/intelligence/recommendations.js +463 -9
- package/intelligence/workflow-composer.js +1451 -0
- package/marketplace/index.d.ts +324 -0
- package/marketplace/index.js +1921 -0
- package/mcp/contracts/mcp-contract.v1.json +342 -4
- package/mcp/registry.js +680 -3
- package/mcp/response-formatter.js +23 -0
- package/mcp/tools/assist-tool.js +78 -4
- package/mcp/tools/autopilot-tool.js +408 -0
- package/mcp/tools/content-tool.js +571 -0
- package/mcp/tools/dashboard-tool.js +251 -5
- package/mcp/tools/mvp-tool.js +344 -0
- package/mcp/tools/plugin-tool.js +23 -1
- package/mcp/tools/prd-tool.js +579 -0
- package/mcp/tools/seed-tool.js +447 -0
- package/mcp/tools/skill-tool.js +43 -14
- package/mcp/tools/suggest-tool.js +147 -0
- package/package.json +15 -6
- package/agents/README.md +0 -93
- package/agents/ai-integration-expert/context.md +0 -386
- package/agents/api-expert/context.md +0 -416
- package/agents/architecture-expert/context.md +0 -454
- package/agents/auth-expert/context.md +0 -399
- package/agents/backend-expert/context.md +0 -483
- package/agents/business-strategy-expert/context.md +0 -180
- package/agents/code-review-expert/context.md +0 -365
- package/agents/competitive-analysis-expert/context.md +0 -239
- package/agents/data-modeling-expert/context.md +0 -352
- package/agents/database-expert/context.md +0 -250
- package/agents/devops-expert/context.md +0 -446
- package/agents/email-expert/context.md +0 -379
- package/agents/financial-expert/context.md +0 -213
- package/agents/frontend-expert/context.md +0 -364
- package/agents/fundraising-expert/context.md +0 -257
- package/agents/growth-expert/context.md +0 -249
- package/agents/index.js +0 -140
- package/agents/investor-relations-expert/context.md +0 -266
- package/agents/legal-expert/context.md +0 -284
- package/agents/marketing-expert/context.md +0 -236
- package/agents/monitoring-expert/context.md +0 -362
- package/agents/operations-expert/context.md +0 -279
- package/agents/partnerships-expert/context.md +0 -286
- package/agents/payment-expert/context.md +0 -340
- package/agents/performance-expert/context.md +0 -377
- package/agents/private-equity-expert/context.md +0 -246
- package/agents/railway-expert/context.md +0 -284
- package/agents/research-expert/context.md +0 -245
- package/agents/sales-expert/context.md +0 -241
- package/agents/security-expert/context.md +0 -343
- package/agents/testing-expert/context.md +0 -414
- package/agents/ui-ux-expert/context.md +0 -448
- package/agents/vercel-expert/context.md +0 -426
- package/skills/index.js +0 -787
- package/skills/patterns/README.md +0 -163
- package/skills/patterns/ai/agents.md +0 -281
- package/skills/patterns/ai/claude.md +0 -138
- package/skills/patterns/ai/embeddings.md +0 -150
- package/skills/patterns/ai/rag.md +0 -266
- package/skills/patterns/ai/streaming.md +0 -170
- package/skills/patterns/ai/structured-output.md +0 -162
- package/skills/patterns/ai/tools.md +0 -154
- package/skills/patterns/analytics/tracking.md +0 -220
- package/skills/patterns/api/errors.md +0 -296
- package/skills/patterns/api/graphql.md +0 -440
- package/skills/patterns/api/middleware.md +0 -279
- package/skills/patterns/api/openapi.md +0 -285
- package/skills/patterns/api/rate-limiting.md +0 -231
- package/skills/patterns/api/route-handler.md +0 -217
- package/skills/patterns/api/server-action.md +0 -249
- package/skills/patterns/api/versioning.md +0 -443
- package/skills/patterns/api/webhooks.md +0 -247
- package/skills/patterns/auth/clerk.md +0 -132
- package/skills/patterns/auth/mfa.md +0 -313
- package/skills/patterns/auth/nextauth.md +0 -140
- package/skills/patterns/auth/oauth.md +0 -237
- package/skills/patterns/auth/rbac.md +0 -152
- package/skills/patterns/auth/session-management.md +0 -367
- package/skills/patterns/auth/session.md +0 -120
- package/skills/patterns/database/audit.md +0 -177
- package/skills/patterns/database/migrations.md +0 -177
- package/skills/patterns/database/pagination.md +0 -230
- package/skills/patterns/database/pooling.md +0 -357
- package/skills/patterns/database/prisma.md +0 -180
- package/skills/patterns/database/relations.md +0 -187
- package/skills/patterns/database/seeding.md +0 -246
- package/skills/patterns/database/soft-delete.md +0 -153
- package/skills/patterns/database/transactions.md +0 -162
- package/skills/patterns/deployment/ci-cd.md +0 -231
- package/skills/patterns/deployment/docker.md +0 -188
- package/skills/patterns/deployment/monitoring.md +0 -387
- package/skills/patterns/deployment/vercel.md +0 -160
- package/skills/patterns/email/resend.md +0 -143
- package/skills/patterns/email/templates.md +0 -245
- package/skills/patterns/email/transactional.md +0 -503
- package/skills/patterns/email/verification.md +0 -176
- package/skills/patterns/files/download.md +0 -243
- package/skills/patterns/files/upload.md +0 -239
- package/skills/patterns/i18n/nextintl.md +0 -188
- package/skills/patterns/logging/structured.md +0 -292
- package/skills/patterns/notifications/email-queue.md +0 -248
- package/skills/patterns/notifications/push.md +0 -279
- package/skills/patterns/payments/checkout.md +0 -303
- package/skills/patterns/payments/invoices.md +0 -287
- package/skills/patterns/payments/portal.md +0 -245
- package/skills/patterns/payments/stripe.md +0 -272
- package/skills/patterns/payments/subscriptions.md +0 -300
- package/skills/patterns/payments/usage.md +0 -279
- package/skills/patterns/performance/caching.md +0 -276
- package/skills/patterns/performance/code-splitting.md +0 -233
- package/skills/patterns/performance/edge.md +0 -254
- package/skills/patterns/performance/isr.md +0 -266
- package/skills/patterns/performance/lazy-loading.md +0 -281
- package/skills/patterns/realtime/sse.md +0 -327
- package/skills/patterns/realtime/websockets.md +0 -336
- package/skills/patterns/search/filtering.md +0 -329
- package/skills/patterns/search/fulltext.md +0 -260
- package/skills/patterns/security/audit-logging.md +0 -444
- package/skills/patterns/security/csrf.md +0 -234
- package/skills/patterns/security/headers.md +0 -252
- package/skills/patterns/security/sanitization.md +0 -258
- package/skills/patterns/security/secrets.md +0 -261
- package/skills/patterns/security/validation.md +0 -268
- package/skills/patterns/security/xss.md +0 -229
- package/skills/patterns/seo/metadata.md +0 -252
- package/skills/patterns/state/context.md +0 -349
- package/skills/patterns/state/react-query.md +0 -313
- package/skills/patterns/state/url-state.md +0 -482
- package/skills/patterns/state/zustand.md +0 -262
- package/skills/patterns/testing/api.md +0 -259
- package/skills/patterns/testing/component.md +0 -233
- package/skills/patterns/testing/coverage.md +0 -207
- package/skills/patterns/testing/fixtures.md +0 -225
- package/skills/patterns/testing/integration.md +0 -436
- package/skills/patterns/testing/mocking.md +0 -177
- package/skills/patterns/testing/playwright.md +0 -162
- package/skills/patterns/testing/snapshot.md +0 -175
- package/skills/patterns/testing/vitest.md +0 -307
- package/skills/patterns/ui/accordions.md +0 -395
- package/skills/patterns/ui/cards.md +0 -299
- package/skills/patterns/ui/dropdowns.md +0 -476
- package/skills/patterns/ui/empty-states.md +0 -320
- package/skills/patterns/ui/forms.md +0 -405
- package/skills/patterns/ui/inputs.md +0 -319
- package/skills/patterns/ui/layouts.md +0 -282
- package/skills/patterns/ui/loading.md +0 -291
- package/skills/patterns/ui/modals.md +0 -338
- package/skills/patterns/ui/navigation.md +0 -374
- package/skills/patterns/ui/tables.md +0 -407
- package/skills/patterns/ui/toasts.md +0 -300
- package/skills/patterns/ui/tooltips.md +0 -396
- package/skills/patterns/utils/dates.md +0 -435
- package/skills/patterns/utils/errors.md +0 -451
- package/skills/patterns/utils/formatting.md +0 -345
- package/skills/patterns/utils/validation.md +0 -434
- package/templates/bootspring.config.js +0 -83
- package/templates/business/business-model-canvas.md +0 -246
- package/templates/business/business-plan.md +0 -266
- package/templates/business/competitive-analysis.md +0 -312
- package/templates/fundraising/data-room-checklist.md +0 -300
- package/templates/fundraising/investor-research.md +0 -243
- package/templates/fundraising/pitch-deck-outline.md +0 -253
- package/templates/legal/gdpr-checklist.md +0 -339
- package/templates/legal/privacy-policy.md +0 -285
- package/templates/legal/terms-of-service.md +0 -222
- package/templates/mcp.json +0 -9
|
@@ -1,296 +0,0 @@
|
|
|
1
|
-
# API Error Handling Patterns
|
|
2
|
-
|
|
3
|
-
Patterns for consistent API error responses.
|
|
4
|
-
|
|
5
|
-
## Error Classes
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
// lib/errors.ts
|
|
9
|
-
export class AppError extends Error {
|
|
10
|
-
constructor(
|
|
11
|
-
message: string,
|
|
12
|
-
public statusCode: number = 500,
|
|
13
|
-
public code: string = 'INTERNAL_ERROR'
|
|
14
|
-
) {
|
|
15
|
-
super(message)
|
|
16
|
-
this.name = 'AppError'
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export class NotFoundError extends AppError {
|
|
21
|
-
constructor(resource: string) {
|
|
22
|
-
super(`${resource} not found`, 404, 'NOT_FOUND')
|
|
23
|
-
this.name = 'NotFoundError'
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export class UnauthorizedError extends AppError {
|
|
28
|
-
constructor(message = 'Unauthorized') {
|
|
29
|
-
super(message, 401, 'UNAUTHORIZED')
|
|
30
|
-
this.name = 'UnauthorizedError'
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export class ForbiddenError extends AppError {
|
|
35
|
-
constructor(message = 'Forbidden') {
|
|
36
|
-
super(message, 403, 'FORBIDDEN')
|
|
37
|
-
this.name = 'ForbiddenError'
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export class ValidationError extends AppError {
|
|
42
|
-
constructor(
|
|
43
|
-
message: string,
|
|
44
|
-
public errors: Record<string, string[]> = {}
|
|
45
|
-
) {
|
|
46
|
-
super(message, 400, 'VALIDATION_ERROR')
|
|
47
|
-
this.name = 'ValidationError'
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export class ConflictError extends AppError {
|
|
52
|
-
constructor(message: string) {
|
|
53
|
-
super(message, 409, 'CONFLICT')
|
|
54
|
-
this.name = 'ConflictError'
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export class RateLimitError extends AppError {
|
|
59
|
-
constructor(retryAfter: number) {
|
|
60
|
-
super('Too many requests', 429, 'RATE_LIMIT_EXCEEDED')
|
|
61
|
-
this.name = 'RateLimitError'
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
## Error Response Format
|
|
67
|
-
|
|
68
|
-
```typescript
|
|
69
|
-
// lib/api-response.ts
|
|
70
|
-
interface ErrorResponse {
|
|
71
|
-
error: {
|
|
72
|
-
message: string
|
|
73
|
-
code: string
|
|
74
|
-
details?: Record<string, unknown>
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
interface SuccessResponse<T> {
|
|
79
|
-
data: T
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export function errorResponse(
|
|
83
|
-
error: AppError | Error
|
|
84
|
-
): { body: ErrorResponse; status: number } {
|
|
85
|
-
if (error instanceof AppError) {
|
|
86
|
-
return {
|
|
87
|
-
body: {
|
|
88
|
-
error: {
|
|
89
|
-
message: error.message,
|
|
90
|
-
code: error.code,
|
|
91
|
-
details: error instanceof ValidationError ? { errors: error.errors } : undefined
|
|
92
|
-
}
|
|
93
|
-
},
|
|
94
|
-
status: error.statusCode
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Unknown error - don't expose details
|
|
99
|
-
console.error('Unhandled error:', error)
|
|
100
|
-
return {
|
|
101
|
-
body: {
|
|
102
|
-
error: {
|
|
103
|
-
message: 'An unexpected error occurred',
|
|
104
|
-
code: 'INTERNAL_ERROR'
|
|
105
|
-
}
|
|
106
|
-
},
|
|
107
|
-
status: 500
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export function successResponse<T>(data: T): SuccessResponse<T> {
|
|
112
|
-
return { data }
|
|
113
|
-
}
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
## Error Handler Wrapper
|
|
117
|
-
|
|
118
|
-
```typescript
|
|
119
|
-
// lib/api-handler.ts
|
|
120
|
-
import { NextRequest, NextResponse } from 'next/server'
|
|
121
|
-
import { AppError, errorResponse } from '@/lib/errors'
|
|
122
|
-
import { ZodError } from 'zod'
|
|
123
|
-
|
|
124
|
-
type Handler = (request: NextRequest) => Promise<Response>
|
|
125
|
-
|
|
126
|
-
export function withErrorHandler(handler: Handler): Handler {
|
|
127
|
-
return async (request: NextRequest) => {
|
|
128
|
-
try {
|
|
129
|
-
return await handler(request)
|
|
130
|
-
} catch (error) {
|
|
131
|
-
// Handle Zod validation errors
|
|
132
|
-
if (error instanceof ZodError) {
|
|
133
|
-
const validationError = new ValidationError(
|
|
134
|
-
'Validation failed',
|
|
135
|
-
formatZodErrors(error)
|
|
136
|
-
)
|
|
137
|
-
const { body, status } = errorResponse(validationError)
|
|
138
|
-
return NextResponse.json(body, { status })
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Handle app errors
|
|
142
|
-
if (error instanceof AppError) {
|
|
143
|
-
const { body, status } = errorResponse(error)
|
|
144
|
-
return NextResponse.json(body, { status })
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Handle unknown errors
|
|
148
|
-
console.error('Unhandled API error:', error)
|
|
149
|
-
return NextResponse.json(
|
|
150
|
-
{ error: { message: 'Internal server error', code: 'INTERNAL_ERROR' } },
|
|
151
|
-
{ status: 500 }
|
|
152
|
-
)
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function formatZodErrors(error: ZodError): Record<string, string[]> {
|
|
158
|
-
const errors: Record<string, string[]> = {}
|
|
159
|
-
|
|
160
|
-
for (const issue of error.issues) {
|
|
161
|
-
const path = issue.path.join('.')
|
|
162
|
-
if (!errors[path]) {
|
|
163
|
-
errors[path] = []
|
|
164
|
-
}
|
|
165
|
-
errors[path].push(issue.message)
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
return errors
|
|
169
|
-
}
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
## Route Handler with Error Handling
|
|
173
|
-
|
|
174
|
-
```typescript
|
|
175
|
-
// app/api/users/[id]/route.ts
|
|
176
|
-
import { NextRequest, NextResponse } from 'next/server'
|
|
177
|
-
import { withErrorHandler } from '@/lib/api-handler'
|
|
178
|
-
import { NotFoundError, ForbiddenError } from '@/lib/errors'
|
|
179
|
-
import { auth } from '@/auth'
|
|
180
|
-
|
|
181
|
-
export const GET = withErrorHandler(async (
|
|
182
|
-
request: NextRequest,
|
|
183
|
-
{ params }: { params: { id: string } }
|
|
184
|
-
) => {
|
|
185
|
-
const session = await auth()
|
|
186
|
-
|
|
187
|
-
if (!session) {
|
|
188
|
-
throw new UnauthorizedError()
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const user = await prisma.user.findUnique({
|
|
192
|
-
where: { id: params.id }
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
if (!user) {
|
|
196
|
-
throw new NotFoundError('User')
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (user.id !== session.user.id && session.user.role !== 'ADMIN') {
|
|
200
|
-
throw new ForbiddenError('Cannot access other users')
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return NextResponse.json({ data: user })
|
|
204
|
-
})
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
## Error Logging
|
|
208
|
-
|
|
209
|
-
```typescript
|
|
210
|
-
// lib/error-logger.ts
|
|
211
|
-
import { AppError } from './errors'
|
|
212
|
-
|
|
213
|
-
interface ErrorContext {
|
|
214
|
-
userId?: string
|
|
215
|
-
requestId?: string
|
|
216
|
-
path?: string
|
|
217
|
-
method?: string
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
export function logError(error: Error, context: ErrorContext = {}) {
|
|
221
|
-
const isOperational = error instanceof AppError
|
|
222
|
-
|
|
223
|
-
const logData = {
|
|
224
|
-
message: error.message,
|
|
225
|
-
stack: error.stack,
|
|
226
|
-
isOperational,
|
|
227
|
-
...context,
|
|
228
|
-
timestamp: new Date().toISOString()
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (isOperational) {
|
|
232
|
-
console.warn('Operational error:', logData)
|
|
233
|
-
} else {
|
|
234
|
-
console.error('System error:', logData)
|
|
235
|
-
// Send to error tracking service
|
|
236
|
-
// await sendToSentry(error, context)
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
## Client-Side Error Handling
|
|
242
|
-
|
|
243
|
-
```typescript
|
|
244
|
-
// lib/api-client.ts
|
|
245
|
-
import { AppError } from './errors'
|
|
246
|
-
|
|
247
|
-
interface ApiError {
|
|
248
|
-
error: {
|
|
249
|
-
message: string
|
|
250
|
-
code: string
|
|
251
|
-
details?: Record<string, unknown>
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
export async function apiRequest<T>(
|
|
256
|
-
url: string,
|
|
257
|
-
options?: RequestInit
|
|
258
|
-
): Promise<T> {
|
|
259
|
-
const response = await fetch(url, {
|
|
260
|
-
...options,
|
|
261
|
-
headers: {
|
|
262
|
-
'Content-Type': 'application/json',
|
|
263
|
-
...options?.headers
|
|
264
|
-
}
|
|
265
|
-
})
|
|
266
|
-
|
|
267
|
-
const data = await response.json()
|
|
268
|
-
|
|
269
|
-
if (!response.ok) {
|
|
270
|
-
const apiError = data as ApiError
|
|
271
|
-
throw new AppError(
|
|
272
|
-
apiError.error.message,
|
|
273
|
-
response.status,
|
|
274
|
-
apiError.error.code
|
|
275
|
-
)
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
return data.data as T
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// Usage
|
|
282
|
-
try {
|
|
283
|
-
const user = await apiRequest<User>('/api/users/123')
|
|
284
|
-
} catch (error) {
|
|
285
|
-
if (error instanceof AppError && error.code === 'NOT_FOUND') {
|
|
286
|
-
// Handle not found
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
## When to Use
|
|
292
|
-
|
|
293
|
-
- RESTful APIs
|
|
294
|
-
- Consistent error format
|
|
295
|
-
- Client error handling
|
|
296
|
-
- Error monitoring
|
|
@@ -1,440 +0,0 @@
|
|
|
1
|
-
# GraphQL Patterns
|
|
2
|
-
|
|
3
|
-
Patterns for implementing GraphQL APIs.
|
|
4
|
-
|
|
5
|
-
## Apollo Server Setup
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
// app/api/graphql/route.ts
|
|
9
|
-
import { ApolloServer } from '@apollo/server'
|
|
10
|
-
import { startServerAndCreateNextHandler } from '@as-integrations/next'
|
|
11
|
-
import { typeDefs } from '@/graphql/schema'
|
|
12
|
-
import { resolvers } from '@/graphql/resolvers'
|
|
13
|
-
import { createContext } from '@/graphql/context'
|
|
14
|
-
|
|
15
|
-
const server = new ApolloServer({
|
|
16
|
-
typeDefs,
|
|
17
|
-
resolvers
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
const handler = startServerAndCreateNextHandler(server, {
|
|
21
|
-
context: createContext
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
export { handler as GET, handler as POST }
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
## Schema Definition
|
|
28
|
-
|
|
29
|
-
```typescript
|
|
30
|
-
// graphql/schema.ts
|
|
31
|
-
import { gql } from 'graphql-tag'
|
|
32
|
-
|
|
33
|
-
export const typeDefs = gql`
|
|
34
|
-
type User {
|
|
35
|
-
id: ID!
|
|
36
|
-
email: String!
|
|
37
|
-
name: String
|
|
38
|
-
posts: [Post!]!
|
|
39
|
-
createdAt: String!
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
type Post {
|
|
43
|
-
id: ID!
|
|
44
|
-
title: String!
|
|
45
|
-
content: String
|
|
46
|
-
published: Boolean!
|
|
47
|
-
author: User!
|
|
48
|
-
comments: [Comment!]!
|
|
49
|
-
createdAt: String!
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
type Comment {
|
|
53
|
-
id: ID!
|
|
54
|
-
content: String!
|
|
55
|
-
author: User!
|
|
56
|
-
post: Post!
|
|
57
|
-
createdAt: String!
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
type Query {
|
|
61
|
-
me: User
|
|
62
|
-
user(id: ID!): User
|
|
63
|
-
users(limit: Int, offset: Int): [User!]!
|
|
64
|
-
post(id: ID!): Post
|
|
65
|
-
posts(published: Boolean, limit: Int, offset: Int): [Post!]!
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
type Mutation {
|
|
69
|
-
createUser(input: CreateUserInput!): User!
|
|
70
|
-
updateUser(id: ID!, input: UpdateUserInput!): User!
|
|
71
|
-
deleteUser(id: ID!): Boolean!
|
|
72
|
-
|
|
73
|
-
createPost(input: CreatePostInput!): Post!
|
|
74
|
-
updatePost(id: ID!, input: UpdatePostInput!): Post!
|
|
75
|
-
deletePost(id: ID!): Boolean!
|
|
76
|
-
publishPost(id: ID!): Post!
|
|
77
|
-
|
|
78
|
-
createComment(input: CreateCommentInput!): Comment!
|
|
79
|
-
deleteComment(id: ID!): Boolean!
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
input CreateUserInput {
|
|
83
|
-
email: String!
|
|
84
|
-
name: String
|
|
85
|
-
password: String!
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
input UpdateUserInput {
|
|
89
|
-
email: String
|
|
90
|
-
name: String
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
input CreatePostInput {
|
|
94
|
-
title: String!
|
|
95
|
-
content: String
|
|
96
|
-
published: Boolean
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
input UpdatePostInput {
|
|
100
|
-
title: String
|
|
101
|
-
content: String
|
|
102
|
-
published: Boolean
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
input CreateCommentInput {
|
|
106
|
-
postId: ID!
|
|
107
|
-
content: String!
|
|
108
|
-
}
|
|
109
|
-
`
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
## Resolvers
|
|
113
|
-
|
|
114
|
-
```typescript
|
|
115
|
-
// graphql/resolvers.ts
|
|
116
|
-
import { prisma } from '@/lib/db'
|
|
117
|
-
import { GraphQLError } from 'graphql'
|
|
118
|
-
import { Context } from './context'
|
|
119
|
-
|
|
120
|
-
export const resolvers = {
|
|
121
|
-
Query: {
|
|
122
|
-
me: async (_: unknown, __: unknown, ctx: Context) => {
|
|
123
|
-
if (!ctx.user) return null
|
|
124
|
-
return prisma.user.findUnique({ where: { id: ctx.user.id } })
|
|
125
|
-
},
|
|
126
|
-
|
|
127
|
-
user: async (_: unknown, { id }: { id: string }) => {
|
|
128
|
-
return prisma.user.findUnique({ where: { id } })
|
|
129
|
-
},
|
|
130
|
-
|
|
131
|
-
users: async (_: unknown, { limit = 20, offset = 0 }: { limit?: number; offset?: number }) => {
|
|
132
|
-
return prisma.user.findMany({ take: limit, skip: offset })
|
|
133
|
-
},
|
|
134
|
-
|
|
135
|
-
post: async (_: unknown, { id }: { id: string }) => {
|
|
136
|
-
return prisma.post.findUnique({ where: { id } })
|
|
137
|
-
},
|
|
138
|
-
|
|
139
|
-
posts: async (_: unknown, args: { published?: boolean; limit?: number; offset?: number }) => {
|
|
140
|
-
return prisma.post.findMany({
|
|
141
|
-
where: args.published !== undefined ? { published: args.published } : undefined,
|
|
142
|
-
take: args.limit ?? 20,
|
|
143
|
-
skip: args.offset ?? 0,
|
|
144
|
-
orderBy: { createdAt: 'desc' }
|
|
145
|
-
})
|
|
146
|
-
}
|
|
147
|
-
},
|
|
148
|
-
|
|
149
|
-
Mutation: {
|
|
150
|
-
createPost: async (
|
|
151
|
-
_: unknown,
|
|
152
|
-
{ input }: { input: { title: string; content?: string; published?: boolean } },
|
|
153
|
-
ctx: Context
|
|
154
|
-
) => {
|
|
155
|
-
if (!ctx.user) {
|
|
156
|
-
throw new GraphQLError('Not authenticated', {
|
|
157
|
-
extensions: { code: 'UNAUTHENTICATED' }
|
|
158
|
-
})
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return prisma.post.create({
|
|
162
|
-
data: {
|
|
163
|
-
...input,
|
|
164
|
-
authorId: ctx.user.id
|
|
165
|
-
}
|
|
166
|
-
})
|
|
167
|
-
},
|
|
168
|
-
|
|
169
|
-
updatePost: async (
|
|
170
|
-
_: unknown,
|
|
171
|
-
{ id, input }: { id: string; input: Partial<{ title: string; content: string; published: boolean }> },
|
|
172
|
-
ctx: Context
|
|
173
|
-
) => {
|
|
174
|
-
const post = await prisma.post.findUnique({ where: { id } })
|
|
175
|
-
|
|
176
|
-
if (!post) {
|
|
177
|
-
throw new GraphQLError('Post not found', {
|
|
178
|
-
extensions: { code: 'NOT_FOUND' }
|
|
179
|
-
})
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (post.authorId !== ctx.user?.id) {
|
|
183
|
-
throw new GraphQLError('Not authorized', {
|
|
184
|
-
extensions: { code: 'FORBIDDEN' }
|
|
185
|
-
})
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return prisma.post.update({
|
|
189
|
-
where: { id },
|
|
190
|
-
data: input
|
|
191
|
-
})
|
|
192
|
-
},
|
|
193
|
-
|
|
194
|
-
deletePost: async (_: unknown, { id }: { id: string }, ctx: Context) => {
|
|
195
|
-
const post = await prisma.post.findUnique({ where: { id } })
|
|
196
|
-
|
|
197
|
-
if (!post || post.authorId !== ctx.user?.id) {
|
|
198
|
-
throw new GraphQLError('Not authorized', {
|
|
199
|
-
extensions: { code: 'FORBIDDEN' }
|
|
200
|
-
})
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
await prisma.post.delete({ where: { id } })
|
|
204
|
-
return true
|
|
205
|
-
}
|
|
206
|
-
},
|
|
207
|
-
|
|
208
|
-
// Field resolvers for relations
|
|
209
|
-
User: {
|
|
210
|
-
posts: (parent: { id: string }) => {
|
|
211
|
-
return prisma.post.findMany({ where: { authorId: parent.id } })
|
|
212
|
-
}
|
|
213
|
-
},
|
|
214
|
-
|
|
215
|
-
Post: {
|
|
216
|
-
author: (parent: { authorId: string }) => {
|
|
217
|
-
return prisma.user.findUnique({ where: { id: parent.authorId } })
|
|
218
|
-
},
|
|
219
|
-
comments: (parent: { id: string }) => {
|
|
220
|
-
return prisma.comment.findMany({ where: { postId: parent.id } })
|
|
221
|
-
}
|
|
222
|
-
},
|
|
223
|
-
|
|
224
|
-
Comment: {
|
|
225
|
-
author: (parent: { userId: string }) => {
|
|
226
|
-
return prisma.user.findUnique({ where: { id: parent.userId } })
|
|
227
|
-
},
|
|
228
|
-
post: (parent: { postId: string }) => {
|
|
229
|
-
return prisma.post.findUnique({ where: { id: parent.postId } })
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
## Context
|
|
236
|
-
|
|
237
|
-
```typescript
|
|
238
|
-
// graphql/context.ts
|
|
239
|
-
import { NextRequest } from 'next/server'
|
|
240
|
-
import { auth } from '@/auth'
|
|
241
|
-
|
|
242
|
-
export interface Context {
|
|
243
|
-
user: { id: string; email: string } | null
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
export async function createContext({ req }: { req: NextRequest }): Promise<Context> {
|
|
247
|
-
const session = await auth()
|
|
248
|
-
|
|
249
|
-
return {
|
|
250
|
-
user: session?.user ? {
|
|
251
|
-
id: session.user.id!,
|
|
252
|
-
email: session.user.email!
|
|
253
|
-
} : null
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
## DataLoader for N+1 Prevention
|
|
259
|
-
|
|
260
|
-
```typescript
|
|
261
|
-
// graphql/loaders.ts
|
|
262
|
-
import DataLoader from 'dataloader'
|
|
263
|
-
import { prisma } from '@/lib/db'
|
|
264
|
-
import { User, Post } from '@prisma/client'
|
|
265
|
-
|
|
266
|
-
export function createLoaders() {
|
|
267
|
-
return {
|
|
268
|
-
userLoader: new DataLoader<string, User | null>(async (ids) => {
|
|
269
|
-
const users = await prisma.user.findMany({
|
|
270
|
-
where: { id: { in: [...ids] } }
|
|
271
|
-
})
|
|
272
|
-
|
|
273
|
-
const userMap = new Map(users.map(u => [u.id, u]))
|
|
274
|
-
return ids.map(id => userMap.get(id) ?? null)
|
|
275
|
-
}),
|
|
276
|
-
|
|
277
|
-
userPostsLoader: new DataLoader<string, Post[]>(async (userIds) => {
|
|
278
|
-
const posts = await prisma.post.findMany({
|
|
279
|
-
where: { authorId: { in: [...userIds] } }
|
|
280
|
-
})
|
|
281
|
-
|
|
282
|
-
const postsByUser = new Map<string, Post[]>()
|
|
283
|
-
posts.forEach(post => {
|
|
284
|
-
const existing = postsByUser.get(post.authorId) ?? []
|
|
285
|
-
postsByUser.set(post.authorId, [...existing, post])
|
|
286
|
-
})
|
|
287
|
-
|
|
288
|
-
return userIds.map(id => postsByUser.get(id) ?? [])
|
|
289
|
-
})
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// Updated context
|
|
294
|
-
export interface Context {
|
|
295
|
-
user: { id: string; email: string } | null
|
|
296
|
-
loaders: ReturnType<typeof createLoaders>
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// Updated resolvers using loaders
|
|
300
|
-
export const resolvers = {
|
|
301
|
-
Post: {
|
|
302
|
-
author: (parent: { authorId: string }, _: unknown, ctx: Context) => {
|
|
303
|
-
return ctx.loaders.userLoader.load(parent.authorId)
|
|
304
|
-
}
|
|
305
|
-
},
|
|
306
|
-
User: {
|
|
307
|
-
posts: (parent: { id: string }, _: unknown, ctx: Context) => {
|
|
308
|
-
return ctx.loaders.userPostsLoader.load(parent.id)
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
## Client-Side Usage
|
|
315
|
-
|
|
316
|
-
```typescript
|
|
317
|
-
// lib/graphql/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 = typeof window !== 'undefined'
|
|
327
|
-
? localStorage.getItem('token')
|
|
328
|
-
: null
|
|
329
|
-
|
|
330
|
-
return {
|
|
331
|
-
headers: {
|
|
332
|
-
...headers,
|
|
333
|
-
authorization: token ? `Bearer ${token}` : ''
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
})
|
|
337
|
-
|
|
338
|
-
export const apolloClient = new ApolloClient({
|
|
339
|
-
link: authLink.concat(httpLink),
|
|
340
|
-
cache: new InMemoryCache()
|
|
341
|
-
})
|
|
342
|
-
|
|
343
|
-
// hooks/usePosts.ts
|
|
344
|
-
import { gql, useQuery, useMutation } from '@apollo/client'
|
|
345
|
-
|
|
346
|
-
const GET_POSTS = gql`
|
|
347
|
-
query GetPosts($limit: Int, $offset: Int) {
|
|
348
|
-
posts(limit: $limit, offset: $offset) {
|
|
349
|
-
id
|
|
350
|
-
title
|
|
351
|
-
content
|
|
352
|
-
published
|
|
353
|
-
author {
|
|
354
|
-
id
|
|
355
|
-
name
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
`
|
|
360
|
-
|
|
361
|
-
const CREATE_POST = gql`
|
|
362
|
-
mutation CreatePost($input: CreatePostInput!) {
|
|
363
|
-
createPost(input: $input) {
|
|
364
|
-
id
|
|
365
|
-
title
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
`
|
|
369
|
-
|
|
370
|
-
export function usePosts(limit = 10) {
|
|
371
|
-
const { data, loading, error, fetchMore } = useQuery(GET_POSTS, {
|
|
372
|
-
variables: { limit, offset: 0 }
|
|
373
|
-
})
|
|
374
|
-
|
|
375
|
-
const loadMore = () => {
|
|
376
|
-
fetchMore({
|
|
377
|
-
variables: { offset: data?.posts.length ?? 0 }
|
|
378
|
-
})
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
return { posts: data?.posts ?? [], loading, error, loadMore }
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
export function useCreatePost() {
|
|
385
|
-
const [createPost, { loading, error }] = useMutation(CREATE_POST, {
|
|
386
|
-
refetchQueries: [{ query: GET_POSTS }]
|
|
387
|
-
})
|
|
388
|
-
|
|
389
|
-
return { createPost, loading, error }
|
|
390
|
-
}
|
|
391
|
-
```
|
|
392
|
-
|
|
393
|
-
## Subscriptions with WebSocket
|
|
394
|
-
|
|
395
|
-
```typescript
|
|
396
|
-
// graphql/schema.ts (add subscription type)
|
|
397
|
-
const typeDefs = gql`
|
|
398
|
-
type Subscription {
|
|
399
|
-
postCreated: Post!
|
|
400
|
-
commentAdded(postId: ID!): Comment!
|
|
401
|
-
}
|
|
402
|
-
`
|
|
403
|
-
|
|
404
|
-
// graphql/resolvers.ts
|
|
405
|
-
import { PubSub } from 'graphql-subscriptions'
|
|
406
|
-
|
|
407
|
-
const pubsub = new PubSub()
|
|
408
|
-
|
|
409
|
-
export const resolvers = {
|
|
410
|
-
Mutation: {
|
|
411
|
-
createPost: async (_: unknown, { input }: any, ctx: Context) => {
|
|
412
|
-
const post = await prisma.post.create({
|
|
413
|
-
data: { ...input, authorId: ctx.user!.id }
|
|
414
|
-
})
|
|
415
|
-
|
|
416
|
-
pubsub.publish('POST_CREATED', { postCreated: post })
|
|
417
|
-
|
|
418
|
-
return post
|
|
419
|
-
}
|
|
420
|
-
},
|
|
421
|
-
|
|
422
|
-
Subscription: {
|
|
423
|
-
postCreated: {
|
|
424
|
-
subscribe: () => pubsub.asyncIterableIterator(['POST_CREATED'])
|
|
425
|
-
},
|
|
426
|
-
commentAdded: {
|
|
427
|
-
subscribe: (_: unknown, { postId }: { postId: string }) => {
|
|
428
|
-
return pubsub.asyncIterableIterator([`COMMENT_ADDED_${postId}`])
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
```
|
|
434
|
-
|
|
435
|
-
## When to Use
|
|
436
|
-
|
|
437
|
-
- Complex data requirements
|
|
438
|
-
- Multiple clients (web, mobile)
|
|
439
|
-
- Real-time features
|
|
440
|
-
- Flexible API queries
|