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
@@ -0,0 +1,220 @@
1
+ import type { DMMF } from '@prisma/generator-helper'
2
+ import { generateRouteConfigType } from './generateRouteConfigType'
3
+ import type { ImportStyle } from '../utils/resolveImportStyle'
4
+ import { importExt } from '../utils/importExt'
5
+ import type { WriteStrategy } from '../constants'
6
+ import { modelPathSegment, type PathCase } from '../utils/pathCasing'
7
+ import { OPERATION_METADATA } from '../copy/operationDefinitions'
8
+
9
+ function opKindFor(opName: string): string {
10
+ switch (opName) {
11
+ case 'findUnique':
12
+ case 'findUniqueOrThrow':
13
+ return 'readUnique'
14
+ case 'findMany':
15
+ case 'findFirst':
16
+ case 'findFirstOrThrow':
17
+ case 'findManyPaginated':
18
+ case 'count':
19
+ case 'aggregate':
20
+ case 'groupBy':
21
+ return 'read'
22
+ case 'create':
23
+ return 'create'
24
+ case 'createMany':
25
+ case 'createManyAndReturn':
26
+ return 'createMany'
27
+ case 'update':
28
+ return 'update'
29
+ case 'updateMany':
30
+ case 'updateManyAndReturn':
31
+ return 'updateMany'
32
+ case 'upsert':
33
+ return 'upsert'
34
+ case 'delete':
35
+ return 'delete'
36
+ case 'deleteMany':
37
+ return 'deleteMany'
38
+ default:
39
+ return 'noop'
40
+ }
41
+ }
42
+
43
+ /**
44
+ * The phase 9 static assembly boundary, emitted side: one named factory per
45
+ * operation, each returning a Hono sub-app with exactly that route. A consumer
46
+ * imports only the parts it serves; everything not imported is absent from the
47
+ * bundle — which `enableAll` at runtime can never do, because route
48
+ * configuration is runtime data. Heavy features are referenced only by their
49
+ * own parts (`userOpenApi` is the sole static referencer of the OpenAPI
50
+ * renderer and `yaml`).
51
+ */
52
+ export function generateHonoRouterParts({
53
+ model,
54
+ enums,
55
+ guardShapesImport,
56
+ clientImport,
57
+ importStyle,
58
+ writeStrategy,
59
+ dropGuard,
60
+ pathCase,
61
+ }: {
62
+ model: DMMF.Model
63
+ enums: DMMF.DatamodelEnum[]
64
+ guardShapesImport: string | null
65
+ clientImport?: string
66
+ importStyle: ImportStyle
67
+ writeStrategy: WriteStrategy
68
+ dropGuard: boolean
69
+ pathCase: PathCase
70
+ }): string {
71
+ const ext = importExt(importStyle)
72
+ const modelName = model.name
73
+ const modelSegment = modelPathSegment(modelName, pathCase)
74
+ const lower = modelName.charAt(0).toLowerCase() + modelName.slice(1)
75
+ const cap = (s: string) => s.charAt(0).toUpperCase() + s.slice(1)
76
+
77
+ const handlerImports = OPERATION_METADATA.map(
78
+ (m) => ` ${modelName}${cap(m.name)},`,
79
+ ).join('\n')
80
+
81
+ const readOps = OPERATION_METADATA.filter((m) => m.kind === 'read')
82
+ const writeOps = OPERATION_METADATA.filter(
83
+ (m) => m.kind === 'write' || m.kind === 'batch',
84
+ ).filter((m) => m.name !== 'updateEach')
85
+
86
+ const pathExpr = (suffix: string) =>
87
+ !suffix ? `setup.basePath || '/'` : `\`\${setup.basePath}${suffix}\``
88
+
89
+ const readFactories = readOps
90
+ .map((meta) => {
91
+ const fn = `${lower}${cap(meta.name)}`
92
+ const handler = `${modelName}${cap(meta.name)}`
93
+ const opKind = opKindFor(meta.name)
94
+ const pathValue = pathExpr(meta.pathSuffix)
95
+ const postPathValue =
96
+ meta.name === 'findMany'
97
+ ? `setup.basePath ? \`\${setup.basePath}/read\` : '/read'`
98
+ : pathExpr(meta.pathSuffix)
99
+ return `
100
+ export function ${fn}<TCtx = unknown, TPrisma extends PrismaClientLike = PrismaClientLike, TEnv extends HonoEnvBase = HonoEnvBase>(config: ${modelName}RouteConfig<TCtx, TPrisma, TEnv> = {}): Hono<GeneratedHonoEnv<TEnv>> {
101
+ const setup = ${lower}PartsSetup(config)
102
+ const opConfig = setup.opFor('${meta.configKey}')
103
+ const app = new Hono<GeneratedHonoEnv<TEnv>>()
104
+ app.get(${pathValue}, createReadRoute<TCtx, TPrisma, TEnv>({ config, opConfig, opKind: '${opKind}', handler: ${handler}, parse: 'query', dropGuard: setup.dropGuard, settleBeforeHooks: setup.settleBeforeHooks }))
105
+ return app
106
+ }
107
+
108
+ export function ${fn}PostRead<TCtx = unknown, TPrisma extends PrismaClientLike = PrismaClientLike, TEnv extends HonoEnvBase = HonoEnvBase>(config: ${modelName}RouteConfig<TCtx, TPrisma, TEnv> = {}): Hono<GeneratedHonoEnv<TEnv>> {
109
+ const setup = ${lower}PartsSetup(config)
110
+ const opConfig = setup.opFor('${meta.configKey}')
111
+ const app = new Hono<GeneratedHonoEnv<TEnv>>()
112
+ app.post(${postPathValue}, createReadRoute<TCtx, TPrisma, TEnv>({ config, opConfig, opKind: '${opKind}', handler: ${handler}, parse: 'body', dropGuard: setup.dropGuard, settleBeforeHooks: setup.settleBeforeHooks }))
113
+ return app
114
+ }
115
+ `
116
+ })
117
+ .join('\n')
118
+
119
+ const writeFactories = writeOps
120
+ .map((meta) => {
121
+ const fn = `${lower}${cap(meta.name)}`
122
+ const handler = `${modelName}${cap(meta.name)}`
123
+ const opKind = opKindFor(meta.name)
124
+ const pathValue = pathExpr(meta.pathSuffix)
125
+ return `
126
+ export function ${fn}<TCtx = unknown, TPrisma extends PrismaClientLike = PrismaClientLike, TEnv extends HonoEnvBase = HonoEnvBase>(config: ${modelName}RouteConfig<TCtx, TPrisma, TEnv> = {}): Hono<GeneratedHonoEnv<TEnv>> {
127
+ const setup = ${lower}PartsSetup(config)
128
+ const opConfig = setup.opFor('${meta.configKey}')
129
+ const app = new Hono<GeneratedHonoEnv<TEnv>>()
130
+ app.${meta.method}(${pathValue}, createWriteRoute<TCtx, TPrisma, TEnv>({ config, opConfig, opKind: '${opKind}', handler: ${handler}, dropGuard: setup.dropGuard, settleBeforeHooks: setup.settleBeforeHooks }))
131
+ return app
132
+ }
133
+ `
134
+ })
135
+ .join('\n')
136
+
137
+ return `import { Hono } from 'hono'
138
+ import {
139
+ ${handlerImports}
140
+ } from './${modelName}Handlers${ext}'
141
+ import type {
142
+ RouteConfig,
143
+ HonoEnvBase,
144
+ GeneratedHonoEnv,
145
+ PrismaClientLike,
146
+ } from '../routeConfig.target${ext}'
147
+ import { normalizePrefix, getEnv, resolveDropGuardEnv } from '../misc${ext}'
148
+ import {
149
+ resolveGuardPolicy,
150
+ validateCountSourceWhere,
151
+ warnIfUnguardedRoutes,
152
+ } from '../routeConfig${ext}'
153
+ import {
154
+ createReadRoute,
155
+ createWriteRoute,
156
+ createUpdateEachRoute,
157
+ opConfigFor,
158
+ normalizeHonoOperation,
159
+ type HonoOpConfig,
160
+ } from '../routerParts${ext}'
161
+ import { MODEL_FIELDS, MODEL_ENUMS } from './${modelName}Metadata${ext}'
162
+
163
+ ${generateRouteConfigType(modelName, 'HonoBeforeHook', guardShapesImport, importStyle, 'hono', clientImport)}
164
+
165
+ // Fixed at generation time, identical to the legacy router factory.
166
+ const DROP_GUARD = ${dropGuard}
167
+
168
+ // No module-level side effects: this file must be tree-shakeable, so unused
169
+ // per-op exports — and everything only they reference, down to the OpenAPI
170
+ // renderer and yaml — drop out of a bundler's output. The environment is read
171
+ // inside the functions that need it.
172
+
173
+ function ${lower}PartsSetup<TCtx = unknown, TPrisma extends PrismaClientLike = PrismaClientLike, TEnv extends HonoEnvBase = HonoEnvBase>(config: ${modelName}RouteConfig<TCtx, TPrisma, TEnv>) {
174
+ validateCountSourceWhere(config.pagination?.countSource, '${modelName} pagination')
175
+ validateCountSourceWhere(
176
+ (config.findManyPaginated && typeof config.findManyPaginated === 'object' ? config.findManyPaginated : undefined)?.pagination?.countSource,
177
+ '${modelName} findManyPaginated pagination',
178
+ )
179
+
180
+ const isEnabled = (value: unknown): boolean => value !== false && !!(config.enableAll || value)
181
+ warnIfUnguardedRoutes('${modelName}', ['findMany', 'findUnique', 'findUniqueOrThrow', 'findFirst', 'findFirstOrThrow', 'findManyPaginated', 'count', 'aggregate', 'groupBy', 'create', 'createMany', 'createManyAndReturn', 'update', 'updateMany', 'updateManyAndReturn', 'upsert', 'delete', 'deleteMany'], config, isEnabled)
182
+
183
+ const customPrefix = normalizePrefix(config.customUrlPrefix || '')
184
+ const modelPrefix = config.addModelPrefix !== false ? '/${modelSegment}' : ''
185
+ const basePath = customPrefix + modelPrefix
186
+
187
+ const POLICY = resolveGuardPolicy(config)
188
+ const dropGuard =
189
+ DROP_GUARD || (POLICY.allowE2EGuardBypass && resolveDropGuardEnv(getEnv()))
190
+ const settleBeforeHooks = POLICY.guardResolutionOrder === 'before-hooks'
191
+
192
+ const opFor = <K extends keyof ${modelName}RouteConfig<TCtx, TPrisma, TEnv>>(
193
+ key: K,
194
+ ): HonoOpConfig<TEnv> => {
195
+ const raw = config[key] as unknown as Parameters<typeof normalizeHonoOperation<TEnv>>[0]
196
+ return opConfigFor<TEnv>(raw, '${modelName}.' + String(key), POLICY)
197
+ }
198
+
199
+ return { basePath, POLICY, dropGuard, settleBeforeHooks, opFor }
200
+ }
201
+ ${readFactories}
202
+ ${writeFactories}
203
+ export function ${lower}UpdateEach<TCtx = unknown, TPrisma extends PrismaClientLike = PrismaClientLike, TEnv extends HonoEnvBase = HonoEnvBase>(config: ${modelName}RouteConfig<TCtx, TPrisma, TEnv> = {}): Hono<GeneratedHonoEnv<TEnv>> {
204
+ const setup = ${lower}PartsSetup(config)
205
+ if (!setup.POLICY.enableUpdateEach) {
206
+ throw new Error(
207
+ '${modelName}.updateEach: enableUpdateEach is false, so this part ' +
208
+ 'is not available. It bypasses guard shapes entirely, so there is no ' +
209
+ 'configuration that makes it safe to expose.',
210
+ )
211
+ }
212
+ const raw = config.updateEach as unknown as Parameters<typeof normalizeHonoOperation<TEnv>>[0]
213
+ const opConfig = normalizeHonoOperation<TEnv>(raw)
214
+ const app = new Hono<GeneratedHonoEnv<TEnv>>()
215
+ app.post(setup.basePath ? \`\${setup.basePath}/each\` : '/each', createUpdateEachRoute<TCtx, TPrisma, TEnv>({ config, opConfig, handler: ${modelName}UpdateEach, dropGuard: setup.dropGuard }))
216
+ return app
217
+ }
218
+
219
+ `
220
+ }
@@ -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
 
@@ -11,31 +11,53 @@ export interface UnifiedHandlerOptions {
11
11
  export function generateUnifiedHandler(options: UnifiedHandlerOptions): string {
12
12
  const ext = importExt(options.importStyle)
13
13
  const modelName = options.model.name
14
-
15
14
  const dispatchOps = OPERATION_METADATA.filter((m) => m.name !== 'updateEach')
16
15
 
17
- const handlers = dispatchOps.map((meta) => {
18
- const exportName = `${modelName}${meta.name.charAt(0).toUpperCase() + meta.name.slice(1)}`
16
+ const handlers = dispatchOps
17
+ .map((meta) => {
18
+ const exportName = `${modelName}${meta.name.charAt(0).toUpperCase() + meta.name.slice(1)}`
19
19
 
20
- return `
20
+ return `
21
21
  export async function ${exportName}(
22
22
  req: Request,
23
23
  res: Response,
24
24
  next: NextFunction,
25
25
  ) {
26
26
  try {
27
- ;(res.locals as LocalsBag).data = await core.${meta.coreName}(buildContext(req, res))
27
+ const locals = readLocals(res)
28
+ ;locals.data = await executeOperation(
29
+ { variantKey: locals.guardVariantKey, caller: locals.guardCaller },
30
+ { guardShape: locals.guardShape },
31
+ {
32
+ core: core.${meta.coreName},
33
+ args: queryChannel(res),
34
+ body: req.body,
35
+ prisma: requirePrisma((req as ExtendedRequest).prisma),
36
+ postgres: (req as ExtendedRequest).postgres,
37
+ sqlite: (req as ExtendedRequest).sqlite,
38
+ pagination: locals.routeConfig?.pagination,
39
+ override: locals.operationOverride,
40
+ getContext: locals.getContext,
41
+ },
42
+ )
28
43
  next()
29
44
  } catch (error: unknown) {
30
- next(mapError(error))
45
+ next(classifyError(error))
31
46
  }
32
47
  }`
33
- }).join('\n')
48
+ })
49
+ .join('\n')
34
50
 
35
51
  return `import type { Request, Response, NextFunction } from 'express'
36
52
  import * as core from './${modelName}Core${ext}'
37
- import { OperationContext } from '../operationRuntime${ext}'
38
- import { mapError } from '../errorMapper${ext}'
53
+ import type { RuntimeOperationOverride } from '../operationRuntime${ext}'
54
+ import type { PaginationConfig } from '../routeConfig${ext}'
55
+ import {
56
+ executeOperation,
57
+ classifyError,
58
+ requirePrisma,
59
+ type ArgsChannel,
60
+ } from '../operationPipeline${ext}'
39
61
 
40
62
  type ExtendedRequest = Request & {
41
63
  prisma?: unknown
@@ -45,26 +67,27 @@ type ExtendedRequest = Request & {
45
67
 
46
68
  type LocalsBag = {
47
69
  parsedQuery?: Record<string, unknown>
48
- routeConfig?: { pagination?: unknown }
70
+ routeConfig?: { pagination?: PaginationConfig }
49
71
  guardShape?: Record<string, unknown>
50
72
  guardCaller?: string
73
+ guardVariantKey?: string
74
+ operationOverride?: RuntimeOperationOverride
75
+ getContext?: () => Promise<unknown>
51
76
  data?: unknown
52
77
  }
53
78
 
54
- function buildContext(req: Request, res: Response): OperationContext {
55
- const extReq = req as ExtendedRequest
56
- const locals = res.locals as LocalsBag
79
+ function readLocals(res: Response): LocalsBag {
80
+ return res.locals as LocalsBag
81
+ }
82
+
83
+ function queryChannel(res: Response): ArgsChannel {
57
84
  return {
58
- prisma: extReq.prisma,
59
- postgres: extReq.postgres,
60
- sqlite: extReq.sqlite,
61
- parsedQuery: locals.parsedQuery,
62
- body: req.body,
63
- guardShape: locals.guardShape,
64
- guardCaller: locals.guardCaller,
65
- paginationConfig: (locals.routeConfig?.pagination) as OperationContext['paginationConfig'],
85
+ read: () => readLocals(res).parsedQuery,
86
+ write: (next) => {
87
+ readLocals(res).parsedQuery = next
88
+ },
66
89
  }
67
90
  }
68
91
  ${handlers}
69
92
  `
70
- }
93
+ }
@@ -215,4 +215,4 @@ const MODEL_CONTEXT: DocsModelContext = {
215
215
 
216
216
  ${docsExport}
217
217
  `
218
- }
218
+ }
@@ -98,90 +98,94 @@ export type GuardOptionMetadata = {
98
98
  * them; declaring them on the shared base made Express and Fastify accept them in
99
99
  * TypeScript and ignore them at runtime, which is a false public API.
100
100
  */
101
- export const GUARD_OPTION_METADATA: readonly GuardOptionMetadata[] = Object.freeze([
102
- {
103
- name: 'requireGuardShape',
104
- type: 'boolean',
105
- default: UPSTREAM_GUARD_DEFAULTS.requireGuardShape,
106
- hardened: HARDENED_GUARD_PROFILE.requireGuardShape,
107
- label: 'Require a guard on every enabled operation',
108
- description:
109
- 'Refuses at router construction when an operation defines neither "shape" nor "variants".',
110
- warning:
111
- "Off, an operation nobody configured passes the caller's own where/select/include straight to Prisma.",
112
- target: 'hono',
113
- },
114
- {
115
- name: 'validateGuardShapes',
116
- type: 'boolean',
117
- default: UPSTREAM_GUARD_DEFAULTS.validateGuardShapes,
118
- hardened: HARDENED_GUARD_PROFILE.validateGuardShapes,
119
- label: 'Validate guard shapes',
120
- description:
121
- 'Refuses an empty shape, and one mixing guard keys with non-guard keys, at the top level and inside variants.',
122
- warning:
123
- 'Off, "shape: {}" reads as guarded and constrains nothing. On, strict validation may reject existing route configs.',
124
- target: 'hono',
125
- },
126
- {
127
- name: 'requireDefaultVariantOptIn',
128
- type: 'boolean',
129
- default: UPSTREAM_GUARD_DEFAULTS.requireDefaultVariantOptIn,
130
- hardened: HARDENED_GUARD_PROFILE.requireDefaultVariantOptIn,
131
- label: 'Require confirmation for a "default" variant',
132
- description:
133
- 'A variant named "default" must set allowDefaultVariant: true to be accepted.',
134
- warning: 'A "default" variant answers every unrecognised, blank and missing caller.',
135
- target: 'hono',
136
- },
137
- {
138
- name: 'enableUpdateEach',
139
- type: 'boolean',
140
- default: UPSTREAM_GUARD_DEFAULTS.enableUpdateEach,
141
- hardened: HARDENED_GUARD_PROFILE.enableUpdateEach,
142
- label: 'Register the updateEach batch route',
143
- description: 'Mounts POST <base>/each. Set false to refuse it at router construction.',
144
- warning:
145
- 'updateEach bypasses guard shapes entirely; only a development-only warning stands between it and an unguarded mass update.',
146
- target: 'hono',
147
- },
148
- {
149
- name: 'guardResolutionOrder',
150
- type: 'enum',
151
- values: Object.freeze(['after-hooks', 'before-hooks']),
152
- default: UPSTREAM_GUARD_DEFAULTS.guardResolutionOrder,
153
- hardened: HARDENED_GUARD_PROFILE.guardResolutionOrder,
154
- label: 'When guard failures are raised',
155
- description:
156
- 'after-hooks is the upstream behaviour; before-hooks settles variant and shape resolution before any operation hook runs.',
157
- warning:
158
- 'With after-hooks, a hook that returns a Response can answer a request whose guard was never established.',
159
- target: 'hono',
160
- },
161
- {
162
- name: 'allowE2EGuardBypass',
163
- type: 'boolean',
164
- default: UPSTREAM_GUARD_DEFAULTS.allowE2EGuardBypass,
165
- hardened: HARDENED_GUARD_PROFILE.allowE2EGuardBypass,
166
- label: 'Honour the E2E environment guard bypass',
167
- description: 'Treats E2E=true in the environment as a request to drop the guard.',
168
- warning:
169
- 'A runtime variable can weaken a deployed artifact after generation. On an edge runtime this is an ordinary config var.',
170
- target: 'hono',
171
- },
172
- {
173
- name: 'validateResolvedShapes',
174
- type: 'boolean',
175
- default: UPSTREAM_GUARD_DEFAULTS.validateResolvedShapes,
176
- hardened: HARDENED_GUARD_PROFILE.validateResolvedShapes,
177
- label: 'Validate what a shape function returns',
178
- description:
179
- 'Checks a dynamically resolved shape before use and fails the request with 500 if it is unusable. Also resolves it exactly once.',
180
- warning:
181
- 'Off, a shape function returning {} or undefined runs the operation unguarded, once per request.',
182
- target: 'hono',
183
- },
184
- ])
101
+ export const GUARD_OPTION_METADATA: readonly GuardOptionMetadata[] =
102
+ Object.freeze([
103
+ {
104
+ name: 'requireGuardShape',
105
+ type: 'boolean',
106
+ default: UPSTREAM_GUARD_DEFAULTS.requireGuardShape,
107
+ hardened: HARDENED_GUARD_PROFILE.requireGuardShape,
108
+ label: 'Require a guard on every enabled operation',
109
+ description:
110
+ 'Refuses at router construction when an operation defines neither "shape" nor "variants".',
111
+ warning:
112
+ "Off, an operation nobody configured passes the caller's own where/select/include straight to Prisma.",
113
+ target: 'hono',
114
+ },
115
+ {
116
+ name: 'validateGuardShapes',
117
+ type: 'boolean',
118
+ default: UPSTREAM_GUARD_DEFAULTS.validateGuardShapes,
119
+ hardened: HARDENED_GUARD_PROFILE.validateGuardShapes,
120
+ label: 'Validate guard shapes',
121
+ description:
122
+ 'Refuses an empty shape, and one mixing guard keys with non-guard keys, at the top level and inside variants.',
123
+ warning:
124
+ 'Off, "shape: {}" reads as guarded and constrains nothing. On, strict validation may reject existing route configs.',
125
+ target: 'hono',
126
+ },
127
+ {
128
+ name: 'requireDefaultVariantOptIn',
129
+ type: 'boolean',
130
+ default: UPSTREAM_GUARD_DEFAULTS.requireDefaultVariantOptIn,
131
+ hardened: HARDENED_GUARD_PROFILE.requireDefaultVariantOptIn,
132
+ label: 'Require confirmation for a "default" variant',
133
+ description:
134
+ 'A variant named "default" must set allowDefaultVariant: true to be accepted.',
135
+ warning:
136
+ 'A "default" variant answers every unrecognised, blank and missing caller.',
137
+ target: 'hono',
138
+ },
139
+ {
140
+ name: 'enableUpdateEach',
141
+ type: 'boolean',
142
+ default: UPSTREAM_GUARD_DEFAULTS.enableUpdateEach,
143
+ hardened: HARDENED_GUARD_PROFILE.enableUpdateEach,
144
+ label: 'Register the updateEach batch route',
145
+ description:
146
+ 'Mounts POST <base>/each. Set false to refuse it at router construction.',
147
+ warning:
148
+ 'updateEach bypasses guard shapes entirely; only a development-only warning stands between it and an unguarded mass update.',
149
+ target: 'hono',
150
+ },
151
+ {
152
+ name: 'guardResolutionOrder',
153
+ type: 'enum',
154
+ values: Object.freeze(['after-hooks', 'before-hooks']),
155
+ default: UPSTREAM_GUARD_DEFAULTS.guardResolutionOrder,
156
+ hardened: HARDENED_GUARD_PROFILE.guardResolutionOrder,
157
+ label: 'When guard failures are raised',
158
+ description:
159
+ 'after-hooks is the upstream behaviour; before-hooks settles variant and shape resolution before any operation hook runs.',
160
+ warning:
161
+ 'With after-hooks, a hook that returns a Response can answer a request whose guard was never established.',
162
+ target: 'hono',
163
+ },
164
+ {
165
+ name: 'allowE2EGuardBypass',
166
+ type: 'boolean',
167
+ default: UPSTREAM_GUARD_DEFAULTS.allowE2EGuardBypass,
168
+ hardened: HARDENED_GUARD_PROFILE.allowE2EGuardBypass,
169
+ label: 'Honour the PGE_DROP_GUARD environment guard bypass',
170
+ description:
171
+ 'Treats PGE_DROP_GUARD=true in the environment as a request to drop the guard. Deprecated alias: E2E=true.',
172
+ warning:
173
+ 'A runtime variable can weaken a deployed artifact after generation. On an edge runtime this is an ordinary config var.',
174
+ target: 'hono',
175
+ },
176
+ {
177
+ name: 'validateResolvedShapes',
178
+ type: 'boolean',
179
+ default: UPSTREAM_GUARD_DEFAULTS.validateResolvedShapes,
180
+ hardened: HARDENED_GUARD_PROFILE.validateResolvedShapes,
181
+ label: 'Validate what a shape function returns',
182
+ description:
183
+ 'Checks a dynamically resolved shape before use and fails the request with 500 if it is unusable. Also resolves it exactly once.',
184
+ warning:
185
+ 'Off, a shape function returning {} or undefined runs the operation unguarded, once per request.',
186
+ target: 'hono',
187
+ },
188
+ ])
185
189
 
186
190
  /**
187
191
  * What this metadata does NOT cover.
@@ -208,19 +212,28 @@ export const GUARD_METADATA_SCOPE = Object.freeze({
208
212
  })
209
213
 
210
214
  /** Fill in the defaults. Absent means upstream, never an opinion. */
211
- export function resolveGuardPolicy(config: Partial<GuardPolicy> | undefined): GuardPolicy {
215
+ export function resolveGuardPolicy(
216
+ config: Partial<GuardPolicy> | undefined,
217
+ ): GuardPolicy {
212
218
  return {
213
- requireGuardShape: config?.requireGuardShape ?? UPSTREAM_GUARD_DEFAULTS.requireGuardShape,
219
+ requireGuardShape:
220
+ config?.requireGuardShape ?? UPSTREAM_GUARD_DEFAULTS.requireGuardShape,
214
221
  validateGuardShapes:
215
- config?.validateGuardShapes ?? UPSTREAM_GUARD_DEFAULTS.validateGuardShapes,
222
+ config?.validateGuardShapes ??
223
+ UPSTREAM_GUARD_DEFAULTS.validateGuardShapes,
216
224
  requireDefaultVariantOptIn:
217
- config?.requireDefaultVariantOptIn ?? UPSTREAM_GUARD_DEFAULTS.requireDefaultVariantOptIn,
218
- enableUpdateEach: config?.enableUpdateEach ?? UPSTREAM_GUARD_DEFAULTS.enableUpdateEach,
225
+ config?.requireDefaultVariantOptIn ??
226
+ UPSTREAM_GUARD_DEFAULTS.requireDefaultVariantOptIn,
227
+ enableUpdateEach:
228
+ config?.enableUpdateEach ?? UPSTREAM_GUARD_DEFAULTS.enableUpdateEach,
219
229
  guardResolutionOrder:
220
- config?.guardResolutionOrder ?? UPSTREAM_GUARD_DEFAULTS.guardResolutionOrder,
230
+ config?.guardResolutionOrder ??
231
+ UPSTREAM_GUARD_DEFAULTS.guardResolutionOrder,
221
232
  allowE2EGuardBypass:
222
- config?.allowE2EGuardBypass ?? UPSTREAM_GUARD_DEFAULTS.allowE2EGuardBypass,
233
+ config?.allowE2EGuardBypass ??
234
+ UPSTREAM_GUARD_DEFAULTS.allowE2EGuardBypass,
223
235
  validateResolvedShapes:
224
- config?.validateResolvedShapes ?? UPSTREAM_GUARD_DEFAULTS.validateResolvedShapes,
236
+ config?.validateResolvedShapes ??
237
+ UPSTREAM_GUARD_DEFAULTS.validateResolvedShapes,
225
238
  }
226
239
  }