prisma-generator-express 1.62.3 → 1.63.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/README.md +234 -95
- package/dist/generators/generateRouteConfigType.js +26 -6
- package/dist/generators/generateRouteConfigType.js.map +1 -1
- package/dist/generators/generateRouter.js +245 -44
- package/dist/generators/generateRouter.js.map +1 -1
- package/dist/generators/generateRouterFastify.js +100 -28
- package/dist/generators/generateRouterFastify.js.map +1 -1
- package/dist/generators/generateRouterHono.js +104 -36
- package/dist/generators/generateRouterHono.js.map +1 -1
- package/dist/utils/copyFiles.js +2 -0
- package/dist/utils/copyFiles.js.map +1 -1
- package/package.json +1 -1
- package/src/copy/docsRenderer.ts +1 -0
- package/src/copy/guardVariantError.ts +28 -0
- package/src/copy/guardVariantRouting.ts +109 -0
- package/src/copy/projectionDefaults.ts +5 -6
- package/src/copy/routeConfig.hono.ts +15 -5
- package/src/copy/routeConfig.ts +220 -27
- package/src/generators/generateRouteConfigType.ts +37 -6
- package/src/generators/generateRouter.ts +246 -44
- package/src/generators/generateRouterFastify.ts +100 -28
- package/src/generators/generateRouterHono.ts +104 -36
- package/src/utils/copyFiles.ts +2 -0
|
@@ -11,6 +11,40 @@ function pathExpr(basePath: string, suffix: string): string {
|
|
|
11
11
|
return `\`\${basePath}${suffix}\``
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
function opKindFor(opName: string): string {
|
|
15
|
+
switch (opName) {
|
|
16
|
+
case 'findUnique':
|
|
17
|
+
case 'findUniqueOrThrow':
|
|
18
|
+
return 'readUnique'
|
|
19
|
+
case 'findMany':
|
|
20
|
+
case 'findFirst':
|
|
21
|
+
case 'findFirstOrThrow':
|
|
22
|
+
case 'findManyPaginated':
|
|
23
|
+
case 'count':
|
|
24
|
+
case 'aggregate':
|
|
25
|
+
case 'groupBy':
|
|
26
|
+
return 'read'
|
|
27
|
+
case 'create':
|
|
28
|
+
return 'create'
|
|
29
|
+
case 'createMany':
|
|
30
|
+
case 'createManyAndReturn':
|
|
31
|
+
return 'createMany'
|
|
32
|
+
case 'update':
|
|
33
|
+
return 'update'
|
|
34
|
+
case 'updateMany':
|
|
35
|
+
case 'updateManyAndReturn':
|
|
36
|
+
return 'updateMany'
|
|
37
|
+
case 'upsert':
|
|
38
|
+
return 'upsert'
|
|
39
|
+
case 'delete':
|
|
40
|
+
return 'delete'
|
|
41
|
+
case 'deleteMany':
|
|
42
|
+
return 'deleteMany'
|
|
43
|
+
default:
|
|
44
|
+
return 'noop'
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
14
48
|
function emitReadOp(
|
|
15
49
|
meta: (typeof OPERATION_METADATA)[number],
|
|
16
50
|
modelName: string,
|
|
@@ -18,19 +52,42 @@ function emitReadOp(
|
|
|
18
52
|
const c = meta.name.charAt(0).toUpperCase() + meta.name.slice(1)
|
|
19
53
|
const handlerName = `${modelName}${c}`
|
|
20
54
|
const pathValue = pathExpr('basePath', meta.pathSuffix)
|
|
55
|
+
const opKind = opKindFor(meta.name)
|
|
21
56
|
|
|
22
57
|
const postReadBlock = meta.supportsPostRead
|
|
23
58
|
? ` if (postReadsEnabled) {
|
|
24
59
|
const postPath = ${meta.name === 'findMany' ? "basePath ? `${basePath}/read` : '/read'" : `path`}
|
|
25
|
-
router.post(
|
|
60
|
+
router.post(
|
|
61
|
+
postPath,
|
|
62
|
+
parseBodyAsQuery,
|
|
63
|
+
setShape(opConfig, '${opKind}'),
|
|
64
|
+
...opConfig.operationBefore,
|
|
65
|
+
requireVariantKey(),
|
|
66
|
+
variantBeforeDispatcher(opConfig),
|
|
67
|
+
${handlerName} as RequestHandler,
|
|
68
|
+
variantAfterDispatcher(opConfig),
|
|
69
|
+
...opConfig.operationAfter,
|
|
70
|
+
respond,
|
|
71
|
+
)
|
|
26
72
|
}`
|
|
27
73
|
: ''
|
|
28
74
|
|
|
29
75
|
return ` if (isEnabled(config.${meta.configKey})) {
|
|
30
|
-
const opConfig
|
|
31
|
-
const { before = [], after = [] } = opConfig
|
|
76
|
+
const opConfig = opFor('${meta.configKey}')
|
|
32
77
|
const path = ${pathValue}
|
|
33
|
-
router.get(
|
|
78
|
+
router.get(
|
|
79
|
+
path,
|
|
80
|
+
parseQuery,
|
|
81
|
+
setShape(opConfig, '${opKind}'),
|
|
82
|
+
...opConfig.operationBefore,
|
|
83
|
+
requireVariantKey(),
|
|
84
|
+
variantBeforeDispatcher(opConfig),
|
|
85
|
+
maybeProgressiveSSE(opConfig, core.${meta.coreName}, '${meta.name}'),
|
|
86
|
+
${handlerName} as RequestHandler,
|
|
87
|
+
variantAfterDispatcher(opConfig),
|
|
88
|
+
...opConfig.operationAfter,
|
|
89
|
+
respond,
|
|
90
|
+
)
|
|
34
91
|
${postReadBlock}
|
|
35
92
|
}`
|
|
36
93
|
}
|
|
@@ -43,12 +100,22 @@ function emitWriteOp(
|
|
|
43
100
|
const handlerName = `${modelName}${c}`
|
|
44
101
|
const pathValue = pathExpr('basePath', meta.pathSuffix)
|
|
45
102
|
const respondFn = meta.successStatus === 201 ? 'respondCreated' : 'respond'
|
|
103
|
+
const opKind = opKindFor(meta.name)
|
|
46
104
|
|
|
47
105
|
return ` if (isEnabled(config.${meta.configKey})) {
|
|
48
|
-
const opConfig
|
|
49
|
-
const { before = [], after = [] } = opConfig
|
|
106
|
+
const opConfig = opFor('${meta.configKey}')
|
|
50
107
|
const path = ${pathValue}
|
|
51
|
-
router.${meta.method}(
|
|
108
|
+
router.${meta.method}(
|
|
109
|
+
path,
|
|
110
|
+
setShape(opConfig, '${opKind}'),
|
|
111
|
+
...opConfig.operationBefore,
|
|
112
|
+
requireVariantKey(),
|
|
113
|
+
variantBeforeDispatcher(opConfig),
|
|
114
|
+
${handlerName} as RequestHandler,
|
|
115
|
+
variantAfterDispatcher(opConfig),
|
|
116
|
+
...opConfig.operationAfter,
|
|
117
|
+
${respondFn},
|
|
118
|
+
)
|
|
52
119
|
}`
|
|
53
120
|
}
|
|
54
121
|
|
|
@@ -110,10 +177,19 @@ import type {
|
|
|
110
177
|
import { parseQueryParams } from '../parseQueryParams${ext}'
|
|
111
178
|
import { sanitizeKeys, normalizePrefix, getEnv, isPlainObject } from '../misc${ext}'
|
|
112
179
|
import { buildModelOpenApi } from '../buildModelOpenApi${ext}'
|
|
113
|
-
import {
|
|
180
|
+
import {
|
|
181
|
+
normalizeOperation,
|
|
182
|
+
resolveOperationVariantKey,
|
|
183
|
+
validateCountSourceWhere,
|
|
184
|
+
validateOperationConfig,
|
|
185
|
+
validateUpdateEachConfig,
|
|
186
|
+
} from '../routeConfig${ext}'
|
|
187
|
+
import type { NormalizedOperationConfig } from '../routeConfig${ext}'
|
|
114
188
|
import type { OperationContext } from '../operationRuntime${ext}'
|
|
115
189
|
import { transformResult } from '../operationRuntime${ext}'
|
|
116
190
|
import { HttpError, mapError } from '../errorMapper${ext}'
|
|
191
|
+
import { formatGuardVariantResolutionError } from '../guardVariantError${ext}'
|
|
192
|
+
import type { GuardVariantResolution } from '../guardVariantRouting${ext}'
|
|
117
193
|
import { mergePaginationConfig } from '../pagination${ext}'
|
|
118
194
|
import {
|
|
119
195
|
acceptsEventStream,
|
|
@@ -124,10 +200,8 @@ import {
|
|
|
124
200
|
} from '../sse${ext}'
|
|
125
201
|
import { relationModels } from '../relationModels${ext}'
|
|
126
202
|
import { runAutoIncludeProgressive } from '../autoIncludeRuntime${ext}'
|
|
127
|
-
import {
|
|
128
|
-
|
|
129
|
-
applyProjectionToTarget,
|
|
130
|
-
} from '../projectionDefaults${ext}'
|
|
203
|
+
import { applyDroppedGuard } from '../projectionDefaults${ext}'
|
|
204
|
+
import type { OpKind } from '../projectionDefaults${ext}'
|
|
131
205
|
import { MODEL_FIELDS, MODEL_ENUMS } from './${modelName}Metadata${ext}'
|
|
132
206
|
|
|
133
207
|
${generateRouteConfigType(modelName, 'RequestHandler', guardShapesImport, importStyle, 'express')}
|
|
@@ -139,12 +213,25 @@ const DROP_GUARD = ${dropGuard} || _env.E2E === 'true'
|
|
|
139
213
|
type OperationConfigLike = {
|
|
140
214
|
before?: RequestHandler[]
|
|
141
215
|
after?: RequestHandler[]
|
|
142
|
-
shape?:
|
|
216
|
+
shape?: unknown
|
|
217
|
+
variants?: Record<
|
|
218
|
+
string,
|
|
219
|
+
{
|
|
220
|
+
shape?: unknown
|
|
221
|
+
before?: RequestHandler[]
|
|
222
|
+
after?: RequestHandler[]
|
|
223
|
+
}
|
|
224
|
+
>
|
|
143
225
|
pagination?: Partial<PaginationConfig>
|
|
144
226
|
progressive?: Record<string, ProgressiveVariantConfig>
|
|
145
227
|
progressiveStages?: Record<string, ProgressiveStage<unknown>>
|
|
146
228
|
}
|
|
147
229
|
|
|
230
|
+
type NormalizedOp = NormalizedOperationConfig<RequestHandler, RequestHandler> & {
|
|
231
|
+
progressive?: Record<string, ProgressiveVariantConfig>
|
|
232
|
+
progressiveStages?: Record<string, ProgressiveStage<unknown>>
|
|
233
|
+
}
|
|
234
|
+
|
|
148
235
|
type ExtendedRequest = Request & {
|
|
149
236
|
prisma?: unknown
|
|
150
237
|
postgres?: unknown
|
|
@@ -156,13 +243,20 @@ type LocalsBag = {
|
|
|
156
243
|
routeConfig?: { pagination?: PaginationConfig }
|
|
157
244
|
guardShape?: Record<string, unknown>
|
|
158
245
|
guardCaller?: string
|
|
246
|
+
guardVariantKey?: string
|
|
247
|
+
guardVariantFailure?: Extract<GuardVariantResolution, { ok: false }>
|
|
159
248
|
data?: unknown
|
|
160
249
|
}
|
|
161
250
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
251
|
+
function normalizeExpressOperation(
|
|
252
|
+
config: OperationConfigLike | undefined,
|
|
253
|
+
): NormalizedOp {
|
|
254
|
+
return {
|
|
255
|
+
...normalizeOperation<RequestHandler, RequestHandler>(config),
|
|
256
|
+
progressive: config?.progressive,
|
|
257
|
+
progressiveStages: config?.progressiveStages,
|
|
258
|
+
}
|
|
259
|
+
}
|
|
166
260
|
|
|
167
261
|
function isQueryBuilderEnabled(config: { queryBuilder?: QueryBuilderConfig | false }): boolean {
|
|
168
262
|
if (config.queryBuilder === false) return false
|
|
@@ -181,6 +275,87 @@ function readLocals(res: Response): LocalsBag {
|
|
|
181
275
|
return res.locals as LocalsBag
|
|
182
276
|
}
|
|
183
277
|
|
|
278
|
+
function runHandlerSequence(
|
|
279
|
+
handlers: readonly RequestHandler[],
|
|
280
|
+
req: Request,
|
|
281
|
+
res: Response,
|
|
282
|
+
done: (error?: unknown) => void,
|
|
283
|
+
): void {
|
|
284
|
+
let index = 0
|
|
285
|
+
|
|
286
|
+
const dispatch = (error?: unknown): void => {
|
|
287
|
+
if (error !== undefined) {
|
|
288
|
+
done(error)
|
|
289
|
+
return
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (res.writableEnded || res.destroyed) return
|
|
293
|
+
|
|
294
|
+
const handler = handlers[index++]
|
|
295
|
+
if (!handler) {
|
|
296
|
+
done()
|
|
297
|
+
return
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
let settled = false
|
|
301
|
+
const next = (nextError?: unknown): void => {
|
|
302
|
+
if (settled) return
|
|
303
|
+
settled = true
|
|
304
|
+
dispatch(nextError)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
const result = handler(req, res, next)
|
|
309
|
+
if (result && typeof (result as Promise<unknown>).then === 'function') {
|
|
310
|
+
void (result as Promise<unknown>).catch((promiseError) => {
|
|
311
|
+
if (settled) return
|
|
312
|
+
settled = true
|
|
313
|
+
dispatch(promiseError)
|
|
314
|
+
})
|
|
315
|
+
}
|
|
316
|
+
} catch (handlerError) {
|
|
317
|
+
if (settled) return
|
|
318
|
+
settled = true
|
|
319
|
+
dispatch(handlerError)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
dispatch()
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function variantBeforeDispatcher(opConfig: NormalizedOp): RequestHandler {
|
|
327
|
+
return (req, res, next) => {
|
|
328
|
+
const key = readLocals(res).guardVariantKey
|
|
329
|
+
const hooks =
|
|
330
|
+
key !== undefined
|
|
331
|
+
? (opConfig.variantHooks[key]?.before ?? [])
|
|
332
|
+
: []
|
|
333
|
+
runHandlerSequence(hooks, req, res, next)
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function variantAfterDispatcher(opConfig: NormalizedOp): RequestHandler {
|
|
338
|
+
return (req, res, next) => {
|
|
339
|
+
const key = readLocals(res).guardVariantKey
|
|
340
|
+
const hooks =
|
|
341
|
+
key !== undefined
|
|
342
|
+
? (opConfig.variantHooks[key]?.after ?? [])
|
|
343
|
+
: []
|
|
344
|
+
runHandlerSequence(hooks, req, res, next)
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function requireVariantKey(): RequestHandler {
|
|
349
|
+
return (_req, res, next) => {
|
|
350
|
+
const failure = readLocals(res).guardVariantFailure
|
|
351
|
+
if (!failure) {
|
|
352
|
+
next()
|
|
353
|
+
return
|
|
354
|
+
}
|
|
355
|
+
next(new HttpError(400, formatGuardVariantResolutionError(failure)))
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
184
359
|
export function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(config: ${modelName}RouteConfig<TCtx, TPrisma> = {}) {
|
|
185
360
|
validateCountSourceWhere(config.pagination?.countSource, '${modelName} pagination')
|
|
186
361
|
validateCountSourceWhere(
|
|
@@ -192,6 +367,12 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(config: ${m
|
|
|
192
367
|
|
|
193
368
|
const isEnabled = (value: unknown): boolean => value !== false && !!(config.enableAll || value)
|
|
194
369
|
|
|
370
|
+
const opFor = (key: string): NormalizedOp => {
|
|
371
|
+
const raw = (config as unknown as Record<string, unknown>)[key] as OperationConfigLike | undefined
|
|
372
|
+
validateOperationConfig(raw, '${modelName}.' + key)
|
|
373
|
+
return normalizeExpressOperation(raw)
|
|
374
|
+
}
|
|
375
|
+
|
|
195
376
|
const customPrefix = normalizePrefix(config.customUrlPrefix || '')
|
|
196
377
|
const modelPrefix = config.addModelPrefix !== false ? '/${modelNameLower}' : ''
|
|
197
378
|
const basePath = customPrefix + modelPrefix
|
|
@@ -278,7 +459,7 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(config: ${m
|
|
|
278
459
|
next()
|
|
279
460
|
}
|
|
280
461
|
|
|
281
|
-
const setShape = (opConfig:
|
|
462
|
+
const setShape = (opConfig: NormalizedOp, opKind: OpKind): RequestHandler => {
|
|
282
463
|
return async (req, res, next) => {
|
|
283
464
|
try {
|
|
284
465
|
const locals = readLocals(res)
|
|
@@ -286,35 +467,54 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(config: ${m
|
|
|
286
467
|
if (merged) {
|
|
287
468
|
locals.routeConfig = { pagination: merged }
|
|
288
469
|
}
|
|
470
|
+
|
|
289
471
|
const headerName = config.guard?.variantHeader || 'x-api-variant'
|
|
290
472
|
const headerValue = req.get(headerName)
|
|
291
473
|
const caller = config.guard?.resolveVariant?.(req) ?? headerValue ?? undefined
|
|
292
|
-
if (caller) locals.guardCaller = caller
|
|
293
|
-
|
|
474
|
+
if (typeof caller === 'string') locals.guardCaller = caller
|
|
475
|
+
|
|
476
|
+
const resolution = resolveOperationVariantKey(opConfig.guardRouting, caller)
|
|
477
|
+
if (!resolution.ok) {
|
|
478
|
+
locals.guardVariantFailure = resolution
|
|
479
|
+
next()
|
|
480
|
+
return
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const resolvedKey =
|
|
484
|
+
opConfig.guardRouting.kind === 'named'
|
|
485
|
+
? resolution.key
|
|
486
|
+
: undefined
|
|
487
|
+
if (resolvedKey !== undefined) locals.guardVariantKey = resolvedKey
|
|
488
|
+
|
|
489
|
+
if (opConfig.guardShape) {
|
|
294
490
|
if (!DROP_GUARD) {
|
|
295
|
-
locals.guardShape = opConfig.
|
|
491
|
+
locals.guardShape = opConfig.guardShape
|
|
296
492
|
} else {
|
|
297
|
-
|
|
298
|
-
opConfig.
|
|
299
|
-
|
|
493
|
+
await applyDroppedGuard(
|
|
494
|
+
opConfig.guardShape,
|
|
495
|
+
resolvedKey,
|
|
300
496
|
buildResolveContext(req),
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
497
|
+
opKind,
|
|
498
|
+
{
|
|
499
|
+
readQuery: locals.parsedQuery,
|
|
500
|
+
writeBody: isPlainObject(req.body)
|
|
501
|
+
? (req.body as Record<string, unknown>)
|
|
502
|
+
: undefined,
|
|
503
|
+
},
|
|
504
|
+
() => {
|
|
304
505
|
if (!locals.parsedQuery) locals.parsedQuery = {}
|
|
305
|
-
|
|
306
|
-
}
|
|
506
|
+
return locals.parsedQuery
|
|
507
|
+
},
|
|
508
|
+
() => {
|
|
307
509
|
if (!isPlainObject(req.body)) {
|
|
308
510
|
req.body = {}
|
|
309
511
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
)
|
|
314
|
-
}
|
|
315
|
-
}
|
|
512
|
+
return req.body as Record<string, unknown>
|
|
513
|
+
},
|
|
514
|
+
)
|
|
316
515
|
}
|
|
317
516
|
}
|
|
517
|
+
|
|
318
518
|
next()
|
|
319
519
|
} catch (err) {
|
|
320
520
|
next(mapError(err))
|
|
@@ -323,7 +523,7 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(config: ${m
|
|
|
323
523
|
}
|
|
324
524
|
|
|
325
525
|
const maybeProgressiveSSE = (
|
|
326
|
-
opConfig:
|
|
526
|
+
opConfig: NormalizedOp,
|
|
327
527
|
coreFn: (ctx: OperationContext) => Promise<unknown>,
|
|
328
528
|
baseOp: string,
|
|
329
529
|
): RequestHandler => {
|
|
@@ -333,8 +533,9 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(config: ${m
|
|
|
333
533
|
if (!acceptsEventStream(req.headers.accept)) return next()
|
|
334
534
|
|
|
335
535
|
const locals = readLocals(res)
|
|
336
|
-
const variant = locals.guardCaller
|
|
337
|
-
const progressiveConfig =
|
|
536
|
+
const variant = locals.guardVariantKey ?? locals.guardCaller
|
|
537
|
+
const progressiveConfig =
|
|
538
|
+
variant !== undefined ? opConfig.progressive?.[variant] : undefined
|
|
338
539
|
|
|
339
540
|
try {
|
|
340
541
|
if (!progressiveConfig || progressiveConfig.enabled === false) {
|
|
@@ -462,19 +663,20 @@ ${readOpBlocks}
|
|
|
462
663
|
${writeOpBlocks}
|
|
463
664
|
|
|
464
665
|
if (config.updateEach) {
|
|
465
|
-
const
|
|
466
|
-
|
|
666
|
+
const rawUpdateEach = config.updateEach as unknown as OperationConfigLike
|
|
667
|
+
validateUpdateEachConfig(rawUpdateEach, '${modelName}.updateEach')
|
|
668
|
+
const opConfig = normalizeExpressOperation(rawUpdateEach)
|
|
669
|
+
if (opConfig.operationBefore.length === 0 && _env.NODE_ENV !== 'production') {
|
|
467
670
|
console.warn(
|
|
468
671
|
'[${modelName}Router] updateEach is enabled without a before hook. ' +
|
|
469
672
|
'This endpoint bypasses guard shapes and should be protected by authentication middleware.',
|
|
470
673
|
)
|
|
471
674
|
}
|
|
472
|
-
const { before = [], after = [] } = opConfig
|
|
473
675
|
const path = basePath ? \`\${basePath}/each\` : '/each'
|
|
474
676
|
router.post(
|
|
475
677
|
path,
|
|
476
|
-
setShape(opConfig, '
|
|
477
|
-
...
|
|
678
|
+
setShape(opConfig, 'noop'),
|
|
679
|
+
...opConfig.operationBefore,
|
|
478
680
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
479
681
|
try {
|
|
480
682
|
if (!Array.isArray(req.body)) {
|
|
@@ -487,7 +689,7 @@ ${writeOpBlocks}
|
|
|
487
689
|
next(mapError(err))
|
|
488
690
|
}
|
|
489
691
|
},
|
|
490
|
-
...
|
|
692
|
+
...opConfig.operationAfter,
|
|
491
693
|
respond,
|
|
492
694
|
)
|
|
493
695
|
}
|
|
@@ -63,7 +63,7 @@ function emitReadOp(
|
|
|
63
63
|
: ''
|
|
64
64
|
|
|
65
65
|
return ` if (isEnabled(config.${meta.configKey})) {
|
|
66
|
-
const opConfig
|
|
66
|
+
const opConfig = opFor('${meta.configKey}')
|
|
67
67
|
const path = ${pathValue}
|
|
68
68
|
instance.get(path, handleRead(opConfig, ${handlerName}, parseQueryHook, '${opKind}'))
|
|
69
69
|
${postReadLine}
|
|
@@ -80,7 +80,7 @@ function emitWriteOp(
|
|
|
80
80
|
const opKind = opKindFor(meta.name)
|
|
81
81
|
|
|
82
82
|
return ` if (isEnabled(config.${meta.configKey})) {
|
|
83
|
-
const opConfig
|
|
83
|
+
const opConfig = opFor('${meta.configKey}')
|
|
84
84
|
const path = ${pathValue}
|
|
85
85
|
instance.${meta.method}(path, handleWrite(opConfig, ${handlerName}, '${opKind}'))
|
|
86
86
|
}`
|
|
@@ -133,10 +133,19 @@ import type {
|
|
|
133
133
|
import { parseQueryParams } from '../parseQueryParams${ext}'
|
|
134
134
|
import { sanitizeKeys, normalizePrefix, getEnv, isPlainObject } from '../misc${ext}'
|
|
135
135
|
import { buildModelOpenApi } from '../buildModelOpenApi${ext}'
|
|
136
|
-
import {
|
|
136
|
+
import {
|
|
137
|
+
normalizeOperation,
|
|
138
|
+
resolveOperationVariantKey,
|
|
139
|
+
validateCountSourceWhere,
|
|
140
|
+
validateOperationConfig,
|
|
141
|
+
validateUpdateEachConfig,
|
|
142
|
+
} from '../routeConfig${ext}'
|
|
143
|
+
import type { NormalizedOperationConfig } from '../routeConfig${ext}'
|
|
137
144
|
import type { OperationContext } from '../operationRuntime${ext}'
|
|
138
145
|
import { transformResult } from '../operationRuntime${ext}'
|
|
139
146
|
import { HttpError, mapError } from '../errorMapper${ext}'
|
|
147
|
+
import { formatGuardVariantResolutionError } from '../guardVariantError${ext}'
|
|
148
|
+
import type { GuardVariantResolution } from '../guardVariantRouting${ext}'
|
|
140
149
|
import { mergePaginationConfig } from '../pagination${ext}'
|
|
141
150
|
import { applyDroppedGuard } from '../projectionDefaults${ext}'
|
|
142
151
|
import type { OpKind } from '../projectionDefaults${ext}'
|
|
@@ -151,10 +160,23 @@ const DROP_GUARD = ${dropGuard} || _env.E2E === 'true'
|
|
|
151
160
|
type OperationConfigLike = {
|
|
152
161
|
before?: FastifyHookHandler[]
|
|
153
162
|
after?: FastifyHookHandler[]
|
|
154
|
-
shape?:
|
|
163
|
+
shape?: unknown
|
|
164
|
+
variants?: Record<
|
|
165
|
+
string,
|
|
166
|
+
{
|
|
167
|
+
shape?: unknown
|
|
168
|
+
before?: FastifyHookHandler[]
|
|
169
|
+
after?: FastifyHookHandler[]
|
|
170
|
+
}
|
|
171
|
+
>
|
|
155
172
|
pagination?: Partial<PaginationConfig>
|
|
156
173
|
}
|
|
157
174
|
|
|
175
|
+
type NormalizedOp = NormalizedOperationConfig<
|
|
176
|
+
FastifyHookHandler,
|
|
177
|
+
FastifyHookHandler
|
|
178
|
+
>
|
|
179
|
+
|
|
158
180
|
type FastifyExtended = FastifyRequest & {
|
|
159
181
|
prisma?: unknown
|
|
160
182
|
postgres?: unknown
|
|
@@ -163,14 +185,17 @@ type FastifyExtended = FastifyRequest & {
|
|
|
163
185
|
routeConfig?: { pagination?: PaginationConfig }
|
|
164
186
|
guardShape?: Record<string, unknown>
|
|
165
187
|
guardCaller?: string
|
|
188
|
+
guardVariantKey?: string
|
|
189
|
+
guardVariantFailure?: Extract<GuardVariantResolution, { ok: false }>
|
|
166
190
|
resultData?: unknown
|
|
167
191
|
resultStatus?: number
|
|
168
192
|
}
|
|
169
193
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
194
|
+
function normalizeFastifyOperation(
|
|
195
|
+
config: OperationConfigLike | undefined,
|
|
196
|
+
): NormalizedOp {
|
|
197
|
+
return normalizeOperation<FastifyHookHandler, FastifyHookHandler>(config)
|
|
198
|
+
}
|
|
174
199
|
|
|
175
200
|
function isQueryBuilderEnabled(config: RouteConfig): boolean {
|
|
176
201
|
if (config.queryBuilder === false) return false
|
|
@@ -210,7 +235,7 @@ function buildResolveContext(
|
|
|
210
235
|
|
|
211
236
|
function makeShapeHook(
|
|
212
237
|
config: ${modelName}RouteConfig,
|
|
213
|
-
opConfig:
|
|
238
|
+
opConfig: NormalizedOp,
|
|
214
239
|
opKind: OpKind,
|
|
215
240
|
): (request: FastifyRequest) => Promise<void> {
|
|
216
241
|
return async (request: FastifyRequest) => {
|
|
@@ -223,15 +248,27 @@ function makeShapeHook(
|
|
|
223
248
|
const caller = config.guard?.resolveVariant?.(request)
|
|
224
249
|
?? (Array.isArray(headerValue) ? headerValue[0] : headerValue)
|
|
225
250
|
?? undefined
|
|
226
|
-
if (caller) fx.guardCaller = caller
|
|
251
|
+
if (typeof caller === 'string') fx.guardCaller = caller
|
|
252
|
+
|
|
253
|
+
const resolution = resolveOperationVariantKey(opConfig.guardRouting, caller)
|
|
254
|
+
if (!resolution.ok) {
|
|
255
|
+
fx.guardVariantFailure = resolution
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const resolvedKey =
|
|
260
|
+
opConfig.guardRouting.kind === 'named'
|
|
261
|
+
? resolution.key
|
|
262
|
+
: undefined
|
|
263
|
+
if (resolvedKey !== undefined) fx.guardVariantKey = resolvedKey
|
|
227
264
|
|
|
228
|
-
if (opConfig.
|
|
265
|
+
if (opConfig.guardShape) {
|
|
229
266
|
if (!DROP_GUARD) {
|
|
230
|
-
fx.guardShape = opConfig.
|
|
267
|
+
fx.guardShape = opConfig.guardShape
|
|
231
268
|
} else {
|
|
232
269
|
await applyDroppedGuard(
|
|
233
|
-
opConfig.
|
|
234
|
-
|
|
270
|
+
opConfig.guardShape,
|
|
271
|
+
resolvedKey,
|
|
235
272
|
buildResolveContext(config, request),
|
|
236
273
|
opKind,
|
|
237
274
|
{
|
|
@@ -257,7 +294,7 @@ function makeShapeHook(
|
|
|
257
294
|
}
|
|
258
295
|
|
|
259
296
|
async function runHooks(
|
|
260
|
-
hooks: FastifyHookHandler[],
|
|
297
|
+
hooks: readonly FastifyHookHandler[],
|
|
261
298
|
request: FastifyRequest,
|
|
262
299
|
reply: FastifyReply,
|
|
263
300
|
): Promise<boolean> {
|
|
@@ -298,6 +335,12 @@ export async function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(
|
|
|
298
335
|
await fastify.register(async (instance) => {
|
|
299
336
|
const isEnabled = (value: unknown): boolean => value !== false && !!(config.enableAll || value)
|
|
300
337
|
|
|
338
|
+
const opFor = (key: string): NormalizedOp => {
|
|
339
|
+
const raw = (config as unknown as Record<string, unknown>)[key] as OperationConfigLike | undefined
|
|
340
|
+
validateOperationConfig(raw, '${modelName}.' + key)
|
|
341
|
+
return normalizeFastifyOperation(raw)
|
|
342
|
+
}
|
|
343
|
+
|
|
301
344
|
const customPrefix = normalizePrefix(config.customUrlPrefix || '')
|
|
302
345
|
const modelPrefix = config.addModelPrefix !== false ? '/${modelNameLower}' : ''
|
|
303
346
|
const basePath = customPrefix + modelPrefix
|
|
@@ -375,7 +418,7 @@ export async function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(
|
|
|
375
418
|
}
|
|
376
419
|
|
|
377
420
|
const handleRead = (
|
|
378
|
-
opConfig:
|
|
421
|
+
opConfig: NormalizedOp,
|
|
379
422
|
handlerFn: (req: FastifyRequest, reply: FastifyReply) => Promise<void>,
|
|
380
423
|
parseFn: (req: FastifyRequest) => void,
|
|
381
424
|
opKind: OpKind,
|
|
@@ -383,10 +426,24 @@ export async function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(
|
|
|
383
426
|
try {
|
|
384
427
|
parseFn(request)
|
|
385
428
|
await makeShapeHook(config, opConfig, opKind)(request)
|
|
386
|
-
|
|
387
|
-
|
|
429
|
+
if (await runHooks(opConfig.operationBefore, request, reply)) return
|
|
430
|
+
|
|
431
|
+
const fx = request as FastifyExtended
|
|
432
|
+
if (fx.guardVariantFailure) {
|
|
433
|
+
throw new HttpError(
|
|
434
|
+
400,
|
|
435
|
+
formatGuardVariantResolutionError(fx.guardVariantFailure),
|
|
436
|
+
)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const key = fx.guardVariantKey
|
|
440
|
+
const variantHooks =
|
|
441
|
+
key !== undefined ? opConfig.variantHooks[key] : undefined
|
|
442
|
+
|
|
443
|
+
if (await runHooks(variantHooks?.before ?? [], request, reply)) return
|
|
388
444
|
await handlerFn(request, reply)
|
|
389
|
-
if (await runHooks(after, request, reply)) return
|
|
445
|
+
if (await runHooks(variantHooks?.after ?? [], request, reply)) return
|
|
446
|
+
if (await runHooks(opConfig.operationAfter, request, reply)) return
|
|
390
447
|
sendResult(request, reply)
|
|
391
448
|
} catch (error: unknown) {
|
|
392
449
|
sendError(reply, error)
|
|
@@ -394,16 +451,30 @@ export async function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(
|
|
|
394
451
|
}
|
|
395
452
|
|
|
396
453
|
const handleWrite = (
|
|
397
|
-
opConfig:
|
|
454
|
+
opConfig: NormalizedOp,
|
|
398
455
|
handlerFn: (req: FastifyRequest, reply: FastifyReply) => Promise<void>,
|
|
399
456
|
opKind: OpKind,
|
|
400
457
|
) => async (request: FastifyRequest, reply: FastifyReply) => {
|
|
401
458
|
try {
|
|
402
459
|
await makeShapeHook(config, opConfig, opKind)(request)
|
|
403
|
-
|
|
404
|
-
|
|
460
|
+
if (await runHooks(opConfig.operationBefore, request, reply)) return
|
|
461
|
+
|
|
462
|
+
const fx = request as FastifyExtended
|
|
463
|
+
if (fx.guardVariantFailure) {
|
|
464
|
+
throw new HttpError(
|
|
465
|
+
400,
|
|
466
|
+
formatGuardVariantResolutionError(fx.guardVariantFailure),
|
|
467
|
+
)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const key = fx.guardVariantKey
|
|
471
|
+
const variantHooks =
|
|
472
|
+
key !== undefined ? opConfig.variantHooks[key] : undefined
|
|
473
|
+
|
|
474
|
+
if (await runHooks(variantHooks?.before ?? [], request, reply)) return
|
|
405
475
|
await handlerFn(request, reply)
|
|
406
|
-
if (await runHooks(after, request, reply)) return
|
|
476
|
+
if (await runHooks(variantHooks?.after ?? [], request, reply)) return
|
|
477
|
+
if (await runHooks(opConfig.operationAfter, request, reply)) return
|
|
407
478
|
sendResult(request, reply)
|
|
408
479
|
} catch (error: unknown) {
|
|
409
480
|
sendError(reply, error)
|
|
@@ -415,8 +486,10 @@ ${readOpBlocks}
|
|
|
415
486
|
${writeOpBlocks}
|
|
416
487
|
|
|
417
488
|
if (config.updateEach) {
|
|
418
|
-
const
|
|
419
|
-
|
|
489
|
+
const rawUpdateEach = config.updateEach as unknown as OperationConfigLike
|
|
490
|
+
validateUpdateEachConfig(rawUpdateEach, '${modelName}.updateEach')
|
|
491
|
+
const opConfig = normalizeFastifyOperation(rawUpdateEach)
|
|
492
|
+
if (opConfig.operationBefore.length === 0 && _env.NODE_ENV !== 'production') {
|
|
420
493
|
console.warn(
|
|
421
494
|
'[${modelName}Router] updateEach is enabled without a before hook. ' +
|
|
422
495
|
'This endpoint bypasses guard shapes and should be protected by authentication middleware.',
|
|
@@ -429,10 +502,9 @@ ${writeOpBlocks}
|
|
|
429
502
|
throw new HttpError(400, 'updateEach body must be an array of { where, data } items')
|
|
430
503
|
}
|
|
431
504
|
await makeShapeHook(config, opConfig, 'noop')(request)
|
|
432
|
-
|
|
433
|
-
if (await runHooks(before, request, reply)) return
|
|
505
|
+
if (await runHooks(opConfig.operationBefore, request, reply)) return
|
|
434
506
|
await ${modelName}UpdateEach(request, reply)
|
|
435
|
-
if (await runHooks(
|
|
507
|
+
if (await runHooks(opConfig.operationAfter, request, reply)) return
|
|
436
508
|
sendResult(request, reply)
|
|
437
509
|
} catch (error: unknown) {
|
|
438
510
|
sendError(reply, error)
|