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.
@@ -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(postPath, parseBodyAsQuery, setShape(opConfig, 'read'), ...before, ${handlerName} as RequestHandler, ...after, respond)
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: OperationConfigLike = (config.${meta.configKey} as OperationConfigLike | undefined) ?? defaultOpConfig
31
- const { before = [], after = [] } = opConfig
76
+ const opConfig = opFor('${meta.configKey}')
32
77
  const path = ${pathValue}
33
- router.get(path, parseQuery, setShape(opConfig, 'read'), ...before, maybeProgressiveSSE(opConfig, core.${meta.coreName}, '${meta.name}'), ${handlerName} as RequestHandler, ...after, respond)
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: OperationConfigLike = (config.${meta.configKey} as OperationConfigLike | undefined) ?? defaultOpConfig
49
- const { before = [], after = [] } = opConfig
106
+ const opConfig = opFor('${meta.configKey}')
50
107
  const path = ${pathValue}
51
- router.${meta.method}(path, setShape(opConfig, 'write'), ...before, ${handlerName} as RequestHandler, ...after, ${respondFn})
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 { validateCountSourceWhere } from '../routeConfig${ext}'
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
- resolveDroppedGuardProjection,
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?: Record<string, unknown>
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
- const defaultOpConfig: OperationConfigLike = Object.freeze({
163
- before: Object.freeze([]) as unknown as RequestHandler[],
164
- after: Object.freeze([]) as unknown as RequestHandler[],
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: OperationConfigLike, kind: 'read' | 'write'): RequestHandler => {
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
- if (opConfig.shape) {
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.shape
491
+ locals.guardShape = opConfig.guardShape
296
492
  } else {
297
- const projection = await resolveDroppedGuardProjection(
298
- opConfig.shape,
299
- caller,
493
+ await applyDroppedGuard(
494
+ opConfig.guardShape,
495
+ resolvedKey,
300
496
  buildResolveContext(req),
301
- )
302
- if (projection) {
303
- if (kind === 'read') {
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
- applyProjectionToTarget(locals.parsedQuery, projection)
306
- } else {
506
+ return locals.parsedQuery
507
+ },
508
+ () => {
307
509
  if (!isPlainObject(req.body)) {
308
510
  req.body = {}
309
511
  }
310
- applyProjectionToTarget(
311
- req.body as Record<string, unknown>,
312
- projection,
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: OperationConfigLike,
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 = variant ? opConfig.progressive?.[variant] : undefined
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 opConfig: OperationConfigLike = (config.updateEach as OperationConfigLike | undefined) ?? defaultOpConfig
466
- if ((!opConfig.before || opConfig.before.length === 0) && _env.NODE_ENV !== 'production') {
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, 'write'),
477
- ...before,
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
- ...after,
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: OperationConfigLike = (config.${meta.configKey} as OperationConfigLike | undefined) ?? defaultOpConfig
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: OperationConfigLike = (config.${meta.configKey} as OperationConfigLike | undefined) ?? defaultOpConfig
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 { validateCountSourceWhere } from '../routeConfig${ext}'
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?: Record<string, unknown>
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
- const defaultOpConfig: OperationConfigLike = Object.freeze({
171
- before: Object.freeze([]) as unknown as FastifyHookHandler[],
172
- after: Object.freeze([]) as unknown as FastifyHookHandler[],
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: OperationConfigLike,
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.shape) {
265
+ if (opConfig.guardShape) {
229
266
  if (!DROP_GUARD) {
230
- fx.guardShape = opConfig.shape
267
+ fx.guardShape = opConfig.guardShape
231
268
  } else {
232
269
  await applyDroppedGuard(
233
- opConfig.shape,
234
- caller,
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: OperationConfigLike,
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
- const { before = [], after = [] } = opConfig
387
- if (await runHooks(before, request, reply)) return
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: OperationConfigLike,
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
- const { before = [], after = [] } = opConfig
404
- if (await runHooks(before, request, reply)) return
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 opConfig: OperationConfigLike = (config.updateEach as OperationConfigLike | undefined) ?? defaultOpConfig
419
- if ((!opConfig.before || opConfig.before.length === 0) && _env.NODE_ENV !== 'production') {
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
- const { before = [], after = [] } = opConfig
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(after, request, reply)) return
507
+ if (await runHooks(opConfig.operationAfter, request, reply)) return
436
508
  sendResult(request, reply)
437
509
  } catch (error: unknown) {
438
510
  sendError(reply, error)