prisma-generator-express 1.66.0 → 1.68.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 (130) hide show
  1. package/README.md +188 -3432
  2. package/dist/client/encodeQueryParams.js +3 -1
  3. package/dist/client/encodeQueryParams.js.map +1 -1
  4. package/dist/copy/misc.d.ts +1 -0
  5. package/dist/copy/misc.js +14 -0
  6. package/dist/copy/misc.js.map +1 -1
  7. package/dist/copy/operationDefinitions.d.ts +4 -1
  8. package/dist/copy/operationDefinitions.js +133 -9
  9. package/dist/copy/operationDefinitions.js.map +1 -1
  10. package/dist/generators/generateFastifyHandler.d.ts +2 -2
  11. package/dist/generators/generateFastifyHandler.js +68 -12
  12. package/dist/generators/generateFastifyHandler.js.map +1 -1
  13. package/dist/generators/generateHonoHandler.d.ts +2 -2
  14. package/dist/generators/generateHonoHandler.js +53 -6
  15. package/dist/generators/generateHonoHandler.js.map +1 -1
  16. package/dist/generators/generateHonoOpenApi.d.ts +13 -0
  17. package/dist/generators/generateHonoOpenApi.js +70 -0
  18. package/dist/generators/generateHonoOpenApi.js.map +1 -0
  19. package/dist/generators/generateImportPrismaStatement.d.ts +1 -0
  20. package/dist/generators/generateImportPrismaStatement.js +6 -0
  21. package/dist/generators/generateImportPrismaStatement.js.map +1 -1
  22. package/dist/generators/generateMcpApp.d.ts +10 -0
  23. package/dist/generators/generateMcpApp.js +113 -0
  24. package/dist/generators/generateMcpApp.js.map +1 -0
  25. package/dist/generators/generateModelMcp.d.ts +9 -0
  26. package/dist/generators/generateModelMcp.js +145 -0
  27. package/dist/generators/generateModelMcp.js.map +1 -0
  28. package/dist/generators/generateModelMetadata.js +29 -15
  29. package/dist/generators/generateModelMetadata.js.map +1 -1
  30. package/dist/generators/generateOperationCore.d.ts +3 -3
  31. package/dist/generators/generateOperationCore.js +51 -16
  32. package/dist/generators/generateOperationCore.js.map +1 -1
  33. package/dist/generators/generateRelationMeta.js +2 -2
  34. package/dist/generators/generateRelationMeta.js.map +1 -1
  35. package/dist/generators/generateRouteConfigType.d.ts +2 -2
  36. package/dist/generators/generateRouteConfigType.js +29 -11
  37. package/dist/generators/generateRouteConfigType.js.map +1 -1
  38. package/dist/generators/generateRouter.d.ts +7 -4
  39. package/dist/generators/generateRouter.js +164 -65
  40. package/dist/generators/generateRouter.js.map +1 -1
  41. package/dist/generators/generateRouterFastify.d.ts +7 -4
  42. package/dist/generators/generateRouterFastify.js +106 -86
  43. package/dist/generators/generateRouterFastify.js.map +1 -1
  44. package/dist/generators/generateRouterHono.d.ts +7 -4
  45. package/dist/generators/generateRouterHono.js +59 -414
  46. package/dist/generators/generateRouterHono.js.map +1 -1
  47. package/dist/generators/generateRouterPartsHono.d.ts +14 -0
  48. package/dist/generators/generateRouterPartsHono.js +180 -0
  49. package/dist/generators/generateRouterPartsHono.js.map +1 -0
  50. package/dist/generators/generateUnifiedDocs.d.ts +1 -1
  51. package/dist/generators/generateUnifiedDocs.js +15 -8
  52. package/dist/generators/generateUnifiedDocs.js.map +1 -1
  53. package/dist/generators/generateUnifiedHandler.d.ts +2 -2
  54. package/dist/generators/generateUnifiedHandler.js +42 -18
  55. package/dist/generators/generateUnifiedHandler.js.map +1 -1
  56. package/dist/guardOptions.js +12 -7
  57. package/dist/guardOptions.js.map +1 -1
  58. package/dist/index.d.ts +3 -1
  59. package/dist/index.js +123 -4
  60. package/dist/index.js.map +1 -1
  61. package/dist/utils/copyFiles.d.ts +1 -1
  62. package/dist/utils/copyFiles.js +42 -7
  63. package/dist/utils/copyFiles.js.map +1 -1
  64. package/dist/utils/importExt.d.ts +1 -1
  65. package/dist/utils/mcpGate.d.ts +10 -0
  66. package/dist/utils/mcpGate.js +118 -0
  67. package/dist/utils/mcpGate.js.map +1 -0
  68. package/dist/utils/pathCasing.d.ts +4 -0
  69. package/dist/utils/pathCasing.js +40 -0
  70. package/dist/utils/pathCasing.js.map +1 -0
  71. package/dist/utils/resolveImportStyle.js +13 -5
  72. package/dist/utils/resolveImportStyle.js.map +1 -1
  73. package/dist/utils/writeFileSafely.js +6 -0
  74. package/dist/utils/writeFileSafely.js.map +1 -1
  75. package/package.json +22 -5
  76. package/src/bin.ts +1 -1
  77. package/src/client/encodeQueryParams.ts +39 -35
  78. package/src/constants.ts +1 -1
  79. package/src/copy/autoIncludePlanner.ts +93 -27
  80. package/src/copy/autoIncludePlannerGuarded.ts +171 -48
  81. package/src/copy/autoIncludeRuntimeGuarded.ts +25 -7
  82. package/src/copy/buildModelOpenApi.ts +229 -176
  83. package/src/copy/concurrency.ts +10 -8
  84. package/src/copy/docsRenderer.ts +14 -15
  85. package/src/copy/errorMapper.ts +68 -19
  86. package/src/copy/guardHelpers.ts +30 -9
  87. package/src/copy/materializedCount.ts +17 -5
  88. package/src/copy/mcpRuntime.ts +1017 -0
  89. package/src/copy/misc.ts +88 -68
  90. package/src/copy/ndjson.ts +72 -0
  91. package/src/copy/operationDefinitions.ts +177 -25
  92. package/src/copy/operationPipeline.ts +267 -0
  93. package/src/copy/operationRuntime.ts +124 -26
  94. package/src/copy/operationSchemas.ts +2773 -0
  95. package/src/copy/pagination.ts +35 -13
  96. package/src/copy/parseQueryParams.ts +47 -48
  97. package/src/copy/projectionDefaults.ts +6 -4
  98. package/src/copy/routeConfig.express.ts +16 -4
  99. package/src/copy/routeConfig.fastify.ts +16 -2
  100. package/src/copy/routeConfig.hono.ts +46 -14
  101. package/src/copy/routeConfig.ts +175 -40
  102. package/src/copy/routerParts.ts +524 -0
  103. package/src/copy/scalarTypes.ts +7 -2
  104. package/src/copy/sse.ts +72 -19
  105. package/src/generators/generateFastifyHandler.ts +77 -20
  106. package/src/generators/generateHonoHandler.ts +62 -14
  107. package/src/generators/generateHonoOpenApi.ts +93 -0
  108. package/src/generators/generateImportPrismaStatement.ts +93 -81
  109. package/src/generators/generateMcpApp.ts +133 -0
  110. package/src/generators/generateModelMcp.ts +208 -0
  111. package/src/generators/generateModelMetadata.ts +40 -22
  112. package/src/generators/generateOperationCore.ts +81 -33
  113. package/src/generators/generateRelationMeta.ts +23 -11
  114. package/src/generators/generateRouteConfigType.ts +39 -13
  115. package/src/generators/generateRouter.ts +171 -68
  116. package/src/generators/generateRouterFastify.ts +117 -89
  117. package/src/generators/generateRouterHono.ts +84 -464
  118. package/src/generators/generateRouterPartsHono.ts +220 -0
  119. package/src/generators/generateUnifiedDocs.ts +29 -8
  120. package/src/generators/generateUnifiedHandler.ts +47 -24
  121. package/src/generators/generateUnifiedScalarUI.ts +1 -1
  122. package/src/guardOptions.ts +105 -92
  123. package/src/index.ts +161 -6
  124. package/src/utils/copyFiles.ts +235 -168
  125. package/src/utils/importExt.ts +2 -2
  126. package/src/utils/mcpGate.ts +157 -0
  127. package/src/utils/pathCasing.ts +42 -0
  128. package/src/utils/resolveImportStyle.ts +37 -15
  129. package/src/utils/strings.ts +2 -2
  130. package/src/utils/writeFileSafely.ts +12 -2
@@ -1,8 +1,9 @@
1
- import { DMMF } from '@prisma/generator-helper'
1
+ import type { DMMF } from '@prisma/generator-helper'
2
2
  import { generateRouteConfigType } from './generateRouteConfigType'
3
- import { ImportStyle } from '../utils/resolveImportStyle'
3
+ import type { ImportStyle } from '../utils/resolveImportStyle'
4
4
  import { importExt } from '../utils/importExt'
5
- import { WriteStrategy } from '../constants'
5
+ import type { WriteStrategy } from '../constants'
6
+ import { modelPathSegment, type PathCase } from '../utils/pathCasing'
6
7
  import { OPERATION_METADATA } from '../copy/operationDefinitions'
7
8
 
8
9
  function pathExpr(suffix: string): string {
@@ -44,7 +45,10 @@ function opKindFor(opName: string): string {
44
45
  }
45
46
  }
46
47
 
47
- function emitReadOp(meta: (typeof OPERATION_METADATA)[number], modelName: string): string {
48
+ function emitReadOp(
49
+ meta: (typeof OPERATION_METADATA)[number],
50
+ modelName: string,
51
+ ): string {
48
52
  const c = meta.name.charAt(0).toUpperCase() + meta.name.slice(1)
49
53
  const handlerName = `${modelName}${c}`
50
54
  const pathValue = pathExpr(meta.pathSuffix)
@@ -52,22 +56,25 @@ function emitReadOp(meta: (typeof OPERATION_METADATA)[number], modelName: string
52
56
 
53
57
  const postReadLine = meta.supportsPostRead
54
58
  ? meta.name === 'findMany'
55
- ? ` if (postReadsEnabled) {
59
+ ? ` if (resolvePostReadsEnabled(config.disablePostReads, opConfig.disablePostReads)) {
56
60
  const postPath = basePath ? \`\${basePath}/read\` : '/read'
57
- 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 }))
58
62
  }`
59
- : ` if (postReadsEnabled) 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 }))`
60
64
  : ''
61
65
 
62
66
  return ` if (isEnabled(config.${meta.configKey})) {
63
67
  const opConfig = opFor('${meta.configKey}')
64
68
  const path = ${pathValue}
65
- 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 }))
66
70
  ${postReadLine}
67
71
  }`
68
72
  }
69
73
 
70
- function emitWriteOp(meta: (typeof OPERATION_METADATA)[number], modelName: string): string {
74
+ function emitWriteOp(
75
+ meta: (typeof OPERATION_METADATA)[number],
76
+ modelName: string,
77
+ ): string {
71
78
  const c = meta.name.charAt(0).toUpperCase() + meta.name.slice(1)
72
79
  const handlerName = `${modelName}${c}`
73
80
  const pathValue = pathExpr(meta.pathSuffix)
@@ -76,128 +83,94 @@ function emitWriteOp(meta: (typeof OPERATION_METADATA)[number], modelName: strin
76
83
  return ` if (isEnabled(config.${meta.configKey})) {
77
84
  const opConfig = opFor('${meta.configKey}')
78
85
  const path = ${pathValue}
79
- 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 }))
80
87
  }`
81
88
  }
82
89
 
83
90
  /**
84
91
  * Guard behaviour is SEVEN INDEPENDENT OPT-IN CONTROLS on the route config.
92
+ * See routeConfig.ts; the long rationale lives with the controls.
85
93
  *
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.
106
- *
107
- * The emitted router used to compute `DROP_GUARD = <flag> || _env.E2E === 'true'`,
108
- * so setting `E2E=true` in a deployed environment downgraded enforcement even
109
- * when the generator had been told to keep the guard. The two modes are not
110
- * equivalent: with the guard, the shape goes to prisma-guard; with it dropped,
111
- * `applyDroppedGuard` applies projection defaults and forced `where` clauses and
112
- * nothing else is validated against the shape.
113
- *
114
- * On an edge runtime that variable is an ordinary config var — set on a staging
115
- * deployment, copied forward, flagged as security-relevant nowhere. A
116
- * deployment's guard behaviour has to be a property of the artifact, not of the
117
- * environment it happens to land in.
118
- *
119
- * The rationale lives here rather than in the emitted file on purpose: generated
120
- * output is an artifact, and a paragraph about a bypass that no longer exists
121
- * would be copied into every router this generator writes.
122
- *
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.
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.
131
99
  */
132
100
  export function generateHonoRouterFunction({
133
101
  model,
134
102
  enums,
135
103
  guardShapesImport,
104
+ clientImport,
136
105
  importStyle,
137
106
  writeStrategy,
138
107
  dropGuard,
108
+ pathCase,
139
109
  }: {
140
110
  model: DMMF.Model
141
111
  enums: DMMF.DatamodelEnum[]
142
112
  guardShapesImport: string | null
113
+ clientImport?: string
143
114
  importStyle: ImportStyle
144
115
  writeStrategy: WriteStrategy
145
116
  dropGuard: boolean
117
+ pathCase: PathCase
146
118
  }): string {
147
119
  const ext = importExt(importStyle)
148
120
  const modelName = model.name
149
- const modelNameLower = modelName.toLowerCase()
121
+ const modelSegment = modelPathSegment(modelName, pathCase)
150
122
  const routerFunctionName = `${modelName}Router`
151
123
 
152
- const handlerImports = OPERATION_METADATA
153
- .map((m) => ` ${modelName}${m.name.charAt(0).toUpperCase() + m.name.slice(1)},`)
154
- .join('\n')
124
+ const handlerImports = OPERATION_METADATA.map(
125
+ (m) => ` ${modelName}${m.name.charAt(0).toUpperCase() + m.name.slice(1)},`,
126
+ ).join('\n')
155
127
 
156
128
  const readOps = OPERATION_METADATA.filter((m) => m.kind === 'read')
157
- const writeOps = OPERATION_METADATA.filter((m) => m.kind === 'write' || m.kind === 'batch')
158
- .filter((m) => m.name !== 'updateEach')
129
+ const writeOps = OPERATION_METADATA.filter(
130
+ (m) => m.kind === 'write' || m.kind === 'batch',
131
+ ).filter((m) => m.name !== 'updateEach')
159
132
 
160
133
  const readOpBlocks = readOps.map((m) => emitReadOp(m, modelName)).join('\n\n')
161
- const writeOpBlocks = writeOps.map((m) => emitWriteOp(m, modelName)).join('\n\n')
134
+ const writeOpBlocks = writeOps
135
+ .map((m) => emitWriteOp(m, modelName))
136
+ .join('\n\n')
162
137
 
163
138
  return `import { Hono } from 'hono'
164
- import type { Context } from 'hono'
165
- import type { ContentfulStatusCode } from 'hono/utils/http-status'
166
- import { HTTPException } from 'hono/http-exception'
167
139
  import {
168
140
  ${handlerImports}
169
141
  } from './${modelName}Handlers${ext}'
170
142
  import type {
171
143
  RouteConfig,
172
- HonoBeforeHook,
173
- HonoAfterHook,
174
144
  HonoEnvBase,
175
- HonoInternalVariables,
176
145
  GeneratedHonoEnv,
177
- PaginationConfig,
146
+ PrismaClientLike,
178
147
  } from '../routeConfig.target${ext}'
179
- import { parseQueryParams } from '../parseQueryParams${ext}'
180
- import { normalizePrefix, getEnv, sanitizeKeys, isPlainObject } from '../misc${ext}'
148
+ import { normalizePrefix, getEnv, resolveDropGuardEnv } from '../misc${ext}'
181
149
  import { buildModelOpenApi } from '../buildModelOpenApi${ext}'
182
150
  import {
183
- normalizeOperation,
184
- resolveOperationVariantKey,
185
151
  resolveGuardPolicy,
186
- resolveGuardShapeOnce,
152
+ resolvePostReadsEnabled,
187
153
  validateCountSourceWhere,
188
- validateOperationConfig,
189
154
  validateUpdateEachConfig,
155
+ warnIfUnguardedRoutes,
190
156
  } from '../routeConfig${ext}'
191
- import type { NormalizedOperationConfig } from '../routeConfig${ext}'
192
- import { transformResult } from '../operationRuntime${ext}'
193
- import { mapError } from '../errorMapper${ext}'
194
- import { formatGuardVariantResolutionError } from '../guardVariantError${ext}'
195
- import { mergePaginationConfig } from '../pagination${ext}'
196
- import { applyDroppedGuard } from '../projectionDefaults${ext}'
197
- 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.
198
171
  import { MODEL_FIELDS, MODEL_ENUMS } from './${modelName}Metadata${ext}'
199
172
 
200
- ${generateRouteConfigType(modelName, 'HonoBeforeHook', guardShapesImport, importStyle, 'hono')}
173
+ ${generateRouteConfigType(modelName, 'HonoBeforeHook', guardShapesImport, importStyle, 'hono', clientImport)}
201
174
  const _env = getEnv()
202
175
 
203
176
  // Fixed at generation time. The \`allowE2EGuardBypass\` control decides whether the
@@ -205,243 +178,7 @@ const _env = getEnv()
205
178
  // which is upstream behaviour. See generateRouterHono.ts.
206
179
  const DROP_GUARD = ${dropGuard}
207
180
 
208
- /**
209
- * DELIBERATELY NOT RECURSIVE.
210
- *
211
- * A self-referential JSON type sent through \`c.json()\` makes Hono's response
212
- * inference instantiate without a fixed point, and TypeScript answers
213
- * "type instantiation is excessively deep" — in the emitted file, for every
214
- * consumer. One level is all this needs: the value is serialised, not walked.
215
- */
216
- type JsonLike = string | number | boolean | null | unknown[] | Record<string, unknown>
217
-
218
- type OperationConfigLike<TEnv extends HonoEnvBase> = {
219
- before?: HonoBeforeHook<TEnv>[]
220
- after?: HonoAfterHook<TEnv>[]
221
- shape?: unknown
222
- variants?: Record<
223
- string,
224
- {
225
- shape?: unknown
226
- before?: HonoBeforeHook<TEnv>[]
227
- after?: HonoAfterHook<TEnv>[]
228
- }
229
- >
230
- pagination?: Partial<PaginationConfig>
231
- }
232
-
233
- type NormalizedOp<TEnv extends HonoEnvBase> = NormalizedOperationConfig<
234
- HonoBeforeHook<TEnv>,
235
- HonoAfterHook<TEnv>
236
- >
237
-
238
- function normalizeHonoOperation<TEnv extends HonoEnvBase>(
239
- config: OperationConfigLike<TEnv> | undefined,
240
- ): NormalizedOp<TEnv> {
241
- return normalizeOperation<HonoBeforeHook<TEnv>, HonoAfterHook<TEnv>>(config)
242
- }
243
-
244
- type HandlerContext = Context<{ Variables: HonoInternalVariables }>
245
-
246
- async function parseQueryMiddleware(c: HandlerContext): Promise<void> {
247
- const raw = c.req.query() as Record<string, unknown>
248
- if (raw && Object.keys(raw).length > 0) {
249
- c.set('parsedQuery', parseQueryParams(raw) as Record<string, unknown>)
250
- }
251
- }
252
-
253
- async function parseBodyAsQueryMiddleware(c: HandlerContext): Promise<void> {
254
- let body: unknown
255
- try {
256
- body = await c.req.json()
257
- } catch {
258
- throw new HTTPException(400, { message: 'Request body must be a JSON object' })
259
- }
260
- if (!body || typeof body !== 'object' || Array.isArray(body)) {
261
- throw new HTTPException(400, { message: 'Request body must be a JSON object' })
262
- }
263
- c.set('parsedQuery', sanitizeKeys(body as Record<string, unknown>))
264
- }
265
-
266
- async function parseUpdateEachBodyMiddleware(c: HandlerContext): Promise<void> {
267
- let body: unknown
268
- try {
269
- body = await c.req.json()
270
- } catch {
271
- throw new HTTPException(400, { message: 'updateEach body must be an array of { where, data } items' })
272
- }
273
- if (!Array.isArray(body)) {
274
- throw new HTTPException(400, { message: 'updateEach body must be an array of { where, data } items' })
275
- }
276
- c.set('body', body)
277
- }
278
-
279
- async function parseWriteBodyMiddleware(c: HandlerContext): Promise<void> {
280
- let body: unknown
281
- try {
282
- body = await c.req.json()
283
- } catch {
284
- throw new HTTPException(400, { message: 'Request body must be a JSON object' })
285
- }
286
- if (!body || typeof body !== 'object' || Array.isArray(body)) {
287
- throw new HTTPException(400, { message: 'Request body must be a JSON object' })
288
- }
289
- c.set('body', body)
290
- }
291
-
292
- function makeShapeMiddleware<TCtx, TPrisma, TEnv extends HonoEnvBase>(
293
- config: ${modelName}RouteConfig<TCtx, TPrisma, TEnv>,
294
- opConfig: NormalizedOp<TEnv>,
295
- opKind: OpKind,
296
- ) {
297
- const policy = resolveGuardPolicy(config)
298
-
299
- /**
300
- * The environment bypass, honoured unless the consumer turned it off.
301
- *
302
- * \`E2E=true\` downgrading enforcement in a deployed environment is a real
303
- * hazard, and it is also upstream behaviour — so it is a control, not a
304
- * decision made here.
305
- */
306
- const dropGuard = DROP_GUARD || (policy.allowE2EGuardBypass && _env.E2E === 'true')
307
-
308
- return async (c: Context<GeneratedHonoEnv<TEnv>>): Promise<void> => {
309
- /**
310
- * INTERNAL REQUEST STATE IS SET THROUGH A CONCRETE CONTEXT.
311
- *
312
- * \`GeneratedHonoEnv<TEnv>['Variables']\` is \`HonoInternalVariables & TEnv['Variables']\`,
313
- * and inside this generic function TypeScript cannot know that the consumer's
314
- * half does not also declare \`routeConfig\` — so setting that key straight
315
- * onto \`c\` is unassignable to the intersection and the emitted router does not
316
- * typecheck under \`strict\` for ANY consumer. These keys belong to this
317
- * router, so they are written through the internal shape they were declared
318
- * in.
319
- */
320
- const vars = c as unknown as HandlerContext
321
-
322
- const merged = mergePaginationConfig(config.pagination, opConfig.pagination)
323
- if (merged) vars.set('routeConfig', { pagination: merged })
324
-
325
- const headerName = config.guard?.variantHeader || 'x-api-variant'
326
- const headerValue = c.req.header(headerName)
327
- const caller = config.guard?.resolveVariant?.(c) ?? headerValue ?? undefined
328
- if (typeof caller === 'string') vars.set('guardCaller', caller)
329
-
330
- const resolution = resolveOperationVariantKey(opConfig.guardRouting, caller)
331
- if (!resolution.ok) {
332
- vars.set('guardVariantFailure', resolution)
333
- return
334
- }
335
-
336
- const resolvedKey =
337
- opConfig.guardRouting.kind === 'named'
338
- ? resolution.key
339
- : undefined
340
- if (resolvedKey !== undefined) vars.set('guardVariantKey', resolvedKey)
341
-
342
- if (opConfig.guardShape) {
343
- const resolveCtx = typeof config.resolveContext === 'function'
344
- ? () => (config.resolveContext as unknown as (ctx: Context<GeneratedHonoEnv<TEnv>>) => unknown | Promise<unknown>)(c)
345
- : undefined
346
-
347
- /**
348
- * With \`validateResolvedShapes\`, resolved ONCE here and the validated value
349
- * is what travels onward — see resolveGuardShapeOnce. Passing the function
350
- * on would let it return a different shape when it is enforced than when it
351
- * was checked.
352
- *
353
- * Without it the raw shape is passed through untouched, which is upstream
354
- * behaviour: it is resolved at the point of use, and a function returning
355
- * something unusable is not this router's business to refuse.
356
- */
357
- let effectiveShape: Record<string, unknown> | undefined = opConfig.guardShape
358
- if (policy.validateResolvedShapes) {
359
- const resolution = await resolveGuardShapeOnce(opConfig.guardShape, resolvedKey, resolveCtx)
360
- if (!resolution.ok) {
361
- vars.set('guardShapeFailure', resolution.problem)
362
- return
363
- }
364
- // \`ok\` means the resolved value is a shape object; the fallback keeps the
365
- // declared shape rather than storing something the context cannot hold.
366
- effectiveShape = isPlainObject(resolution.shape) ? resolution.shape : opConfig.guardShape
367
- }
368
-
369
- if (!dropGuard) {
370
- vars.set('guardShape', effectiveShape)
371
- } else {
372
- await applyDroppedGuard(
373
- effectiveShape,
374
- resolvedKey,
375
- resolveCtx,
376
- opKind,
377
- {
378
- readQuery: vars.get('parsedQuery'),
379
- writeBody: isPlainObject(vars.get('body'))
380
- ? (vars.get('body') as Record<string, unknown>)
381
- : undefined,
382
- },
383
- () => {
384
- let target = vars.get('parsedQuery')
385
- if (!target) {
386
- target = {}
387
- vars.set('parsedQuery', target)
388
- }
389
- return target
390
- },
391
- () => {
392
- let target = vars.get('body')
393
- if (!isPlainObject(target)) {
394
- target = {}
395
- vars.set('body', target)
396
- }
397
- return target as Record<string, unknown>
398
- },
399
- )
400
- }
401
- }
402
- }
403
- }
404
-
405
- async function runBeforeHooks<TEnv extends HonoEnvBase>(
406
- hooks: readonly HonoBeforeHook<TEnv>[],
407
- c: Context<GeneratedHonoEnv<TEnv>>,
408
- ): Promise<Response | undefined> {
409
- for (const hook of hooks) {
410
- const result = await hook(c)
411
- if (result instanceof Response) return result
412
- }
413
- return undefined
414
- }
415
-
416
- async function runAfterHooks<TEnv extends HonoEnvBase>(
417
- hooks: readonly HonoAfterHook<TEnv>[],
418
- c: Context<GeneratedHonoEnv<TEnv>>,
419
- ): Promise<Response | undefined> {
420
- for (const hook of hooks) {
421
- const result = await hook(c)
422
- if (result instanceof Response) return result
423
- }
424
- return undefined
425
- }
426
-
427
- function sendResult(c: HandlerContext): Response {
428
- const data = c.get('resultData')
429
- const status = (c.get('resultStatus') as number | undefined) ?? 200
430
- if (data === undefined) {
431
- throw new HTTPException(500, { message: 'No data set by handler' })
432
- }
433
- return c.json(transformResult(data) as JsonLike, status as ContentfulStatusCode)
434
- }
435
-
436
- function sendError(c: HandlerContext, error: unknown): Response {
437
- if (error instanceof HTTPException) {
438
- return c.json({ message: error.message }, error.status as ContentfulStatusCode)
439
- }
440
- const httpError = mapError(error)
441
- return c.json({ message: httpError.message }, httpError.status as ContentfulStatusCode)
442
- }
443
-
444
- export function ${routerFunctionName}<TCtx = unknown, TPrisma = any, TEnv extends HonoEnvBase = HonoEnvBase>(config: ${modelName}RouteConfig<TCtx, TPrisma, TEnv> = {}): Hono<GeneratedHonoEnv<TEnv>> {
181
+ export function ${routerFunctionName}<TCtx = unknown, TPrisma extends PrismaClientLike = PrismaClientLike, TEnv extends HonoEnvBase = HonoEnvBase>(config: ${modelName}RouteConfig<TCtx, TPrisma, TEnv> = {}): Hono<GeneratedHonoEnv<TEnv>> {
445
182
  validateCountSourceWhere(config.pagination?.countSource, '${modelName} pagination')
446
183
  validateCountSourceWhere(
447
184
  (config.findManyPaginated && typeof config.findManyPaginated === 'object' ? config.findManyPaginated : undefined)?.pagination?.countSource,
@@ -451,19 +188,36 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any, TEnv extend
451
188
  const app = new Hono<GeneratedHonoEnv<TEnv>>()
452
189
 
453
190
  const isEnabled = (value: unknown): boolean => value !== false && !!(config.enableAll || value)
191
+ warnIfUnguardedRoutes('${modelName}', ['findMany', 'findUnique', 'findUniqueOrThrow', 'findFirst', 'findFirstOrThrow', 'findManyPaginated', 'count', 'aggregate', 'groupBy', 'create', 'createMany', 'createManyAndReturn', 'update', 'updateMany', 'updateManyAndReturn', 'upsert', 'delete', 'deleteMany'], config, isEnabled)
454
192
 
455
193
  const customPrefix = normalizePrefix(config.customUrlPrefix || '')
456
- const modelPrefix = config.addModelPrefix !== false ? '/${modelNameLower}' : ''
194
+ const modelPrefix = config.addModelPrefix !== false ? '/${modelSegment}' : ''
457
195
  const basePath = customPrefix + modelPrefix
458
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
+
459
215
  const openApiDisabled = config.disableOpenApi === true
460
216
  || (config.disableOpenApi !== false && (
461
217
  _env.NODE_ENV === 'production'
462
218
  || _env.DISABLE_OPENAPI === 'true'
463
219
  ))
464
220
 
465
- const postReadsEnabled = !config.disablePostReads
466
-
467
221
  let _openApiJsonCache: unknown = undefined
468
222
  const getOpenApiJson = (): unknown => {
469
223
  if (_openApiJsonCache === undefined) {
@@ -472,7 +226,7 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any, TEnv extend
472
226
  MODEL_FIELDS as unknown as Parameters<typeof buildModelOpenApi>[1],
473
227
  MODEL_ENUMS as unknown as Parameters<typeof buildModelOpenApi>[2],
474
228
  config as unknown as Parameters<typeof buildModelOpenApi>[3],
475
- { format: 'json', writeStrategy: '${writeStrategy}' },
229
+ { format: 'json', writeStrategy: '${writeStrategy}', pathSegment: '${modelSegment}' },
476
230
  )
477
231
  }
478
232
  return _openApiJsonCache
@@ -485,7 +239,7 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any, TEnv extend
485
239
  MODEL_FIELDS as unknown as Parameters<typeof buildModelOpenApi>[1],
486
240
  MODEL_ENUMS as unknown as Parameters<typeof buildModelOpenApi>[2],
487
241
  config as unknown as Parameters<typeof buildModelOpenApi>[3],
488
- { format: 'yaml', writeStrategy: '${writeStrategy}' },
242
+ { format: 'yaml', writeStrategy: '${writeStrategy}', pathSegment: '${modelSegment}' },
489
243
  ) as string
490
244
  }
491
245
  return _openApiYamlCache
@@ -503,125 +257,7 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any, TEnv extend
503
257
  })
504
258
 
505
259
  if (!openApiDisabled) {
506
- const openapiJsonPath = basePath ? \`\${basePath}/openapi.json\` : '/openapi.json'
507
- const openapiYamlPath = basePath ? \`\${basePath}/openapi.yaml\` : '/openapi.yaml'
508
- app.get(openapiJsonPath, (c) => c.json(getOpenApiJson() as JsonLike))
509
- app.get(openapiYamlPath, (c) => {
510
- c.header('Content-Type', 'application/yaml')
511
- return c.body(getOpenApiYaml())
512
- })
513
- }
514
-
515
- const POLICY = resolveGuardPolicy(config)
516
- const SETTLE_BEFORE_HOOKS = POLICY.guardResolutionOrder === 'before-hooks'
517
-
518
- /**
519
- * The guard-resolution failures, raised in one place so both handlers and both
520
- * orderings share exactly one definition of what a failure is.
521
- *
522
- * WHERE it is called is the behavioural difference, and it is the
523
- * \`guardResolutionOrder\` control. \`'before-hooks'\` runs it before any operation
524
- * hook, because a hook that returns a Response — an auth gate, a cache, a
525
- * short-circuit for a known caller — would otherwise answer a request whose
526
- * guard was never established. \`'after-hooks'\` is upstream behaviour.
527
- *
528
- * \`guardShapeFailure\` is only ever set when \`validateResolvedShapes\` is on, so
529
- * that branch is inert rather than merely unreached when it is off.
530
- */
531
- const settleGuard = (c: Context<GeneratedHonoEnv<TEnv>>): void => {
532
- const vars = c as unknown as HandlerContext
533
- const failure = vars.get('guardVariantFailure')
534
- if (failure) {
535
- throw new HTTPException(400, {
536
- message: formatGuardVariantResolutionError(failure),
537
- })
538
- }
539
-
540
- const shapeFailure = vars.get('guardShapeFailure')
541
- if (shapeFailure) {
542
- throw new HTTPException(500, {
543
- message: 'guard shape could not be resolved: ' + shapeFailure,
544
- })
545
- }
546
- }
547
-
548
- const handleRead = (
549
- opConfig: NormalizedOp<TEnv>,
550
- handlerFn: (c: HandlerContext) => Promise<void>,
551
- parseFn: (c: HandlerContext) => Promise<void>,
552
- opKind: OpKind,
553
- ) => async (c: Context<GeneratedHonoEnv<TEnv>>): Promise<Response> => {
554
- try {
555
- await parseFn(c as unknown as HandlerContext)
556
- await makeShapeMiddleware<TCtx, TPrisma, TEnv>(config, opConfig, opKind)(c)
557
-
558
- // Settled BEFORE any hook can answer the request, when asked for.
559
- if (SETTLE_BEFORE_HOOKS) settleGuard(c)
560
-
561
- const operationBefore = await runBeforeHooks<TEnv>(opConfig.operationBefore, c)
562
- if (operationBefore) return operationBefore
563
-
564
- // Upstream order: hooks first, guard failure after.
565
- if (!SETTLE_BEFORE_HOOKS) settleGuard(c)
566
-
567
- const key = (c as unknown as HandlerContext).get('guardVariantKey')
568
- const variantHooks =
569
- key !== undefined ? opConfig.variantHooks[key] : undefined
570
-
571
- const variantBefore = await runBeforeHooks<TEnv>(variantHooks?.before ?? [], c)
572
- if (variantBefore) return variantBefore
573
- await handlerFn(c as unknown as HandlerContext)
574
- const variantAfter = await runAfterHooks<TEnv>(variantHooks?.after ?? [], c)
575
- if (variantAfter) return variantAfter
576
- const operationAfter = await runAfterHooks<TEnv>(opConfig.operationAfter, c)
577
- if (operationAfter) return operationAfter
578
- return sendResult(c as unknown as HandlerContext)
579
- } catch (error: unknown) {
580
- return sendError(c as unknown as HandlerContext, error)
581
- }
582
- }
583
-
584
- const handleWrite = (
585
- opConfig: NormalizedOp<TEnv>,
586
- handlerFn: (c: HandlerContext) => Promise<void>,
587
- opKind: OpKind,
588
- ) => async (c: Context<GeneratedHonoEnv<TEnv>>): Promise<Response> => {
589
- try {
590
- await parseWriteBodyMiddleware(c as unknown as HandlerContext)
591
- await makeShapeMiddleware<TCtx, TPrisma, TEnv>(config, opConfig, opKind)(c)
592
-
593
- // Settled BEFORE any hook can answer the request, when asked for.
594
- if (SETTLE_BEFORE_HOOKS) settleGuard(c)
595
-
596
- const operationBefore = await runBeforeHooks<TEnv>(opConfig.operationBefore, c)
597
- if (operationBefore) return operationBefore
598
-
599
- // Upstream order: hooks first, guard failure after.
600
- if (!SETTLE_BEFORE_HOOKS) settleGuard(c)
601
-
602
- const key = (c as unknown as HandlerContext).get('guardVariantKey')
603
- const variantHooks =
604
- key !== undefined ? opConfig.variantHooks[key] : undefined
605
-
606
- const variantBefore = await runBeforeHooks<TEnv>(variantHooks?.before ?? [], c)
607
- if (variantBefore) return variantBefore
608
- await handlerFn(c as unknown as HandlerContext)
609
- const variantAfter = await runAfterHooks<TEnv>(variantHooks?.after ?? [], c)
610
- if (variantAfter) return variantAfter
611
- const operationAfter = await runAfterHooks<TEnv>(opConfig.operationAfter, c)
612
- if (operationAfter) return operationAfter
613
- return sendResult(c as unknown as HandlerContext)
614
- } catch (error: unknown) {
615
- return sendError(c as unknown as HandlerContext, error)
616
- }
617
- }
618
-
619
- const opFor = <K extends keyof ${modelName}RouteConfig<TCtx, TPrisma, TEnv>>(
620
- key: K,
621
- ): NormalizedOp<TEnv> => {
622
- const raw = config[key] as unknown as OperationConfigLike<TEnv> | undefined
623
- validateOperationConfig(raw, '${modelName}.' + String(key), POLICY)
624
- return normalizeHonoOperation(raw)
260
+ registerOpenApiRoutes(app, basePath, getOpenApiJson, getOpenApiYaml)
625
261
  }
626
262
 
627
263
  ${readOpBlocks}
@@ -634,9 +270,6 @@ ${writeOpBlocks}
634
270
  * endpoint is a batch of { where, data } applied directly — and the only thing
635
271
  * between it and an unguarded mass mutation is a console.warn suppressed in
636
272
  * production. A warning is not a security boundary.
637
- *
638
- * Removing the route outright was a breaking change to a published package,
639
- * so by default it registers exactly as it did, warning and all.
640
273
  */
641
274
  if (!POLICY.enableUpdateEach) {
642
275
  throw new Error(
@@ -648,9 +281,9 @@ ${writeOpBlocks}
648
281
  )
649
282
  }
650
283
 
651
- const rawUpdateEach = config.updateEach as unknown as OperationConfigLike<TEnv>
284
+ const rawUpdateEach = config.updateEach as unknown as Parameters<typeof normalizeHonoOperation<TEnv>>[0]
652
285
  validateUpdateEachConfig(rawUpdateEach, '${modelName}.updateEach')
653
- const opConfig = normalizeHonoOperation(rawUpdateEach)
286
+ const opConfig = normalizeHonoOperation<TEnv>(rawUpdateEach)
654
287
  if (opConfig.operationBefore.length === 0 && _env.NODE_ENV !== 'production') {
655
288
  console.warn(
656
289
  '[${modelName}Router] updateEach is enabled without a before hook. ' +
@@ -658,23 +291,10 @@ ${writeOpBlocks}
658
291
  )
659
292
  }
660
293
  const path = basePath ? \`\${basePath}/each\` : '/each'
661
- app.post(path, async (c: Context<GeneratedHonoEnv<TEnv>>): Promise<Response> => {
662
- try {
663
- await parseUpdateEachBodyMiddleware(c as unknown as HandlerContext)
664
- await makeShapeMiddleware<TCtx, TPrisma, TEnv>(config, opConfig, 'noop')(c)
665
- const beforeResponse = await runBeforeHooks<TEnv>(opConfig.operationBefore, c)
666
- if (beforeResponse) return beforeResponse
667
- await ${modelName}UpdateEach(c as unknown as HandlerContext)
668
- const afterResponse = await runAfterHooks<TEnv>(opConfig.operationAfter, c)
669
- if (afterResponse) return afterResponse
670
- return sendResult(c as unknown as HandlerContext)
671
- } catch (error: unknown) {
672
- return sendError(c as unknown as HandlerContext, error)
673
- }
674
- })
294
+ app.post(path, createUpdateEachRoute<TCtx, TPrisma, TEnv>({ config, opConfig, handler: ${modelName}UpdateEach, dropGuard }))
675
295
  }
676
296
 
677
297
  return app
678
298
  }
679
299
  `
680
- }
300
+ }