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
|
@@ -21,11 +21,25 @@ export interface OpMetadata {
|
|
|
21
21
|
argsSchema: readonly string[]
|
|
22
22
|
requiredArgs: readonly string[]
|
|
23
23
|
notes: string
|
|
24
|
+
/**
|
|
25
|
+
* EXPLICIT behaviour annotations, never inferred from `kind`. Mapped to MCP
|
|
26
|
+
* tool hints (readOnlyHint / destructiveHint / idempotentHint) and available
|
|
27
|
+
* for any other transport that wants the same vocabulary. MCP semantics:
|
|
28
|
+
* `destructive: false` means ONLY additive updates — anything that can
|
|
29
|
+
* overwrite or remove existing data (update, upsert, delete families) is
|
|
30
|
+
* destructive.
|
|
31
|
+
*/
|
|
32
|
+
readOnly: boolean
|
|
33
|
+
destructive: boolean
|
|
34
|
+
idempotent: boolean
|
|
24
35
|
}
|
|
25
36
|
|
|
26
37
|
export const OPERATION_METADATA: readonly OpMetadata[] = [
|
|
27
38
|
{
|
|
28
39
|
name: 'findMany',
|
|
40
|
+
readOnly: true,
|
|
41
|
+
destructive: false,
|
|
42
|
+
idempotent: true,
|
|
29
43
|
method: 'get',
|
|
30
44
|
pathSuffix: '',
|
|
31
45
|
configKey: 'findMany',
|
|
@@ -56,6 +70,9 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
|
|
|
56
70
|
},
|
|
57
71
|
{
|
|
58
72
|
name: 'findUnique',
|
|
73
|
+
readOnly: true,
|
|
74
|
+
destructive: false,
|
|
75
|
+
idempotent: true,
|
|
59
76
|
method: 'get',
|
|
60
77
|
pathSuffix: '/unique',
|
|
61
78
|
configKey: 'findUnique',
|
|
@@ -76,6 +93,9 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
|
|
|
76
93
|
},
|
|
77
94
|
{
|
|
78
95
|
name: 'findUniqueOrThrow',
|
|
96
|
+
readOnly: true,
|
|
97
|
+
destructive: false,
|
|
98
|
+
idempotent: true,
|
|
79
99
|
method: 'get',
|
|
80
100
|
pathSuffix: '/unique/strict',
|
|
81
101
|
configKey: 'findUniqueOrThrow',
|
|
@@ -96,6 +116,9 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
|
|
|
96
116
|
},
|
|
97
117
|
{
|
|
98
118
|
name: 'findFirst',
|
|
119
|
+
readOnly: true,
|
|
120
|
+
destructive: false,
|
|
121
|
+
idempotent: true,
|
|
99
122
|
method: 'get',
|
|
100
123
|
pathSuffix: '/first',
|
|
101
124
|
configKey: 'findFirst',
|
|
@@ -126,6 +149,9 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
|
|
|
126
149
|
},
|
|
127
150
|
{
|
|
128
151
|
name: 'findFirstOrThrow',
|
|
152
|
+
readOnly: true,
|
|
153
|
+
destructive: false,
|
|
154
|
+
idempotent: true,
|
|
129
155
|
method: 'get',
|
|
130
156
|
pathSuffix: '/first/strict',
|
|
131
157
|
configKey: 'findFirstOrThrow',
|
|
@@ -156,6 +182,9 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
|
|
|
156
182
|
},
|
|
157
183
|
{
|
|
158
184
|
name: 'findManyPaginated',
|
|
185
|
+
readOnly: true,
|
|
186
|
+
destructive: false,
|
|
187
|
+
idempotent: true,
|
|
159
188
|
method: 'get',
|
|
160
189
|
pathSuffix: '/paginated',
|
|
161
190
|
configKey: 'findManyPaginated',
|
|
@@ -187,6 +216,9 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
|
|
|
187
216
|
},
|
|
188
217
|
{
|
|
189
218
|
name: 'count',
|
|
219
|
+
readOnly: true,
|
|
220
|
+
destructive: false,
|
|
221
|
+
idempotent: true,
|
|
190
222
|
method: 'get',
|
|
191
223
|
pathSuffix: '/count',
|
|
192
224
|
configKey: 'count',
|
|
@@ -208,6 +240,9 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
|
|
|
208
240
|
},
|
|
209
241
|
{
|
|
210
242
|
name: 'aggregate',
|
|
243
|
+
readOnly: true,
|
|
244
|
+
destructive: false,
|
|
245
|
+
idempotent: true,
|
|
211
246
|
method: 'get',
|
|
212
247
|
pathSuffix: '/aggregate',
|
|
213
248
|
configKey: 'aggregate',
|
|
@@ -240,6 +275,9 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
|
|
|
240
275
|
},
|
|
241
276
|
{
|
|
242
277
|
name: 'groupBy',
|
|
278
|
+
readOnly: true,
|
|
279
|
+
destructive: false,
|
|
280
|
+
idempotent: true,
|
|
243
281
|
method: 'get',
|
|
244
282
|
pathSuffix: '/groupby',
|
|
245
283
|
configKey: 'groupBy',
|
|
@@ -274,6 +312,9 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
|
|
|
274
312
|
},
|
|
275
313
|
{
|
|
276
314
|
name: 'create',
|
|
315
|
+
readOnly: false,
|
|
316
|
+
destructive: false,
|
|
317
|
+
idempotent: false,
|
|
277
318
|
method: 'post',
|
|
278
319
|
pathSuffix: '',
|
|
279
320
|
configKey: 'create',
|
|
@@ -294,6 +335,9 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
|
|
|
294
335
|
},
|
|
295
336
|
{
|
|
296
337
|
name: 'createMany',
|
|
338
|
+
readOnly: false,
|
|
339
|
+
destructive: false,
|
|
340
|
+
idempotent: false,
|
|
297
341
|
method: 'post',
|
|
298
342
|
pathSuffix: '/many',
|
|
299
343
|
configKey: 'createMany',
|
|
@@ -315,6 +359,9 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
|
|
|
315
359
|
},
|
|
316
360
|
{
|
|
317
361
|
name: 'createManyAndReturn',
|
|
362
|
+
readOnly: false,
|
|
363
|
+
destructive: false,
|
|
364
|
+
idempotent: false,
|
|
318
365
|
method: 'post',
|
|
319
366
|
pathSuffix: '/many/return',
|
|
320
367
|
configKey: 'createManyAndReturn',
|
|
@@ -336,6 +383,9 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
|
|
|
336
383
|
},
|
|
337
384
|
{
|
|
338
385
|
name: 'update',
|
|
386
|
+
readOnly: false,
|
|
387
|
+
destructive: true,
|
|
388
|
+
idempotent: false,
|
|
339
389
|
method: 'put',
|
|
340
390
|
pathSuffix: '',
|
|
341
391
|
configKey: 'update',
|
|
@@ -357,6 +407,9 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
|
|
|
357
407
|
},
|
|
358
408
|
{
|
|
359
409
|
name: 'updateMany',
|
|
410
|
+
readOnly: false,
|
|
411
|
+
destructive: true,
|
|
412
|
+
idempotent: false,
|
|
360
413
|
method: 'put',
|
|
361
414
|
pathSuffix: '/many',
|
|
362
415
|
configKey: 'updateMany',
|
|
@@ -378,6 +431,9 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
|
|
|
378
431
|
},
|
|
379
432
|
{
|
|
380
433
|
name: 'updateManyAndReturn',
|
|
434
|
+
readOnly: false,
|
|
435
|
+
destructive: true,
|
|
436
|
+
idempotent: false,
|
|
381
437
|
method: 'put',
|
|
382
438
|
pathSuffix: '/many/return',
|
|
383
439
|
configKey: 'updateManyAndReturn',
|
|
@@ -399,6 +455,9 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
|
|
|
399
455
|
},
|
|
400
456
|
{
|
|
401
457
|
name: 'upsert',
|
|
458
|
+
readOnly: false,
|
|
459
|
+
destructive: true,
|
|
460
|
+
idempotent: true,
|
|
402
461
|
method: 'patch',
|
|
403
462
|
pathSuffix: '',
|
|
404
463
|
configKey: 'upsert',
|
|
@@ -419,6 +478,9 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
|
|
|
419
478
|
},
|
|
420
479
|
{
|
|
421
480
|
name: 'delete',
|
|
481
|
+
readOnly: false,
|
|
482
|
+
destructive: true,
|
|
483
|
+
idempotent: true,
|
|
422
484
|
method: 'delete',
|
|
423
485
|
pathSuffix: '',
|
|
424
486
|
configKey: 'delete',
|
|
@@ -439,6 +501,9 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
|
|
|
439
501
|
},
|
|
440
502
|
{
|
|
441
503
|
name: 'deleteMany',
|
|
504
|
+
readOnly: false,
|
|
505
|
+
destructive: true,
|
|
506
|
+
idempotent: true,
|
|
442
507
|
method: 'delete',
|
|
443
508
|
pathSuffix: '/many',
|
|
444
509
|
configKey: 'deleteMany',
|
|
@@ -459,6 +524,9 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
|
|
|
459
524
|
},
|
|
460
525
|
{
|
|
461
526
|
name: 'updateEach',
|
|
527
|
+
readOnly: false,
|
|
528
|
+
destructive: true,
|
|
529
|
+
idempotent: false,
|
|
462
530
|
method: 'post',
|
|
463
531
|
pathSuffix: '/each',
|
|
464
532
|
configKey: 'updateEach',
|
|
@@ -517,10 +585,16 @@ export function getPostReadPathSuffix(opName: string): string {
|
|
|
517
585
|
}
|
|
518
586
|
|
|
519
587
|
export function isOperationEnabled(
|
|
520
|
-
config:
|
|
588
|
+
config: object,
|
|
521
589
|
def: OperationDef | OpMetadata,
|
|
522
590
|
): boolean {
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
591
|
+
// Route configs are interfaces without index signatures, so the value is
|
|
592
|
+
// reached through one runtime narrowing here at the single point that reads it.
|
|
593
|
+
if (!config || typeof config !== 'object') {
|
|
594
|
+
return false
|
|
595
|
+
}
|
|
596
|
+
const cfg = config as Record<string, unknown>
|
|
597
|
+
if (cfg[def.configKey] === false) return false
|
|
598
|
+
if (def.excludeFromEnableAll) return !!cfg[def.configKey]
|
|
599
|
+
return !!(cfg.enableAll || cfg[def.configKey])
|
|
526
600
|
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { HttpError, mapError } from './errorMapper'
|
|
2
|
+
import { formatGuardVariantResolutionError } from './guardVariantError'
|
|
3
|
+
import { isPlainObject } from './misc'
|
|
4
|
+
import {
|
|
5
|
+
applyDroppedGuard,
|
|
6
|
+
type ContextResolver,
|
|
7
|
+
type OpKind,
|
|
8
|
+
} from './projectionDefaults'
|
|
9
|
+
import {
|
|
10
|
+
resolveOperationVariantKey,
|
|
11
|
+
resolveGuardShapeOnce,
|
|
12
|
+
type NormalizedGuardRouting,
|
|
13
|
+
type PaginationConfig,
|
|
14
|
+
type FindManyPaginatedMode,
|
|
15
|
+
} from './routeConfig'
|
|
16
|
+
import type { GuardVariantResolution } from './guardVariantRouting'
|
|
17
|
+
import type {
|
|
18
|
+
OperationContext,
|
|
19
|
+
PrismaClientLike,
|
|
20
|
+
RuntimeOperationOverride,
|
|
21
|
+
} from './operationRuntime'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Transport-neutral orchestration shared by the Express, Fastify, Hono and MCP
|
|
25
|
+
* emitters. Five stages: route, prepareGuard, settle, execute, classifyError.
|
|
26
|
+
*
|
|
27
|
+
* The pipeline never reads a request, a header or an authentication. Adapters
|
|
28
|
+
* resolve the caller themselves and pass it in; `caller` is a routing key for
|
|
29
|
+
* variant selection only, never an identity.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/** Resolves application context at most once per request. */
|
|
33
|
+
export type GetContext<T = unknown> = () => Promise<T>
|
|
34
|
+
|
|
35
|
+
export function memoizeContext<T>(): GetContext<T>
|
|
36
|
+
export function memoizeContext<T>(resolver: () => T | undefined): GetContext<T>
|
|
37
|
+
export function memoizeContext(resolver?: () => unknown): GetContext {
|
|
38
|
+
let cached: Promise<unknown> | undefined
|
|
39
|
+
return () => (cached ??= Promise.resolve(resolver?.()))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* One request-scoped argument channel. Backed by adapter request storage
|
|
44
|
+
* (Express `res.locals.parsedQuery` / `req.body`, the Fastify request, Hono
|
|
45
|
+
* context vars). Dropped-guard transformations, framework hooks, overrides and
|
|
46
|
+
* execution all observe the same storage through it, so guard transforms
|
|
47
|
+
* cannot be dropped between stages.
|
|
48
|
+
*
|
|
49
|
+
* `read()` returns the STORED value — `undefined` when nothing was stored —
|
|
50
|
+
* exactly like the storage it fronts; mutation of a stored object is visible
|
|
51
|
+
* to every later stage.
|
|
52
|
+
*/
|
|
53
|
+
export type ArgsChannel = {
|
|
54
|
+
read(): Record<string, unknown> | undefined
|
|
55
|
+
write(next: Record<string, unknown>): void
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Runtime narrowing for the adapter boundary: request bags store the client
|
|
60
|
+
* as `unknown` (Express `req.prisma`, the Fastify request, Hono vars), and
|
|
61
|
+
* this is the ONE place that narrows it to the structural client contract the
|
|
62
|
+
* pipeline types require. Missing client is the classic deployment fault —
|
|
63
|
+
* classified 500, same message the operation core raises later.
|
|
64
|
+
*/
|
|
65
|
+
export function requirePrisma(value: unknown): PrismaClientLike {
|
|
66
|
+
if (value === null || typeof value !== 'object') {
|
|
67
|
+
throw new HttpError(
|
|
68
|
+
500,
|
|
69
|
+
'PrismaClient not found on request. Set req.prisma in middleware.',
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
return value as PrismaClientLike
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Ensure-style accessor over a channel: materialises and stores empties. */
|
|
76
|
+
export function ensureArgs(channel: ArgsChannel): Record<string, unknown> {
|
|
77
|
+
const current = channel.read()
|
|
78
|
+
if (current) return current
|
|
79
|
+
const fresh: Record<string, unknown> = {}
|
|
80
|
+
channel.write(fresh)
|
|
81
|
+
return fresh
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Minimal execution policy; `GuardPolicy` itself carries target defaults. */
|
|
85
|
+
export type PipelinePolicy = {
|
|
86
|
+
dropGuard: boolean
|
|
87
|
+
validateResolvedShapes: boolean
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export type RouteStageOk = {
|
|
91
|
+
ok: true
|
|
92
|
+
variantKey: string | undefined
|
|
93
|
+
caller: string | undefined
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export type RouteStageResult = RouteStageOk | { ok: false; failure: HttpError }
|
|
97
|
+
|
|
98
|
+
export type GuardStageOk = {
|
|
99
|
+
ok: true
|
|
100
|
+
guardShape: Record<string, unknown> | undefined
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export type GuardStageResult = GuardStageOk | { ok: false; failure: HttpError }
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Variant routing. Synchronous; no application context, no Prisma. A variant
|
|
107
|
+
* failure is stored already classified as 400, so `settleStage` rethrows the
|
|
108
|
+
* same classified error wherever the adapter chooses to settle it.
|
|
109
|
+
*/
|
|
110
|
+
export function routeOperation(input: {
|
|
111
|
+
guardRouting: NormalizedGuardRouting
|
|
112
|
+
caller: string | undefined
|
|
113
|
+
}): RouteStageResult {
|
|
114
|
+
const resolution: GuardVariantResolution = resolveOperationVariantKey(
|
|
115
|
+
input.guardRouting,
|
|
116
|
+
input.caller,
|
|
117
|
+
)
|
|
118
|
+
if (!resolution.ok) {
|
|
119
|
+
return {
|
|
120
|
+
ok: false,
|
|
121
|
+
failure: new HttpError(
|
|
122
|
+
400,
|
|
123
|
+
formatGuardVariantResolutionError(resolution),
|
|
124
|
+
),
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const variantKey =
|
|
128
|
+
input.guardRouting.kind === 'named' ? resolution.key : undefined
|
|
129
|
+
return {
|
|
130
|
+
ok: true,
|
|
131
|
+
variantKey,
|
|
132
|
+
caller: typeof input.caller === 'string' ? input.caller : undefined,
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Guard-shape preparation. Accepts only a successful route result at the type
|
|
138
|
+
* level. Shape handling preserves upstream behaviour: default passes the raw
|
|
139
|
+
* shape through unresolved; resolution happens only when the policy asks for
|
|
140
|
+
* it (`validateResolvedShapes`, always on for MCP) or the guard is dropped
|
|
141
|
+
* (local apply, written through the argument channels).
|
|
142
|
+
*
|
|
143
|
+
* Resolved-shape failures are stored classified as 500; `settleStage`
|
|
144
|
+
* rethrows them unchanged.
|
|
145
|
+
*/
|
|
146
|
+
export async function prepareGuardOperation(
|
|
147
|
+
routed: RouteStageOk,
|
|
148
|
+
input: {
|
|
149
|
+
guardShape: Record<string, unknown> | undefined
|
|
150
|
+
opKind: OpKind
|
|
151
|
+
policy: PipelinePolicy
|
|
152
|
+
getContext: ContextResolver | undefined
|
|
153
|
+
args: ArgsChannel
|
|
154
|
+
writeArgs: ArgsChannel | undefined
|
|
155
|
+
},
|
|
156
|
+
): Promise<GuardStageResult> {
|
|
157
|
+
const { guardShape } = input
|
|
158
|
+
if (!guardShape) return { ok: true, guardShape: undefined }
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
if (input.policy.dropGuard) {
|
|
162
|
+
await applyDroppedGuard(
|
|
163
|
+
guardShape,
|
|
164
|
+
routed.variantKey,
|
|
165
|
+
input.getContext,
|
|
166
|
+
input.opKind,
|
|
167
|
+
{
|
|
168
|
+
readQuery: input.args.read(),
|
|
169
|
+
writeBody: input.writeArgs?.read(),
|
|
170
|
+
},
|
|
171
|
+
() => ensureArgs(input.args),
|
|
172
|
+
input.writeArgs
|
|
173
|
+
? () => ensureArgs(input.writeArgs as ArgsChannel)
|
|
174
|
+
: () => {
|
|
175
|
+
throw new HttpError(
|
|
176
|
+
500,
|
|
177
|
+
'dropped guard requires a write argument channel for this operation',
|
|
178
|
+
)
|
|
179
|
+
},
|
|
180
|
+
)
|
|
181
|
+
return { ok: true, guardShape: undefined }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let effectiveShape: Record<string, unknown> = guardShape
|
|
185
|
+
if (input.policy.validateResolvedShapes) {
|
|
186
|
+
const resolution = await resolveGuardShapeOnce(
|
|
187
|
+
guardShape,
|
|
188
|
+
routed.variantKey,
|
|
189
|
+
input.getContext,
|
|
190
|
+
)
|
|
191
|
+
if (!resolution.ok) {
|
|
192
|
+
return {
|
|
193
|
+
ok: false,
|
|
194
|
+
failure: new HttpError(
|
|
195
|
+
500,
|
|
196
|
+
'guard shape could not be resolved: ' + resolution.problem,
|
|
197
|
+
),
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
effectiveShape = isPlainObject(resolution.shape)
|
|
201
|
+
? resolution.shape
|
|
202
|
+
: guardShape
|
|
203
|
+
}
|
|
204
|
+
return { ok: true, guardShape: effectiveShape }
|
|
205
|
+
} catch (error) {
|
|
206
|
+
return { ok: false, failure: mapError(error) }
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Rethrows the stored classified failure without reclassification. Placement
|
|
212
|
+
* is the adapter's behavioural choice (default order, Hono `before-hooks`,
|
|
213
|
+
* MCP before authorize continuation).
|
|
214
|
+
*/
|
|
215
|
+
export function settleStage(
|
|
216
|
+
result: RouteStageResult | GuardStageResult,
|
|
217
|
+
): asserts result is RouteStageOk | GuardStageOk {
|
|
218
|
+
if (!result.ok) throw result.failure
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Shared error classification; encoding stays per transport. */
|
|
222
|
+
export function classifyError(error: unknown): HttpError {
|
|
223
|
+
return mapError(error)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export type ExecuteDeps<TPrisma extends PrismaClientLike = PrismaClientLike> = {
|
|
227
|
+
core: (ctx: OperationContext) => Promise<unknown>
|
|
228
|
+
args: ArgsChannel
|
|
229
|
+
body?: unknown
|
|
230
|
+
/** The caller's client; typed so `TPrisma` flows through the shared stage. */
|
|
231
|
+
prisma: TPrisma
|
|
232
|
+
postgres?: unknown
|
|
233
|
+
sqlite?: unknown
|
|
234
|
+
pagination?: PaginationConfig
|
|
235
|
+
override?: RuntimeOperationOverride
|
|
236
|
+
getContext: (() => unknown | Promise<unknown>) | undefined
|
|
237
|
+
findManyPaginatedMode?: FindManyPaginatedMode
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Execution. Reads the argument channel at call time, so every write the
|
|
242
|
+
* dropped-guard transformation made is what reaches the operation core — the
|
|
243
|
+
* same core, pagination, override and transformResult path REST uses.
|
|
244
|
+
*/
|
|
245
|
+
export async function executeOperation<
|
|
246
|
+
TPrisma extends PrismaClientLike = PrismaClientLike,
|
|
247
|
+
>(
|
|
248
|
+
routed: { variantKey?: string; caller?: string },
|
|
249
|
+
guard: { guardShape?: Record<string, unknown> },
|
|
250
|
+
deps: ExecuteDeps<TPrisma>,
|
|
251
|
+
): Promise<unknown> {
|
|
252
|
+
const ctx: OperationContext = {
|
|
253
|
+
prisma: deps.prisma,
|
|
254
|
+
postgres: deps.postgres,
|
|
255
|
+
sqlite: deps.sqlite,
|
|
256
|
+
parsedQuery: deps.args.read() ?? {},
|
|
257
|
+
body: deps.body,
|
|
258
|
+
guardShape: guard.guardShape,
|
|
259
|
+
guardCaller: routed.caller,
|
|
260
|
+
guardVariantKey: routed.variantKey,
|
|
261
|
+
paginationConfig: deps.pagination,
|
|
262
|
+
operationOverride: deps.override,
|
|
263
|
+
resolveOperationContext: deps.getContext,
|
|
264
|
+
findManyPaginatedMode: deps.findManyPaginatedMode,
|
|
265
|
+
}
|
|
266
|
+
return deps.core(ctx)
|
|
267
|
+
}
|
|
@@ -72,22 +72,32 @@ type SpeedExtensionFactory = (opts: {
|
|
|
72
72
|
}) => unknown
|
|
73
73
|
|
|
74
74
|
let _speedExtension: SpeedExtensionFactory | null = null
|
|
75
|
+
let _prismasqlReady: Promise<void> | undefined
|
|
75
76
|
|
|
76
77
|
const _prismasqlModule = 'prisma-' + 'sql'
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Loaded on FIRST USE, not at module load: the copied runtime stays free of
|
|
81
|
+
* top-level side effects, so a bundler can evaluate the import graph without
|
|
82
|
+
* triggering the optional `prisma-sql` lookup.
|
|
83
|
+
*/
|
|
84
|
+
function ensurePrismaSqlReady(): Promise<void> {
|
|
85
|
+
return (_prismasqlReady ??= (async () => {
|
|
86
|
+
try {
|
|
87
|
+
const mod = (await import(_prismasqlModule)) as {
|
|
88
|
+
speedExtension?: SpeedExtensionFactory
|
|
89
|
+
default?: { speedExtension?: SpeedExtensionFactory }
|
|
90
|
+
}
|
|
91
|
+
_speedExtension =
|
|
92
|
+
mod.speedExtension ?? mod.default?.speedExtension ?? null
|
|
93
|
+
} catch (err) {
|
|
94
|
+
const code = (err as { code?: string } | null)?.code
|
|
95
|
+
if (code !== 'MODULE_NOT_FOUND' && code !== 'ERR_MODULE_NOT_FOUND') {
|
|
96
|
+
console.warn(LOG_PREFIX, 'prisma-sql initialization failed:', err)
|
|
97
|
+
}
|
|
88
98
|
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
99
|
+
})())
|
|
100
|
+
}
|
|
91
101
|
|
|
92
102
|
const _extendedClients = new WeakMap<object, WeakMap<object, unknown>>()
|
|
93
103
|
|
|
@@ -101,7 +111,7 @@ export async function getExtendedClient(
|
|
|
101
111
|
'PrismaClient not found on request. Set req.prisma in middleware.',
|
|
102
112
|
)
|
|
103
113
|
}
|
|
104
|
-
await
|
|
114
|
+
await ensurePrismaSqlReady()
|
|
105
115
|
if (!_speedExtension) return base
|
|
106
116
|
const connector = (ctx.postgres ?? ctx.sqlite) as object | undefined
|
|
107
117
|
if (!connector) return base
|