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
|
@@ -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
|
+
}
|
|
@@ -11,7 +11,6 @@ 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
16
|
const handlers = dispatchOps
|
|
@@ -25,10 +24,25 @@ export async function ${exportName}(
|
|
|
25
24
|
next: NextFunction,
|
|
26
25
|
) {
|
|
27
26
|
try {
|
|
28
|
-
|
|
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
|
+
)
|
|
29
43
|
next()
|
|
30
44
|
} catch (error: unknown) {
|
|
31
|
-
next(
|
|
45
|
+
next(classifyError(error))
|
|
32
46
|
}
|
|
33
47
|
}`
|
|
34
48
|
})
|
|
@@ -36,8 +50,14 @@ export async function ${exportName}(
|
|
|
36
50
|
|
|
37
51
|
return `import type { Request, Response, NextFunction } from 'express'
|
|
38
52
|
import * as core from './${modelName}Core${ext}'
|
|
39
|
-
import type {
|
|
40
|
-
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}'
|
|
41
61
|
|
|
42
62
|
type ExtendedRequest = Request & {
|
|
43
63
|
prisma?: unknown
|
|
@@ -47,30 +67,25 @@ type ExtendedRequest = Request & {
|
|
|
47
67
|
|
|
48
68
|
type LocalsBag = {
|
|
49
69
|
parsedQuery?: Record<string, unknown>
|
|
50
|
-
routeConfig?: { pagination?:
|
|
70
|
+
routeConfig?: { pagination?: PaginationConfig }
|
|
51
71
|
guardShape?: Record<string, unknown>
|
|
52
72
|
guardCaller?: string
|
|
53
73
|
guardVariantKey?: string
|
|
54
74
|
operationOverride?: RuntimeOperationOverride
|
|
55
|
-
|
|
75
|
+
getContext?: () => Promise<unknown>
|
|
56
76
|
data?: unknown
|
|
57
77
|
}
|
|
58
78
|
|
|
59
|
-
function
|
|
60
|
-
|
|
61
|
-
|
|
79
|
+
function readLocals(res: Response): LocalsBag {
|
|
80
|
+
return res.locals as LocalsBag
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function queryChannel(res: Response): ArgsChannel {
|
|
62
84
|
return {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
sqlite: extReq.sqlite,
|
|
68
|
-
parsedQuery: locals.parsedQuery,
|
|
69
|
-
body: req.body,
|
|
70
|
-
guardShape: locals.guardShape,
|
|
71
|
-
guardCaller: locals.guardCaller,
|
|
72
|
-
guardVariantKey: locals.guardVariantKey,
|
|
73
|
-
paginationConfig: (locals.routeConfig?.pagination) as OperationContext['paginationConfig'],
|
|
85
|
+
read: () => readLocals(res).parsedQuery,
|
|
86
|
+
write: (next) => {
|
|
87
|
+
readLocals(res).parsedQuery = next
|
|
88
|
+
},
|
|
74
89
|
}
|
|
75
90
|
}
|
|
76
91
|
${handlers}
|
package/src/index.ts
CHANGED
|
@@ -33,6 +33,10 @@ import { generateHonoHandler } from './generators/generateHonoHandler'
|
|
|
33
33
|
import { generateRouterFunction } from './generators/generateRouter'
|
|
34
34
|
import { generateFastifyRouterFunction } from './generators/generateRouterFastify'
|
|
35
35
|
import { generateHonoRouterFunction } from './generators/generateRouterHono'
|
|
36
|
+
import { generateHonoRouterParts } from './generators/generateRouterPartsHono'
|
|
37
|
+
import { generateHonoOpenApiRoutes } from './generators/generateHonoOpenApi'
|
|
38
|
+
import { generateModelMcp } from './generators/generateModelMcp'
|
|
39
|
+
import { generateMcpApp, generateMcpMount } from './generators/generateMcpApp'
|
|
36
40
|
import { generateScalarUIHandler } from './generators/generateUnifiedScalarUI'
|
|
37
41
|
import { generateUnifiedDocs } from './generators/generateUnifiedDocs'
|
|
38
42
|
import { generateQueryBuilderHelper } from './generators/generateQueryBuilderHelper'
|
|
@@ -48,6 +52,10 @@ import {
|
|
|
48
52
|
getGuardShapesImport,
|
|
49
53
|
} from './generators/generateImportPrismaStatement'
|
|
50
54
|
import { writeFileSafely } from './utils/writeFileSafely'
|
|
55
|
+
import {
|
|
56
|
+
assertMcpGuardCompatibility,
|
|
57
|
+
assertMcpGuardVersion,
|
|
58
|
+
} from './utils/mcpGate'
|
|
51
59
|
import { copyFiles } from './utils/copyFiles'
|
|
52
60
|
import { resolveImportStyle, ImportStyle } from './utils/resolveImportStyle'
|
|
53
61
|
import { parsePathCase, modelPathSegment, PathCase } from './utils/pathCasing'
|
|
@@ -111,6 +119,26 @@ function getPathCase(options: GeneratorOptions): PathCase {
|
|
|
111
119
|
)
|
|
112
120
|
}
|
|
113
121
|
|
|
122
|
+
function getMcp(options: GeneratorOptions): boolean {
|
|
123
|
+
const raw = (options.generator.config as Record<string, unknown>).mcp
|
|
124
|
+
if (raw === undefined || raw === null) return false
|
|
125
|
+
if (typeof raw === 'boolean') return raw
|
|
126
|
+
if (typeof raw === 'string') {
|
|
127
|
+
const lower = raw.trim().toLowerCase()
|
|
128
|
+
if (lower === 'true' || lower === '1' || lower === 'yes') return true
|
|
129
|
+
if (
|
|
130
|
+
lower === 'false' ||
|
|
131
|
+
lower === '0' ||
|
|
132
|
+
lower === 'no' ||
|
|
133
|
+
raw.trim() === ''
|
|
134
|
+
)
|
|
135
|
+
return false
|
|
136
|
+
}
|
|
137
|
+
throw new Error(
|
|
138
|
+
'Invalid mcp option "' + String(raw) + '". Expected true or false.',
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
114
142
|
function getDropGuard(options: GeneratorOptions): boolean {
|
|
115
143
|
const raw = (options.generator.config as Record<string, unknown>).dropGuard
|
|
116
144
|
if (raw === undefined || raw === null) return false
|
|
@@ -149,12 +177,26 @@ generatorHandler({
|
|
|
149
177
|
},
|
|
150
178
|
|
|
151
179
|
async onGenerate(options: GeneratorOptions) {
|
|
180
|
+
await runGenerate(options)
|
|
181
|
+
},
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* The full generation path, exported so tests exercise what `prisma generate`
|
|
186
|
+
* runs — including the mcp/dropGuard gate — instead of re-implementing pieces.
|
|
187
|
+
*/
|
|
188
|
+
export async function runGenerate(options: GeneratorOptions): Promise<void> {
|
|
189
|
+
{
|
|
152
190
|
const target = getTarget(options)
|
|
153
191
|
const writeStrategy = getWriteStrategy(options)
|
|
154
192
|
const findManyPaginatedMode = getFindManyPaginatedMode(options)
|
|
155
193
|
const dropGuard = getDropGuard(options)
|
|
194
|
+
const mcp = getMcp(options)
|
|
156
195
|
const pathCase = getPathCase(options)
|
|
157
196
|
|
|
197
|
+
assertMcpGuardCompatibility(mcp, dropGuard)
|
|
198
|
+
assertMcpGuardVersion(mcp, options.schemaPath)
|
|
199
|
+
|
|
158
200
|
const manifestDefaultAbs = path.resolve(
|
|
159
201
|
__dirname,
|
|
160
202
|
'..',
|
|
@@ -174,6 +216,7 @@ generatorHandler({
|
|
|
174
216
|
const importStyle = resolveImportStyle(options)
|
|
175
217
|
|
|
176
218
|
console.log(`\n═══ Prisma Generator (${target.toUpperCase()}) ═══`)
|
|
219
|
+
if (mcp) console.log(` MCP: enabled (reads + opt-in writes)`)
|
|
177
220
|
console.log(` Target: ${target}`)
|
|
178
221
|
console.log(` Output: ${options.generator.output?.value}`)
|
|
179
222
|
console.log(` Import style: ${importStyle}`)
|
|
@@ -193,7 +236,7 @@ generatorHandler({
|
|
|
193
236
|
validateClientGeneratorPresent(options)
|
|
194
237
|
}
|
|
195
238
|
|
|
196
|
-
await copyFiles(options, target, importStyle)
|
|
239
|
+
await copyFiles(options, target, importStyle, mcp)
|
|
197
240
|
|
|
198
241
|
const modelNames: string[] = []
|
|
199
242
|
let guardArtifactsSeen = false
|
|
@@ -292,6 +335,38 @@ generatorHandler({
|
|
|
292
335
|
operation: 'Router',
|
|
293
336
|
})
|
|
294
337
|
|
|
338
|
+
if (target === 'hono') {
|
|
339
|
+
await writeFileSafely({
|
|
340
|
+
content: generateHonoRouterParts({
|
|
341
|
+
model: model as DMMF.Model,
|
|
342
|
+
enums: options.dmmf.datamodel.enums as DMMF.DatamodelEnum[],
|
|
343
|
+
guardShapesImport,
|
|
344
|
+
clientImport: getRelativeClientTypeImport(options, model.name),
|
|
345
|
+
importStyle,
|
|
346
|
+
writeStrategy,
|
|
347
|
+
dropGuard,
|
|
348
|
+
pathCase,
|
|
349
|
+
}),
|
|
350
|
+
options,
|
|
351
|
+
model: model as DMMF.Model,
|
|
352
|
+
operation: 'RouterParts',
|
|
353
|
+
})
|
|
354
|
+
await writeFileSafely({
|
|
355
|
+
content: generateHonoOpenApiRoutes({
|
|
356
|
+
model: model as DMMF.Model,
|
|
357
|
+
enums: options.dmmf.datamodel.enums as DMMF.DatamodelEnum[],
|
|
358
|
+
guardShapesImport,
|
|
359
|
+
clientImport: getRelativeClientTypeImport(options, model.name),
|
|
360
|
+
importStyle,
|
|
361
|
+
writeStrategy,
|
|
362
|
+
pathCase,
|
|
363
|
+
}),
|
|
364
|
+
options,
|
|
365
|
+
model: model as DMMF.Model,
|
|
366
|
+
operation: 'OpenApiRoutes',
|
|
367
|
+
})
|
|
368
|
+
}
|
|
369
|
+
|
|
295
370
|
await writeFileSafely({
|
|
296
371
|
content: generateScalarUIHandler({
|
|
297
372
|
model: model as DMMF.Model,
|
|
@@ -327,6 +402,27 @@ generatorHandler({
|
|
|
327
402
|
})
|
|
328
403
|
}
|
|
329
404
|
|
|
405
|
+
if (mcp) {
|
|
406
|
+
// after the loop: each tool factory narrows schemas with THIS model's
|
|
407
|
+
// metadata plus the metadata of every model it relates to
|
|
408
|
+
for (const model of allModels) {
|
|
409
|
+
if (!modelNames.includes(model.name)) continue
|
|
410
|
+
await writeFileSafely({
|
|
411
|
+
content: generateModelMcp({
|
|
412
|
+
model,
|
|
413
|
+
allModels: allModels.filter((m) =>
|
|
414
|
+
modelNames.includes(m.name),
|
|
415
|
+
) as DMMF.Model[],
|
|
416
|
+
importStyle,
|
|
417
|
+
writeStrategy,
|
|
418
|
+
}),
|
|
419
|
+
options,
|
|
420
|
+
model: model as DMMF.Model,
|
|
421
|
+
operation: 'Mcp',
|
|
422
|
+
})
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
330
426
|
const pathSegments = Object.fromEntries(
|
|
331
427
|
modelNames.map((n) => [n, modelPathSegment(n, pathCase)]),
|
|
332
428
|
)
|
|
@@ -348,6 +444,23 @@ generatorHandler({
|
|
|
348
444
|
operation: 'queryBuilder',
|
|
349
445
|
})
|
|
350
446
|
|
|
447
|
+
if (mcp) {
|
|
448
|
+
await writeFileSafely({
|
|
449
|
+
content: generateMcpApp({
|
|
450
|
+
serverName: 'prisma-api',
|
|
451
|
+
serverVersion: require('../package.json').version,
|
|
452
|
+
importStyle,
|
|
453
|
+
}),
|
|
454
|
+
options,
|
|
455
|
+
operation: 'mcpApp',
|
|
456
|
+
})
|
|
457
|
+
await writeFileSafely({
|
|
458
|
+
content: generateMcpMount({ target, importStyle }),
|
|
459
|
+
options,
|
|
460
|
+
operation: 'mcpMount',
|
|
461
|
+
})
|
|
462
|
+
}
|
|
463
|
+
|
|
351
464
|
if (modelNames.length > 0 && !guardArtifactsSeen) {
|
|
352
465
|
console.log('')
|
|
353
466
|
console.log(
|
|
@@ -361,5 +474,5 @@ generatorHandler({
|
|
|
361
474
|
console.log('\n═══ Generation Complete ═══')
|
|
362
475
|
console.log(`✓ ${modelNames.length} models (${target})`)
|
|
363
476
|
console.log('')
|
|
364
|
-
}
|
|
365
|
-
}
|
|
477
|
+
}
|
|
478
|
+
}
|
package/src/utils/copyFiles.ts
CHANGED
|
@@ -9,10 +9,12 @@ const SHARED_FILES = [
|
|
|
9
9
|
'parseQueryParams.ts',
|
|
10
10
|
'buildModelOpenApi.ts',
|
|
11
11
|
'operationDefinitions.ts',
|
|
12
|
+
'operationSchemas.ts',
|
|
12
13
|
'misc.ts',
|
|
13
14
|
'routeConfig.ts',
|
|
14
15
|
'docsRenderer.ts',
|
|
15
16
|
'operationRuntime.ts',
|
|
17
|
+
'operationPipeline.ts',
|
|
16
18
|
'errorMapper.ts',
|
|
17
19
|
'sse.ts',
|
|
18
20
|
'pagination.ts',
|
|
@@ -126,6 +128,7 @@ export async function copyFiles(
|
|
|
126
128
|
options: GeneratorOptions,
|
|
127
129
|
target: Target,
|
|
128
130
|
importStyle: ImportStyle,
|
|
131
|
+
mcp = false,
|
|
129
132
|
): Promise<void> {
|
|
130
133
|
const outputPath = options.generator.output?.value
|
|
131
134
|
if (!outputPath) return
|
|
@@ -153,6 +156,39 @@ export async function copyFiles(
|
|
|
153
156
|
}
|
|
154
157
|
}
|
|
155
158
|
|
|
159
|
+
if (target === 'hono') {
|
|
160
|
+
// The phase 9 assembly boundary. Imports the Hono route-config types by
|
|
161
|
+
// their source name; emitted as routeConfig.target.ts like every target.
|
|
162
|
+
const err = copyFileSync(
|
|
163
|
+
copyBase,
|
|
164
|
+
outputPath,
|
|
165
|
+
'routerParts.ts',
|
|
166
|
+
importStyle,
|
|
167
|
+
{
|
|
168
|
+
required: true,
|
|
169
|
+
importRewrites: [
|
|
170
|
+
{ from: './routeConfig.hono', to: './routeConfig.target' },
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
)
|
|
174
|
+
if (err) errors.push(err)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (mcp) {
|
|
178
|
+
// MCP runtime is copied ONLY for mcp=true generations: an mcp=false
|
|
179
|
+
// output contains no MCP files, imports or dependencies at all.
|
|
180
|
+
const err = copyFileSync(
|
|
181
|
+
copyBase,
|
|
182
|
+
outputPath,
|
|
183
|
+
'mcpRuntime.ts',
|
|
184
|
+
importStyle,
|
|
185
|
+
{
|
|
186
|
+
required: true,
|
|
187
|
+
},
|
|
188
|
+
)
|
|
189
|
+
if (err) errors.push(err)
|
|
190
|
+
}
|
|
191
|
+
|
|
156
192
|
const targetConfigFile = 'routeConfig.' + target + '.ts'
|
|
157
193
|
const err = copyFileSync(
|
|
158
194
|
copyBase,
|