prisma-generator-express 1.62.4 → 1.63.1

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.
@@ -1,3 +1,10 @@
1
+ import { GUARD_SHAPE_CONFIG_KEYS } from './guardHelpers'
2
+ import { isPlainObject } from './misc'
3
+ import {
4
+ resolveGuardVariantKey,
5
+ type GuardVariantResolution,
6
+ } from './guardVariantRouting'
7
+
1
8
  export interface QueryBuilderConfig {
2
9
  enabled?: boolean
3
10
  port?: number
@@ -87,23 +94,43 @@ export type ProgressiveVariantConfig =
87
94
  | ManualProgressiveVariantConfig
88
95
  | AutoIncludeProgressiveVariantConfig
89
96
 
90
- export interface BaseOperationConfig<HookHandler, TShape = Record<string, unknown>> {
91
- before?: HookHandler[]
92
- after?: HookHandler[]
93
- shape?: TShape
97
+ export type VariantEntry<TShape, TBefore, TAfter> = {
98
+ shape: TShape
99
+ before?: TBefore[]
100
+ after?: TAfter[]
101
+ }
102
+
103
+ type OperationShapeConfig<TShape, TBefore, TAfter> =
104
+ | {
105
+ shape?: TShape
106
+ variants?: never
107
+ }
108
+ | {
109
+ shape?: never
110
+ variants: Record<string, VariantEntry<TShape, TBefore, TAfter>>
111
+ }
112
+
113
+ export type BaseOperationConfig<
114
+ TBefore,
115
+ TShape = Record<string, unknown>,
116
+ TAfter = TBefore,
117
+ > = OperationShapeConfig<TShape, TBefore, TAfter> & {
118
+ before?: TBefore[]
119
+ after?: TAfter[]
94
120
  pagination?: Partial<PaginationConfig>
95
121
  }
96
122
 
97
- export interface BaseUpdateEachConfig<HookHandler> {
98
- before?: HookHandler[]
99
- after?: HookHandler[]
123
+ export interface BaseUpdateEachConfig<TBefore, TAfter = TBefore> {
124
+ before?: TBefore[]
125
+ after?: TAfter[]
100
126
  }
101
127
 
102
128
  export interface BaseRouteConfig<
103
- HookHandler,
129
+ TBefore,
104
130
  RequestType,
105
131
  TShape = Record<string, unknown>,
106
132
  TCtx = unknown,
133
+ TAfter = TBefore,
107
134
  > {
108
135
  enableAll?: boolean
109
136
  addModelPrefix?: boolean
@@ -125,30 +152,196 @@ export interface BaseRouteConfig<
125
152
  resolveContext?: (request: RequestType) => TCtx | Promise<TCtx>
126
153
  queryBuilder?: QueryBuilderConfig | false
127
154
  pagination?: PaginationConfig
128
- findUnique?: BaseOperationConfig<HookHandler, TShape> | false
129
- findUniqueOrThrow?: BaseOperationConfig<HookHandler, TShape> | false
130
- findFirst?: BaseOperationConfig<HookHandler, TShape> | false
131
- findFirstOrThrow?: BaseOperationConfig<HookHandler, TShape> | false
132
- findMany?: BaseOperationConfig<HookHandler, TShape> | false
133
- findManyPaginated?: BaseOperationConfig<HookHandler, TShape> | false
134
- create?: BaseOperationConfig<HookHandler, TShape> | false
135
- createMany?: BaseOperationConfig<HookHandler, TShape> | false
136
- createManyAndReturn?: BaseOperationConfig<HookHandler, TShape> | false
137
- update?: BaseOperationConfig<HookHandler, TShape> | false
138
- updateMany?: BaseOperationConfig<HookHandler, TShape> | false
139
- updateManyAndReturn?: BaseOperationConfig<HookHandler, TShape> | false
140
- upsert?: BaseOperationConfig<HookHandler, TShape> | false
141
- delete?: BaseOperationConfig<HookHandler, TShape> | false
142
- deleteMany?: BaseOperationConfig<HookHandler, TShape> | false
143
- updateEach?: BaseUpdateEachConfig<HookHandler>
144
- aggregate?: BaseOperationConfig<HookHandler, TShape> | false
145
- count?: BaseOperationConfig<HookHandler, TShape> | false
146
- groupBy?: BaseOperationConfig<HookHandler, TShape> | false
155
+ findUnique?: BaseOperationConfig<TBefore, TShape, TAfter> | false
156
+ findUniqueOrThrow?: BaseOperationConfig<TBefore, TShape, TAfter> | false
157
+ findFirst?: BaseOperationConfig<TBefore, TShape, TAfter> | false
158
+ findFirstOrThrow?: BaseOperationConfig<TBefore, TShape, TAfter> | false
159
+ findMany?: BaseOperationConfig<TBefore, TShape, TAfter> | false
160
+ findManyPaginated?: BaseOperationConfig<TBefore, TShape, TAfter> | false
161
+ create?: BaseOperationConfig<TBefore, TShape, TAfter> | false
162
+ createMany?: BaseOperationConfig<TBefore, TShape, TAfter> | false
163
+ createManyAndReturn?: BaseOperationConfig<TBefore, TShape, TAfter> | false
164
+ update?: BaseOperationConfig<TBefore, TShape, TAfter> | false
165
+ updateMany?: BaseOperationConfig<TBefore, TShape, TAfter> | false
166
+ updateManyAndReturn?: BaseOperationConfig<TBefore, TShape, TAfter> | false
167
+ upsert?: BaseOperationConfig<TBefore, TShape, TAfter> | false
168
+ delete?: BaseOperationConfig<TBefore, TShape, TAfter> | false
169
+ deleteMany?: BaseOperationConfig<TBefore, TShape, TAfter> | false
170
+ updateEach?: BaseUpdateEachConfig<TBefore, TAfter>
171
+ aggregate?: BaseOperationConfig<TBefore, TShape, TAfter> | false
172
+ count?: BaseOperationConfig<TBefore, TShape, TAfter> | false
173
+ groupBy?: BaseOperationConfig<TBefore, TShape, TAfter> | false
147
174
  }
148
175
 
149
176
  export type OperationConfig = BaseOperationConfig<unknown>
150
177
  export type RouteConfig = BaseRouteConfig<unknown, unknown>
151
178
 
179
+ export type NormalizedGuardRouting =
180
+ | { kind: 'none' }
181
+ | { kind: 'single' }
182
+ | { kind: 'named'; keys: readonly string[] }
183
+
184
+ export type NormalizedVariantHooks<TBefore, TAfter> = Readonly<
185
+ Record<
186
+ string,
187
+ {
188
+ before: readonly TBefore[]
189
+ after: readonly TAfter[]
190
+ }
191
+ >
192
+ >
193
+
194
+ export interface NormalizedOperationConfig<TBefore, TAfter> {
195
+ guardShape?: Record<string, unknown>
196
+ guardRouting: NormalizedGuardRouting
197
+ operationBefore: readonly TBefore[]
198
+ operationAfter: readonly TAfter[]
199
+ variantHooks: NormalizedVariantHooks<TBefore, TAfter>
200
+ pagination?: Readonly<Partial<PaginationConfig>>
201
+ }
202
+
203
+ type OperationConfigInput<TBefore, TAfter> = {
204
+ before?: TBefore[]
205
+ after?: TAfter[]
206
+ shape?: unknown
207
+ variants?: Record<
208
+ string,
209
+ {
210
+ shape?: unknown
211
+ before?: TBefore[]
212
+ after?: TAfter[]
213
+ }
214
+ >
215
+ pagination?: Partial<PaginationConfig>
216
+ }
217
+
218
+ function classifyGuardRouting(shape: unknown): NormalizedGuardRouting {
219
+ if (shape === undefined) return { kind: 'none' }
220
+ if (typeof shape === 'function') return { kind: 'single' }
221
+ if (!isPlainObject(shape)) return { kind: 'single' }
222
+
223
+ const keys = Object.keys(shape)
224
+ const isSingle =
225
+ keys.length === 0 ||
226
+ keys.every((key) => GUARD_SHAPE_CONFIG_KEYS.has(key))
227
+
228
+ return isSingle ? { kind: 'single' } : { kind: 'named', keys }
229
+ }
230
+
231
+ export function validateOperationConfig(
232
+ config: { shape?: unknown; variants?: unknown } | undefined,
233
+ location: string,
234
+ ): void {
235
+ if (!config) return
236
+
237
+ const hasShape = config.shape !== undefined
238
+ const hasVariants = config.variants !== undefined
239
+
240
+ if (hasShape && hasVariants) {
241
+ throw new Error(location + ': shape and variants cannot both be defined')
242
+ }
243
+
244
+ if (!hasVariants) return
245
+
246
+ const variants = config.variants
247
+ if (!isPlainObject(variants)) {
248
+ throw new Error(location + ': variants must be a non-array object')
249
+ }
250
+
251
+ const entries = Object.entries(variants)
252
+ if (entries.length === 0) {
253
+ throw new Error(location + ': variants must contain at least one entry')
254
+ }
255
+
256
+ for (const [key, rawEntry] of entries) {
257
+ if (GUARD_SHAPE_CONFIG_KEYS.has(key)) {
258
+ throw new Error(
259
+ location + ': variant name "' + key +
260
+ '" collides with a reserved guard shape key',
261
+ )
262
+ }
263
+
264
+ if (!isPlainObject(rawEntry)) {
265
+ throw new Error(
266
+ location + ': variant "' + key + '" must be an object with a shape',
267
+ )
268
+ }
269
+
270
+ if (rawEntry.shape === undefined) {
271
+ throw new Error(
272
+ location + ': variant "' + key + '" is missing "shape"',
273
+ )
274
+ }
275
+ }
276
+ }
277
+
278
+ export function validateUpdateEachConfig(
279
+ config: { variants?: unknown } | undefined,
280
+ location: string,
281
+ ): void {
282
+ if (config?.variants !== undefined) {
283
+ throw new Error(location + ': updateEach does not support variants')
284
+ }
285
+ }
286
+
287
+ export function normalizeOperation<TBefore, TAfter>(
288
+ config: OperationConfigInput<TBefore, TAfter> | undefined,
289
+ ): NormalizedOperationConfig<TBefore, TAfter> {
290
+ const operationBefore = config?.before ?? []
291
+ const operationAfter = config?.after ?? []
292
+
293
+ if (config?.variants !== undefined) {
294
+ const entries = Object.entries(config.variants)
295
+
296
+ return {
297
+ guardShape: Object.fromEntries(
298
+ entries.map(([key, entry]) => [key, entry.shape]),
299
+ ),
300
+ guardRouting: {
301
+ kind: 'named',
302
+ keys: entries.map(([key]) => key),
303
+ },
304
+ operationBefore,
305
+ operationAfter,
306
+ variantHooks: Object.fromEntries(
307
+ entries.map(([key, entry]) => [
308
+ key,
309
+ {
310
+ before: entry.before ?? [],
311
+ after: entry.after ?? [],
312
+ },
313
+ ]),
314
+ ),
315
+ pagination: config.pagination,
316
+ }
317
+ }
318
+
319
+ return {
320
+ guardShape: config?.shape as Record<string, unknown> | undefined,
321
+ guardRouting: classifyGuardRouting(config?.shape),
322
+ operationBefore,
323
+ operationAfter,
324
+ variantHooks: {},
325
+ pagination: config?.pagination,
326
+ }
327
+ }
328
+
329
+ export function resolveOperationVariantKey(
330
+ routing: NormalizedGuardRouting,
331
+ caller: string | undefined,
332
+ ): GuardVariantResolution {
333
+ if (routing.kind === 'none' || routing.kind === 'single') {
334
+ return resolveGuardVariantKey({ kind: 'single' })
335
+ }
336
+
337
+ return resolveGuardVariantKey({
338
+ kind: 'named',
339
+ keys: routing.keys,
340
+ caller,
341
+ reservedKeys: GUARD_SHAPE_CONFIG_KEYS,
342
+ })
343
+ }
344
+
152
345
  export function validateCountSourceWhere(
153
346
  cs: PaginationCountSource | undefined,
154
347
  location: string,
@@ -74,25 +74,55 @@ export function generateRouteConfigType(
74
74
 
75
75
  const shapeOps = Array.from(new Set(ROUTER_OPERATIONS))
76
76
  const opShapeImports = shapeOps
77
- .map((op) => `${m}${capitalize(op)}ShapeInput`)
77
+ .flatMap((op) => {
78
+ const prefix = `${m}${capitalize(op)}Shape`
79
+ return [prefix, `${prefix}Input`]
80
+ })
78
81
  .join(',\n ')
79
82
 
83
+ const shapeOrFnAliases = shapeOps
84
+ .map((op) => {
85
+ const prefix = `${m}${capitalize(op)}Shape`
86
+ return (
87
+ `type ${prefix}OrFn<TCtx = unknown> =\n` +
88
+ ` | ${prefix}\n` +
89
+ ` | ((ctx: TCtx) => ${prefix})`
90
+ )
91
+ })
92
+ .join('\n\n')
93
+
80
94
  const overrides = ROUTER_OPERATIONS.map((routerOp) => {
81
95
  const c = capitalize(routerOp)
82
96
  const isRead = READ_OPERATION_NAMES.has(routerOp)
83
- const lines = [
97
+ const commonLines = [
84
98
  ` before?: ${beforeRef}[]`,
85
99
  ` after?: ${afterRef}[]`,
86
- ` shape?: ${m}${c}ShapeInput<TCtx>`,
87
100
  ` pagination?: Partial<PaginationConfig>`,
88
101
  ]
102
+
89
103
  if (isRead && supportsProgressive) {
90
- lines.push(` progressive?: Record<string, ProgressiveVariantConfig>`)
91
- lines.push(
104
+ commonLines.push(
105
+ ` progressive?: Record<string, ProgressiveVariantConfig>`,
106
+ )
107
+ commonLines.push(
92
108
  ` progressiveStages?: Record<string, ProgressiveStage<TCtx, TPrisma>>`,
93
109
  )
94
110
  }
95
- return ` ${routerOp}?: {\n${lines.join('\n')}\n } | false`
111
+
112
+ const commonConfig = `{\n${commonLines.join('\n')}\n }`
113
+ const variantsConfig =
114
+ `Record<string, {\n` +
115
+ ` shape: ${m}${c}ShapeOrFn<TCtx>\n` +
116
+ ` before?: ${beforeRef}[]\n` +
117
+ ` after?: ${afterRef}[]\n` +
118
+ ` }>`
119
+
120
+ return (
121
+ ` ${routerOp}?: (${commonConfig} & (\n` +
122
+ ` | { shape?: ${m}${c}ShapeInput<TCtx>; variants?: never }\n` +
123
+ ` | { shape?: never; variants: ${variantsConfig} }\n` +
124
+ ` )) | false`
125
+ )
96
126
  }).join('\n')
97
127
 
98
128
  const omitKeys = ROUTER_OPERATIONS.map((k) => `'${k}'`).join('\n | ')
@@ -100,6 +130,7 @@ export function generateRouteConfigType(
100
130
  return (
101
131
  progressiveTypeImport +
102
132
  `import type {\n ${opShapeImports}\n} from '${guardShapesImport}${ext}'\n\n` +
133
+ `${shapeOrFnAliases}\n\n` +
103
134
  `export type ${m}RouteConfig${generics} = Omit<\n` +
104
135
  ` ${baseConfig},\n` +
105
136
  ` | ${omitKeys}\n` +
@@ -57,15 +57,37 @@ function emitReadOp(
57
57
  const postReadBlock = meta.supportsPostRead
58
58
  ? ` if (postReadsEnabled) {
59
59
  const postPath = ${meta.name === 'findMany' ? "basePath ? `${basePath}/read` : '/read'" : `path`}
60
- router.post(postPath, parseBodyAsQuery, setShape(opConfig, '${opKind}'), ...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
+ )
61
72
  }`
62
73
  : ''
63
74
 
64
75
  return ` if (isEnabled(config.${meta.configKey})) {
65
- const opConfig: OperationConfigLike = (config.${meta.configKey} as OperationConfigLike | undefined) ?? defaultOpConfig
66
- const { before = [], after = [] } = opConfig
76
+ const opConfig = opFor('${meta.configKey}')
67
77
  const path = ${pathValue}
68
- router.get(path, parseQuery, setShape(opConfig, '${opKind}'), ...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
+ )
69
91
  ${postReadBlock}
70
92
  }`
71
93
  }
@@ -81,10 +103,19 @@ function emitWriteOp(
81
103
  const opKind = opKindFor(meta.name)
82
104
 
83
105
  return ` if (isEnabled(config.${meta.configKey})) {
84
- const opConfig: OperationConfigLike = (config.${meta.configKey} as OperationConfigLike | undefined) ?? defaultOpConfig
85
- const { before = [], after = [] } = opConfig
106
+ const opConfig = opFor('${meta.configKey}')
86
107
  const path = ${pathValue}
87
- router.${meta.method}(path, setShape(opConfig, '${opKind}'), ...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
+ )
88
119
  }`
89
120
  }
90
121
 
@@ -146,10 +177,19 @@ import type {
146
177
  import { parseQueryParams } from '../parseQueryParams${ext}'
147
178
  import { sanitizeKeys, normalizePrefix, getEnv, isPlainObject } from '../misc${ext}'
148
179
  import { buildModelOpenApi } from '../buildModelOpenApi${ext}'
149
- 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}'
150
188
  import type { OperationContext } from '../operationRuntime${ext}'
151
189
  import { transformResult } from '../operationRuntime${ext}'
152
190
  import { HttpError, mapError } from '../errorMapper${ext}'
191
+ import { formatGuardVariantResolutionError } from '../guardVariantError${ext}'
192
+ import type { GuardVariantResolution } from '../guardVariantRouting${ext}'
153
193
  import { mergePaginationConfig } from '../pagination${ext}'
154
194
  import {
155
195
  acceptsEventStream,
@@ -173,12 +213,25 @@ const DROP_GUARD = ${dropGuard} || _env.E2E === 'true'
173
213
  type OperationConfigLike = {
174
214
  before?: RequestHandler[]
175
215
  after?: RequestHandler[]
176
- shape?: Record<string, unknown>
216
+ shape?: unknown
217
+ variants?: Record<
218
+ string,
219
+ {
220
+ shape?: unknown
221
+ before?: RequestHandler[]
222
+ after?: RequestHandler[]
223
+ }
224
+ >
177
225
  pagination?: Partial<PaginationConfig>
178
226
  progressive?: Record<string, ProgressiveVariantConfig>
179
227
  progressiveStages?: Record<string, ProgressiveStage<unknown>>
180
228
  }
181
229
 
230
+ type NormalizedOp = NormalizedOperationConfig<RequestHandler, RequestHandler> & {
231
+ progressive?: Record<string, ProgressiveVariantConfig>
232
+ progressiveStages?: Record<string, ProgressiveStage<unknown>>
233
+ }
234
+
182
235
  type ExtendedRequest = Request & {
183
236
  prisma?: unknown
184
237
  postgres?: unknown
@@ -190,13 +243,20 @@ type LocalsBag = {
190
243
  routeConfig?: { pagination?: PaginationConfig }
191
244
  guardShape?: Record<string, unknown>
192
245
  guardCaller?: string
246
+ guardVariantKey?: string
247
+ guardVariantFailure?: Extract<GuardVariantResolution, { ok: false }>
193
248
  data?: unknown
194
249
  }
195
250
 
196
- const defaultOpConfig: OperationConfigLike = Object.freeze({
197
- before: Object.freeze([]) as unknown as RequestHandler[],
198
- after: Object.freeze([]) as unknown as RequestHandler[],
199
- })
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
+ }
200
260
 
201
261
  function isQueryBuilderEnabled(config: { queryBuilder?: QueryBuilderConfig | false }): boolean {
202
262
  if (config.queryBuilder === false) return false
@@ -215,6 +275,91 @@ function readLocals(res: Response): LocalsBag {
215
275
  return res.locals as LocalsBag
216
276
  }
217
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: unknown = handler(req, res, next)
309
+ if (
310
+ typeof result === 'object' &&
311
+ result !== null &&
312
+ typeof (result as Promise<unknown>).then === 'function'
313
+ ) {
314
+ void (result as Promise<unknown>).catch((promiseError) => {
315
+ if (settled) return
316
+ settled = true
317
+ dispatch(promiseError)
318
+ })
319
+ }
320
+ } catch (handlerError) {
321
+ if (settled) return
322
+ settled = true
323
+ dispatch(handlerError)
324
+ }
325
+ }
326
+
327
+ dispatch()
328
+ }
329
+
330
+ function variantBeforeDispatcher(opConfig: NormalizedOp): RequestHandler {
331
+ return (req, res, next) => {
332
+ const key = readLocals(res).guardVariantKey
333
+ const hooks =
334
+ key !== undefined
335
+ ? (opConfig.variantHooks[key]?.before ?? [])
336
+ : []
337
+ runHandlerSequence(hooks, req, res, next)
338
+ }
339
+ }
340
+
341
+ function variantAfterDispatcher(opConfig: NormalizedOp): RequestHandler {
342
+ return (req, res, next) => {
343
+ const key = readLocals(res).guardVariantKey
344
+ const hooks =
345
+ key !== undefined
346
+ ? (opConfig.variantHooks[key]?.after ?? [])
347
+ : []
348
+ runHandlerSequence(hooks, req, res, next)
349
+ }
350
+ }
351
+
352
+ function requireVariantKey(): RequestHandler {
353
+ return (_req, res, next) => {
354
+ const failure = readLocals(res).guardVariantFailure
355
+ if (!failure) {
356
+ next()
357
+ return
358
+ }
359
+ next(new HttpError(400, formatGuardVariantResolutionError(failure)))
360
+ }
361
+ }
362
+
218
363
  export function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(config: ${modelName}RouteConfig<TCtx, TPrisma> = {}) {
219
364
  validateCountSourceWhere(config.pagination?.countSource, '${modelName} pagination')
220
365
  validateCountSourceWhere(
@@ -226,6 +371,12 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(config: ${m
226
371
 
227
372
  const isEnabled = (value: unknown): boolean => value !== false && !!(config.enableAll || value)
228
373
 
374
+ const opFor = (key: string): NormalizedOp => {
375
+ const raw = (config as unknown as Record<string, unknown>)[key] as OperationConfigLike | undefined
376
+ validateOperationConfig(raw, '${modelName}.' + key)
377
+ return normalizeExpressOperation(raw)
378
+ }
379
+
229
380
  const customPrefix = normalizePrefix(config.customUrlPrefix || '')
230
381
  const modelPrefix = config.addModelPrefix !== false ? '/${modelNameLower}' : ''
231
382
  const basePath = customPrefix + modelPrefix
@@ -312,7 +463,7 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(config: ${m
312
463
  next()
313
464
  }
314
465
 
315
- const setShape = (opConfig: OperationConfigLike, opKind: OpKind): RequestHandler => {
466
+ const setShape = (opConfig: NormalizedOp, opKind: OpKind): RequestHandler => {
316
467
  return async (req, res, next) => {
317
468
  try {
318
469
  const locals = readLocals(res)
@@ -320,17 +471,32 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(config: ${m
320
471
  if (merged) {
321
472
  locals.routeConfig = { pagination: merged }
322
473
  }
474
+
323
475
  const headerName = config.guard?.variantHeader || 'x-api-variant'
324
476
  const headerValue = req.get(headerName)
325
477
  const caller = config.guard?.resolveVariant?.(req) ?? headerValue ?? undefined
326
- if (caller) locals.guardCaller = caller
327
- if (opConfig.shape) {
478
+ if (typeof caller === 'string') locals.guardCaller = caller
479
+
480
+ const resolution = resolveOperationVariantKey(opConfig.guardRouting, caller)
481
+ if (!resolution.ok) {
482
+ locals.guardVariantFailure = resolution
483
+ next()
484
+ return
485
+ }
486
+
487
+ const resolvedKey =
488
+ opConfig.guardRouting.kind === 'named'
489
+ ? resolution.key
490
+ : undefined
491
+ if (resolvedKey !== undefined) locals.guardVariantKey = resolvedKey
492
+
493
+ if (opConfig.guardShape) {
328
494
  if (!DROP_GUARD) {
329
- locals.guardShape = opConfig.shape
495
+ locals.guardShape = opConfig.guardShape
330
496
  } else {
331
497
  await applyDroppedGuard(
332
- opConfig.shape,
333
- caller,
498
+ opConfig.guardShape,
499
+ resolvedKey,
334
500
  buildResolveContext(req),
335
501
  opKind,
336
502
  {
@@ -352,6 +518,7 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(config: ${m
352
518
  )
353
519
  }
354
520
  }
521
+
355
522
  next()
356
523
  } catch (err) {
357
524
  next(mapError(err))
@@ -360,7 +527,7 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(config: ${m
360
527
  }
361
528
 
362
529
  const maybeProgressiveSSE = (
363
- opConfig: OperationConfigLike,
530
+ opConfig: NormalizedOp,
364
531
  coreFn: (ctx: OperationContext) => Promise<unknown>,
365
532
  baseOp: string,
366
533
  ): RequestHandler => {
@@ -370,8 +537,9 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(config: ${m
370
537
  if (!acceptsEventStream(req.headers.accept)) return next()
371
538
 
372
539
  const locals = readLocals(res)
373
- const variant = locals.guardCaller
374
- const progressiveConfig = variant ? opConfig.progressive?.[variant] : undefined
540
+ const variant = locals.guardVariantKey ?? locals.guardCaller
541
+ const progressiveConfig =
542
+ variant !== undefined ? opConfig.progressive?.[variant] : undefined
375
543
 
376
544
  try {
377
545
  if (!progressiveConfig || progressiveConfig.enabled === false) {
@@ -499,19 +667,20 @@ ${readOpBlocks}
499
667
  ${writeOpBlocks}
500
668
 
501
669
  if (config.updateEach) {
502
- const opConfig: OperationConfigLike = (config.updateEach as OperationConfigLike | undefined) ?? defaultOpConfig
503
- if ((!opConfig.before || opConfig.before.length === 0) && _env.NODE_ENV !== 'production') {
670
+ const rawUpdateEach = config.updateEach as unknown as OperationConfigLike
671
+ validateUpdateEachConfig(rawUpdateEach, '${modelName}.updateEach')
672
+ const opConfig = normalizeExpressOperation(rawUpdateEach)
673
+ if (opConfig.operationBefore.length === 0 && _env.NODE_ENV !== 'production') {
504
674
  console.warn(
505
675
  '[${modelName}Router] updateEach is enabled without a before hook. ' +
506
676
  'This endpoint bypasses guard shapes and should be protected by authentication middleware.',
507
677
  )
508
678
  }
509
- const { before = [], after = [] } = opConfig
510
679
  const path = basePath ? \`\${basePath}/each\` : '/each'
511
680
  router.post(
512
681
  path,
513
682
  setShape(opConfig, 'noop'),
514
- ...before,
683
+ ...opConfig.operationBefore,
515
684
  async (req: Request, res: Response, next: NextFunction) => {
516
685
  try {
517
686
  if (!Array.isArray(req.body)) {
@@ -524,7 +693,7 @@ ${writeOpBlocks}
524
693
  next(mapError(err))
525
694
  }
526
695
  },
527
- ...after,
696
+ ...opConfig.operationAfter,
528
697
  respond,
529
698
  )
530
699
  }