prisma-generator-express 1.65.2 → 1.67.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 (106) hide show
  1. package/README.md +51 -3480
  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.js +69 -6
  8. package/dist/copy/operationDefinitions.js.map +1 -1
  9. package/dist/generators/generateFastifyHandler.d.ts +2 -2
  10. package/dist/generators/generateFastifyHandler.js +15 -5
  11. package/dist/generators/generateFastifyHandler.js.map +1 -1
  12. package/dist/generators/generateHonoHandler.d.ts +2 -2
  13. package/dist/generators/generateHonoHandler.js +12 -5
  14. package/dist/generators/generateHonoHandler.js.map +1 -1
  15. package/dist/generators/generateImportPrismaStatement.d.ts +1 -0
  16. package/dist/generators/generateImportPrismaStatement.js +6 -0
  17. package/dist/generators/generateImportPrismaStatement.js.map +1 -1
  18. package/dist/generators/generateModelMetadata.js +20 -10
  19. package/dist/generators/generateModelMetadata.js.map +1 -1
  20. package/dist/generators/generateOperationCore.d.ts +3 -3
  21. package/dist/generators/generateOperationCore.js +51 -16
  22. package/dist/generators/generateOperationCore.js.map +1 -1
  23. package/dist/generators/generateQueryBuilderHelper.d.ts +1 -1
  24. package/dist/generators/generateQueryBuilderHelper.js +2 -4
  25. package/dist/generators/generateQueryBuilderHelper.js.map +1 -1
  26. package/dist/generators/generateRelationMeta.js +2 -2
  27. package/dist/generators/generateRelationMeta.js.map +1 -1
  28. package/dist/generators/generateRouteConfigType.d.ts +2 -2
  29. package/dist/generators/generateRouteConfigType.js +29 -11
  30. package/dist/generators/generateRouteConfigType.js.map +1 -1
  31. package/dist/generators/generateRouter.d.ts +7 -4
  32. package/dist/generators/generateRouter.js +107 -14
  33. package/dist/generators/generateRouter.js.map +1 -1
  34. package/dist/generators/generateRouterFastify.d.ts +7 -4
  35. package/dist/generators/generateRouterFastify.js +56 -28
  36. package/dist/generators/generateRouterFastify.js.map +1 -1
  37. package/dist/generators/generateRouterHono.d.ts +7 -4
  38. package/dist/generators/generateRouterHono.js +84 -53
  39. package/dist/generators/generateRouterHono.js.map +1 -1
  40. package/dist/generators/generateUnifiedDocs.d.ts +1 -1
  41. package/dist/generators/generateUnifiedDocs.js +15 -8
  42. package/dist/generators/generateUnifiedDocs.js.map +1 -1
  43. package/dist/generators/generateUnifiedHandler.d.ts +2 -2
  44. package/dist/generators/generateUnifiedHandler.js +11 -3
  45. package/dist/generators/generateUnifiedHandler.js.map +1 -1
  46. package/dist/guardOptions.js +12 -7
  47. package/dist/guardOptions.js.map +1 -1
  48. package/dist/index.d.ts +1 -1
  49. package/dist/index.js +22 -1
  50. package/dist/index.js.map +1 -1
  51. package/dist/utils/copyFiles.js +22 -6
  52. package/dist/utils/copyFiles.js.map +1 -1
  53. package/dist/utils/importExt.d.ts +1 -1
  54. package/dist/utils/pathCasing.d.ts +4 -0
  55. package/dist/utils/pathCasing.js +40 -0
  56. package/dist/utils/pathCasing.js.map +1 -0
  57. package/dist/utils/resolveImportStyle.js +13 -5
  58. package/dist/utils/resolveImportStyle.js.map +1 -1
  59. package/dist/utils/writeFileSafely.js.map +1 -1
  60. package/package.json +16 -3
  61. package/src/bin.ts +1 -1
  62. package/src/client/encodeQueryParams.ts +39 -35
  63. package/src/constants.ts +1 -1
  64. package/src/copy/autoIncludePlanner.ts +93 -27
  65. package/src/copy/autoIncludePlannerGuarded.ts +171 -48
  66. package/src/copy/autoIncludeRuntimeGuarded.ts +25 -7
  67. package/src/copy/buildModelOpenApi.ts +153 -50
  68. package/src/copy/concurrency.ts +10 -8
  69. package/src/copy/errorMapper.ts +68 -19
  70. package/src/copy/guardHelpers.ts +30 -9
  71. package/src/copy/materializedCount.ts +17 -5
  72. package/src/copy/misc.ts +88 -68
  73. package/src/copy/ndjson.ts +72 -0
  74. package/src/copy/operationDefinitions.ts +99 -21
  75. package/src/copy/operationRuntime.ts +100 -12
  76. package/src/copy/pagination.ts +25 -11
  77. package/src/copy/parseQueryParams.ts +47 -48
  78. package/src/copy/projectionDefaults.ts +6 -4
  79. package/src/copy/routeConfig.express.ts +16 -4
  80. package/src/copy/routeConfig.fastify.ts +16 -2
  81. package/src/copy/routeConfig.hono.ts +19 -9
  82. package/src/copy/routeConfig.ts +174 -39
  83. package/src/copy/scalarTypes.ts +7 -2
  84. package/src/copy/sse.ts +72 -19
  85. package/src/generators/generateFastifyHandler.ts +24 -13
  86. package/src/generators/generateHonoHandler.ts +21 -13
  87. package/src/generators/generateImportPrismaStatement.ts +93 -81
  88. package/src/generators/generateModelMetadata.ts +24 -12
  89. package/src/generators/generateOperationCore.ts +81 -33
  90. package/src/generators/generateQueryBuilderHelper.ts +20 -4
  91. package/src/generators/generateRelationMeta.ts +23 -11
  92. package/src/generators/generateRouteConfigType.ts +39 -13
  93. package/src/generators/generateRouter.ts +114 -17
  94. package/src/generators/generateRouterFastify.ts +67 -31
  95. package/src/generators/generateRouterHono.ts +108 -60
  96. package/src/generators/generateUnifiedDocs.ts +29 -8
  97. package/src/generators/generateUnifiedHandler.ts +16 -8
  98. package/src/generators/generateUnifiedScalarUI.ts +1 -1
  99. package/src/guardOptions.ts +105 -92
  100. package/src/index.ts +46 -4
  101. package/src/utils/copyFiles.ts +199 -168
  102. package/src/utils/importExt.ts +2 -2
  103. package/src/utils/pathCasing.ts +42 -0
  104. package/src/utils/resolveImportStyle.ts +37 -15
  105. package/src/utils/strings.ts +2 -2
  106. package/src/utils/writeFileSafely.ts +6 -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,11 +56,11 @@ 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
61
  app.post(postPath, handleRead(opConfig, ${handlerName}, parseBodyAsQueryMiddleware, '${opKind}'))
58
62
  }`
59
- : ` if (postReadsEnabled) app.post(path, handleRead(opConfig, ${handlerName}, parseBodyAsQueryMiddleware, '${opKind}'))`
63
+ : ` if (resolvePostReadsEnabled(config.disablePostReads, opConfig.disablePostReads)) app.post(path, handleRead(opConfig, ${handlerName}, parseBodyAsQueryMiddleware, '${opKind}'))`
60
64
  : ''
61
65
 
62
66
  return ` if (isEnabled(config.${meta.configKey})) {
@@ -67,7 +71,10 @@ ${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)
@@ -96,7 +103,7 @@ function emitWriteOp(meta: (typeof OPERATION_METADATA)[number], modelName: strin
96
103
  * - `requireDefaultVariantOptIn` confirm a `default` variant
97
104
  * - `enableUpdateEach` register the batch route (default true)
98
105
  * - `guardResolutionOrder` settle the guard before or after hooks
99
- * - `allowE2EGuardBypass` honour E2E=true (default true)
106
+ * - `allowE2EGuardBypass` honour PGE_DROP_GUARD=true (default true)
100
107
  * - `validateResolvedShapes` check what a shape function returned
101
108
  *
102
109
  * `HARDENED_GUARD_PROFILE` selects all seven, as values to spread rather than a
@@ -106,7 +113,10 @@ function emitWriteOp(meta: (typeof OPERATION_METADATA)[number], modelName: strin
106
113
  *
107
114
  * The emitted router used to compute `DROP_GUARD = <flag> || _env.E2E === 'true'`,
108
115
  * 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
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
110
120
  * equivalent: with the guard, the shape goes to prisma-guard; with it dropped,
111
121
  * `applyDroppedGuard` applies projection defaults and forced `where` clauses and
112
122
  * nothing else is validated against the shape.
@@ -133,32 +143,39 @@ export function generateHonoRouterFunction({
133
143
  model,
134
144
  enums,
135
145
  guardShapesImport,
146
+ clientImport,
136
147
  importStyle,
137
148
  writeStrategy,
138
149
  dropGuard,
150
+ pathCase,
139
151
  }: {
140
152
  model: DMMF.Model
141
153
  enums: DMMF.DatamodelEnum[]
142
154
  guardShapesImport: string | null
155
+ clientImport?: string
143
156
  importStyle: ImportStyle
144
157
  writeStrategy: WriteStrategy
145
158
  dropGuard: boolean
159
+ pathCase: PathCase
146
160
  }): string {
147
161
  const ext = importExt(importStyle)
148
162
  const modelName = model.name
149
- const modelNameLower = modelName.toLowerCase()
163
+ const modelSegment = modelPathSegment(modelName, pathCase)
150
164
  const routerFunctionName = `${modelName}Router`
151
165
 
152
- const handlerImports = OPERATION_METADATA
153
- .map((m) => ` ${modelName}${m.name.charAt(0).toUpperCase() + m.name.slice(1)},`)
154
- .join('\n')
166
+ const handlerImports = OPERATION_METADATA.map(
167
+ (m) => ` ${modelName}${m.name.charAt(0).toUpperCase() + m.name.slice(1)},`,
168
+ ).join('\n')
155
169
 
156
170
  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')
171
+ const writeOps = OPERATION_METADATA.filter(
172
+ (m) => m.kind === 'write' || m.kind === 'batch',
173
+ ).filter((m) => m.name !== 'updateEach')
159
174
 
160
175
  const readOpBlocks = readOps.map((m) => emitReadOp(m, modelName)).join('\n\n')
161
- const writeOpBlocks = writeOps.map((m) => emitWriteOp(m, modelName)).join('\n\n')
176
+ const writeOpBlocks = writeOps
177
+ .map((m) => emitWriteOp(m, modelName))
178
+ .join('\n\n')
162
179
 
163
180
  return `import { Hono } from 'hono'
164
181
  import type { Context } from 'hono'
@@ -175,9 +192,10 @@ import type {
175
192
  HonoInternalVariables,
176
193
  GeneratedHonoEnv,
177
194
  PaginationConfig,
195
+ PrismaClientLike,
178
196
  } from '../routeConfig.target${ext}'
179
197
  import { parseQueryParams } from '../parseQueryParams${ext}'
180
- import { normalizePrefix, getEnv, sanitizeKeys, isPlainObject } from '../misc${ext}'
198
+ import { normalizePrefix, getEnv, sanitizeKeys, isPlainObject, resolveDropGuardEnv } from '../misc${ext}'
181
199
  import { buildModelOpenApi } from '../buildModelOpenApi${ext}'
182
200
  import {
183
201
  normalizeOperation,
@@ -187,7 +205,10 @@ import {
187
205
  validateCountSourceWhere,
188
206
  validateOperationConfig,
189
207
  validateUpdateEachConfig,
208
+ resolvePostReadsEnabled,
209
+ warnIfUnguardedRoutes,
190
210
  } from '../routeConfig${ext}'
211
+ import type { RuntimeOperationOverride } from '../operationRuntime${ext}'
191
212
  import type { NormalizedOperationConfig } from '../routeConfig${ext}'
192
213
  import { transformResult } from '../operationRuntime${ext}'
193
214
  import { mapError } from '../errorMapper${ext}'
@@ -197,7 +218,7 @@ import { applyDroppedGuard } from '../projectionDefaults${ext}'
197
218
  import type { OpKind } from '../projectionDefaults${ext}'
198
219
  import { MODEL_FIELDS, MODEL_ENUMS } from './${modelName}Metadata${ext}'
199
220
 
200
- ${generateRouteConfigType(modelName, 'HonoBeforeHook', guardShapesImport, importStyle, 'hono')}
221
+ ${generateRouteConfigType(modelName, 'HonoBeforeHook', guardShapesImport, importStyle, 'hono', clientImport)}
201
222
  const _env = getEnv()
202
223
 
203
224
  // Fixed at generation time. The \`allowE2EGuardBypass\` control decides whether the
@@ -205,15 +226,19 @@ const _env = getEnv()
205
226
  // which is upstream behaviour. See generateRouterHono.ts.
206
227
  const DROP_GUARD = ${dropGuard}
207
228
 
208
- type JsonLike =
209
- | string
210
- | number
211
- | boolean
212
- | null
213
- | JsonLike[]
214
- | { [k: string]: JsonLike }
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>
215
238
 
216
239
  type OperationConfigLike<TEnv extends HonoEnvBase> = {
240
+ authorize?: HonoBeforeHook<TEnv>
241
+ override?: RuntimeOperationOverride
217
242
  before?: HonoBeforeHook<TEnv>[]
218
243
  after?: HonoAfterHook<TEnv>[]
219
244
  shape?: unknown
@@ -287,7 +312,7 @@ async function parseWriteBodyMiddleware(c: HandlerContext): Promise<void> {
287
312
  c.set('body', body)
288
313
  }
289
314
 
290
- function makeShapeMiddleware<TCtx, TPrisma, TEnv extends HonoEnvBase>(
315
+ function makeShapeMiddleware<TCtx, TPrisma extends PrismaClientLike, TEnv extends HonoEnvBase>(
291
316
  config: ${modelName}RouteConfig<TCtx, TPrisma, TEnv>,
292
317
  opConfig: NormalizedOp<TEnv>,
293
318
  opKind: OpKind,
@@ -297,24 +322,40 @@ function makeShapeMiddleware<TCtx, TPrisma, TEnv extends HonoEnvBase>(
297
322
  /**
298
323
  * The environment bypass, honoured unless the consumer turned it off.
299
324
  *
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.
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.
303
328
  */
304
- const dropGuard = DROP_GUARD || (policy.allowE2EGuardBypass && _env.E2E === 'true')
329
+ const dropGuard = DROP_GUARD || (policy.allowE2EGuardBypass && resolveDropGuardEnv(_env))
305
330
 
306
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
+
307
348
  const merged = mergePaginationConfig(config.pagination, opConfig.pagination)
308
- if (merged) c.set('routeConfig', { pagination: merged })
349
+ if (merged) vars.set('routeConfig', { pagination: merged })
309
350
 
310
351
  const headerName = config.guard?.variantHeader || 'x-api-variant'
311
352
  const headerValue = c.req.header(headerName)
312
353
  const caller = config.guard?.resolveVariant?.(c) ?? headerValue ?? undefined
313
- if (typeof caller === 'string') c.set('guardCaller', caller)
354
+ if (typeof caller === 'string') vars.set('guardCaller', caller)
314
355
 
315
356
  const resolution = resolveOperationVariantKey(opConfig.guardRouting, caller)
316
357
  if (!resolution.ok) {
317
- c.set('guardVariantFailure', resolution)
358
+ vars.set('guardVariantFailure', resolution)
318
359
  return
319
360
  }
320
361
 
@@ -322,11 +363,11 @@ function makeShapeMiddleware<TCtx, TPrisma, TEnv extends HonoEnvBase>(
322
363
  opConfig.guardRouting.kind === 'named'
323
364
  ? resolution.key
324
365
  : undefined
325
- if (resolvedKey !== undefined) c.set('guardVariantKey', resolvedKey)
366
+ if (resolvedKey !== undefined) vars.set('guardVariantKey', resolvedKey)
326
367
 
327
368
  if (opConfig.guardShape) {
328
369
  const resolveCtx = typeof config.resolveContext === 'function'
329
- ? () => (config.resolveContext as (ctx: Context<GeneratedHonoEnv<TEnv>>) => unknown | Promise<unknown>)(c)
370
+ ? vars.get('resolveOperationContext')
330
371
  : undefined
331
372
 
332
373
  /**
@@ -339,18 +380,20 @@ function makeShapeMiddleware<TCtx, TPrisma, TEnv extends HonoEnvBase>(
339
380
  * behaviour: it is resolved at the point of use, and a function returning
340
381
  * something unusable is not this router's business to refuse.
341
382
  */
342
- let effectiveShape: unknown = opConfig.guardShape
383
+ let effectiveShape: Record<string, unknown> | undefined = opConfig.guardShape
343
384
  if (policy.validateResolvedShapes) {
344
385
  const resolution = await resolveGuardShapeOnce(opConfig.guardShape, resolvedKey, resolveCtx)
345
386
  if (!resolution.ok) {
346
- c.set('guardShapeFailure', resolution.problem)
387
+ vars.set('guardShapeFailure', resolution.problem)
347
388
  return
348
389
  }
349
- effectiveShape = resolution.shape
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
350
393
  }
351
394
 
352
395
  if (!dropGuard) {
353
- c.set('guardShape', effectiveShape)
396
+ vars.set('guardShape', effectiveShape)
354
397
  } else {
355
398
  await applyDroppedGuard(
356
399
  effectiveShape,
@@ -358,24 +401,24 @@ function makeShapeMiddleware<TCtx, TPrisma, TEnv extends HonoEnvBase>(
358
401
  resolveCtx,
359
402
  opKind,
360
403
  {
361
- readQuery: c.get('parsedQuery'),
362
- writeBody: isPlainObject(c.get('body'))
363
- ? (c.get('body') as Record<string, unknown>)
404
+ readQuery: vars.get('parsedQuery'),
405
+ writeBody: isPlainObject(vars.get('body'))
406
+ ? (vars.get('body') as Record<string, unknown>)
364
407
  : undefined,
365
408
  },
366
409
  () => {
367
- let target = c.get('parsedQuery')
410
+ let target = vars.get('parsedQuery')
368
411
  if (!target) {
369
412
  target = {}
370
- c.set('parsedQuery', target)
413
+ vars.set('parsedQuery', target)
371
414
  }
372
415
  return target
373
416
  },
374
417
  () => {
375
- let target = c.get('body')
418
+ let target = vars.get('body')
376
419
  if (!isPlainObject(target)) {
377
420
  target = {}
378
- c.set('body', target)
421
+ vars.set('body', target)
379
422
  }
380
423
  return target as Record<string, unknown>
381
424
  },
@@ -424,7 +467,7 @@ function sendError(c: HandlerContext, error: unknown): Response {
424
467
  return c.json({ message: httpError.message }, httpError.status as ContentfulStatusCode)
425
468
  }
426
469
 
427
- export function ${routerFunctionName}<TCtx = unknown, TPrisma = any, TEnv extends HonoEnvBase = HonoEnvBase>(config: ${modelName}RouteConfig<TCtx, TPrisma, TEnv> = {}): Hono<GeneratedHonoEnv<TEnv>> {
470
+ export function ${routerFunctionName}<TCtx = unknown, TPrisma extends PrismaClientLike = PrismaClientLike, TEnv extends HonoEnvBase = HonoEnvBase>(config: ${modelName}RouteConfig<TCtx, TPrisma, TEnv> = {}): Hono<GeneratedHonoEnv<TEnv>> {
428
471
  validateCountSourceWhere(config.pagination?.countSource, '${modelName} pagination')
429
472
  validateCountSourceWhere(
430
473
  (config.findManyPaginated && typeof config.findManyPaginated === 'object' ? config.findManyPaginated : undefined)?.pagination?.countSource,
@@ -434,9 +477,10 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any, TEnv extend
434
477
  const app = new Hono<GeneratedHonoEnv<TEnv>>()
435
478
 
436
479
  const isEnabled = (value: unknown): boolean => value !== false && !!(config.enableAll || value)
480
+ warnIfUnguardedRoutes('${modelName}', ['findMany', 'findUnique', 'findUniqueOrThrow', 'findFirst', 'findFirstOrThrow', 'findManyPaginated', 'count', 'aggregate', 'groupBy', 'create', 'createMany', 'createManyAndReturn', 'update', 'updateMany', 'updateManyAndReturn', 'upsert', 'delete', 'deleteMany'], config, isEnabled)
437
481
 
438
482
  const customPrefix = normalizePrefix(config.customUrlPrefix || '')
439
- const modelPrefix = config.addModelPrefix !== false ? '/${modelNameLower}' : ''
483
+ const modelPrefix = config.addModelPrefix !== false ? '/${modelSegment}' : ''
440
484
  const basePath = customPrefix + modelPrefix
441
485
 
442
486
  const openApiDisabled = config.disableOpenApi === true
@@ -445,7 +489,6 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any, TEnv extend
445
489
  || _env.DISABLE_OPENAPI === 'true'
446
490
  ))
447
491
 
448
- const postReadsEnabled = !config.disablePostReads
449
492
 
450
493
  let _openApiJsonCache: unknown = undefined
451
494
  const getOpenApiJson = (): unknown => {
@@ -454,8 +497,8 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any, TEnv extend
454
497
  '${modelName}',
455
498
  MODEL_FIELDS as unknown as Parameters<typeof buildModelOpenApi>[1],
456
499
  MODEL_ENUMS as unknown as Parameters<typeof buildModelOpenApi>[2],
457
- config as RouteConfig,
458
- { format: 'json', writeStrategy: '${writeStrategy}' },
500
+ config as unknown as Parameters<typeof buildModelOpenApi>[3],
501
+ { format: 'json', writeStrategy: '${writeStrategy}', pathSegment: '${modelSegment}' },
459
502
  )
460
503
  }
461
504
  return _openApiJsonCache
@@ -467,14 +510,14 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any, TEnv extend
467
510
  '${modelName}',
468
511
  MODEL_FIELDS as unknown as Parameters<typeof buildModelOpenApi>[1],
469
512
  MODEL_ENUMS as unknown as Parameters<typeof buildModelOpenApi>[2],
470
- config as RouteConfig,
471
- { format: 'yaml', writeStrategy: '${writeStrategy}' },
513
+ config as unknown as Parameters<typeof buildModelOpenApi>[3],
514
+ { format: 'yaml', writeStrategy: '${writeStrategy}', pathSegment: '${modelSegment}' },
472
515
  ) as string
473
516
  }
474
517
  return _openApiYamlCache
475
518
  }
476
519
 
477
- if (config.queryBuilder && config.queryBuilder !== false && _env.NODE_ENV !== 'production') {
520
+ if (config.queryBuilder && _env.NODE_ENV !== 'production') {
478
521
  console.warn(
479
522
  '[${modelName}Router] queryBuilder config is present but Hono target does not auto-start it. ' +
480
523
  'Run \`npx prisma-query-builder-ui\` in a separate process.',
@@ -482,7 +525,7 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any, TEnv extend
482
525
  }
483
526
 
484
527
  app.onError((err, c) => {
485
- return sendError(c as HandlerContext, err)
528
+ return sendError(c as unknown as HandlerContext, err)
486
529
  })
487
530
 
488
531
  if (!openApiDisabled) {
@@ -512,14 +555,15 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any, TEnv extend
512
555
  * that branch is inert rather than merely unreached when it is off.
513
556
  */
514
557
  const settleGuard = (c: Context<GeneratedHonoEnv<TEnv>>): void => {
515
- const failure = c.get('guardVariantFailure')
558
+ const vars = c as unknown as HandlerContext
559
+ const failure = vars.get('guardVariantFailure')
516
560
  if (failure) {
517
561
  throw new HTTPException(400, {
518
562
  message: formatGuardVariantResolutionError(failure),
519
563
  })
520
564
  }
521
565
 
522
- const shapeFailure = c.get('guardShapeFailure')
566
+ const shapeFailure = vars.get('guardShapeFailure')
523
567
  if (shapeFailure) {
524
568
  throw new HTTPException(500, {
525
569
  message: 'guard shape could not be resolved: ' + shapeFailure,
@@ -534,6 +578,8 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any, TEnv extend
534
578
  opKind: OpKind,
535
579
  ) => async (c: Context<GeneratedHonoEnv<TEnv>>): Promise<Response> => {
536
580
  try {
581
+ const authorized = await runBeforeHooks<TEnv>(opConfig.authorize ? [opConfig.authorize] : [], c)
582
+ if (authorized) return authorized
537
583
  await parseFn(c as unknown as HandlerContext)
538
584
  await makeShapeMiddleware<TCtx, TPrisma, TEnv>(config, opConfig, opKind)(c)
539
585
 
@@ -546,7 +592,7 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any, TEnv extend
546
592
  // Upstream order: hooks first, guard failure after.
547
593
  if (!SETTLE_BEFORE_HOOKS) settleGuard(c)
548
594
 
549
- const key = c.get('guardVariantKey')
595
+ const key = (c as unknown as HandlerContext).get('guardVariantKey')
550
596
  const variantHooks =
551
597
  key !== undefined ? opConfig.variantHooks[key] : undefined
552
598
 
@@ -569,6 +615,8 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any, TEnv extend
569
615
  opKind: OpKind,
570
616
  ) => async (c: Context<GeneratedHonoEnv<TEnv>>): Promise<Response> => {
571
617
  try {
618
+ const authorized = await runBeforeHooks<TEnv>(opConfig.authorize ? [opConfig.authorize] : [], c)
619
+ if (authorized) return authorized
572
620
  await parseWriteBodyMiddleware(c as unknown as HandlerContext)
573
621
  await makeShapeMiddleware<TCtx, TPrisma, TEnv>(config, opConfig, opKind)(c)
574
622
 
@@ -581,7 +629,7 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any, TEnv extend
581
629
  // Upstream order: hooks first, guard failure after.
582
630
  if (!SETTLE_BEFORE_HOOKS) settleGuard(c)
583
631
 
584
- const key = c.get('guardVariantKey')
632
+ const key = (c as unknown as HandlerContext).get('guardVariantKey')
585
633
  const variantHooks =
586
634
  key !== undefined ? opConfig.variantHooks[key] : undefined
587
635
 
@@ -659,4 +707,4 @@ ${writeOpBlocks}
659
707
  return app
660
708
  }
661
709
  `
662
- }
710
+ }
@@ -4,18 +4,21 @@ import type { Target } from '../constants'
4
4
 
5
5
  export function generateUnifiedDocs(
6
6
  modelNames: string[],
7
+ pathSegments: Record<string, string>,
7
8
  target: Target,
8
9
  importStyle: ImportStyle,
9
10
  ): string {
10
11
  const ext = importExt(importStyle)
11
- if (target === 'fastify') return generateFastify(modelNames, ext)
12
- if (target === 'hono') return generateHono(modelNames, ext)
13
- return generateExpress(modelNames, ext)
12
+ if (target === 'fastify')
13
+ return generateFastify(modelNames, pathSegments, ext)
14
+ if (target === 'hono') return generateHono(modelNames, pathSegments, ext)
15
+ return generateExpress(modelNames, pathSegments, ext)
14
16
  }
15
17
 
16
18
  const renderIndexFn = `function renderIndex(title: string, modelNames: string[], basePath: string): string {
19
+ const segments = MODEL_PATH_SEGMENTS as Record<string, string>
17
20
  const links = modelNames
18
- .map((n) => '<li><a href="' + basePath + '/' + n.toLowerCase() + '">' + n + '</a></li>')
21
+ .map((n) => '<li><a href="' + basePath + '/' + (segments[n] ?? n.toLowerCase()) + '">' + n + '</a></li>')
19
22
  .join('')
20
23
 
21
24
  return '<!DOCTYPE html><html><head><meta charset="utf-8" /><title>' + title +
@@ -27,7 +30,11 @@ const renderIndexFn = `function renderIndex(title: string, modelNames: string[],
27
30
  '</div></body></html>'
28
31
  }`
29
32
 
30
- function generateExpress(modelNames: string[], ext: string): string {
33
+ function generateExpress(
34
+ modelNames: string[],
35
+ pathSegments: Record<string, string>,
36
+ ext: string,
37
+ ): string {
31
38
  return `import type { Router } from 'express'
32
39
  import { getEnv, removeTrailingSlash } from './misc${ext}'
33
40
 
@@ -35,6 +42,8 @@ const _env = getEnv()
35
42
 
36
43
  const MODELS = ${JSON.stringify(modelNames)} as const
37
44
 
45
+ const MODEL_PATH_SEGMENTS = ${JSON.stringify(pathSegments)} as const
46
+
38
47
  interface UnifiedDocsConfig {
39
48
  title?: string
40
49
  basePath?: string
@@ -62,7 +71,11 @@ export default registerUnifiedDocs
62
71
  `
63
72
  }
64
73
 
65
- function generateFastify(modelNames: string[], ext: string): string {
74
+ function generateFastify(
75
+ modelNames: string[],
76
+ pathSegments: Record<string, string>,
77
+ ext: string,
78
+ ): string {
66
79
  return `import type { FastifyInstance } from 'fastify'
67
80
  import { getEnv, removeTrailingSlash } from './misc${ext}'
68
81
 
@@ -70,6 +83,8 @@ const _env = getEnv()
70
83
 
71
84
  const MODELS = ${JSON.stringify(modelNames)} as const
72
85
 
86
+ const MODEL_PATH_SEGMENTS = ${JSON.stringify(pathSegments)} as const
87
+
73
88
  interface UnifiedDocsConfig {
74
89
  title?: string
75
90
  basePath?: string
@@ -100,7 +115,11 @@ export default registerUnifiedDocs
100
115
  `
101
116
  }
102
117
 
103
- function generateHono(modelNames: string[], ext: string): string {
118
+ function generateHono(
119
+ modelNames: string[],
120
+ pathSegments: Record<string, string>,
121
+ ext: string,
122
+ ): string {
104
123
  return `import type { Hono } from 'hono'
105
124
  import { getEnv, removeTrailingSlash } from './misc${ext}'
106
125
 
@@ -108,6 +127,8 @@ const _env = getEnv()
108
127
 
109
128
  const MODELS = ${JSON.stringify(modelNames)} as const
110
129
 
130
+ const MODEL_PATH_SEGMENTS = ${JSON.stringify(pathSegments)} as const
131
+
111
132
  interface UnifiedDocsConfig {
112
133
  title?: string
113
134
  basePath?: string
@@ -133,4 +154,4 @@ ${renderIndexFn}
133
154
 
134
155
  export default registerUnifiedDocs
135
156
  `
136
- }
157
+ }
@@ -1,5 +1,5 @@
1
- import { DMMF } from '@prisma/generator-helper'
2
- import { ImportStyle } from '../utils/resolveImportStyle'
1
+ import type { DMMF } from '@prisma/generator-helper'
2
+ import type { ImportStyle } from '../utils/resolveImportStyle'
3
3
  import { importExt } from '../utils/importExt'
4
4
  import { OPERATION_METADATA } from '../copy/operationDefinitions'
5
5
 
@@ -14,10 +14,11 @@ export function generateUnifiedHandler(options: UnifiedHandlerOptions): string {
14
14
 
15
15
  const dispatchOps = OPERATION_METADATA.filter((m) => m.name !== 'updateEach')
16
16
 
17
- const handlers = dispatchOps.map((meta) => {
18
- const exportName = `${modelName}${meta.name.charAt(0).toUpperCase() + meta.name.slice(1)}`
17
+ const handlers = dispatchOps
18
+ .map((meta) => {
19
+ const exportName = `${modelName}${meta.name.charAt(0).toUpperCase() + meta.name.slice(1)}`
19
20
 
20
- return `
21
+ return `
21
22
  export async function ${exportName}(
22
23
  req: Request,
23
24
  res: Response,
@@ -30,11 +31,12 @@ export async function ${exportName}(
30
31
  next(mapError(error))
31
32
  }
32
33
  }`
33
- }).join('\n')
34
+ })
35
+ .join('\n')
34
36
 
35
37
  return `import type { Request, Response, NextFunction } from 'express'
36
38
  import * as core from './${modelName}Core${ext}'
37
- import { OperationContext } from '../operationRuntime${ext}'
39
+ import type { OperationContext, RuntimeOperationOverride } from '../operationRuntime${ext}'
38
40
  import { mapError } from '../errorMapper${ext}'
39
41
 
40
42
  type ExtendedRequest = Request & {
@@ -48,6 +50,9 @@ type LocalsBag = {
48
50
  routeConfig?: { pagination?: unknown }
49
51
  guardShape?: Record<string, unknown>
50
52
  guardCaller?: string
53
+ guardVariantKey?: string
54
+ operationOverride?: RuntimeOperationOverride
55
+ resolveOperationContext?: () => unknown | Promise<unknown>
51
56
  data?: unknown
52
57
  }
53
58
 
@@ -55,6 +60,8 @@ function buildContext(req: Request, res: Response): OperationContext {
55
60
  const extReq = req as ExtendedRequest
56
61
  const locals = res.locals as LocalsBag
57
62
  return {
63
+ operationOverride: locals.operationOverride,
64
+ resolveOperationContext: locals.resolveOperationContext,
58
65
  prisma: extReq.prisma,
59
66
  postgres: extReq.postgres,
60
67
  sqlite: extReq.sqlite,
@@ -62,9 +69,10 @@ function buildContext(req: Request, res: Response): OperationContext {
62
69
  body: req.body,
63
70
  guardShape: locals.guardShape,
64
71
  guardCaller: locals.guardCaller,
72
+ guardVariantKey: locals.guardVariantKey,
65
73
  paginationConfig: (locals.routeConfig?.pagination) as OperationContext['paginationConfig'],
66
74
  }
67
75
  }
68
76
  ${handlers}
69
77
  `
70
- }
78
+ }
@@ -215,4 +215,4 @@ const MODEL_CONTEXT: DocsModelContext = {
215
215
 
216
216
  ${docsExport}
217
217
  `
218
- }
218
+ }