prisma-generator-express 1.67.0 → 1.69.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 +223 -28
- package/dist/copy/operationDefinitions.d.ts +4 -1
- package/dist/copy/operationDefinitions.js +64 -3
- package/dist/copy/operationDefinitions.js.map +1 -1
- package/dist/generators/generateFastifyHandler.js +55 -9
- package/dist/generators/generateFastifyHandler.js.map +1 -1
- package/dist/generators/generateHonoHandler.js +44 -4
- 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/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 +9 -5
- package/dist/generators/generateModelMetadata.js.map +1 -1
- package/dist/generators/generateRouter.js +62 -56
- package/dist/generators/generateRouter.js.map +1 -1
- package/dist/generators/generateRouterFastify.js +56 -64
- package/dist/generators/generateRouterFastify.js.map +1 -1
- package/dist/generators/generateRouterHono.js +43 -411
- 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/generateUnifiedHandler.js +36 -20
- package/dist/generators/generateUnifiedHandler.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +101 -3
- package/dist/index.js.map +1 -1
- package/dist/utils/copyFiles.d.ts +1 -1
- package/dist/utils/copyFiles.js +20 -1
- package/dist/utils/copyFiles.js.map +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/writeFileSafely.js +6 -0
- package/dist/utils/writeFileSafely.js.map +1 -1
- package/package.json +18 -3
- package/src/copy/buildModelOpenApi.ts +76 -126
- package/src/copy/docsRenderer.ts +14 -15
- package/src/copy/mcpRuntime.ts +1017 -0
- package/src/copy/operationDefinitions.ts +78 -4
- package/src/copy/operationPipeline.ts +267 -0
- package/src/copy/operationRuntime.ts +24 -14
- package/src/copy/operationSchemas.ts +2773 -0
- package/src/copy/pagination.ts +10 -2
- package/src/copy/routeConfig.hono.ts +34 -5
- package/src/copy/routeConfig.ts +1 -1
- package/src/copy/routerParts.ts +524 -0
- package/src/generators/generateFastifyHandler.ts +55 -9
- package/src/generators/generateHonoHandler.ts +44 -4
- package/src/generators/generateHonoOpenApi.ts +93 -0
- package/src/generators/generateMcpApp.ts +133 -0
- package/src/generators/generateModelMcp.ts +208 -0
- package/src/generators/generateModelMetadata.ts +16 -10
- package/src/generators/generateRouter.ts +62 -56
- package/src/generators/generateRouterFastify.ts +56 -64
- package/src/generators/generateRouterHono.ts +49 -459
- package/src/generators/generateRouterPartsHono.ts +220 -0
- package/src/generators/generateUnifiedHandler.ts +36 -21
- package/src/index.ts +116 -3
- package/src/utils/copyFiles.ts +36 -0
- package/src/utils/mcpGate.ts +157 -0
- package/src/utils/writeFileSafely.ts +6 -0
|
@@ -58,15 +58,15 @@ function emitReadOp(
|
|
|
58
58
|
? meta.name === 'findMany'
|
|
59
59
|
? ` if (resolvePostReadsEnabled(config.disablePostReads, opConfig.disablePostReads)) {
|
|
60
60
|
const postPath = basePath ? \`\${basePath}/read\` : '/read'
|
|
61
|
-
app.post(postPath,
|
|
61
|
+
app.post(postPath, createReadRoute<TCtx, TPrisma, TEnv>({ config, opConfig, opKind: '${opKind}', handler: ${handlerName}, parse: 'body', dropGuard, settleBeforeHooks: SETTLE_BEFORE_HOOKS }))
|
|
62
62
|
}`
|
|
63
|
-
: ` if (resolvePostReadsEnabled(config.disablePostReads, opConfig.disablePostReads)) app.post(path,
|
|
63
|
+
: ` if (resolvePostReadsEnabled(config.disablePostReads, opConfig.disablePostReads)) app.post(path, createReadRoute<TCtx, TPrisma, TEnv>({ config, opConfig, opKind: '${opKind}', handler: ${handlerName}, parse: 'body', dropGuard, settleBeforeHooks: SETTLE_BEFORE_HOOKS }))`
|
|
64
64
|
: ''
|
|
65
65
|
|
|
66
66
|
return ` if (isEnabled(config.${meta.configKey})) {
|
|
67
67
|
const opConfig = opFor('${meta.configKey}')
|
|
68
68
|
const path = ${pathValue}
|
|
69
|
-
app.get(path,
|
|
69
|
+
app.get(path, createReadRoute<TCtx, TPrisma, TEnv>({ config, opConfig, opKind: '${opKind}', handler: ${handlerName}, parse: 'query', dropGuard, settleBeforeHooks: SETTLE_BEFORE_HOOKS }))
|
|
70
70
|
${postReadLine}
|
|
71
71
|
}`
|
|
72
72
|
}
|
|
@@ -83,61 +83,19 @@ function emitWriteOp(
|
|
|
83
83
|
return ` if (isEnabled(config.${meta.configKey})) {
|
|
84
84
|
const opConfig = opFor('${meta.configKey}')
|
|
85
85
|
const path = ${pathValue}
|
|
86
|
-
app.${meta.method}(path,
|
|
86
|
+
app.${meta.method}(path, createWriteRoute<TCtx, TPrisma, TEnv>({ config, opConfig, opKind: '${opKind}', handler: ${handlerName}, dropGuard, settleBeforeHooks: SETTLE_BEFORE_HOOKS }))
|
|
87
87
|
}`
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
/**
|
|
91
91
|
* Guard behaviour is SEVEN INDEPENDENT OPT-IN CONTROLS on the route config.
|
|
92
|
+
* See routeConfig.ts; the long rationale lives with the controls.
|
|
92
93
|
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* Each is now its own option, each defaulting to the pre-1.64.2 behaviour:
|
|
100
|
-
*
|
|
101
|
-
* - `requireGuardShape` refuse an operation with no guard
|
|
102
|
-
* - `validateGuardShapes` refuse an empty or key-mixing shape
|
|
103
|
-
* - `requireDefaultVariantOptIn` confirm a `default` variant
|
|
104
|
-
* - `enableUpdateEach` register the batch route (default true)
|
|
105
|
-
* - `guardResolutionOrder` settle the guard before or after hooks
|
|
106
|
-
* - `allowE2EGuardBypass` honour PGE_DROP_GUARD=true (default true)
|
|
107
|
-
* - `validateResolvedShapes` check what a shape function returned
|
|
108
|
-
*
|
|
109
|
-
* `HARDENED_GUARD_PROFILE` selects all seven, as values to spread rather than a
|
|
110
|
-
* mode to store. See routeConfig.ts.
|
|
111
|
-
*
|
|
112
|
-
* Guard dropping is then a GENERATION-TIME decision, never a runtime one.
|
|
113
|
-
*
|
|
114
|
-
* The emitted router used to compute `DROP_GUARD = <flag> || _env.E2E === 'true'`,
|
|
115
|
-
* so setting `E2E=true` in a deployed environment downgraded enforcement even
|
|
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
|
|
120
|
-
* equivalent: with the guard, the shape goes to prisma-guard; with it dropped,
|
|
121
|
-
* `applyDroppedGuard` applies projection defaults and forced `where` clauses and
|
|
122
|
-
* nothing else is validated against the shape.
|
|
123
|
-
*
|
|
124
|
-
* On an edge runtime that variable is an ordinary config var — set on a staging
|
|
125
|
-
* deployment, copied forward, flagged as security-relevant nowhere. A
|
|
126
|
-
* deployment's guard behaviour has to be a property of the artifact, not of the
|
|
127
|
-
* environment it happens to land in.
|
|
128
|
-
*
|
|
129
|
-
* The rationale lives here rather than in the emitted file on purpose: generated
|
|
130
|
-
* output is an artifact, and a paragraph about a bypass that no longer exists
|
|
131
|
-
* would be copied into every router this generator writes.
|
|
132
|
-
*
|
|
133
|
-
* `updateEach` is refused for the same reason, one step further on — and, again,
|
|
134
|
-
* only under the flag. It bypasses guard shapes by design — the endpoint is a
|
|
135
|
-
* batch of `{ where, data }` items applied directly — and the only thing between
|
|
136
|
-
* it and an unguarded mass mutation is a `console.warn` suppressed in
|
|
137
|
-
* production. A warning is not a security boundary; it is advice to whoever
|
|
138
|
-
* happens to be reading a development log. Refused at construction rather than
|
|
139
|
-
* silently dropped, so a deployment expecting the route learns at boot instead
|
|
140
|
-
* of at the first 404.
|
|
94
|
+
* Since phase S the orchestration itself is shared: every route is created by
|
|
95
|
+
* `../routerParts` through the same pipeline stages the other targets and MCP
|
|
96
|
+
* use, and this factory only decides WHICH routes exist. That module is also
|
|
97
|
+
* the phase 9 static boundary — per-op route factories live there, so a
|
|
98
|
+
* bundle importing only selected parts omits the rest.
|
|
141
99
|
*/
|
|
142
100
|
export function generateHonoRouterFunction({
|
|
143
101
|
model,
|
|
@@ -178,44 +136,38 @@ export function generateHonoRouterFunction({
|
|
|
178
136
|
.join('\n\n')
|
|
179
137
|
|
|
180
138
|
return `import { Hono } from 'hono'
|
|
181
|
-
import type { Context } from 'hono'
|
|
182
|
-
import type { ContentfulStatusCode } from 'hono/utils/http-status'
|
|
183
|
-
import { HTTPException } from 'hono/http-exception'
|
|
184
139
|
import {
|
|
185
140
|
${handlerImports}
|
|
186
141
|
} from './${modelName}Handlers${ext}'
|
|
187
142
|
import type {
|
|
188
143
|
RouteConfig,
|
|
189
|
-
HonoBeforeHook,
|
|
190
|
-
HonoAfterHook,
|
|
191
144
|
HonoEnvBase,
|
|
192
|
-
HonoInternalVariables,
|
|
193
145
|
GeneratedHonoEnv,
|
|
194
|
-
PaginationConfig,
|
|
195
146
|
PrismaClientLike,
|
|
196
147
|
} from '../routeConfig.target${ext}'
|
|
197
|
-
import {
|
|
198
|
-
import { normalizePrefix, getEnv, sanitizeKeys, isPlainObject, resolveDropGuardEnv } from '../misc${ext}'
|
|
148
|
+
import { normalizePrefix, getEnv, resolveDropGuardEnv } from '../misc${ext}'
|
|
199
149
|
import { buildModelOpenApi } from '../buildModelOpenApi${ext}'
|
|
200
150
|
import {
|
|
201
|
-
normalizeOperation,
|
|
202
|
-
resolveOperationVariantKey,
|
|
203
151
|
resolveGuardPolicy,
|
|
204
|
-
|
|
152
|
+
resolvePostReadsEnabled,
|
|
205
153
|
validateCountSourceWhere,
|
|
206
|
-
validateOperationConfig,
|
|
207
154
|
validateUpdateEachConfig,
|
|
208
|
-
resolvePostReadsEnabled,
|
|
209
155
|
warnIfUnguardedRoutes,
|
|
210
156
|
} from '../routeConfig${ext}'
|
|
211
|
-
import
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
157
|
+
import {
|
|
158
|
+
createReadRoute,
|
|
159
|
+
createWriteRoute,
|
|
160
|
+
createUpdateEachRoute,
|
|
161
|
+
opConfigFor,
|
|
162
|
+
normalizeHonoOperation,
|
|
163
|
+
registerOpenApiRoutes,
|
|
164
|
+
sendError,
|
|
165
|
+
type HandlerContext,
|
|
166
|
+
type HonoOpConfig,
|
|
167
|
+
} from '../routerParts${ext}'
|
|
168
|
+
// The legacy router factory preserves pre-parts behavior: materialized-view
|
|
169
|
+
// counting is registered on import. The per-op parts boundary keeps this
|
|
170
|
+
// module out of opt-in small bundles.
|
|
219
171
|
import { MODEL_FIELDS, MODEL_ENUMS } from './${modelName}Metadata${ext}'
|
|
220
172
|
|
|
221
173
|
${generateRouteConfigType(modelName, 'HonoBeforeHook', guardShapesImport, importStyle, 'hono', clientImport)}
|
|
@@ -226,247 +178,6 @@ const _env = getEnv()
|
|
|
226
178
|
// which is upstream behaviour. See generateRouterHono.ts.
|
|
227
179
|
const DROP_GUARD = ${dropGuard}
|
|
228
180
|
|
|
229
|
-
/**
|
|
230
|
-
* DELIBERATELY NOT RECURSIVE.
|
|
231
|
-
*
|
|
232
|
-
* A self-referential JSON type sent through \`c.json()\` makes Hono's response
|
|
233
|
-
* inference instantiate without a fixed point, and TypeScript answers
|
|
234
|
-
* "type instantiation is excessively deep" — in the emitted file, for every
|
|
235
|
-
* consumer. One level is all this needs: the value is serialised, not walked.
|
|
236
|
-
*/
|
|
237
|
-
type JsonLike = string | number | boolean | null | unknown[] | Record<string, unknown>
|
|
238
|
-
|
|
239
|
-
type OperationConfigLike<TEnv extends HonoEnvBase> = {
|
|
240
|
-
authorize?: HonoBeforeHook<TEnv>
|
|
241
|
-
override?: RuntimeOperationOverride
|
|
242
|
-
before?: HonoBeforeHook<TEnv>[]
|
|
243
|
-
after?: HonoAfterHook<TEnv>[]
|
|
244
|
-
shape?: unknown
|
|
245
|
-
variants?: Record<
|
|
246
|
-
string,
|
|
247
|
-
{
|
|
248
|
-
shape?: unknown
|
|
249
|
-
before?: HonoBeforeHook<TEnv>[]
|
|
250
|
-
after?: HonoAfterHook<TEnv>[]
|
|
251
|
-
}
|
|
252
|
-
>
|
|
253
|
-
pagination?: Partial<PaginationConfig>
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
type NormalizedOp<TEnv extends HonoEnvBase> = NormalizedOperationConfig<
|
|
257
|
-
HonoBeforeHook<TEnv>,
|
|
258
|
-
HonoAfterHook<TEnv>
|
|
259
|
-
>
|
|
260
|
-
|
|
261
|
-
function normalizeHonoOperation<TEnv extends HonoEnvBase>(
|
|
262
|
-
config: OperationConfigLike<TEnv> | undefined,
|
|
263
|
-
): NormalizedOp<TEnv> {
|
|
264
|
-
return normalizeOperation<HonoBeforeHook<TEnv>, HonoAfterHook<TEnv>>(config)
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
type HandlerContext = Context<{ Variables: HonoInternalVariables }>
|
|
268
|
-
|
|
269
|
-
async function parseQueryMiddleware(c: HandlerContext): Promise<void> {
|
|
270
|
-
const raw = c.req.query() as Record<string, unknown>
|
|
271
|
-
if (raw && Object.keys(raw).length > 0) {
|
|
272
|
-
c.set('parsedQuery', parseQueryParams(raw) as Record<string, unknown>)
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
async function parseBodyAsQueryMiddleware(c: HandlerContext): Promise<void> {
|
|
277
|
-
let body: unknown
|
|
278
|
-
try {
|
|
279
|
-
body = await c.req.json()
|
|
280
|
-
} catch {
|
|
281
|
-
throw new HTTPException(400, { message: 'Request body must be a JSON object' })
|
|
282
|
-
}
|
|
283
|
-
if (!body || typeof body !== 'object' || Array.isArray(body)) {
|
|
284
|
-
throw new HTTPException(400, { message: 'Request body must be a JSON object' })
|
|
285
|
-
}
|
|
286
|
-
c.set('parsedQuery', sanitizeKeys(body as Record<string, unknown>))
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
async function parseUpdateEachBodyMiddleware(c: HandlerContext): Promise<void> {
|
|
290
|
-
let body: unknown
|
|
291
|
-
try {
|
|
292
|
-
body = await c.req.json()
|
|
293
|
-
} catch {
|
|
294
|
-
throw new HTTPException(400, { message: 'updateEach body must be an array of { where, data } items' })
|
|
295
|
-
}
|
|
296
|
-
if (!Array.isArray(body)) {
|
|
297
|
-
throw new HTTPException(400, { message: 'updateEach body must be an array of { where, data } items' })
|
|
298
|
-
}
|
|
299
|
-
c.set('body', body)
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
async function parseWriteBodyMiddleware(c: HandlerContext): Promise<void> {
|
|
303
|
-
let body: unknown
|
|
304
|
-
try {
|
|
305
|
-
body = await c.req.json()
|
|
306
|
-
} catch {
|
|
307
|
-
throw new HTTPException(400, { message: 'Request body must be a JSON object' })
|
|
308
|
-
}
|
|
309
|
-
if (!body || typeof body !== 'object' || Array.isArray(body)) {
|
|
310
|
-
throw new HTTPException(400, { message: 'Request body must be a JSON object' })
|
|
311
|
-
}
|
|
312
|
-
c.set('body', body)
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function makeShapeMiddleware<TCtx, TPrisma extends PrismaClientLike, TEnv extends HonoEnvBase>(
|
|
316
|
-
config: ${modelName}RouteConfig<TCtx, TPrisma, TEnv>,
|
|
317
|
-
opConfig: NormalizedOp<TEnv>,
|
|
318
|
-
opKind: OpKind,
|
|
319
|
-
) {
|
|
320
|
-
const policy = resolveGuardPolicy(config)
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* The environment bypass, honoured unless the consumer turned it off.
|
|
324
|
-
*
|
|
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.
|
|
328
|
-
*/
|
|
329
|
-
const dropGuard = DROP_GUARD || (policy.allowE2EGuardBypass && resolveDropGuardEnv(_env))
|
|
330
|
-
|
|
331
|
-
return async (c: Context<GeneratedHonoEnv<TEnv>>): Promise<void> => {
|
|
332
|
-
/**
|
|
333
|
-
* INTERNAL REQUEST STATE IS SET THROUGH A CONCRETE CONTEXT.
|
|
334
|
-
*
|
|
335
|
-
* \`GeneratedHonoEnv<TEnv>['Variables']\` is \`HonoInternalVariables & TEnv['Variables']\`,
|
|
336
|
-
* and inside this generic function TypeScript cannot know that the consumer's
|
|
337
|
-
* half does not also declare \`routeConfig\` — so setting that key straight
|
|
338
|
-
* onto \`c\` is unassignable to the intersection and the emitted router does not
|
|
339
|
-
* typecheck under \`strict\` for ANY consumer. These keys belong to this
|
|
340
|
-
* router, so they are written through the internal shape they were declared
|
|
341
|
-
* in.
|
|
342
|
-
*/
|
|
343
|
-
const vars = c as unknown as HandlerContext
|
|
344
|
-
vars.set('operationOverride', opConfig.override)
|
|
345
|
-
let context: Promise<unknown> | undefined
|
|
346
|
-
vars.set('resolveOperationContext', () => context ??= Promise.resolve(config.resolveContext?.(c)))
|
|
347
|
-
|
|
348
|
-
const merged = mergePaginationConfig(config.pagination, opConfig.pagination)
|
|
349
|
-
if (merged) vars.set('routeConfig', { pagination: merged })
|
|
350
|
-
|
|
351
|
-
const headerName = config.guard?.variantHeader || 'x-api-variant'
|
|
352
|
-
const headerValue = c.req.header(headerName)
|
|
353
|
-
const caller = config.guard?.resolveVariant?.(c) ?? headerValue ?? undefined
|
|
354
|
-
if (typeof caller === 'string') vars.set('guardCaller', caller)
|
|
355
|
-
|
|
356
|
-
const resolution = resolveOperationVariantKey(opConfig.guardRouting, caller)
|
|
357
|
-
if (!resolution.ok) {
|
|
358
|
-
vars.set('guardVariantFailure', resolution)
|
|
359
|
-
return
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
const resolvedKey =
|
|
363
|
-
opConfig.guardRouting.kind === 'named'
|
|
364
|
-
? resolution.key
|
|
365
|
-
: undefined
|
|
366
|
-
if (resolvedKey !== undefined) vars.set('guardVariantKey', resolvedKey)
|
|
367
|
-
|
|
368
|
-
if (opConfig.guardShape) {
|
|
369
|
-
const resolveCtx = typeof config.resolveContext === 'function'
|
|
370
|
-
? vars.get('resolveOperationContext')
|
|
371
|
-
: undefined
|
|
372
|
-
|
|
373
|
-
/**
|
|
374
|
-
* With \`validateResolvedShapes\`, resolved ONCE here and the validated value
|
|
375
|
-
* is what travels onward — see resolveGuardShapeOnce. Passing the function
|
|
376
|
-
* on would let it return a different shape when it is enforced than when it
|
|
377
|
-
* was checked.
|
|
378
|
-
*
|
|
379
|
-
* Without it the raw shape is passed through untouched, which is upstream
|
|
380
|
-
* behaviour: it is resolved at the point of use, and a function returning
|
|
381
|
-
* something unusable is not this router's business to refuse.
|
|
382
|
-
*/
|
|
383
|
-
let effectiveShape: Record<string, unknown> | undefined = opConfig.guardShape
|
|
384
|
-
if (policy.validateResolvedShapes) {
|
|
385
|
-
const resolution = await resolveGuardShapeOnce(opConfig.guardShape, resolvedKey, resolveCtx)
|
|
386
|
-
if (!resolution.ok) {
|
|
387
|
-
vars.set('guardShapeFailure', resolution.problem)
|
|
388
|
-
return
|
|
389
|
-
}
|
|
390
|
-
// \`ok\` means the resolved value is a shape object; the fallback keeps the
|
|
391
|
-
// declared shape rather than storing something the context cannot hold.
|
|
392
|
-
effectiveShape = isPlainObject(resolution.shape) ? resolution.shape : opConfig.guardShape
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
if (!dropGuard) {
|
|
396
|
-
vars.set('guardShape', effectiveShape)
|
|
397
|
-
} else {
|
|
398
|
-
await applyDroppedGuard(
|
|
399
|
-
effectiveShape,
|
|
400
|
-
resolvedKey,
|
|
401
|
-
resolveCtx,
|
|
402
|
-
opKind,
|
|
403
|
-
{
|
|
404
|
-
readQuery: vars.get('parsedQuery'),
|
|
405
|
-
writeBody: isPlainObject(vars.get('body'))
|
|
406
|
-
? (vars.get('body') as Record<string, unknown>)
|
|
407
|
-
: undefined,
|
|
408
|
-
},
|
|
409
|
-
() => {
|
|
410
|
-
let target = vars.get('parsedQuery')
|
|
411
|
-
if (!target) {
|
|
412
|
-
target = {}
|
|
413
|
-
vars.set('parsedQuery', target)
|
|
414
|
-
}
|
|
415
|
-
return target
|
|
416
|
-
},
|
|
417
|
-
() => {
|
|
418
|
-
let target = vars.get('body')
|
|
419
|
-
if (!isPlainObject(target)) {
|
|
420
|
-
target = {}
|
|
421
|
-
vars.set('body', target)
|
|
422
|
-
}
|
|
423
|
-
return target as Record<string, unknown>
|
|
424
|
-
},
|
|
425
|
-
)
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
async function runBeforeHooks<TEnv extends HonoEnvBase>(
|
|
432
|
-
hooks: readonly HonoBeforeHook<TEnv>[],
|
|
433
|
-
c: Context<GeneratedHonoEnv<TEnv>>,
|
|
434
|
-
): Promise<Response | undefined> {
|
|
435
|
-
for (const hook of hooks) {
|
|
436
|
-
const result = await hook(c)
|
|
437
|
-
if (result instanceof Response) return result
|
|
438
|
-
}
|
|
439
|
-
return undefined
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
async function runAfterHooks<TEnv extends HonoEnvBase>(
|
|
443
|
-
hooks: readonly HonoAfterHook<TEnv>[],
|
|
444
|
-
c: Context<GeneratedHonoEnv<TEnv>>,
|
|
445
|
-
): Promise<Response | undefined> {
|
|
446
|
-
for (const hook of hooks) {
|
|
447
|
-
const result = await hook(c)
|
|
448
|
-
if (result instanceof Response) return result
|
|
449
|
-
}
|
|
450
|
-
return undefined
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
function sendResult(c: HandlerContext): Response {
|
|
454
|
-
const data = c.get('resultData')
|
|
455
|
-
const status = (c.get('resultStatus') as number | undefined) ?? 200
|
|
456
|
-
if (data === undefined) {
|
|
457
|
-
throw new HTTPException(500, { message: 'No data set by handler' })
|
|
458
|
-
}
|
|
459
|
-
return c.json(transformResult(data) as JsonLike, status as ContentfulStatusCode)
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
function sendError(c: HandlerContext, error: unknown): Response {
|
|
463
|
-
if (error instanceof HTTPException) {
|
|
464
|
-
return c.json({ message: error.message }, error.status as ContentfulStatusCode)
|
|
465
|
-
}
|
|
466
|
-
const httpError = mapError(error)
|
|
467
|
-
return c.json({ message: httpError.message }, httpError.status as ContentfulStatusCode)
|
|
468
|
-
}
|
|
469
|
-
|
|
470
181
|
export function ${routerFunctionName}<TCtx = unknown, TPrisma extends PrismaClientLike = PrismaClientLike, TEnv extends HonoEnvBase = HonoEnvBase>(config: ${modelName}RouteConfig<TCtx, TPrisma, TEnv> = {}): Hono<GeneratedHonoEnv<TEnv>> {
|
|
471
182
|
validateCountSourceWhere(config.pagination?.countSource, '${modelName} pagination')
|
|
472
183
|
validateCountSourceWhere(
|
|
@@ -480,16 +191,33 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma extends PrismaClie
|
|
|
480
191
|
warnIfUnguardedRoutes('${modelName}', ['findMany', 'findUnique', 'findUniqueOrThrow', 'findFirst', 'findFirstOrThrow', 'findManyPaginated', 'count', 'aggregate', 'groupBy', 'create', 'createMany', 'createManyAndReturn', 'update', 'updateMany', 'updateManyAndReturn', 'upsert', 'delete', 'deleteMany'], config, isEnabled)
|
|
481
192
|
|
|
482
193
|
const customPrefix = normalizePrefix(config.customUrlPrefix || '')
|
|
483
|
-
|
|
194
|
+
const modelPrefix = config.addModelPrefix !== false ? '/${modelSegment}' : ''
|
|
484
195
|
const basePath = customPrefix + modelPrefix
|
|
485
196
|
|
|
197
|
+
const POLICY = resolveGuardPolicy(config)
|
|
198
|
+
const SETTLE_BEFORE_HOOKS = POLICY.guardResolutionOrder === 'before-hooks'
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* The environment bypass, honoured unless the consumer turned it off.
|
|
202
|
+
* \`PGE_DROP_GUARD=true\` downgrading enforcement in a deployed environment is
|
|
203
|
+
* a real hazard, and it is also upstream behaviour (under its deprecated
|
|
204
|
+
* \`E2E=true\` spelling) — so it is a control, not a decision made here.
|
|
205
|
+
*/
|
|
206
|
+
const dropGuard = DROP_GUARD || (POLICY.allowE2EGuardBypass && resolveDropGuardEnv(_env))
|
|
207
|
+
|
|
208
|
+
const opFor = <K extends keyof ${modelName}RouteConfig<TCtx, TPrisma, TEnv>>(
|
|
209
|
+
key: K,
|
|
210
|
+
): HonoOpConfig<TEnv> => {
|
|
211
|
+
const raw = config[key] as unknown as Parameters<typeof normalizeHonoOperation<TEnv>>[0]
|
|
212
|
+
return opConfigFor<TEnv>(raw, '${modelName}.' + String(key), POLICY)
|
|
213
|
+
}
|
|
214
|
+
|
|
486
215
|
const openApiDisabled = config.disableOpenApi === true
|
|
487
216
|
|| (config.disableOpenApi !== false && (
|
|
488
217
|
_env.NODE_ENV === 'production'
|
|
489
218
|
|| _env.DISABLE_OPENAPI === 'true'
|
|
490
219
|
))
|
|
491
220
|
|
|
492
|
-
|
|
493
221
|
let _openApiJsonCache: unknown = undefined
|
|
494
222
|
const getOpenApiJson = (): unknown => {
|
|
495
223
|
if (_openApiJsonCache === undefined) {
|
|
@@ -529,129 +257,7 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma extends PrismaClie
|
|
|
529
257
|
})
|
|
530
258
|
|
|
531
259
|
if (!openApiDisabled) {
|
|
532
|
-
|
|
533
|
-
const openapiYamlPath = basePath ? \`\${basePath}/openapi.yaml\` : '/openapi.yaml'
|
|
534
|
-
app.get(openapiJsonPath, (c) => c.json(getOpenApiJson() as JsonLike))
|
|
535
|
-
app.get(openapiYamlPath, (c) => {
|
|
536
|
-
c.header('Content-Type', 'application/yaml')
|
|
537
|
-
return c.body(getOpenApiYaml())
|
|
538
|
-
})
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
const POLICY = resolveGuardPolicy(config)
|
|
542
|
-
const SETTLE_BEFORE_HOOKS = POLICY.guardResolutionOrder === 'before-hooks'
|
|
543
|
-
|
|
544
|
-
/**
|
|
545
|
-
* The guard-resolution failures, raised in one place so both handlers and both
|
|
546
|
-
* orderings share exactly one definition of what a failure is.
|
|
547
|
-
*
|
|
548
|
-
* WHERE it is called is the behavioural difference, and it is the
|
|
549
|
-
* \`guardResolutionOrder\` control. \`'before-hooks'\` runs it before any operation
|
|
550
|
-
* hook, because a hook that returns a Response — an auth gate, a cache, a
|
|
551
|
-
* short-circuit for a known caller — would otherwise answer a request whose
|
|
552
|
-
* guard was never established. \`'after-hooks'\` is upstream behaviour.
|
|
553
|
-
*
|
|
554
|
-
* \`guardShapeFailure\` is only ever set when \`validateResolvedShapes\` is on, so
|
|
555
|
-
* that branch is inert rather than merely unreached when it is off.
|
|
556
|
-
*/
|
|
557
|
-
const settleGuard = (c: Context<GeneratedHonoEnv<TEnv>>): void => {
|
|
558
|
-
const vars = c as unknown as HandlerContext
|
|
559
|
-
const failure = vars.get('guardVariantFailure')
|
|
560
|
-
if (failure) {
|
|
561
|
-
throw new HTTPException(400, {
|
|
562
|
-
message: formatGuardVariantResolutionError(failure),
|
|
563
|
-
})
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
const shapeFailure = vars.get('guardShapeFailure')
|
|
567
|
-
if (shapeFailure) {
|
|
568
|
-
throw new HTTPException(500, {
|
|
569
|
-
message: 'guard shape could not be resolved: ' + shapeFailure,
|
|
570
|
-
})
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
const handleRead = (
|
|
575
|
-
opConfig: NormalizedOp<TEnv>,
|
|
576
|
-
handlerFn: (c: HandlerContext) => Promise<void>,
|
|
577
|
-
parseFn: (c: HandlerContext) => Promise<void>,
|
|
578
|
-
opKind: OpKind,
|
|
579
|
-
) => async (c: Context<GeneratedHonoEnv<TEnv>>): Promise<Response> => {
|
|
580
|
-
try {
|
|
581
|
-
const authorized = await runBeforeHooks<TEnv>(opConfig.authorize ? [opConfig.authorize] : [], c)
|
|
582
|
-
if (authorized) return authorized
|
|
583
|
-
await parseFn(c as unknown as HandlerContext)
|
|
584
|
-
await makeShapeMiddleware<TCtx, TPrisma, TEnv>(config, opConfig, opKind)(c)
|
|
585
|
-
|
|
586
|
-
// Settled BEFORE any hook can answer the request, when asked for.
|
|
587
|
-
if (SETTLE_BEFORE_HOOKS) settleGuard(c)
|
|
588
|
-
|
|
589
|
-
const operationBefore = await runBeforeHooks<TEnv>(opConfig.operationBefore, c)
|
|
590
|
-
if (operationBefore) return operationBefore
|
|
591
|
-
|
|
592
|
-
// Upstream order: hooks first, guard failure after.
|
|
593
|
-
if (!SETTLE_BEFORE_HOOKS) settleGuard(c)
|
|
594
|
-
|
|
595
|
-
const key = (c as unknown as HandlerContext).get('guardVariantKey')
|
|
596
|
-
const variantHooks =
|
|
597
|
-
key !== undefined ? opConfig.variantHooks[key] : undefined
|
|
598
|
-
|
|
599
|
-
const variantBefore = await runBeforeHooks<TEnv>(variantHooks?.before ?? [], c)
|
|
600
|
-
if (variantBefore) return variantBefore
|
|
601
|
-
await handlerFn(c as unknown as HandlerContext)
|
|
602
|
-
const variantAfter = await runAfterHooks<TEnv>(variantHooks?.after ?? [], c)
|
|
603
|
-
if (variantAfter) return variantAfter
|
|
604
|
-
const operationAfter = await runAfterHooks<TEnv>(opConfig.operationAfter, c)
|
|
605
|
-
if (operationAfter) return operationAfter
|
|
606
|
-
return sendResult(c as unknown as HandlerContext)
|
|
607
|
-
} catch (error: unknown) {
|
|
608
|
-
return sendError(c as unknown as HandlerContext, error)
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
const handleWrite = (
|
|
613
|
-
opConfig: NormalizedOp<TEnv>,
|
|
614
|
-
handlerFn: (c: HandlerContext) => Promise<void>,
|
|
615
|
-
opKind: OpKind,
|
|
616
|
-
) => async (c: Context<GeneratedHonoEnv<TEnv>>): Promise<Response> => {
|
|
617
|
-
try {
|
|
618
|
-
const authorized = await runBeforeHooks<TEnv>(opConfig.authorize ? [opConfig.authorize] : [], c)
|
|
619
|
-
if (authorized) return authorized
|
|
620
|
-
await parseWriteBodyMiddleware(c as unknown as HandlerContext)
|
|
621
|
-
await makeShapeMiddleware<TCtx, TPrisma, TEnv>(config, opConfig, opKind)(c)
|
|
622
|
-
|
|
623
|
-
// Settled BEFORE any hook can answer the request, when asked for.
|
|
624
|
-
if (SETTLE_BEFORE_HOOKS) settleGuard(c)
|
|
625
|
-
|
|
626
|
-
const operationBefore = await runBeforeHooks<TEnv>(opConfig.operationBefore, c)
|
|
627
|
-
if (operationBefore) return operationBefore
|
|
628
|
-
|
|
629
|
-
// Upstream order: hooks first, guard failure after.
|
|
630
|
-
if (!SETTLE_BEFORE_HOOKS) settleGuard(c)
|
|
631
|
-
|
|
632
|
-
const key = (c as unknown as HandlerContext).get('guardVariantKey')
|
|
633
|
-
const variantHooks =
|
|
634
|
-
key !== undefined ? opConfig.variantHooks[key] : undefined
|
|
635
|
-
|
|
636
|
-
const variantBefore = await runBeforeHooks<TEnv>(variantHooks?.before ?? [], c)
|
|
637
|
-
if (variantBefore) return variantBefore
|
|
638
|
-
await handlerFn(c as unknown as HandlerContext)
|
|
639
|
-
const variantAfter = await runAfterHooks<TEnv>(variantHooks?.after ?? [], c)
|
|
640
|
-
if (variantAfter) return variantAfter
|
|
641
|
-
const operationAfter = await runAfterHooks<TEnv>(opConfig.operationAfter, c)
|
|
642
|
-
if (operationAfter) return operationAfter
|
|
643
|
-
return sendResult(c as unknown as HandlerContext)
|
|
644
|
-
} catch (error: unknown) {
|
|
645
|
-
return sendError(c as unknown as HandlerContext, error)
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
const opFor = <K extends keyof ${modelName}RouteConfig<TCtx, TPrisma, TEnv>>(
|
|
650
|
-
key: K,
|
|
651
|
-
): NormalizedOp<TEnv> => {
|
|
652
|
-
const raw = config[key] as unknown as OperationConfigLike<TEnv> | undefined
|
|
653
|
-
validateOperationConfig(raw, '${modelName}.' + String(key), POLICY)
|
|
654
|
-
return normalizeHonoOperation(raw)
|
|
260
|
+
registerOpenApiRoutes(app, basePath, getOpenApiJson, getOpenApiYaml)
|
|
655
261
|
}
|
|
656
262
|
|
|
657
263
|
${readOpBlocks}
|
|
@@ -664,9 +270,6 @@ ${writeOpBlocks}
|
|
|
664
270
|
* endpoint is a batch of { where, data } applied directly — and the only thing
|
|
665
271
|
* between it and an unguarded mass mutation is a console.warn suppressed in
|
|
666
272
|
* production. A warning is not a security boundary.
|
|
667
|
-
*
|
|
668
|
-
* Removing the route outright was a breaking change to a published package,
|
|
669
|
-
* so by default it registers exactly as it did, warning and all.
|
|
670
273
|
*/
|
|
671
274
|
if (!POLICY.enableUpdateEach) {
|
|
672
275
|
throw new Error(
|
|
@@ -678,9 +281,9 @@ ${writeOpBlocks}
|
|
|
678
281
|
)
|
|
679
282
|
}
|
|
680
283
|
|
|
681
|
-
const rawUpdateEach = config.updateEach as unknown as
|
|
284
|
+
const rawUpdateEach = config.updateEach as unknown as Parameters<typeof normalizeHonoOperation<TEnv>>[0]
|
|
682
285
|
validateUpdateEachConfig(rawUpdateEach, '${modelName}.updateEach')
|
|
683
|
-
const opConfig = normalizeHonoOperation(rawUpdateEach)
|
|
286
|
+
const opConfig = normalizeHonoOperation<TEnv>(rawUpdateEach)
|
|
684
287
|
if (opConfig.operationBefore.length === 0 && _env.NODE_ENV !== 'production') {
|
|
685
288
|
console.warn(
|
|
686
289
|
'[${modelName}Router] updateEach is enabled without a before hook. ' +
|
|
@@ -688,20 +291,7 @@ ${writeOpBlocks}
|
|
|
688
291
|
)
|
|
689
292
|
}
|
|
690
293
|
const path = basePath ? \`\${basePath}/each\` : '/each'
|
|
691
|
-
app.post(path,
|
|
692
|
-
try {
|
|
693
|
-
await parseUpdateEachBodyMiddleware(c as unknown as HandlerContext)
|
|
694
|
-
await makeShapeMiddleware<TCtx, TPrisma, TEnv>(config, opConfig, 'noop')(c)
|
|
695
|
-
const beforeResponse = await runBeforeHooks<TEnv>(opConfig.operationBefore, c)
|
|
696
|
-
if (beforeResponse) return beforeResponse
|
|
697
|
-
await ${modelName}UpdateEach(c as unknown as HandlerContext)
|
|
698
|
-
const afterResponse = await runAfterHooks<TEnv>(opConfig.operationAfter, c)
|
|
699
|
-
if (afterResponse) return afterResponse
|
|
700
|
-
return sendResult(c as unknown as HandlerContext)
|
|
701
|
-
} catch (error: unknown) {
|
|
702
|
-
return sendError(c as unknown as HandlerContext, error)
|
|
703
|
-
}
|
|
704
|
-
})
|
|
294
|
+
app.post(path, createUpdateEachRoute<TCtx, TPrisma, TEnv>({ config, opConfig, handler: ${modelName}UpdateEach, dropGuard }))
|
|
705
295
|
}
|
|
706
296
|
|
|
707
297
|
return app
|