prisma-generator-express 1.67.0 → 1.69.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.
Files changed (68) hide show
  1. package/README.md +223 -28
  2. package/dist/copy/operationDefinitions.d.ts +4 -1
  3. package/dist/copy/operationDefinitions.js +64 -3
  4. package/dist/copy/operationDefinitions.js.map +1 -1
  5. package/dist/generators/generateFastifyHandler.js +55 -9
  6. package/dist/generators/generateFastifyHandler.js.map +1 -1
  7. package/dist/generators/generateHonoHandler.js +44 -4
  8. package/dist/generators/generateHonoHandler.js.map +1 -1
  9. package/dist/generators/generateHonoOpenApi.d.ts +13 -0
  10. package/dist/generators/generateHonoOpenApi.js +70 -0
  11. package/dist/generators/generateHonoOpenApi.js.map +1 -0
  12. package/dist/generators/generateMcpApp.d.ts +10 -0
  13. package/dist/generators/generateMcpApp.js +113 -0
  14. package/dist/generators/generateMcpApp.js.map +1 -0
  15. package/dist/generators/generateModelMcp.d.ts +9 -0
  16. package/dist/generators/generateModelMcp.js +145 -0
  17. package/dist/generators/generateModelMcp.js.map +1 -0
  18. package/dist/generators/generateModelMetadata.js +9 -5
  19. package/dist/generators/generateModelMetadata.js.map +1 -1
  20. package/dist/generators/generateRouter.js +62 -56
  21. package/dist/generators/generateRouter.js.map +1 -1
  22. package/dist/generators/generateRouterFastify.js +56 -64
  23. package/dist/generators/generateRouterFastify.js.map +1 -1
  24. package/dist/generators/generateRouterHono.js +43 -411
  25. package/dist/generators/generateRouterHono.js.map +1 -1
  26. package/dist/generators/generateRouterPartsHono.d.ts +14 -0
  27. package/dist/generators/generateRouterPartsHono.js +180 -0
  28. package/dist/generators/generateRouterPartsHono.js.map +1 -0
  29. package/dist/generators/generateUnifiedHandler.js +36 -20
  30. package/dist/generators/generateUnifiedHandler.js.map +1 -1
  31. package/dist/index.d.ts +2 -0
  32. package/dist/index.js +101 -3
  33. package/dist/index.js.map +1 -1
  34. package/dist/utils/copyFiles.d.ts +1 -1
  35. package/dist/utils/copyFiles.js +20 -1
  36. package/dist/utils/copyFiles.js.map +1 -1
  37. package/dist/utils/mcpGate.d.ts +10 -0
  38. package/dist/utils/mcpGate.js +118 -0
  39. package/dist/utils/mcpGate.js.map +1 -0
  40. package/dist/utils/writeFileSafely.js +6 -0
  41. package/dist/utils/writeFileSafely.js.map +1 -1
  42. package/package.json +18 -3
  43. package/src/copy/buildModelOpenApi.ts +76 -126
  44. package/src/copy/docsRenderer.ts +14 -15
  45. package/src/copy/mcpRuntime.ts +1017 -0
  46. package/src/copy/operationDefinitions.ts +78 -4
  47. package/src/copy/operationPipeline.ts +267 -0
  48. package/src/copy/operationRuntime.ts +24 -14
  49. package/src/copy/operationSchemas.ts +2773 -0
  50. package/src/copy/pagination.ts +10 -2
  51. package/src/copy/routeConfig.hono.ts +34 -5
  52. package/src/copy/routeConfig.ts +1 -1
  53. package/src/copy/routerParts.ts +524 -0
  54. package/src/generators/generateFastifyHandler.ts +55 -9
  55. package/src/generators/generateHonoHandler.ts +44 -4
  56. package/src/generators/generateHonoOpenApi.ts +93 -0
  57. package/src/generators/generateMcpApp.ts +133 -0
  58. package/src/generators/generateModelMcp.ts +208 -0
  59. package/src/generators/generateModelMetadata.ts +16 -10
  60. package/src/generators/generateRouter.ts +62 -56
  61. package/src/generators/generateRouterFastify.ts +56 -64
  62. package/src/generators/generateRouterHono.ts +49 -459
  63. package/src/generators/generateRouterPartsHono.ts +220 -0
  64. package/src/generators/generateUnifiedHandler.ts +36 -21
  65. package/src/index.ts +116 -3
  66. package/src/utils/copyFiles.ts +36 -0
  67. package/src/utils/mcpGate.ts +157 -0
  68. package/src/utils/writeFileSafely.ts +6 -0
@@ -58,15 +58,15 @@ function emitReadOp(
58
58
  ? meta.name === 'findMany'
59
59
  ? ` if (resolvePostReadsEnabled(config.disablePostReads, opConfig.disablePostReads)) {
60
60
  const postPath = basePath ? \`\${basePath}/read\` : '/read'
61
- app.post(postPath, handleRead(opConfig, ${handlerName}, parseBodyAsQueryMiddleware, '${opKind}'))
61
+ app.post(postPath, createReadRoute<TCtx, TPrisma, TEnv>({ config, opConfig, opKind: '${opKind}', handler: ${handlerName}, parse: 'body', dropGuard, settleBeforeHooks: SETTLE_BEFORE_HOOKS }))
62
62
  }`
63
- : ` if (resolvePostReadsEnabled(config.disablePostReads, opConfig.disablePostReads)) app.post(path, handleRead(opConfig, ${handlerName}, parseBodyAsQueryMiddleware, '${opKind}'))`
63
+ : ` if (resolvePostReadsEnabled(config.disablePostReads, opConfig.disablePostReads)) app.post(path, createReadRoute<TCtx, TPrisma, TEnv>({ config, opConfig, opKind: '${opKind}', handler: ${handlerName}, parse: 'body', dropGuard, settleBeforeHooks: SETTLE_BEFORE_HOOKS }))`
64
64
  : ''
65
65
 
66
66
  return ` if (isEnabled(config.${meta.configKey})) {
67
67
  const opConfig = opFor('${meta.configKey}')
68
68
  const path = ${pathValue}
69
- app.get(path, handleRead(opConfig, ${handlerName}, parseQueryMiddleware, '${opKind}'))
69
+ app.get(path, createReadRoute<TCtx, TPrisma, TEnv>({ config, opConfig, opKind: '${opKind}', handler: ${handlerName}, parse: 'query', dropGuard, settleBeforeHooks: SETTLE_BEFORE_HOOKS }))
70
70
  ${postReadLine}
71
71
  }`
72
72
  }
@@ -83,61 +83,19 @@ function emitWriteOp(
83
83
  return ` if (isEnabled(config.${meta.configKey})) {
84
84
  const opConfig = opFor('${meta.configKey}')
85
85
  const path = ${pathValue}
86
- app.${meta.method}(path, handleWrite(opConfig, ${handlerName}, '${opKind}'))
86
+ app.${meta.method}(path, createWriteRoute<TCtx, TPrisma, TEnv>({ config, opConfig, opKind: '${opKind}', handler: ${handlerName}, dropGuard, settleBeforeHooks: SETTLE_BEFORE_HOOKS }))
87
87
  }`
88
88
  }
89
89
 
90
90
  /**
91
91
  * Guard behaviour is SEVEN INDEPENDENT OPT-IN CONTROLS on the route config.
92
+ * See routeConfig.ts; the long rationale lives with the controls.
92
93
  *
93
- * 1.64.2 shipped all of it as the default, which was a breaking change to a
94
- * published package. The first correction put all of it behind one `requireGuard`
95
- * switch, which was also wrong: these are unrelated decisions, and one switch
96
- * forces a consumer who wants a missing-guard refusal to also accept a
97
- * hook-ordering change and lose a route they may be using.
98
- *
99
- * Each is now its own option, each defaulting to the pre-1.64.2 behaviour:
100
- *
101
- * - `requireGuardShape` refuse an operation with no guard
102
- * - `validateGuardShapes` refuse an empty or key-mixing shape
103
- * - `requireDefaultVariantOptIn` confirm a `default` variant
104
- * - `enableUpdateEach` register the batch route (default true)
105
- * - `guardResolutionOrder` settle the guard before or after hooks
106
- * - `allowE2EGuardBypass` honour PGE_DROP_GUARD=true (default true)
107
- * - `validateResolvedShapes` check what a shape function returned
108
- *
109
- * `HARDENED_GUARD_PROFILE` selects all seven, as values to spread rather than a
110
- * mode to store. See routeConfig.ts.
111
- *
112
- * Guard dropping is then a GENERATION-TIME decision, never a runtime one.
113
- *
114
- * The emitted router used to compute `DROP_GUARD = <flag> || _env.E2E === 'true'`,
115
- * so setting `E2E=true` in a deployed environment downgraded enforcement even
116
- * when the generator had been told to keep the guard. The bypass now lives once
117
- * in the shared runtime as `resolveDropGuardEnv`: it honours `PGE_DROP_GUARD=true`
118
- * and keeps `E2E=true` working as a deprecated alias with a one-time warning.
119
- * The two modes are not
120
- * equivalent: with the guard, the shape goes to prisma-guard; with it dropped,
121
- * `applyDroppedGuard` applies projection defaults and forced `where` clauses and
122
- * nothing else is validated against the shape.
123
- *
124
- * On an edge runtime that variable is an ordinary config var — set on a staging
125
- * deployment, copied forward, flagged as security-relevant nowhere. A
126
- * deployment's guard behaviour has to be a property of the artifact, not of the
127
- * environment it happens to land in.
128
- *
129
- * The rationale lives here rather than in the emitted file on purpose: generated
130
- * output is an artifact, and a paragraph about a bypass that no longer exists
131
- * would be copied into every router this generator writes.
132
- *
133
- * `updateEach` is refused for the same reason, one step further on — and, again,
134
- * only under the flag. It bypasses guard shapes by design — the endpoint is a
135
- * batch of `{ where, data }` items applied directly — and the only thing between
136
- * it and an unguarded mass mutation is a `console.warn` suppressed in
137
- * production. A warning is not a security boundary; it is advice to whoever
138
- * happens to be reading a development log. Refused at construction rather than
139
- * silently dropped, so a deployment expecting the route learns at boot instead
140
- * of at the first 404.
94
+ * Since phase S the orchestration itself is shared: every route is created by
95
+ * `../routerParts` through the same pipeline stages the other targets and MCP
96
+ * use, and this factory only decides WHICH routes exist. That module is also
97
+ * the phase 9 static boundary — per-op route factories live there, so a
98
+ * bundle importing only selected parts omits the rest.
141
99
  */
142
100
  export function generateHonoRouterFunction({
143
101
  model,
@@ -178,44 +136,38 @@ export function generateHonoRouterFunction({
178
136
  .join('\n\n')
179
137
 
180
138
  return `import { Hono } from 'hono'
181
- import type { Context } from 'hono'
182
- import type { ContentfulStatusCode } from 'hono/utils/http-status'
183
- import { HTTPException } from 'hono/http-exception'
184
139
  import {
185
140
  ${handlerImports}
186
141
  } from './${modelName}Handlers${ext}'
187
142
  import type {
188
143
  RouteConfig,
189
- HonoBeforeHook,
190
- HonoAfterHook,
191
144
  HonoEnvBase,
192
- HonoInternalVariables,
193
145
  GeneratedHonoEnv,
194
- PaginationConfig,
195
146
  PrismaClientLike,
196
147
  } from '../routeConfig.target${ext}'
197
- import { parseQueryParams } from '../parseQueryParams${ext}'
198
- import { normalizePrefix, getEnv, sanitizeKeys, isPlainObject, resolveDropGuardEnv } from '../misc${ext}'
148
+ import { normalizePrefix, getEnv, resolveDropGuardEnv } from '../misc${ext}'
199
149
  import { buildModelOpenApi } from '../buildModelOpenApi${ext}'
200
150
  import {
201
- normalizeOperation,
202
- resolveOperationVariantKey,
203
151
  resolveGuardPolicy,
204
- resolveGuardShapeOnce,
152
+ resolvePostReadsEnabled,
205
153
  validateCountSourceWhere,
206
- validateOperationConfig,
207
154
  validateUpdateEachConfig,
208
- resolvePostReadsEnabled,
209
155
  warnIfUnguardedRoutes,
210
156
  } from '../routeConfig${ext}'
211
- import type { RuntimeOperationOverride } from '../operationRuntime${ext}'
212
- import type { NormalizedOperationConfig } from '../routeConfig${ext}'
213
- import { transformResult } from '../operationRuntime${ext}'
214
- import { mapError } from '../errorMapper${ext}'
215
- import { formatGuardVariantResolutionError } from '../guardVariantError${ext}'
216
- import { mergePaginationConfig } from '../pagination${ext}'
217
- import { applyDroppedGuard } from '../projectionDefaults${ext}'
218
- import type { OpKind } from '../projectionDefaults${ext}'
157
+ import {
158
+ createReadRoute,
159
+ createWriteRoute,
160
+ createUpdateEachRoute,
161
+ opConfigFor,
162
+ normalizeHonoOperation,
163
+ registerOpenApiRoutes,
164
+ sendError,
165
+ type HandlerContext,
166
+ type HonoOpConfig,
167
+ } from '../routerParts${ext}'
168
+ // The legacy router factory preserves pre-parts behavior: materialized-view
169
+ // counting is registered on import. The per-op parts boundary keeps this
170
+ // module out of opt-in small bundles.
219
171
  import { MODEL_FIELDS, MODEL_ENUMS } from './${modelName}Metadata${ext}'
220
172
 
221
173
  ${generateRouteConfigType(modelName, 'HonoBeforeHook', guardShapesImport, importStyle, 'hono', clientImport)}
@@ -226,247 +178,6 @@ const _env = getEnv()
226
178
  // which is upstream behaviour. See generateRouterHono.ts.
227
179
  const DROP_GUARD = ${dropGuard}
228
180
 
229
- /**
230
- * DELIBERATELY NOT RECURSIVE.
231
- *
232
- * A self-referential JSON type sent through \`c.json()\` makes Hono's response
233
- * inference instantiate without a fixed point, and TypeScript answers
234
- * "type instantiation is excessively deep" — in the emitted file, for every
235
- * consumer. One level is all this needs: the value is serialised, not walked.
236
- */
237
- type JsonLike = string | number | boolean | null | unknown[] | Record<string, unknown>
238
-
239
- type OperationConfigLike<TEnv extends HonoEnvBase> = {
240
- authorize?: HonoBeforeHook<TEnv>
241
- override?: RuntimeOperationOverride
242
- before?: HonoBeforeHook<TEnv>[]
243
- after?: HonoAfterHook<TEnv>[]
244
- shape?: unknown
245
- variants?: Record<
246
- string,
247
- {
248
- shape?: unknown
249
- before?: HonoBeforeHook<TEnv>[]
250
- after?: HonoAfterHook<TEnv>[]
251
- }
252
- >
253
- pagination?: Partial<PaginationConfig>
254
- }
255
-
256
- type NormalizedOp<TEnv extends HonoEnvBase> = NormalizedOperationConfig<
257
- HonoBeforeHook<TEnv>,
258
- HonoAfterHook<TEnv>
259
- >
260
-
261
- function normalizeHonoOperation<TEnv extends HonoEnvBase>(
262
- config: OperationConfigLike<TEnv> | undefined,
263
- ): NormalizedOp<TEnv> {
264
- return normalizeOperation<HonoBeforeHook<TEnv>, HonoAfterHook<TEnv>>(config)
265
- }
266
-
267
- type HandlerContext = Context<{ Variables: HonoInternalVariables }>
268
-
269
- async function parseQueryMiddleware(c: HandlerContext): Promise<void> {
270
- const raw = c.req.query() as Record<string, unknown>
271
- if (raw && Object.keys(raw).length > 0) {
272
- c.set('parsedQuery', parseQueryParams(raw) as Record<string, unknown>)
273
- }
274
- }
275
-
276
- async function parseBodyAsQueryMiddleware(c: HandlerContext): Promise<void> {
277
- let body: unknown
278
- try {
279
- body = await c.req.json()
280
- } catch {
281
- throw new HTTPException(400, { message: 'Request body must be a JSON object' })
282
- }
283
- if (!body || typeof body !== 'object' || Array.isArray(body)) {
284
- throw new HTTPException(400, { message: 'Request body must be a JSON object' })
285
- }
286
- c.set('parsedQuery', sanitizeKeys(body as Record<string, unknown>))
287
- }
288
-
289
- async function parseUpdateEachBodyMiddleware(c: HandlerContext): Promise<void> {
290
- let body: unknown
291
- try {
292
- body = await c.req.json()
293
- } catch {
294
- throw new HTTPException(400, { message: 'updateEach body must be an array of { where, data } items' })
295
- }
296
- if (!Array.isArray(body)) {
297
- throw new HTTPException(400, { message: 'updateEach body must be an array of { where, data } items' })
298
- }
299
- c.set('body', body)
300
- }
301
-
302
- async function parseWriteBodyMiddleware(c: HandlerContext): Promise<void> {
303
- let body: unknown
304
- try {
305
- body = await c.req.json()
306
- } catch {
307
- throw new HTTPException(400, { message: 'Request body must be a JSON object' })
308
- }
309
- if (!body || typeof body !== 'object' || Array.isArray(body)) {
310
- throw new HTTPException(400, { message: 'Request body must be a JSON object' })
311
- }
312
- c.set('body', body)
313
- }
314
-
315
- function makeShapeMiddleware<TCtx, TPrisma extends PrismaClientLike, TEnv extends HonoEnvBase>(
316
- config: ${modelName}RouteConfig<TCtx, TPrisma, TEnv>,
317
- opConfig: NormalizedOp<TEnv>,
318
- opKind: OpKind,
319
- ) {
320
- const policy = resolveGuardPolicy(config)
321
-
322
- /**
323
- * The environment bypass, honoured unless the consumer turned it off.
324
- *
325
- * \`PGE_DROP_GUARD=true\` downgrading enforcement in a deployed environment is
326
- * a real hazard, and it is also upstream behaviour (under its deprecated
327
- * \`E2E=true\` spelling) — so it is a control, not a decision made here.
328
- */
329
- const dropGuard = DROP_GUARD || (policy.allowE2EGuardBypass && resolveDropGuardEnv(_env))
330
-
331
- return async (c: Context<GeneratedHonoEnv<TEnv>>): Promise<void> => {
332
- /**
333
- * INTERNAL REQUEST STATE IS SET THROUGH A CONCRETE CONTEXT.
334
- *
335
- * \`GeneratedHonoEnv<TEnv>['Variables']\` is \`HonoInternalVariables & TEnv['Variables']\`,
336
- * and inside this generic function TypeScript cannot know that the consumer's
337
- * half does not also declare \`routeConfig\` — so setting that key straight
338
- * onto \`c\` is unassignable to the intersection and the emitted router does not
339
- * typecheck under \`strict\` for ANY consumer. These keys belong to this
340
- * router, so they are written through the internal shape they were declared
341
- * in.
342
- */
343
- const vars = c as unknown as HandlerContext
344
- vars.set('operationOverride', opConfig.override)
345
- let context: Promise<unknown> | undefined
346
- vars.set('resolveOperationContext', () => context ??= Promise.resolve(config.resolveContext?.(c)))
347
-
348
- const merged = mergePaginationConfig(config.pagination, opConfig.pagination)
349
- if (merged) vars.set('routeConfig', { pagination: merged })
350
-
351
- const headerName = config.guard?.variantHeader || 'x-api-variant'
352
- const headerValue = c.req.header(headerName)
353
- const caller = config.guard?.resolveVariant?.(c) ?? headerValue ?? undefined
354
- if (typeof caller === 'string') vars.set('guardCaller', caller)
355
-
356
- const resolution = resolveOperationVariantKey(opConfig.guardRouting, caller)
357
- if (!resolution.ok) {
358
- vars.set('guardVariantFailure', resolution)
359
- return
360
- }
361
-
362
- const resolvedKey =
363
- opConfig.guardRouting.kind === 'named'
364
- ? resolution.key
365
- : undefined
366
- if (resolvedKey !== undefined) vars.set('guardVariantKey', resolvedKey)
367
-
368
- if (opConfig.guardShape) {
369
- const resolveCtx = typeof config.resolveContext === 'function'
370
- ? vars.get('resolveOperationContext')
371
- : undefined
372
-
373
- /**
374
- * With \`validateResolvedShapes\`, resolved ONCE here and the validated value
375
- * is what travels onward — see resolveGuardShapeOnce. Passing the function
376
- * on would let it return a different shape when it is enforced than when it
377
- * was checked.
378
- *
379
- * Without it the raw shape is passed through untouched, which is upstream
380
- * behaviour: it is resolved at the point of use, and a function returning
381
- * something unusable is not this router's business to refuse.
382
- */
383
- let effectiveShape: Record<string, unknown> | undefined = opConfig.guardShape
384
- if (policy.validateResolvedShapes) {
385
- const resolution = await resolveGuardShapeOnce(opConfig.guardShape, resolvedKey, resolveCtx)
386
- if (!resolution.ok) {
387
- vars.set('guardShapeFailure', resolution.problem)
388
- return
389
- }
390
- // \`ok\` means the resolved value is a shape object; the fallback keeps the
391
- // declared shape rather than storing something the context cannot hold.
392
- effectiveShape = isPlainObject(resolution.shape) ? resolution.shape : opConfig.guardShape
393
- }
394
-
395
- if (!dropGuard) {
396
- vars.set('guardShape', effectiveShape)
397
- } else {
398
- await applyDroppedGuard(
399
- effectiveShape,
400
- resolvedKey,
401
- resolveCtx,
402
- opKind,
403
- {
404
- readQuery: vars.get('parsedQuery'),
405
- writeBody: isPlainObject(vars.get('body'))
406
- ? (vars.get('body') as Record<string, unknown>)
407
- : undefined,
408
- },
409
- () => {
410
- let target = vars.get('parsedQuery')
411
- if (!target) {
412
- target = {}
413
- vars.set('parsedQuery', target)
414
- }
415
- return target
416
- },
417
- () => {
418
- let target = vars.get('body')
419
- if (!isPlainObject(target)) {
420
- target = {}
421
- vars.set('body', target)
422
- }
423
- return target as Record<string, unknown>
424
- },
425
- )
426
- }
427
- }
428
- }
429
- }
430
-
431
- async function runBeforeHooks<TEnv extends HonoEnvBase>(
432
- hooks: readonly HonoBeforeHook<TEnv>[],
433
- c: Context<GeneratedHonoEnv<TEnv>>,
434
- ): Promise<Response | undefined> {
435
- for (const hook of hooks) {
436
- const result = await hook(c)
437
- if (result instanceof Response) return result
438
- }
439
- return undefined
440
- }
441
-
442
- async function runAfterHooks<TEnv extends HonoEnvBase>(
443
- hooks: readonly HonoAfterHook<TEnv>[],
444
- c: Context<GeneratedHonoEnv<TEnv>>,
445
- ): Promise<Response | undefined> {
446
- for (const hook of hooks) {
447
- const result = await hook(c)
448
- if (result instanceof Response) return result
449
- }
450
- return undefined
451
- }
452
-
453
- function sendResult(c: HandlerContext): Response {
454
- const data = c.get('resultData')
455
- const status = (c.get('resultStatus') as number | undefined) ?? 200
456
- if (data === undefined) {
457
- throw new HTTPException(500, { message: 'No data set by handler' })
458
- }
459
- return c.json(transformResult(data) as JsonLike, status as ContentfulStatusCode)
460
- }
461
-
462
- function sendError(c: HandlerContext, error: unknown): Response {
463
- if (error instanceof HTTPException) {
464
- return c.json({ message: error.message }, error.status as ContentfulStatusCode)
465
- }
466
- const httpError = mapError(error)
467
- return c.json({ message: httpError.message }, httpError.status as ContentfulStatusCode)
468
- }
469
-
470
181
  export function ${routerFunctionName}<TCtx = unknown, TPrisma extends PrismaClientLike = PrismaClientLike, TEnv extends HonoEnvBase = HonoEnvBase>(config: ${modelName}RouteConfig<TCtx, TPrisma, TEnv> = {}): Hono<GeneratedHonoEnv<TEnv>> {
471
182
  validateCountSourceWhere(config.pagination?.countSource, '${modelName} pagination')
472
183
  validateCountSourceWhere(
@@ -480,16 +191,33 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma extends PrismaClie
480
191
  warnIfUnguardedRoutes('${modelName}', ['findMany', 'findUnique', 'findUniqueOrThrow', 'findFirst', 'findFirstOrThrow', 'findManyPaginated', 'count', 'aggregate', 'groupBy', 'create', 'createMany', 'createManyAndReturn', 'update', 'updateMany', 'updateManyAndReturn', 'upsert', 'delete', 'deleteMany'], config, isEnabled)
481
192
 
482
193
  const customPrefix = normalizePrefix(config.customUrlPrefix || '')
483
- const modelPrefix = config.addModelPrefix !== false ? '/${modelSegment}' : ''
194
+ const modelPrefix = config.addModelPrefix !== false ? '/${modelSegment}' : ''
484
195
  const basePath = customPrefix + modelPrefix
485
196
 
197
+ const POLICY = resolveGuardPolicy(config)
198
+ const SETTLE_BEFORE_HOOKS = POLICY.guardResolutionOrder === 'before-hooks'
199
+
200
+ /**
201
+ * The environment bypass, honoured unless the consumer turned it off.
202
+ * \`PGE_DROP_GUARD=true\` downgrading enforcement in a deployed environment is
203
+ * a real hazard, and it is also upstream behaviour (under its deprecated
204
+ * \`E2E=true\` spelling) — so it is a control, not a decision made here.
205
+ */
206
+ const dropGuard = DROP_GUARD || (POLICY.allowE2EGuardBypass && resolveDropGuardEnv(_env))
207
+
208
+ const opFor = <K extends keyof ${modelName}RouteConfig<TCtx, TPrisma, TEnv>>(
209
+ key: K,
210
+ ): HonoOpConfig<TEnv> => {
211
+ const raw = config[key] as unknown as Parameters<typeof normalizeHonoOperation<TEnv>>[0]
212
+ return opConfigFor<TEnv>(raw, '${modelName}.' + String(key), POLICY)
213
+ }
214
+
486
215
  const openApiDisabled = config.disableOpenApi === true
487
216
  || (config.disableOpenApi !== false && (
488
217
  _env.NODE_ENV === 'production'
489
218
  || _env.DISABLE_OPENAPI === 'true'
490
219
  ))
491
220
 
492
-
493
221
  let _openApiJsonCache: unknown = undefined
494
222
  const getOpenApiJson = (): unknown => {
495
223
  if (_openApiJsonCache === undefined) {
@@ -529,129 +257,7 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma extends PrismaClie
529
257
  })
530
258
 
531
259
  if (!openApiDisabled) {
532
- const openapiJsonPath = basePath ? \`\${basePath}/openapi.json\` : '/openapi.json'
533
- const openapiYamlPath = basePath ? \`\${basePath}/openapi.yaml\` : '/openapi.yaml'
534
- app.get(openapiJsonPath, (c) => c.json(getOpenApiJson() as JsonLike))
535
- app.get(openapiYamlPath, (c) => {
536
- c.header('Content-Type', 'application/yaml')
537
- return c.body(getOpenApiYaml())
538
- })
539
- }
540
-
541
- const POLICY = resolveGuardPolicy(config)
542
- const SETTLE_BEFORE_HOOKS = POLICY.guardResolutionOrder === 'before-hooks'
543
-
544
- /**
545
- * The guard-resolution failures, raised in one place so both handlers and both
546
- * orderings share exactly one definition of what a failure is.
547
- *
548
- * WHERE it is called is the behavioural difference, and it is the
549
- * \`guardResolutionOrder\` control. \`'before-hooks'\` runs it before any operation
550
- * hook, because a hook that returns a Response — an auth gate, a cache, a
551
- * short-circuit for a known caller — would otherwise answer a request whose
552
- * guard was never established. \`'after-hooks'\` is upstream behaviour.
553
- *
554
- * \`guardShapeFailure\` is only ever set when \`validateResolvedShapes\` is on, so
555
- * that branch is inert rather than merely unreached when it is off.
556
- */
557
- const settleGuard = (c: Context<GeneratedHonoEnv<TEnv>>): void => {
558
- const vars = c as unknown as HandlerContext
559
- const failure = vars.get('guardVariantFailure')
560
- if (failure) {
561
- throw new HTTPException(400, {
562
- message: formatGuardVariantResolutionError(failure),
563
- })
564
- }
565
-
566
- const shapeFailure = vars.get('guardShapeFailure')
567
- if (shapeFailure) {
568
- throw new HTTPException(500, {
569
- message: 'guard shape could not be resolved: ' + shapeFailure,
570
- })
571
- }
572
- }
573
-
574
- const handleRead = (
575
- opConfig: NormalizedOp<TEnv>,
576
- handlerFn: (c: HandlerContext) => Promise<void>,
577
- parseFn: (c: HandlerContext) => Promise<void>,
578
- opKind: OpKind,
579
- ) => async (c: Context<GeneratedHonoEnv<TEnv>>): Promise<Response> => {
580
- try {
581
- const authorized = await runBeforeHooks<TEnv>(opConfig.authorize ? [opConfig.authorize] : [], c)
582
- if (authorized) return authorized
583
- await parseFn(c as unknown as HandlerContext)
584
- await makeShapeMiddleware<TCtx, TPrisma, TEnv>(config, opConfig, opKind)(c)
585
-
586
- // Settled BEFORE any hook can answer the request, when asked for.
587
- if (SETTLE_BEFORE_HOOKS) settleGuard(c)
588
-
589
- const operationBefore = await runBeforeHooks<TEnv>(opConfig.operationBefore, c)
590
- if (operationBefore) return operationBefore
591
-
592
- // Upstream order: hooks first, guard failure after.
593
- if (!SETTLE_BEFORE_HOOKS) settleGuard(c)
594
-
595
- const key = (c as unknown as HandlerContext).get('guardVariantKey')
596
- const variantHooks =
597
- key !== undefined ? opConfig.variantHooks[key] : undefined
598
-
599
- const variantBefore = await runBeforeHooks<TEnv>(variantHooks?.before ?? [], c)
600
- if (variantBefore) return variantBefore
601
- await handlerFn(c as unknown as HandlerContext)
602
- const variantAfter = await runAfterHooks<TEnv>(variantHooks?.after ?? [], c)
603
- if (variantAfter) return variantAfter
604
- const operationAfter = await runAfterHooks<TEnv>(opConfig.operationAfter, c)
605
- if (operationAfter) return operationAfter
606
- return sendResult(c as unknown as HandlerContext)
607
- } catch (error: unknown) {
608
- return sendError(c as unknown as HandlerContext, error)
609
- }
610
- }
611
-
612
- const handleWrite = (
613
- opConfig: NormalizedOp<TEnv>,
614
- handlerFn: (c: HandlerContext) => Promise<void>,
615
- opKind: OpKind,
616
- ) => async (c: Context<GeneratedHonoEnv<TEnv>>): Promise<Response> => {
617
- try {
618
- const authorized = await runBeforeHooks<TEnv>(opConfig.authorize ? [opConfig.authorize] : [], c)
619
- if (authorized) return authorized
620
- await parseWriteBodyMiddleware(c as unknown as HandlerContext)
621
- await makeShapeMiddleware<TCtx, TPrisma, TEnv>(config, opConfig, opKind)(c)
622
-
623
- // Settled BEFORE any hook can answer the request, when asked for.
624
- if (SETTLE_BEFORE_HOOKS) settleGuard(c)
625
-
626
- const operationBefore = await runBeforeHooks<TEnv>(opConfig.operationBefore, c)
627
- if (operationBefore) return operationBefore
628
-
629
- // Upstream order: hooks first, guard failure after.
630
- if (!SETTLE_BEFORE_HOOKS) settleGuard(c)
631
-
632
- const key = (c as unknown as HandlerContext).get('guardVariantKey')
633
- const variantHooks =
634
- key !== undefined ? opConfig.variantHooks[key] : undefined
635
-
636
- const variantBefore = await runBeforeHooks<TEnv>(variantHooks?.before ?? [], c)
637
- if (variantBefore) return variantBefore
638
- await handlerFn(c as unknown as HandlerContext)
639
- const variantAfter = await runAfterHooks<TEnv>(variantHooks?.after ?? [], c)
640
- if (variantAfter) return variantAfter
641
- const operationAfter = await runAfterHooks<TEnv>(opConfig.operationAfter, c)
642
- if (operationAfter) return operationAfter
643
- return sendResult(c as unknown as HandlerContext)
644
- } catch (error: unknown) {
645
- return sendError(c as unknown as HandlerContext, error)
646
- }
647
- }
648
-
649
- const opFor = <K extends keyof ${modelName}RouteConfig<TCtx, TPrisma, TEnv>>(
650
- key: K,
651
- ): NormalizedOp<TEnv> => {
652
- const raw = config[key] as unknown as OperationConfigLike<TEnv> | undefined
653
- validateOperationConfig(raw, '${modelName}.' + String(key), POLICY)
654
- return normalizeHonoOperation(raw)
260
+ registerOpenApiRoutes(app, basePath, getOpenApiJson, getOpenApiYaml)
655
261
  }
656
262
 
657
263
  ${readOpBlocks}
@@ -664,9 +270,6 @@ ${writeOpBlocks}
664
270
  * endpoint is a batch of { where, data } applied directly — and the only thing
665
271
  * between it and an unguarded mass mutation is a console.warn suppressed in
666
272
  * production. A warning is not a security boundary.
667
- *
668
- * Removing the route outright was a breaking change to a published package,
669
- * so by default it registers exactly as it did, warning and all.
670
273
  */
671
274
  if (!POLICY.enableUpdateEach) {
672
275
  throw new Error(
@@ -678,9 +281,9 @@ ${writeOpBlocks}
678
281
  )
679
282
  }
680
283
 
681
- const rawUpdateEach = config.updateEach as unknown as OperationConfigLike<TEnv>
284
+ const rawUpdateEach = config.updateEach as unknown as Parameters<typeof normalizeHonoOperation<TEnv>>[0]
682
285
  validateUpdateEachConfig(rawUpdateEach, '${modelName}.updateEach')
683
- const opConfig = normalizeHonoOperation(rawUpdateEach)
286
+ const opConfig = normalizeHonoOperation<TEnv>(rawUpdateEach)
684
287
  if (opConfig.operationBefore.length === 0 && _env.NODE_ENV !== 'production') {
685
288
  console.warn(
686
289
  '[${modelName}Router] updateEach is enabled without a before hook. ' +
@@ -688,20 +291,7 @@ ${writeOpBlocks}
688
291
  )
689
292
  }
690
293
  const path = basePath ? \`\${basePath}/each\` : '/each'
691
- app.post(path, async (c: Context<GeneratedHonoEnv<TEnv>>): Promise<Response> => {
692
- try {
693
- await parseUpdateEachBodyMiddleware(c as unknown as HandlerContext)
694
- await makeShapeMiddleware<TCtx, TPrisma, TEnv>(config, opConfig, 'noop')(c)
695
- const beforeResponse = await runBeforeHooks<TEnv>(opConfig.operationBefore, c)
696
- if (beforeResponse) return beforeResponse
697
- await ${modelName}UpdateEach(c as unknown as HandlerContext)
698
- const afterResponse = await runAfterHooks<TEnv>(opConfig.operationAfter, c)
699
- if (afterResponse) return afterResponse
700
- return sendResult(c as unknown as HandlerContext)
701
- } catch (error: unknown) {
702
- return sendError(c as unknown as HandlerContext, error)
703
- }
704
- })
294
+ app.post(path, createUpdateEachRoute<TCtx, TPrisma, TEnv>({ config, opConfig, handler: ${modelName}UpdateEach, dropGuard }))
705
295
  }
706
296
 
707
297
  return app