prisma-generator-express 1.64.2 → 1.64.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.
@@ -107,6 +107,91 @@ export type RouteConfig<
107
107
  >,
108
108
  HonoOpKeys
109
109
  > & {
110
+ /**
111
+ * SEVEN INDEPENDENT GUARD CONTROLS. Every default reproduces the public
112
+ * package's pre-1.64.2 behaviour exactly.
113
+ *
114
+ * 1.64.2 shipped these behaviours ON by default, which was a breaking change
115
+ * to a published package. The first correction put all seven behind one
116
+ * `requireGuard` switch, which was also wrong: they are unrelated decisions,
117
+ * and bundling them means a consumer who wants a missing-guard refusal is also
118
+ * forced into a hook-ordering change and the removal of a route they may use.
119
+ *
120
+ * So each is its own option. An unset option means the upstream default — never
121
+ * an opinionated value chosen on the consumer's behalf. `HARDENED_GUARD_PROFILE`
122
+ * selects all seven together for those who want them, but it is a set of values
123
+ * to spread, not a mode: what gets stored is the individual settings.
124
+ *
125
+ * `GUARD_OPTION_METADATA` describes these machine-readably, so a UI can render
126
+ * them without a hand-maintained second copy of the defaults.
127
+ */
128
+
129
+ /**
130
+ * Refuse an operation that configures no guard at all. **Default `false`.**
131
+ *
132
+ * With it off — the upstream default — an unconfigured operation is emitted and
133
+ * passes the caller's own where/select/include straight to Prisma. On, router
134
+ * construction throws, which on a Worker is boot.
135
+ */
136
+ requireGuardShape?: boolean
137
+
138
+ /**
139
+ * Validate that a shape constrains something. **Default `false`.**
140
+ *
141
+ * On, `shape: {}` and a shape mixing guard keys with non-guard keys (usually a
142
+ * typo: `wheer`, `iclude`) are refused, at the top level and inside variants.
143
+ */
144
+ validateGuardShapes?: boolean
145
+
146
+ /**
147
+ * Require `allowDefaultVariant: true` on an operation carrying a variant named
148
+ * `default`. **Default `false`.**
149
+ *
150
+ * A `default` variant answers every unrecognised, blank and missing caller.
151
+ * That is safe when it is the most restrictive variant and dangerous when it is
152
+ * the most permissive, and nothing can tell which from the shape.
153
+ */
154
+ requireDefaultVariantOptIn?: boolean
155
+
156
+ /**
157
+ * Register the `updateEach` batch route. **Default `true`.**
158
+ *
159
+ * It bypasses guard shapes by design — a batch of `{ where, data }` applied
160
+ * directly — and is protected only by a development-only warning. Set `false`
161
+ * to refuse it at router construction.
162
+ */
163
+ enableUpdateEach?: boolean
164
+
165
+ /**
166
+ * When guard failures are raised relative to operation `before` hooks.
167
+ * **Default `'after-hooks'`.**
168
+ *
169
+ * `'after-hooks'` is upstream behaviour: a hook may answer the request before
170
+ * variant resolution is settled. `'before-hooks'` settles the guard first, so a
171
+ * hook cannot answer a request whose guard was never established.
172
+ */
173
+ guardResolutionOrder?: 'after-hooks' | 'before-hooks'
174
+
175
+ /**
176
+ * Honour `E2E=true` in the environment as a guard bypass. **Default `true`.**
177
+ *
178
+ * Upstream behaviour, and a real hazard on an edge runtime where that variable
179
+ * is an ordinary config var — set on staging, copied forward, flagged as
180
+ * security-relevant nowhere. `false` makes guard behaviour a property of the
181
+ * artifact alone.
182
+ */
183
+ allowE2EGuardBypass?: boolean
184
+
185
+ /**
186
+ * Check what a shape FUNCTION returned before using it. **Default `false`.**
187
+ *
188
+ * A function shape is opaque at construction time. On, a resolved value that is
189
+ * empty, not an object, or carrying non-guard keys fails the request with 500 —
190
+ * a deployment fault, not a caller fault — instead of running unguarded. It
191
+ * also resolves the function exactly once and passes the validated value on.
192
+ */
193
+ validateResolvedShapes?: boolean
194
+
110
195
  findUnique?: OperationConfig<TShape, TEnv> | false
111
196
  findUniqueOrThrow?: OperationConfig<TShape, TEnv> | false
112
197
  findFirst?: OperationConfig<TShape, TEnv> | false
@@ -119,6 +119,14 @@ export type BaseOperationConfig<
119
119
  before?: TBefore[]
120
120
  after?: TAfter[]
121
121
  pagination?: Partial<PaginationConfig>
122
+ /**
123
+ * Confirms that a variant literally named `default` is meant to answer every
124
+ * unrecognised, blank and missing caller.
125
+ *
126
+ * Only consulted when `requireDefaultVariantOptIn` is on. Otherwise the key
127
+ * behaves as it always has, so this is inert for existing configurations.
128
+ */
129
+ allowDefaultVariant?: boolean
122
130
  }
123
131
 
124
132
  export interface BaseUpdateEachConfig<TBefore, TAfter = TBefore> {
@@ -216,6 +224,77 @@ type OperationConfigInput<TBefore, TAfter> = {
216
224
  pagination?: Partial<PaginationConfig>
217
225
  }
218
226
 
227
+ /**
228
+ * The seven guard controls, resolved — the RUNTIME half.
229
+ *
230
+ * This lives here, not in the compiled `src/guardOptions.ts`, because a generated
231
+ * project gets these files copied in and must run with no dependency on this
232
+ * package. The two cannot import each other across that boundary, so the values
233
+ * appear twice and `guardOptionIndependence.test.ts` asserts they agree — against
234
+ * this resolver, which is the one the router actually runs.
235
+ *
236
+ * The machine-readable METADATA is compiled and public: see src/guardOptions.ts.
237
+ * Defining it here would put it in a file that never reaches `dist`.
238
+ */
239
+ export type GuardPolicy = {
240
+ requireGuardShape: boolean
241
+ validateGuardShapes: boolean
242
+ requireDefaultVariantOptIn: boolean
243
+ enableUpdateEach: boolean
244
+ guardResolutionOrder: 'after-hooks' | 'before-hooks'
245
+ allowE2EGuardBypass: boolean
246
+ validateResolvedShapes: boolean
247
+ }
248
+
249
+ /**
250
+ * PRE-1.64.2 BEHAVIOUR, exactly. Changing any value here changes what an
251
+ * unconfigured consumer gets on upgrade, which is the thing that must not happen.
252
+ */
253
+ export const UPSTREAM_GUARD_DEFAULTS: Readonly<GuardPolicy> = Object.freeze({
254
+ requireGuardShape: false,
255
+ validateGuardShapes: false,
256
+ requireDefaultVariantOptIn: false,
257
+ enableUpdateEach: true,
258
+ guardResolutionOrder: 'after-hooks',
259
+ allowE2EGuardBypass: true,
260
+ validateResolvedShapes: false,
261
+ })
262
+
263
+ /**
264
+ * Every control at its most restrictive — a SHORTCUT, not a mode.
265
+ *
266
+ * Spread it, edit what you disagree with, and store the result. There is
267
+ * deliberately no `preset: 'hardened'` option: a stored preset name would mean a
268
+ * future version of this package could silently change what a consumer's
269
+ * configuration does, which is the same class of mistake as 1.64.2 itself.
270
+ */
271
+ export const HARDENED_GUARD_PROFILE: Readonly<GuardPolicy> = Object.freeze({
272
+ requireGuardShape: true,
273
+ validateGuardShapes: true,
274
+ requireDefaultVariantOptIn: true,
275
+ enableUpdateEach: false,
276
+ guardResolutionOrder: 'before-hooks',
277
+ allowE2EGuardBypass: false,
278
+ validateResolvedShapes: true,
279
+ })
280
+
281
+ /** Fill in the defaults. Absent means upstream, never an opinion. */
282
+ export function resolveGuardPolicy(config: Partial<GuardPolicy> | undefined): GuardPolicy {
283
+ return {
284
+ requireGuardShape: config?.requireGuardShape ?? UPSTREAM_GUARD_DEFAULTS.requireGuardShape,
285
+ validateGuardShapes: config?.validateGuardShapes ?? UPSTREAM_GUARD_DEFAULTS.validateGuardShapes,
286
+ requireDefaultVariantOptIn:
287
+ config?.requireDefaultVariantOptIn ?? UPSTREAM_GUARD_DEFAULTS.requireDefaultVariantOptIn,
288
+ enableUpdateEach: config?.enableUpdateEach ?? UPSTREAM_GUARD_DEFAULTS.enableUpdateEach,
289
+ guardResolutionOrder:
290
+ config?.guardResolutionOrder ?? UPSTREAM_GUARD_DEFAULTS.guardResolutionOrder,
291
+ allowE2EGuardBypass:
292
+ config?.allowE2EGuardBypass ?? UPSTREAM_GUARD_DEFAULTS.allowE2EGuardBypass,
293
+ validateResolvedShapes:
294
+ config?.validateResolvedShapes ?? UPSTREAM_GUARD_DEFAULTS.validateResolvedShapes,
295
+ }
296
+ }
297
+
219
298
  function classifyGuardRouting(shape: unknown): NormalizedGuardRouting {
220
299
  if (shape === undefined) return { kind: 'none' }
221
300
  if (typeof shape === 'function') return { kind: 'single' }
@@ -295,34 +374,79 @@ function validateShapeConfig(shape: unknown, location: string): void {
295
374
  export function validateOperationConfig(
296
375
  config: { shape?: unknown; variants?: unknown; allowDefaultVariant?: unknown } | undefined,
297
376
  location: string,
377
+ policy: Partial<GuardPolicy> = {},
298
378
  ): void {
379
+ const { requireGuardShape, validateGuardShapes, requireDefaultVariantOptIn } =
380
+ resolveGuardPolicy(policy)
381
+
382
+ /**
383
+ * THE CHECKS THAT PREDATE 1.64.2 RUN UNCONDITIONALLY.
384
+ *
385
+ * Everything below them is gated per option. Note each gate stands alone: a
386
+ * consumer who enables `validateGuardShapes` gets shape validation and NOT a
387
+ * missing-guard refusal, and vice versa. That independence is the point, and
388
+ * `guardOptionIndependence.test.ts` asserts it option by option.
389
+ */
299
390
  if (!config) {
300
- throw new Error(
301
- location + ': no guard configured. Every generated operation must define ' +
302
- '"shape" or "variants"; an unguarded operation would pass the caller\'s ' +
303
- 'own where/select/include straight to Prisma.',
304
- )
391
+ if (requireGuardShape) {
392
+ throw new Error(
393
+ location + ': no guard configured. Every generated operation must define ' +
394
+ '"shape" or "variants"; an unguarded operation would pass the caller\'s ' +
395
+ 'own where/select/include straight to Prisma.',
396
+ )
397
+ }
398
+ return
305
399
  }
306
400
 
307
401
  const hasShape = config.shape !== undefined
308
402
  const hasVariants = config.variants !== undefined
309
403
 
404
+ // Pre-1.64.2. Not gated.
310
405
  if (hasShape && hasVariants) {
311
406
  throw new Error(location + ': shape and variants cannot both be defined')
312
407
  }
313
408
 
314
409
  if (!hasShape && !hasVariants) {
315
- throw new Error(
316
- location + ': no guard configured. Define "shape" or "variants".',
317
- )
410
+ if (requireGuardShape) {
411
+ throw new Error(
412
+ location + ': no guard configured. Define "shape" or "variants".',
413
+ )
414
+ }
415
+ return
318
416
  }
319
417
 
320
418
  if (hasShape) {
321
- validateShapeConfig(config.shape, location)
419
+ if (validateGuardShapes) validateShapeConfig(config.shape, location)
322
420
  return
323
421
  }
324
422
 
325
- const variants = config.variants
423
+ validateVariantMap(
424
+ config.variants,
425
+ location,
426
+ { validateGuardShapes, requireDefaultVariantOptIn },
427
+ config.allowDefaultVariant,
428
+ )
429
+ }
430
+
431
+ /**
432
+ * The variant-map checks.
433
+ *
434
+ * The four that predate 1.64.2 — non-array object, at least one entry, no
435
+ * reserved-key collision, every entry an object carrying a `shape` — run
436
+ * ALWAYS, because they are not new and removing them would be its own breaking
437
+ * change.
438
+ *
439
+ * The two the hardening added are gated on their OWN options, independently:
440
+ * `requireDefaultVariantOptIn` confirms a `default` variant,
441
+ * `validateGuardShapes` validates each variant's shape the way a top-level
442
+ * `shape` is validated. Enabling either does not enable the other.
443
+ */
444
+ function validateVariantMap(
445
+ variants: unknown,
446
+ location: string,
447
+ flags: { validateGuardShapes: boolean; requireDefaultVariantOptIn: boolean },
448
+ allowDefaultVariant?: unknown,
449
+ ): void {
326
450
  if (!isPlainObject(variants)) {
327
451
  throw new Error(location + ': variants must be a non-array object')
328
452
  }
@@ -343,18 +467,20 @@ export function validateOperationConfig(
343
467
  * writes on purpose; a key called `default` is one they may have typed without
344
468
  * noticing what it catches.
345
469
  */
346
- if (Object.prototype.hasOwnProperty.call(variants, 'default')) {
347
- if (config.allowDefaultVariant !== true) {
470
+ if (flags.requireDefaultVariantOptIn) {
471
+ if (Object.prototype.hasOwnProperty.call(variants, 'default')) {
472
+ if (allowDefaultVariant !== true) {
473
+ throw new Error(
474
+ location + ': a variant named "default" answers every unrecognised, ' +
475
+ 'blank and missing caller. Set allowDefaultVariant: true to confirm ' +
476
+ 'that is intended, and make sure it is the most restrictive variant.',
477
+ )
478
+ }
479
+ } else if (allowDefaultVariant !== undefined) {
348
480
  throw new Error(
349
- location + ': a variant named "default" answers every unrecognised, ' +
350
- 'blank and missing caller. Set allowDefaultVariant: true to confirm ' +
351
- 'that is intended, and make sure it is the most restrictive variant.',
481
+ location + ': allowDefaultVariant is set but no "default" variant exists.',
352
482
  )
353
483
  }
354
- } else if (config.allowDefaultVariant !== undefined) {
355
- throw new Error(
356
- location + ': allowDefaultVariant is set but no "default" variant exists.',
357
- )
358
484
  }
359
485
 
360
486
  for (const [key, rawEntry] of entries) {
@@ -384,7 +510,9 @@ export function validateOperationConfig(
384
510
  * different key. A guard is not more trustworthy for being written inside a
385
511
  * variant.
386
512
  */
387
- validateShapeConfig(rawEntry.shape, location + ' variant "' + key + '"')
513
+ if (flags.validateGuardShapes) {
514
+ validateShapeConfig(rawEntry.shape, location + ' variant "' + key + '"')
515
+ }
388
516
  }
389
517
  }
390
518
 
@@ -117,10 +117,15 @@ export function generateRouteConfigType(
117
117
  ` after?: ${afterRef}[]\n` +
118
118
  ` }>`
119
119
 
120
+ // Only consulted when `requireDefaultVariantOptIn` is on; inert otherwise.
121
+ // Emitted so a TypeScript consumer who opts in can confirm a `default`
122
+ // variant without a cast — 1.64.2 demanded it and never added it to the type.
123
+ const allowDefault = ` allowDefaultVariant?: boolean`
124
+
120
125
  return (
121
126
  ` ${routerOp}?: (${commonConfig} & (\n` +
122
127
  ` | { shape?: ${m}${c}ShapeInput<TCtx>; variants?: never }\n` +
123
- ` | { shape?: never; variants: ${variantsConfig} }\n` +
128
+ ` | { shape?: never; variants: ${variantsConfig}\n${allowDefault} }\n` +
124
129
  ` )) | false`
125
130
  )
126
131
  }).join('\n')
@@ -81,7 +81,28 @@ function emitWriteOp(meta: (typeof OPERATION_METADATA)[number], modelName: strin
81
81
  }
82
82
 
83
83
  /**
84
- * Guard dropping is a GENERATION-TIME decision, never a runtime one.
84
+ * Guard behaviour is SEVEN INDEPENDENT OPT-IN CONTROLS on the route config.
85
+ *
86
+ * 1.64.2 shipped all of it as the default, which was a breaking change to a
87
+ * published package. The first correction put all of it behind one `requireGuard`
88
+ * switch, which was also wrong: these are unrelated decisions, and one switch
89
+ * forces a consumer who wants a missing-guard refusal to also accept a
90
+ * hook-ordering change and lose a route they may be using.
91
+ *
92
+ * Each is now its own option, each defaulting to the pre-1.64.2 behaviour:
93
+ *
94
+ * - `requireGuardShape` refuse an operation with no guard
95
+ * - `validateGuardShapes` refuse an empty or key-mixing shape
96
+ * - `requireDefaultVariantOptIn` confirm a `default` variant
97
+ * - `enableUpdateEach` register the batch route (default true)
98
+ * - `guardResolutionOrder` settle the guard before or after hooks
99
+ * - `allowE2EGuardBypass` honour E2E=true (default true)
100
+ * - `validateResolvedShapes` check what a shape function returned
101
+ *
102
+ * `HARDENED_GUARD_PROFILE` selects all seven, as values to spread rather than a
103
+ * mode to store. See routeConfig.ts.
104
+ *
105
+ * Guard dropping is then a GENERATION-TIME decision, never a runtime one.
85
106
  *
86
107
  * The emitted router used to compute `DROP_GUARD = <flag> || _env.E2E === 'true'`,
87
108
  * so setting `E2E=true` in a deployed environment downgraded enforcement even
@@ -99,13 +120,14 @@ function emitWriteOp(meta: (typeof OPERATION_METADATA)[number], modelName: strin
99
120
  * output is an artifact, and a paragraph about a bypass that no longer exists
100
121
  * would be copied into every router this generator writes.
101
122
  *
102
- * `updateEach` is refused for the same reason, one step further on. It bypasses
103
- * guard shapes by design — the endpoint is a batch of `{ where, data }` items
104
- * applied directly — and the only thing between it and an unguarded mass
105
- * mutation was a `console.warn` suppressed in production. A warning is not a
106
- * security boundary; it is advice to whoever happens to be reading a development
107
- * log. Refused at construction rather than silently dropped, so a deployment
108
- * expecting the route learns at boot instead of at the first 404.
123
+ * `updateEach` is refused for the same reason, one step further on — and, again,
124
+ * only under the flag. It bypasses guard shapes by design — the endpoint is a
125
+ * batch of `{ where, data }` items applied directly — and the only thing between
126
+ * it and an unguarded mass mutation is a `console.warn` suppressed in
127
+ * production. A warning is not a security boundary; it is advice to whoever
128
+ * happens to be reading a development log. Refused at construction rather than
129
+ * silently dropped, so a deployment expecting the route learns at boot instead
130
+ * of at the first 404.
109
131
  */
110
132
  export function generateHonoRouterFunction({
111
133
  model,
@@ -160,9 +182,11 @@ import { buildModelOpenApi } from '../buildModelOpenApi${ext}'
160
182
  import {
161
183
  normalizeOperation,
162
184
  resolveOperationVariantKey,
185
+ resolveGuardPolicy,
163
186
  resolveGuardShapeOnce,
164
187
  validateCountSourceWhere,
165
188
  validateOperationConfig,
189
+ validateUpdateEachConfig,
166
190
  } from '../routeConfig${ext}'
167
191
  import type { NormalizedOperationConfig } from '../routeConfig${ext}'
168
192
  import { transformResult } from '../operationRuntime${ext}'
@@ -176,8 +200,9 @@ import { MODEL_FIELDS, MODEL_ENUMS } from './${modelName}Metadata${ext}'
176
200
  ${generateRouteConfigType(modelName, 'HonoBeforeHook', guardShapesImport, importStyle, 'hono')}
177
201
  const _env = getEnv()
178
202
 
179
- // Fixed at generation time. Never read from the environment — see
180
- // generateRouterHono.ts.
203
+ // Fixed at generation time. The \`allowE2EGuardBypass\` control decides whether the
204
+ // environment can additionally drop the guard at runtime; it defaults to true,
205
+ // which is upstream behaviour. See generateRouterHono.ts.
181
206
  const DROP_GUARD = ${dropGuard}
182
207
 
183
208
  type JsonLike =
@@ -236,6 +261,19 @@ async function parseBodyAsQueryMiddleware(c: HandlerContext): Promise<void> {
236
261
  c.set('parsedQuery', sanitizeKeys(body as Record<string, unknown>))
237
262
  }
238
263
 
264
+ async function parseUpdateEachBodyMiddleware(c: HandlerContext): Promise<void> {
265
+ let body: unknown
266
+ try {
267
+ body = await c.req.json()
268
+ } catch {
269
+ throw new HTTPException(400, { message: 'updateEach body must be an array of { where, data } items' })
270
+ }
271
+ if (!Array.isArray(body)) {
272
+ throw new HTTPException(400, { message: 'updateEach body must be an array of { where, data } items' })
273
+ }
274
+ c.set('body', body)
275
+ }
276
+
239
277
  async function parseWriteBodyMiddleware(c: HandlerContext): Promise<void> {
240
278
  let body: unknown
241
279
  try {
@@ -254,6 +292,17 @@ function makeShapeMiddleware<TCtx, TPrisma, TEnv extends HonoEnvBase>(
254
292
  opConfig: NormalizedOp<TEnv>,
255
293
  opKind: OpKind,
256
294
  ) {
295
+ const policy = resolveGuardPolicy(config)
296
+
297
+ /**
298
+ * The environment bypass, honoured unless the consumer turned it off.
299
+ *
300
+ * \`E2E=true\` downgrading enforcement in a deployed environment is a real
301
+ * hazard, and it is also upstream behaviour — so it is a control, not a
302
+ * decision made here.
303
+ */
304
+ const dropGuard = DROP_GUARD || (policy.allowE2EGuardBypass && _env.E2E === 'true')
305
+
257
306
  return async (c: Context<GeneratedHonoEnv<TEnv>>): Promise<void> => {
258
307
  const merged = mergePaginationConfig(config.pagination, opConfig.pagination)
259
308
  if (merged) c.set('routeConfig', { pagination: merged })
@@ -281,18 +330,26 @@ function makeShapeMiddleware<TCtx, TPrisma, TEnv extends HonoEnvBase>(
281
330
  : undefined
282
331
 
283
332
  /**
284
- * Resolved ONCE, here, and the validated value is what travels onward —
285
- * see resolveGuardShapeOnce. Passing the function on would let it return a
286
- * different shape when it is enforced than when it was checked.
333
+ * With \`validateResolvedShapes\`, resolved ONCE here and the validated value
334
+ * is what travels onward — see resolveGuardShapeOnce. Passing the function
335
+ * on would let it return a different shape when it is enforced than when it
336
+ * was checked.
337
+ *
338
+ * Without it the raw shape is passed through untouched, which is upstream
339
+ * behaviour: it is resolved at the point of use, and a function returning
340
+ * something unusable is not this router's business to refuse.
287
341
  */
288
- const resolution = await resolveGuardShapeOnce(opConfig.guardShape, resolvedKey, resolveCtx)
289
- if (!resolution.ok) {
290
- c.set('guardShapeFailure', resolution.problem)
291
- return
342
+ let effectiveShape: unknown = opConfig.guardShape
343
+ if (policy.validateResolvedShapes) {
344
+ const resolution = await resolveGuardShapeOnce(opConfig.guardShape, resolvedKey, resolveCtx)
345
+ if (!resolution.ok) {
346
+ c.set('guardShapeFailure', resolution.problem)
347
+ return
348
+ }
349
+ effectiveShape = resolution.shape
292
350
  }
293
- const effectiveShape = resolution.shape
294
351
 
295
- if (!DROP_GUARD) {
352
+ if (!dropGuard) {
296
353
  c.set('guardShape', effectiveShape)
297
354
  } else {
298
355
  await applyDroppedGuard(
@@ -438,6 +495,38 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any, TEnv extend
438
495
  })
439
496
  }
440
497
 
498
+ const POLICY = resolveGuardPolicy(config)
499
+ const SETTLE_BEFORE_HOOKS = POLICY.guardResolutionOrder === 'before-hooks'
500
+
501
+ /**
502
+ * The guard-resolution failures, raised in one place so both handlers and both
503
+ * orderings share exactly one definition of what a failure is.
504
+ *
505
+ * WHERE it is called is the behavioural difference, and it is the
506
+ * \`guardResolutionOrder\` control. \`'before-hooks'\` runs it before any operation
507
+ * hook, because a hook that returns a Response — an auth gate, a cache, a
508
+ * short-circuit for a known caller — would otherwise answer a request whose
509
+ * guard was never established. \`'after-hooks'\` is upstream behaviour.
510
+ *
511
+ * \`guardShapeFailure\` is only ever set when \`validateResolvedShapes\` is on, so
512
+ * that branch is inert rather than merely unreached when it is off.
513
+ */
514
+ const settleGuard = (c: Context<GeneratedHonoEnv<TEnv>>): void => {
515
+ const failure = c.get('guardVariantFailure')
516
+ if (failure) {
517
+ throw new HTTPException(400, {
518
+ message: formatGuardVariantResolutionError(failure),
519
+ })
520
+ }
521
+
522
+ const shapeFailure = c.get('guardShapeFailure')
523
+ if (shapeFailure) {
524
+ throw new HTTPException(500, {
525
+ message: 'guard shape could not be resolved: ' + shapeFailure,
526
+ })
527
+ }
528
+ }
529
+
441
530
  const handleRead = (
442
531
  opConfig: NormalizedOp<TEnv>,
443
532
  handlerFn: (c: HandlerContext) => Promise<void>,
@@ -448,41 +537,15 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any, TEnv extend
448
537
  await parseFn(c as unknown as HandlerContext)
449
538
  await makeShapeMiddleware<TCtx, TPrisma, TEnv>(config, opConfig, opKind)(c)
450
539
 
451
- /**
452
- * Variant resolution is settled BEFORE any operation hook runs.
453
- *
454
- * The check used to sit after \`operationBefore\`, so a hook that returned a
455
- * Response — an auth gate, a cache, a short-circuit for a known caller —
456
- * answered the request before anyone established which guard applied to
457
- * it. A cached response served for a request whose variant could not be
458
- * resolved is a response served under a guard nobody chose.
459
- *
460
- * Hooks that must run first belong outside the generated router, where
461
- * they are visibly not part of guard resolution.
462
- */
463
- const failure = c.get('guardVariantFailure')
464
- if (failure) {
465
- throw new HTTPException(400, {
466
- message: formatGuardVariantResolutionError(failure),
467
- })
468
- }
469
-
470
- /**
471
- * A shape function that returned something unusable is a DEPLOYMENT fault,
472
- * not a caller fault — 500, and refused here, before any hook can answer
473
- * the request and before Prisma is reached. Running unguarded because the
474
- * guard failed to produce a guard is the outcome this exists to prevent.
475
- */
476
- const shapeFailure = c.get('guardShapeFailure')
477
- if (shapeFailure) {
478
- throw new HTTPException(500, {
479
- message: 'guard shape could not be resolved: ' + shapeFailure,
480
- })
481
- }
540
+ // Settled BEFORE any hook can answer the request, when asked for.
541
+ if (SETTLE_BEFORE_HOOKS) settleGuard(c)
482
542
 
483
543
  const operationBefore = await runBeforeHooks<TEnv>(opConfig.operationBefore, c)
484
544
  if (operationBefore) return operationBefore
485
545
 
546
+ // Upstream order: hooks first, guard failure after.
547
+ if (!SETTLE_BEFORE_HOOKS) settleGuard(c)
548
+
486
549
  const key = c.get('guardVariantKey')
487
550
  const variantHooks =
488
551
  key !== undefined ? opConfig.variantHooks[key] : undefined
@@ -509,30 +572,15 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any, TEnv extend
509
572
  await parseWriteBodyMiddleware(c as unknown as HandlerContext)
510
573
  await makeShapeMiddleware<TCtx, TPrisma, TEnv>(config, opConfig, opKind)(c)
511
574
 
512
- // Settled before any operation hook — see the read handler above.
513
- const failure = c.get('guardVariantFailure')
514
- if (failure) {
515
- throw new HTTPException(400, {
516
- message: formatGuardVariantResolutionError(failure),
517
- })
518
- }
519
-
520
- /**
521
- * A shape function that returned something unusable is a DEPLOYMENT fault,
522
- * not a caller fault — 500, and refused here, before any hook can answer
523
- * the request and before Prisma is reached. Running unguarded because the
524
- * guard failed to produce a guard is the outcome this exists to prevent.
525
- */
526
- const shapeFailure = c.get('guardShapeFailure')
527
- if (shapeFailure) {
528
- throw new HTTPException(500, {
529
- message: 'guard shape could not be resolved: ' + shapeFailure,
530
- })
531
- }
575
+ // Settled BEFORE any hook can answer the request, when asked for.
576
+ if (SETTLE_BEFORE_HOOKS) settleGuard(c)
532
577
 
533
578
  const operationBefore = await runBeforeHooks<TEnv>(opConfig.operationBefore, c)
534
579
  if (operationBefore) return operationBefore
535
580
 
581
+ // Upstream order: hooks first, guard failure after.
582
+ if (!SETTLE_BEFORE_HOOKS) settleGuard(c)
583
+
536
584
  const key = c.get('guardVariantKey')
537
585
  const variantHooks =
538
586
  key !== undefined ? opConfig.variantHooks[key] : undefined
@@ -554,7 +602,7 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any, TEnv extend
554
602
  key: K,
555
603
  ): NormalizedOp<TEnv> => {
556
604
  const raw = config[key] as unknown as OperationConfigLike<TEnv> | undefined
557
- validateOperationConfig(raw, '${modelName}.' + String(key))
605
+ validateOperationConfig(raw, '${modelName}.' + String(key), POLICY)
558
606
  return normalizeHonoOperation(raw)
559
607
  }
560
608
 
@@ -562,14 +610,50 @@ ${readOpBlocks}
562
610
 
563
611
  ${writeOpBlocks}
564
612
 
565
- // Not registered on this target — see generateRouterHono.ts for why.
566
613
  if (config.updateEach) {
567
- throw new Error(
568
- '${modelName}.updateEach: this target does not register updateEach. It bypasses ' +
569
- 'guard shapes entirely, so there is no configuration that makes it safe to ' +
570
- 'expose. Perform batch updates through a guarded operation, or behind your own ' +
571
- 'authenticated route outside the generated router.',
572
- )
614
+ /**
615
+ * Refused only when \`enableUpdateEach\` is false. It bypasses guard shapes by design — the
616
+ * endpoint is a batch of { where, data } applied directly — and the only thing
617
+ * between it and an unguarded mass mutation is a console.warn suppressed in
618
+ * production. A warning is not a security boundary.
619
+ *
620
+ * Removing the route outright was a breaking change to a published package,
621
+ * so by default it registers exactly as it did, warning and all.
622
+ */
623
+ if (!POLICY.enableUpdateEach) {
624
+ throw new Error(
625
+ '${modelName}.updateEach: enableUpdateEach is false, so this router ' +
626
+ 'does not register updateEach. It bypasses guard shapes entirely, so ' +
627
+ 'there is no configuration that makes it safe to expose. Perform batch ' +
628
+ 'updates through a guarded operation, or behind your own authenticated ' +
629
+ 'route outside the generated router.',
630
+ )
631
+ }
632
+
633
+ const rawUpdateEach = config.updateEach as unknown as OperationConfigLike<TEnv>
634
+ validateUpdateEachConfig(rawUpdateEach, '${modelName}.updateEach')
635
+ const opConfig = normalizeHonoOperation(rawUpdateEach)
636
+ if (opConfig.operationBefore.length === 0 && _env.NODE_ENV !== 'production') {
637
+ console.warn(
638
+ '[${modelName}Router] updateEach is enabled without a before hook. ' +
639
+ 'This endpoint bypasses guard shapes and should be protected by authentication middleware.',
640
+ )
641
+ }
642
+ const path = basePath ? \`\${basePath}/each\` : '/each'
643
+ app.post(path, async (c: Context<GeneratedHonoEnv<TEnv>>): Promise<Response> => {
644
+ try {
645
+ await parseUpdateEachBodyMiddleware(c as unknown as HandlerContext)
646
+ await makeShapeMiddleware<TCtx, TPrisma, TEnv>(config, opConfig, 'noop')(c)
647
+ const beforeResponse = await runBeforeHooks<TEnv>(opConfig.operationBefore, c)
648
+ if (beforeResponse) return beforeResponse
649
+ await ${modelName}UpdateEach(c as unknown as HandlerContext)
650
+ const afterResponse = await runAfterHooks<TEnv>(opConfig.operationAfter, c)
651
+ if (afterResponse) return afterResponse
652
+ return sendResult(c as unknown as HandlerContext)
653
+ } catch (error: unknown) {
654
+ return sendError(c as unknown as HandlerContext, error)
655
+ }
656
+ })
573
657
  }
574
658
 
575
659
  return app