prisma-generator-express 1.64.0 → 1.64.2

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.
@@ -80,6 +80,33 @@ function emitWriteOp(meta: (typeof OPERATION_METADATA)[number], modelName: strin
80
80
  }`
81
81
  }
82
82
 
83
+ /**
84
+ * Guard dropping is a GENERATION-TIME decision, never a runtime one.
85
+ *
86
+ * The emitted router used to compute `DROP_GUARD = <flag> || _env.E2E === 'true'`,
87
+ * so setting `E2E=true` in a deployed environment downgraded enforcement even
88
+ * when the generator had been told to keep the guard. The two modes are not
89
+ * equivalent: with the guard, the shape goes to prisma-guard; with it dropped,
90
+ * `applyDroppedGuard` applies projection defaults and forced `where` clauses and
91
+ * nothing else is validated against the shape.
92
+ *
93
+ * On an edge runtime that variable is an ordinary config var — set on a staging
94
+ * deployment, copied forward, flagged as security-relevant nowhere. A
95
+ * deployment's guard behaviour has to be a property of the artifact, not of the
96
+ * environment it happens to land in.
97
+ *
98
+ * The rationale lives here rather than in the emitted file on purpose: generated
99
+ * output is an artifact, and a paragraph about a bypass that no longer exists
100
+ * would be copied into every router this generator writes.
101
+ *
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.
109
+ */
83
110
  export function generateHonoRouterFunction({
84
111
  model,
85
112
  enums,
@@ -133,9 +160,9 @@ import { buildModelOpenApi } from '../buildModelOpenApi${ext}'
133
160
  import {
134
161
  normalizeOperation,
135
162
  resolveOperationVariantKey,
163
+ resolveGuardShapeOnce,
136
164
  validateCountSourceWhere,
137
165
  validateOperationConfig,
138
- validateUpdateEachConfig,
139
166
  } from '../routeConfig${ext}'
140
167
  import type { NormalizedOperationConfig } from '../routeConfig${ext}'
141
168
  import { transformResult } from '../operationRuntime${ext}'
@@ -149,7 +176,9 @@ import { MODEL_FIELDS, MODEL_ENUMS } from './${modelName}Metadata${ext}'
149
176
  ${generateRouteConfigType(modelName, 'HonoBeforeHook', guardShapesImport, importStyle, 'hono')}
150
177
  const _env = getEnv()
151
178
 
152
- const DROP_GUARD = ${dropGuard} || _env.E2E === 'true'
179
+ // Fixed at generation time. Never read from the environment — see
180
+ // generateRouterHono.ts.
181
+ const DROP_GUARD = ${dropGuard}
153
182
 
154
183
  type JsonLike =
155
184
  | string
@@ -220,19 +249,6 @@ async function parseWriteBodyMiddleware(c: HandlerContext): Promise<void> {
220
249
  c.set('body', body)
221
250
  }
222
251
 
223
- async function parseUpdateEachBodyMiddleware(c: HandlerContext): Promise<void> {
224
- let body: unknown
225
- try {
226
- body = await c.req.json()
227
- } catch {
228
- throw new HTTPException(400, { message: 'updateEach body must be an array of { where, data } items' })
229
- }
230
- if (!Array.isArray(body)) {
231
- throw new HTTPException(400, { message: 'updateEach body must be an array of { where, data } items' })
232
- }
233
- c.set('body', body)
234
- }
235
-
236
252
  function makeShapeMiddleware<TCtx, TPrisma, TEnv extends HonoEnvBase>(
237
253
  config: ${modelName}RouteConfig<TCtx, TPrisma, TEnv>,
238
254
  opConfig: NormalizedOp<TEnv>,
@@ -260,15 +276,27 @@ function makeShapeMiddleware<TCtx, TPrisma, TEnv extends HonoEnvBase>(
260
276
  if (resolvedKey !== undefined) c.set('guardVariantKey', resolvedKey)
261
277
 
262
278
  if (opConfig.guardShape) {
279
+ const resolveCtx = typeof config.resolveContext === 'function'
280
+ ? () => (config.resolveContext as (ctx: Context<GeneratedHonoEnv<TEnv>>) => unknown | Promise<unknown>)(c)
281
+ : undefined
282
+
283
+ /**
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.
287
+ */
288
+ const resolution = await resolveGuardShapeOnce(opConfig.guardShape, resolvedKey, resolveCtx)
289
+ if (!resolution.ok) {
290
+ c.set('guardShapeFailure', resolution.problem)
291
+ return
292
+ }
293
+ const effectiveShape = resolution.shape
294
+
263
295
  if (!DROP_GUARD) {
264
- c.set('guardShape', opConfig.guardShape)
296
+ c.set('guardShape', effectiveShape)
265
297
  } else {
266
- const resolveCtx = typeof config.resolveContext === 'function'
267
- ? () => (config.resolveContext as (ctx: Context<GeneratedHonoEnv<TEnv>>) => unknown | Promise<unknown>)(c)
268
- : undefined
269
-
270
298
  await applyDroppedGuard(
271
- opConfig.guardShape,
299
+ effectiveShape,
272
300
  resolvedKey,
273
301
  resolveCtx,
274
302
  opKind,
@@ -419,9 +447,19 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any, TEnv extend
419
447
  try {
420
448
  await parseFn(c as unknown as HandlerContext)
421
449
  await makeShapeMiddleware<TCtx, TPrisma, TEnv>(config, opConfig, opKind)(c)
422
- const operationBefore = await runBeforeHooks<TEnv>(opConfig.operationBefore, c)
423
- if (operationBefore) return operationBefore
424
450
 
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
+ */
425
463
  const failure = c.get('guardVariantFailure')
426
464
  if (failure) {
427
465
  throw new HTTPException(400, {
@@ -429,6 +467,22 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any, TEnv extend
429
467
  })
430
468
  }
431
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
+ }
482
+
483
+ const operationBefore = await runBeforeHooks<TEnv>(opConfig.operationBefore, c)
484
+ if (operationBefore) return operationBefore
485
+
432
486
  const key = c.get('guardVariantKey')
433
487
  const variantHooks =
434
488
  key !== undefined ? opConfig.variantHooks[key] : undefined
@@ -454,9 +508,8 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any, TEnv extend
454
508
  try {
455
509
  await parseWriteBodyMiddleware(c as unknown as HandlerContext)
456
510
  await makeShapeMiddleware<TCtx, TPrisma, TEnv>(config, opConfig, opKind)(c)
457
- const operationBefore = await runBeforeHooks<TEnv>(opConfig.operationBefore, c)
458
- if (operationBefore) return operationBefore
459
511
 
512
+ // Settled before any operation hook — see the read handler above.
460
513
  const failure = c.get('guardVariantFailure')
461
514
  if (failure) {
462
515
  throw new HTTPException(400, {
@@ -464,6 +517,22 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any, TEnv extend
464
517
  })
465
518
  }
466
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
+ }
532
+
533
+ const operationBefore = await runBeforeHooks<TEnv>(opConfig.operationBefore, c)
534
+ if (operationBefore) return operationBefore
535
+
467
536
  const key = c.get('guardVariantKey')
468
537
  const variantHooks =
469
538
  key !== undefined ? opConfig.variantHooks[key] : undefined
@@ -493,31 +562,14 @@ ${readOpBlocks}
493
562
 
494
563
  ${writeOpBlocks}
495
564
 
565
+ // Not registered on this target — see generateRouterHono.ts for why.
496
566
  if (config.updateEach) {
497
- const rawUpdateEach = config.updateEach as unknown as OperationConfigLike<TEnv>
498
- validateUpdateEachConfig(rawUpdateEach, '${modelName}.updateEach')
499
- const opConfig = normalizeHonoOperation(rawUpdateEach)
500
- if (opConfig.operationBefore.length === 0 && _env.NODE_ENV !== 'production') {
501
- console.warn(
502
- '[${modelName}Router] updateEach is enabled without a before hook. ' +
503
- 'This endpoint bypasses guard shapes and should be protected by authentication middleware.',
504
- )
505
- }
506
- const path = basePath ? \`\${basePath}/each\` : '/each'
507
- app.post(path, async (c: Context<GeneratedHonoEnv<TEnv>>): Promise<Response> => {
508
- try {
509
- await parseUpdateEachBodyMiddleware(c as unknown as HandlerContext)
510
- await makeShapeMiddleware<TCtx, TPrisma, TEnv>(config, opConfig, 'noop')(c)
511
- const beforeResponse = await runBeforeHooks<TEnv>(opConfig.operationBefore, c)
512
- if (beforeResponse) return beforeResponse
513
- await ${modelName}UpdateEach(c as unknown as HandlerContext)
514
- const afterResponse = await runAfterHooks<TEnv>(opConfig.operationAfter, c)
515
- if (afterResponse) return afterResponse
516
- return sendResult(c as unknown as HandlerContext)
517
- } catch (error: unknown) {
518
- return sendError(c as unknown as HandlerContext, error)
519
- }
520
- })
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
+ )
521
573
  }
522
574
 
523
575
  return app