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.
- package/README.md +188 -3432
- package/dist/client/encodeQueryParams.js +3 -1
- package/dist/client/encodeQueryParams.js.map +1 -1
- package/dist/copy/misc.d.ts +1 -0
- package/dist/copy/misc.js +14 -0
- package/dist/copy/misc.js.map +1 -1
- package/dist/copy/operationDefinitions.d.ts +4 -1
- package/dist/copy/operationDefinitions.js +133 -9
- package/dist/copy/operationDefinitions.js.map +1 -1
- package/dist/generators/generateFastifyHandler.d.ts +2 -2
- package/dist/generators/generateFastifyHandler.js +68 -12
- package/dist/generators/generateFastifyHandler.js.map +1 -1
- package/dist/generators/generateHonoHandler.d.ts +2 -2
- package/dist/generators/generateHonoHandler.js +53 -6
- package/dist/generators/generateHonoHandler.js.map +1 -1
- package/dist/generators/generateHonoOpenApi.d.ts +13 -0
- package/dist/generators/generateHonoOpenApi.js +70 -0
- package/dist/generators/generateHonoOpenApi.js.map +1 -0
- package/dist/generators/generateImportPrismaStatement.d.ts +1 -0
- package/dist/generators/generateImportPrismaStatement.js +6 -0
- package/dist/generators/generateImportPrismaStatement.js.map +1 -1
- package/dist/generators/generateMcpApp.d.ts +10 -0
- package/dist/generators/generateMcpApp.js +113 -0
- package/dist/generators/generateMcpApp.js.map +1 -0
- package/dist/generators/generateModelMcp.d.ts +9 -0
- package/dist/generators/generateModelMcp.js +145 -0
- package/dist/generators/generateModelMcp.js.map +1 -0
- package/dist/generators/generateModelMetadata.js +29 -15
- package/dist/generators/generateModelMetadata.js.map +1 -1
- package/dist/generators/generateOperationCore.d.ts +3 -3
- package/dist/generators/generateOperationCore.js +51 -16
- package/dist/generators/generateOperationCore.js.map +1 -1
- package/dist/generators/generateRelationMeta.js +2 -2
- package/dist/generators/generateRelationMeta.js.map +1 -1
- package/dist/generators/generateRouteConfigType.d.ts +2 -2
- package/dist/generators/generateRouteConfigType.js +29 -11
- package/dist/generators/generateRouteConfigType.js.map +1 -1
- package/dist/generators/generateRouter.d.ts +7 -4
- package/dist/generators/generateRouter.js +164 -65
- package/dist/generators/generateRouter.js.map +1 -1
- package/dist/generators/generateRouterFastify.d.ts +7 -4
- package/dist/generators/generateRouterFastify.js +106 -86
- package/dist/generators/generateRouterFastify.js.map +1 -1
- package/dist/generators/generateRouterHono.d.ts +7 -4
- package/dist/generators/generateRouterHono.js +59 -414
- package/dist/generators/generateRouterHono.js.map +1 -1
- package/dist/generators/generateRouterPartsHono.d.ts +14 -0
- package/dist/generators/generateRouterPartsHono.js +180 -0
- package/dist/generators/generateRouterPartsHono.js.map +1 -0
- package/dist/generators/generateUnifiedDocs.d.ts +1 -1
- package/dist/generators/generateUnifiedDocs.js +15 -8
- package/dist/generators/generateUnifiedDocs.js.map +1 -1
- package/dist/generators/generateUnifiedHandler.d.ts +2 -2
- package/dist/generators/generateUnifiedHandler.js +42 -18
- package/dist/generators/generateUnifiedHandler.js.map +1 -1
- package/dist/guardOptions.js +12 -7
- package/dist/guardOptions.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +123 -4
- package/dist/index.js.map +1 -1
- package/dist/utils/copyFiles.d.ts +1 -1
- package/dist/utils/copyFiles.js +42 -7
- package/dist/utils/copyFiles.js.map +1 -1
- package/dist/utils/importExt.d.ts +1 -1
- package/dist/utils/mcpGate.d.ts +10 -0
- package/dist/utils/mcpGate.js +118 -0
- package/dist/utils/mcpGate.js.map +1 -0
- package/dist/utils/pathCasing.d.ts +4 -0
- package/dist/utils/pathCasing.js +40 -0
- package/dist/utils/pathCasing.js.map +1 -0
- package/dist/utils/resolveImportStyle.js +13 -5
- package/dist/utils/resolveImportStyle.js.map +1 -1
- package/dist/utils/writeFileSafely.js +6 -0
- package/dist/utils/writeFileSafely.js.map +1 -1
- package/package.json +22 -5
- package/src/bin.ts +1 -1
- package/src/client/encodeQueryParams.ts +39 -35
- package/src/constants.ts +1 -1
- package/src/copy/autoIncludePlanner.ts +93 -27
- package/src/copy/autoIncludePlannerGuarded.ts +171 -48
- package/src/copy/autoIncludeRuntimeGuarded.ts +25 -7
- package/src/copy/buildModelOpenApi.ts +229 -176
- package/src/copy/concurrency.ts +10 -8
- package/src/copy/docsRenderer.ts +14 -15
- package/src/copy/errorMapper.ts +68 -19
- package/src/copy/guardHelpers.ts +30 -9
- package/src/copy/materializedCount.ts +17 -5
- package/src/copy/mcpRuntime.ts +1017 -0
- package/src/copy/misc.ts +88 -68
- package/src/copy/ndjson.ts +72 -0
- package/src/copy/operationDefinitions.ts +177 -25
- package/src/copy/operationPipeline.ts +267 -0
- package/src/copy/operationRuntime.ts +124 -26
- package/src/copy/operationSchemas.ts +2773 -0
- package/src/copy/pagination.ts +35 -13
- package/src/copy/parseQueryParams.ts +47 -48
- package/src/copy/projectionDefaults.ts +6 -4
- package/src/copy/routeConfig.express.ts +16 -4
- package/src/copy/routeConfig.fastify.ts +16 -2
- package/src/copy/routeConfig.hono.ts +46 -14
- package/src/copy/routeConfig.ts +175 -40
- package/src/copy/routerParts.ts +524 -0
- package/src/copy/scalarTypes.ts +7 -2
- package/src/copy/sse.ts +72 -19
- package/src/generators/generateFastifyHandler.ts +77 -20
- package/src/generators/generateHonoHandler.ts +62 -14
- package/src/generators/generateHonoOpenApi.ts +93 -0
- package/src/generators/generateImportPrismaStatement.ts +93 -81
- package/src/generators/generateMcpApp.ts +133 -0
- package/src/generators/generateModelMcp.ts +208 -0
- package/src/generators/generateModelMetadata.ts +40 -22
- package/src/generators/generateOperationCore.ts +81 -33
- package/src/generators/generateRelationMeta.ts +23 -11
- package/src/generators/generateRouteConfigType.ts +39 -13
- package/src/generators/generateRouter.ts +171 -68
- package/src/generators/generateRouterFastify.ts +117 -89
- package/src/generators/generateRouterHono.ts +84 -464
- package/src/generators/generateRouterPartsHono.ts +220 -0
- package/src/generators/generateUnifiedDocs.ts +29 -8
- package/src/generators/generateUnifiedHandler.ts +47 -24
- package/src/generators/generateUnifiedScalarUI.ts +1 -1
- package/src/guardOptions.ts +105 -92
- package/src/index.ts +161 -6
- package/src/utils/copyFiles.ts +235 -168
- package/src/utils/importExt.ts +2 -2
- package/src/utils/mcpGate.ts +157 -0
- package/src/utils/pathCasing.ts +42 -0
- package/src/utils/resolveImportStyle.ts +37 -15
- package/src/utils/strings.ts +2 -2
- 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')
|
|
12
|
-
|
|
13
|
-
return
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
18
|
-
|
|
16
|
+
const handlers = dispatchOps
|
|
17
|
+
.map((meta) => {
|
|
18
|
+
const exportName = `${modelName}${meta.name.charAt(0).toUpperCase() + meta.name.slice(1)}`
|
|
19
19
|
|
|
20
|
-
|
|
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
|
-
|
|
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(
|
|
45
|
+
next(classifyError(error))
|
|
31
46
|
}
|
|
32
47
|
}`
|
|
33
|
-
|
|
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 {
|
|
38
|
-
import {
|
|
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?:
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
79
|
+
function readLocals(res: Response): LocalsBag {
|
|
80
|
+
return res.locals as LocalsBag
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function queryChannel(res: Response): ArgsChannel {
|
|
57
84
|
return {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
+
}
|
package/src/guardOptions.ts
CHANGED
|
@@ -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[] =
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
'
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
'
|
|
182
|
-
|
|
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(
|
|
215
|
+
export function resolveGuardPolicy(
|
|
216
|
+
config: Partial<GuardPolicy> | undefined,
|
|
217
|
+
): GuardPolicy {
|
|
212
218
|
return {
|
|
213
|
-
requireGuardShape:
|
|
219
|
+
requireGuardShape:
|
|
220
|
+
config?.requireGuardShape ?? UPSTREAM_GUARD_DEFAULTS.requireGuardShape,
|
|
214
221
|
validateGuardShapes:
|
|
215
|
-
config?.validateGuardShapes ??
|
|
222
|
+
config?.validateGuardShapes ??
|
|
223
|
+
UPSTREAM_GUARD_DEFAULTS.validateGuardShapes,
|
|
216
224
|
requireDefaultVariantOptIn:
|
|
217
|
-
config?.requireDefaultVariantOptIn ??
|
|
218
|
-
|
|
225
|
+
config?.requireDefaultVariantOptIn ??
|
|
226
|
+
UPSTREAM_GUARD_DEFAULTS.requireDefaultVariantOptIn,
|
|
227
|
+
enableUpdateEach:
|
|
228
|
+
config?.enableUpdateEach ?? UPSTREAM_GUARD_DEFAULTS.enableUpdateEach,
|
|
219
229
|
guardResolutionOrder:
|
|
220
|
-
config?.guardResolutionOrder ??
|
|
230
|
+
config?.guardResolutionOrder ??
|
|
231
|
+
UPSTREAM_GUARD_DEFAULTS.guardResolutionOrder,
|
|
221
232
|
allowE2EGuardBypass:
|
|
222
|
-
config?.allowE2EGuardBypass ??
|
|
233
|
+
config?.allowE2EGuardBypass ??
|
|
234
|
+
UPSTREAM_GUARD_DEFAULTS.allowE2EGuardBypass,
|
|
223
235
|
validateResolvedShapes:
|
|
224
|
-
config?.validateResolvedShapes ??
|
|
236
|
+
config?.validateResolvedShapes ??
|
|
237
|
+
UPSTREAM_GUARD_DEFAULTS.validateResolvedShapes,
|
|
225
238
|
}
|
|
226
239
|
}
|