prisma-generator-express 1.66.0 → 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 (102) hide show
  1. package/README.md +37 -3476
  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/generateRelationMeta.js +2 -2
  24. package/dist/generators/generateRelationMeta.js.map +1 -1
  25. package/dist/generators/generateRouteConfigType.d.ts +2 -2
  26. package/dist/generators/generateRouteConfigType.js +29 -11
  27. package/dist/generators/generateRouteConfigType.js.map +1 -1
  28. package/dist/generators/generateRouter.d.ts +7 -4
  29. package/dist/generators/generateRouter.js +107 -14
  30. package/dist/generators/generateRouter.js.map +1 -1
  31. package/dist/generators/generateRouterFastify.d.ts +7 -4
  32. package/dist/generators/generateRouterFastify.js +56 -28
  33. package/dist/generators/generateRouterFastify.js.map +1 -1
  34. package/dist/generators/generateRouterHono.d.ts +7 -4
  35. package/dist/generators/generateRouterHono.js +36 -23
  36. package/dist/generators/generateRouterHono.js.map +1 -1
  37. package/dist/generators/generateUnifiedDocs.d.ts +1 -1
  38. package/dist/generators/generateUnifiedDocs.js +15 -8
  39. package/dist/generators/generateUnifiedDocs.js.map +1 -1
  40. package/dist/generators/generateUnifiedHandler.d.ts +2 -2
  41. package/dist/generators/generateUnifiedHandler.js +11 -3
  42. package/dist/generators/generateUnifiedHandler.js.map +1 -1
  43. package/dist/guardOptions.js +12 -7
  44. package/dist/guardOptions.js.map +1 -1
  45. package/dist/index.d.ts +1 -1
  46. package/dist/index.js +22 -1
  47. package/dist/index.js.map +1 -1
  48. package/dist/utils/copyFiles.js +22 -6
  49. package/dist/utils/copyFiles.js.map +1 -1
  50. package/dist/utils/importExt.d.ts +1 -1
  51. package/dist/utils/pathCasing.d.ts +4 -0
  52. package/dist/utils/pathCasing.js +40 -0
  53. package/dist/utils/pathCasing.js.map +1 -0
  54. package/dist/utils/resolveImportStyle.js +13 -5
  55. package/dist/utils/resolveImportStyle.js.map +1 -1
  56. package/dist/utils/writeFileSafely.js.map +1 -1
  57. package/package.json +5 -3
  58. package/src/bin.ts +1 -1
  59. package/src/client/encodeQueryParams.ts +39 -35
  60. package/src/constants.ts +1 -1
  61. package/src/copy/autoIncludePlanner.ts +93 -27
  62. package/src/copy/autoIncludePlannerGuarded.ts +171 -48
  63. package/src/copy/autoIncludeRuntimeGuarded.ts +25 -7
  64. package/src/copy/buildModelOpenApi.ts +153 -50
  65. package/src/copy/concurrency.ts +10 -8
  66. package/src/copy/errorMapper.ts +68 -19
  67. package/src/copy/guardHelpers.ts +30 -9
  68. package/src/copy/materializedCount.ts +17 -5
  69. package/src/copy/misc.ts +88 -68
  70. package/src/copy/ndjson.ts +72 -0
  71. package/src/copy/operationDefinitions.ts +99 -21
  72. package/src/copy/operationRuntime.ts +100 -12
  73. package/src/copy/pagination.ts +25 -11
  74. package/src/copy/parseQueryParams.ts +47 -48
  75. package/src/copy/projectionDefaults.ts +6 -4
  76. package/src/copy/routeConfig.express.ts +16 -4
  77. package/src/copy/routeConfig.fastify.ts +16 -2
  78. package/src/copy/routeConfig.hono.ts +12 -9
  79. package/src/copy/routeConfig.ts +174 -39
  80. package/src/copy/scalarTypes.ts +7 -2
  81. package/src/copy/sse.ts +72 -19
  82. package/src/generators/generateFastifyHandler.ts +24 -13
  83. package/src/generators/generateHonoHandler.ts +21 -13
  84. package/src/generators/generateImportPrismaStatement.ts +93 -81
  85. package/src/generators/generateModelMetadata.ts +24 -12
  86. package/src/generators/generateOperationCore.ts +81 -33
  87. package/src/generators/generateRelationMeta.ts +23 -11
  88. package/src/generators/generateRouteConfigType.ts +39 -13
  89. package/src/generators/generateRouter.ts +114 -17
  90. package/src/generators/generateRouterFastify.ts +67 -31
  91. package/src/generators/generateRouterHono.ts +60 -30
  92. package/src/generators/generateUnifiedDocs.ts +29 -8
  93. package/src/generators/generateUnifiedHandler.ts +16 -8
  94. package/src/generators/generateUnifiedScalarUI.ts +1 -1
  95. package/src/guardOptions.ts +105 -92
  96. package/src/index.ts +46 -4
  97. package/src/utils/copyFiles.ts +199 -168
  98. package/src/utils/importExt.ts +2 -2
  99. package/src/utils/pathCasing.ts +42 -0
  100. package/src/utils/resolveImportStyle.ts +37 -15
  101. package/src/utils/strings.ts +2 -2
  102. 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, FindManyPaginatedMode } from '../constants'
5
+ import type { WriteStrategy, FindManyPaginatedMode } 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 {
@@ -55,11 +56,11 @@ function emitReadOp(
55
56
 
56
57
  const postReadLine = meta.supportsPostRead
57
58
  ? meta.name === 'findMany'
58
- ? ` if (postReadsEnabled) {
59
+ ? ` if (resolvePostReadsEnabled(config.disablePostReads, opConfig.disablePostReads)) {
59
60
  const postPath = basePath ? \`\${basePath}/read\` : '/read'
60
61
  instance.post(postPath, handleRead(opConfig, ${handlerName}, parseBodyAsQueryHook, '${opKind}'))
61
62
  }`
62
- : ` if (postReadsEnabled) instance.post(path, handleRead(opConfig, ${handlerName}, parseBodyAsQueryHook, '${opKind}'))`
63
+ : ` if (resolvePostReadsEnabled(config.disablePostReads, opConfig.disablePostReads)) instance.post(path, handleRead(opConfig, ${handlerName}, parseBodyAsQueryHook, '${opKind}'))`
63
64
  : ''
64
65
 
65
66
  return ` if (isEnabled(config.${meta.configKey})) {
@@ -90,34 +91,41 @@ export function generateFastifyRouterFunction({
90
91
  model,
91
92
  enums,
92
93
  guardShapesImport,
94
+ clientImport,
93
95
  importStyle,
94
96
  writeStrategy,
95
97
  findManyPaginatedMode,
96
98
  dropGuard,
99
+ pathCase,
97
100
  }: {
98
101
  model: DMMF.Model
99
102
  enums: DMMF.DatamodelEnum[]
100
103
  guardShapesImport: string | null
104
+ clientImport?: string
101
105
  importStyle: ImportStyle
102
106
  writeStrategy: WriteStrategy
103
107
  findManyPaginatedMode: FindManyPaginatedMode
104
108
  dropGuard: boolean
109
+ pathCase: PathCase
105
110
  }): string {
106
111
  const ext = importExt(importStyle)
107
112
  const modelName = model.name
108
- const modelNameLower = modelName.toLowerCase()
109
- const routerFunctionName = `${modelName}Router`
113
+ const modelSegment = modelPathSegment(model.name, pathCase)
114
+ const routerFunctionName = `${model.name}Router`
110
115
 
111
- const handlerImports = OPERATION_METADATA
112
- .map((m) => ` ${modelName}${m.name.charAt(0).toUpperCase() + m.name.slice(1)},`)
113
- .join('\n')
116
+ const handlerImports = OPERATION_METADATA.map(
117
+ (m) => ` ${modelName}${m.name.charAt(0).toUpperCase() + m.name.slice(1)},`,
118
+ ).join('\n')
114
119
 
115
120
  const readOps = OPERATION_METADATA.filter((m) => m.kind === 'read')
116
- const writeOps = OPERATION_METADATA.filter((m) => m.kind === 'write' || m.kind === 'batch')
117
- .filter((m) => m.name !== 'updateEach')
121
+ const writeOps = OPERATION_METADATA.filter(
122
+ (m) => m.kind === 'write' || m.kind === 'batch',
123
+ ).filter((m) => m.name !== 'updateEach')
118
124
 
119
125
  const readOpBlocks = readOps.map((m) => emitReadOp(m, modelName)).join('\n\n')
120
- const writeOpBlocks = writeOps.map((m) => emitWriteOp(m, modelName)).join('\n\n')
126
+ const writeOpBlocks = writeOps
127
+ .map((m) => emitWriteOp(m, modelName))
128
+ .join('\n\n')
121
129
 
122
130
  return `import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyError } from 'fastify'
123
131
  import { startQueryBuilder } from '../queryBuilder${ext}'
@@ -129,9 +137,11 @@ import type {
129
137
  FastifyHookHandler,
130
138
  FindManyPaginatedMode,
131
139
  PaginationConfig,
140
+ PrismaClientLike,
141
+ QueryBuilderConfig,
132
142
  } from '../routeConfig.target${ext}'
133
143
  import { parseQueryParams } from '../parseQueryParams${ext}'
134
- import { sanitizeKeys, normalizePrefix, getEnv, isPlainObject } from '../misc${ext}'
144
+ import { sanitizeKeys, normalizePrefix, getEnv, isPlainObject, resolveDropGuardEnv } from '../misc${ext}'
135
145
  import { buildModelOpenApi } from '../buildModelOpenApi${ext}'
136
146
  import {
137
147
  normalizeOperation,
@@ -139,7 +149,10 @@ import {
139
149
  validateCountSourceWhere,
140
150
  validateOperationConfig,
141
151
  validateUpdateEachConfig,
152
+ resolvePostReadsEnabled,
153
+ warnIfUnguardedRoutes,
142
154
  } from '../routeConfig${ext}'
155
+ import type { RuntimeOperationOverride } from '../operationRuntime${ext}'
143
156
  import type { NormalizedOperationConfig } from '../routeConfig${ext}'
144
157
  import type { OperationContext } from '../operationRuntime${ext}'
145
158
  import { transformResult } from '../operationRuntime${ext}'
@@ -151,13 +164,18 @@ import { applyDroppedGuard } from '../projectionDefaults${ext}'
151
164
  import type { OpKind } from '../projectionDefaults${ext}'
152
165
  import { MODEL_FIELDS, MODEL_ENUMS } from './${modelName}Metadata${ext}'
153
166
 
154
- ${generateRouteConfigType(modelName, 'FastifyHookHandler', guardShapesImport, importStyle, 'fastify')}
167
+ ${generateRouteConfigType(modelName, 'FastifyHookHandler', guardShapesImport, importStyle, 'fastify', clientImport)}
155
168
  const _env = getEnv()
156
169
 
157
170
  const FIND_MANY_PAGINATED_MODE: FindManyPaginatedMode = '${findManyPaginatedMode}'
158
- const DROP_GUARD = ${dropGuard} || _env.E2E === 'true'
171
+ // Fixed at generation time. The environment bypass is resolved per hook,
172
+ // because it requires the configuration's explicit \`allowE2EGuardBypass: true\`:
173
+ // false or omitted means the environment cannot drop guards for this target.
174
+ const DROP_GUARD = ${dropGuard}
159
175
 
160
176
  type OperationConfigLike = {
177
+ authorize?: FastifyHookHandler
178
+ override?: RuntimeOperationOverride
161
179
  before?: FastifyHookHandler[]
162
180
  after?: FastifyHookHandler[]
163
181
  shape?: unknown
@@ -185,6 +203,8 @@ type FastifyExtended = FastifyRequest & {
185
203
  routeConfig?: { pagination?: PaginationConfig }
186
204
  guardShape?: Record<string, unknown>
187
205
  guardCaller?: string
206
+ operationOverride?: RuntimeOperationOverride
207
+ resolveOperationContext?: () => unknown | Promise<unknown>
188
208
  guardVariantKey?: string
189
209
  guardVariantFailure?: Extract<GuardVariantResolution, { ok: false }>
190
210
  resultData?: unknown
@@ -197,14 +217,14 @@ function normalizeFastifyOperation(
197
217
  return normalizeOperation<FastifyHookHandler, FastifyHookHandler>(config)
198
218
  }
199
219
 
200
- function isQueryBuilderEnabled(config: RouteConfig): boolean {
220
+ function isQueryBuilderEnabled(config: { queryBuilder?: QueryBuilderConfig | false }): boolean {
201
221
  if (config.queryBuilder === false) return false
202
222
  if (typeof config.queryBuilder === 'object' && config.queryBuilder.enabled === false) return false
203
223
  if (_env.NODE_ENV === 'production') return false
204
224
  return true
205
225
  }
206
226
 
207
- function getQueryBuilderConfig(config: RouteConfig) {
227
+ function getQueryBuilderConfig(config: { queryBuilder?: QueryBuilderConfig | false }) {
208
228
  if (config.queryBuilder === false) return null
209
229
  if (typeof config.queryBuilder === 'object') return config.queryBuilder
210
230
  return {}
@@ -225,21 +245,35 @@ function parseBodyAsQueryHook(request: FastifyRequest): void {
225
245
  (request as FastifyExtended).parsedQuery = sanitizeKeys(body as Record<string, unknown>)
226
246
  }
227
247
 
228
- function buildResolveContext(
229
- config: ${modelName}RouteConfig,
248
+ function buildResolveContext<TCtx, TPrisma extends PrismaClientLike>(
249
+ config: ${modelName}RouteConfig<TCtx, TPrisma>,
230
250
  request: FastifyRequest,
231
251
  ): (() => unknown | Promise<unknown>) | undefined {
232
252
  if (typeof config.resolveContext !== 'function') return undefined
233
253
  return () => (config.resolveContext as (r: FastifyRequest) => unknown | Promise<unknown>)(request)
234
254
  }
235
255
 
236
- function makeShapeHook(
237
- config: ${modelName}RouteConfig,
256
+ function makeShapeHook<TCtx, TPrisma extends PrismaClientLike>(
257
+ config: ${modelName}RouteConfig<TCtx, TPrisma>,
238
258
  opConfig: NormalizedOp,
239
259
  opKind: OpKind,
240
260
  ): (request: FastifyRequest) => Promise<void> {
261
+ /**
262
+ * The environment bypass, and it is OPT-IN for this target.
263
+ *
264
+ * \`PGE_DROP_GUARD=true\` (deprecated alias \`E2E=true\`) downgrading enforcement
265
+ * in a deployed environment is a real hazard: on most platforms it is an
266
+ * ordinary config var, set on staging, copied forward, flagged as
267
+ * security-relevant nowhere. It is honoured only when this configuration
268
+ * states \`allowE2EGuardBypass: true\` — false or omitted means the environment
269
+ * cannot drop guards. The generation-time \`DROP_GUARD\` literal is unchanged.
270
+ */
271
+ const dropGuard = DROP_GUARD || (config.allowE2EGuardBypass === true && resolveDropGuardEnv(_env))
241
272
  return async (request: FastifyRequest) => {
242
273
  const fx = request as FastifyExtended
274
+ fx.operationOverride = opConfig.override
275
+ let context: Promise<unknown> | undefined
276
+ fx.resolveOperationContext = () => context ??= Promise.resolve(config.resolveContext?.(request))
243
277
  const merged = mergePaginationConfig(config.pagination, opConfig.pagination)
244
278
  if (merged) fx.routeConfig = { pagination: merged }
245
279
 
@@ -263,7 +297,7 @@ function makeShapeHook(
263
297
  if (resolvedKey !== undefined) fx.guardVariantKey = resolvedKey
264
298
 
265
299
  if (opConfig.guardShape) {
266
- if (!DROP_GUARD) {
300
+ if (!dropGuard) {
267
301
  fx.guardShape = opConfig.guardShape
268
302
  } else {
269
303
  await applyDroppedGuard(
@@ -322,7 +356,7 @@ function sendError(reply: FastifyReply, error: unknown): void {
322
356
  reply.code(httpError.status).send({ message: httpError.message })
323
357
  }
324
358
 
325
- export async function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(
359
+ export async function ${routerFunctionName}<TCtx = unknown, TPrisma extends PrismaClientLike = PrismaClientLike>(
326
360
  fastify: FastifyInstance,
327
361
  config: ${modelName}RouteConfig<TCtx, TPrisma> = {},
328
362
  ) {
@@ -334,6 +368,7 @@ export async function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(
334
368
 
335
369
  await fastify.register(async (instance) => {
336
370
  const isEnabled = (value: unknown): boolean => value !== false && !!(config.enableAll || value)
371
+ warnIfUnguardedRoutes('${modelName}', ['findMany', 'findUnique', 'findUniqueOrThrow', 'findFirst', 'findFirstOrThrow', 'findManyPaginated', 'count', 'aggregate', 'groupBy', 'create', 'createMany', 'createManyAndReturn', 'update', 'updateMany', 'updateManyAndReturn', 'upsert', 'delete', 'deleteMany'], config, isEnabled)
337
372
 
338
373
  const opFor = (key: string): NormalizedOp => {
339
374
  const raw = (config as unknown as Record<string, unknown>)[key] as OperationConfigLike | undefined
@@ -342,7 +377,7 @@ export async function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(
342
377
  }
343
378
 
344
379
  const customPrefix = normalizePrefix(config.customUrlPrefix || '')
345
- const modelPrefix = config.addModelPrefix !== false ? '/${modelNameLower}' : ''
380
+ const modelPrefix = config.addModelPrefix !== false ? '/${modelSegment}' : ''
346
381
  const basePath = customPrefix + modelPrefix
347
382
 
348
383
  const openApiDisabled = config.disableOpenApi === true
@@ -351,7 +386,6 @@ export async function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(
351
386
  || _env.DISABLE_OPENAPI === 'true'
352
387
  ))
353
388
 
354
- const postReadsEnabled = !config.disablePostReads
355
389
 
356
390
  let _openApiJsonCache: unknown = undefined
357
391
  const getOpenApiJson = (): unknown => {
@@ -360,8 +394,8 @@ export async function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(
360
394
  '${modelName}',
361
395
  MODEL_FIELDS as unknown as Parameters<typeof buildModelOpenApi>[1],
362
396
  MODEL_ENUMS as unknown as Parameters<typeof buildModelOpenApi>[2],
363
- config,
364
- { format: 'json', writeStrategy: '${writeStrategy}' },
397
+ config as unknown as Parameters<typeof buildModelOpenApi>[3],
398
+ { format: 'json', writeStrategy: '${writeStrategy}', pathSegment: '${modelSegment}' },
365
399
  )
366
400
  }
367
401
  return _openApiJsonCache
@@ -373,8 +407,8 @@ export async function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(
373
407
  '${modelName}',
374
408
  MODEL_FIELDS as unknown as Parameters<typeof buildModelOpenApi>[1],
375
409
  MODEL_ENUMS as unknown as Parameters<typeof buildModelOpenApi>[2],
376
- config,
377
- { format: 'yaml', writeStrategy: '${writeStrategy}' },
410
+ config as unknown as Parameters<typeof buildModelOpenApi>[3],
411
+ { format: 'yaml', writeStrategy: '${writeStrategy}', pathSegment: '${modelSegment}' },
378
412
  ) as string
379
413
  }
380
414
  return _openApiYamlCache
@@ -424,6 +458,7 @@ export async function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(
424
458
  opKind: OpKind,
425
459
  ) => async (request: FastifyRequest, reply: FastifyReply) => {
426
460
  try {
461
+ if (await runHooks(opConfig.authorize ? [opConfig.authorize] : [], request, reply)) return
427
462
  parseFn(request)
428
463
  await makeShapeHook(config, opConfig, opKind)(request)
429
464
  if (await runHooks(opConfig.operationBefore, request, reply)) return
@@ -456,6 +491,7 @@ export async function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(
456
491
  opKind: OpKind,
457
492
  ) => async (request: FastifyRequest, reply: FastifyReply) => {
458
493
  try {
494
+ if (await runHooks(opConfig.authorize ? [opConfig.authorize] : [], request, reply)) return
459
495
  await makeShapeHook(config, opConfig, opKind)(request)
460
496
  if (await runHooks(opConfig.operationBefore, request, reply)) return
461
497
 
@@ -514,4 +550,4 @@ ${writeOpBlocks}
514
550
  })
515
551
  }
516
552
  `
517
- }
553
+ }
@@ -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
@@ -216,6 +237,8 @@ const DROP_GUARD = ${dropGuard}
216
237
  type JsonLike = string | number | boolean | null | unknown[] | Record<string, unknown>
217
238
 
218
239
  type OperationConfigLike<TEnv extends HonoEnvBase> = {
240
+ authorize?: HonoBeforeHook<TEnv>
241
+ override?: RuntimeOperationOverride
219
242
  before?: HonoBeforeHook<TEnv>[]
220
243
  after?: HonoAfterHook<TEnv>[]
221
244
  shape?: unknown
@@ -289,7 +312,7 @@ async function parseWriteBodyMiddleware(c: HandlerContext): Promise<void> {
289
312
  c.set('body', body)
290
313
  }
291
314
 
292
- function makeShapeMiddleware<TCtx, TPrisma, TEnv extends HonoEnvBase>(
315
+ function makeShapeMiddleware<TCtx, TPrisma extends PrismaClientLike, TEnv extends HonoEnvBase>(
293
316
  config: ${modelName}RouteConfig<TCtx, TPrisma, TEnv>,
294
317
  opConfig: NormalizedOp<TEnv>,
295
318
  opKind: OpKind,
@@ -299,11 +322,11 @@ function makeShapeMiddleware<TCtx, TPrisma, TEnv extends HonoEnvBase>(
299
322
  /**
300
323
  * The environment bypass, honoured unless the consumer turned it off.
301
324
  *
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.
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.
305
328
  */
306
- const dropGuard = DROP_GUARD || (policy.allowE2EGuardBypass && _env.E2E === 'true')
329
+ const dropGuard = DROP_GUARD || (policy.allowE2EGuardBypass && resolveDropGuardEnv(_env))
307
330
 
308
331
  return async (c: Context<GeneratedHonoEnv<TEnv>>): Promise<void> => {
309
332
  /**
@@ -318,6 +341,9 @@ function makeShapeMiddleware<TCtx, TPrisma, TEnv extends HonoEnvBase>(
318
341
  * in.
319
342
  */
320
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)))
321
347
 
322
348
  const merged = mergePaginationConfig(config.pagination, opConfig.pagination)
323
349
  if (merged) vars.set('routeConfig', { pagination: merged })
@@ -341,7 +367,7 @@ function makeShapeMiddleware<TCtx, TPrisma, TEnv extends HonoEnvBase>(
341
367
 
342
368
  if (opConfig.guardShape) {
343
369
  const resolveCtx = typeof config.resolveContext === 'function'
344
- ? () => (config.resolveContext as unknown as (ctx: Context<GeneratedHonoEnv<TEnv>>) => unknown | Promise<unknown>)(c)
370
+ ? vars.get('resolveOperationContext')
345
371
  : undefined
346
372
 
347
373
  /**
@@ -441,7 +467,7 @@ function sendError(c: HandlerContext, error: unknown): Response {
441
467
  return c.json({ message: httpError.message }, httpError.status as ContentfulStatusCode)
442
468
  }
443
469
 
444
- 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>> {
445
471
  validateCountSourceWhere(config.pagination?.countSource, '${modelName} pagination')
446
472
  validateCountSourceWhere(
447
473
  (config.findManyPaginated && typeof config.findManyPaginated === 'object' ? config.findManyPaginated : undefined)?.pagination?.countSource,
@@ -451,9 +477,10 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any, TEnv extend
451
477
  const app = new Hono<GeneratedHonoEnv<TEnv>>()
452
478
 
453
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)
454
481
 
455
482
  const customPrefix = normalizePrefix(config.customUrlPrefix || '')
456
- const modelPrefix = config.addModelPrefix !== false ? '/${modelNameLower}' : ''
483
+ const modelPrefix = config.addModelPrefix !== false ? '/${modelSegment}' : ''
457
484
  const basePath = customPrefix + modelPrefix
458
485
 
459
486
  const openApiDisabled = config.disableOpenApi === true
@@ -462,7 +489,6 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any, TEnv extend
462
489
  || _env.DISABLE_OPENAPI === 'true'
463
490
  ))
464
491
 
465
- const postReadsEnabled = !config.disablePostReads
466
492
 
467
493
  let _openApiJsonCache: unknown = undefined
468
494
  const getOpenApiJson = (): unknown => {
@@ -472,7 +498,7 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any, TEnv extend
472
498
  MODEL_FIELDS as unknown as Parameters<typeof buildModelOpenApi>[1],
473
499
  MODEL_ENUMS as unknown as Parameters<typeof buildModelOpenApi>[2],
474
500
  config as unknown as Parameters<typeof buildModelOpenApi>[3],
475
- { format: 'json', writeStrategy: '${writeStrategy}' },
501
+ { format: 'json', writeStrategy: '${writeStrategy}', pathSegment: '${modelSegment}' },
476
502
  )
477
503
  }
478
504
  return _openApiJsonCache
@@ -485,7 +511,7 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any, TEnv extend
485
511
  MODEL_FIELDS as unknown as Parameters<typeof buildModelOpenApi>[1],
486
512
  MODEL_ENUMS as unknown as Parameters<typeof buildModelOpenApi>[2],
487
513
  config as unknown as Parameters<typeof buildModelOpenApi>[3],
488
- { format: 'yaml', writeStrategy: '${writeStrategy}' },
514
+ { format: 'yaml', writeStrategy: '${writeStrategy}', pathSegment: '${modelSegment}' },
489
515
  ) as string
490
516
  }
491
517
  return _openApiYamlCache
@@ -552,6 +578,8 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any, TEnv extend
552
578
  opKind: OpKind,
553
579
  ) => async (c: Context<GeneratedHonoEnv<TEnv>>): Promise<Response> => {
554
580
  try {
581
+ const authorized = await runBeforeHooks<TEnv>(opConfig.authorize ? [opConfig.authorize] : [], c)
582
+ if (authorized) return authorized
555
583
  await parseFn(c as unknown as HandlerContext)
556
584
  await makeShapeMiddleware<TCtx, TPrisma, TEnv>(config, opConfig, opKind)(c)
557
585
 
@@ -587,6 +615,8 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any, TEnv extend
587
615
  opKind: OpKind,
588
616
  ) => async (c: Context<GeneratedHonoEnv<TEnv>>): Promise<Response> => {
589
617
  try {
618
+ const authorized = await runBeforeHooks<TEnv>(opConfig.authorize ? [opConfig.authorize] : [], c)
619
+ if (authorized) return authorized
590
620
  await parseWriteBodyMiddleware(c as unknown as HandlerContext)
591
621
  await makeShapeMiddleware<TCtx, TPrisma, TEnv>(config, opConfig, opKind)(c)
592
622
 
@@ -677,4 +707,4 @@ ${writeOpBlocks}
677
707
  return app
678
708
  }
679
709
  `
680
- }
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
+ }