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
|
@@ -1,81 +1,93 @@
|
|
|
1
|
-
import { GeneratorOptions } from '@prisma/generator-helper'
|
|
2
|
-
import * as fs from 'fs'
|
|
3
|
-
import path from 'path'
|
|
4
|
-
|
|
5
|
-
function findClientGenerator(options: GeneratorOptions) {
|
|
6
|
-
const byName = options.otherGenerators.find((gen) => gen.name === 'client')
|
|
7
|
-
if (byName) return byName
|
|
8
|
-
|
|
9
|
-
const byProvider = options.otherGenerators.find(
|
|
10
|
-
(gen) =>
|
|
11
|
-
gen.provider.value === 'prisma-client-js' ||
|
|
12
|
-
gen.provider.value === '@prisma/client' ||
|
|
13
|
-
gen.provider.value === 'prisma-client',
|
|
14
|
-
)
|
|
15
|
-
return byProvider || null
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function getRelativeImportPath(
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
1
|
+
import { GeneratorOptions } from '@prisma/generator-helper'
|
|
2
|
+
import * as fs from 'fs'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
|
|
5
|
+
function findClientGenerator(options: GeneratorOptions) {
|
|
6
|
+
const byName = options.otherGenerators.find((gen) => gen.name === 'client')
|
|
7
|
+
if (byName) return byName
|
|
8
|
+
|
|
9
|
+
const byProvider = options.otherGenerators.find(
|
|
10
|
+
(gen) =>
|
|
11
|
+
gen.provider.value === 'prisma-client-js' ||
|
|
12
|
+
gen.provider.value === '@prisma/client' ||
|
|
13
|
+
gen.provider.value === 'prisma-client',
|
|
14
|
+
)
|
|
15
|
+
return byProvider || null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getRelativeImportPath(
|
|
19
|
+
fromDir: string,
|
|
20
|
+
clientOutputPath: string,
|
|
21
|
+
): string {
|
|
22
|
+
let relativeImportPath = path.relative(fromDir, clientOutputPath)
|
|
23
|
+
relativeImportPath = relativeImportPath.split(path.sep).join(path.posix.sep)
|
|
24
|
+
if (!relativeImportPath.startsWith('.')) {
|
|
25
|
+
relativeImportPath = './' + relativeImportPath
|
|
26
|
+
}
|
|
27
|
+
return relativeImportPath
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getRelativeClientPath(
|
|
31
|
+
generatorOptions: GeneratorOptions,
|
|
32
|
+
modelName: string,
|
|
33
|
+
): string {
|
|
34
|
+
const clientGenerator = findClientGenerator(generatorOptions)
|
|
35
|
+
|
|
36
|
+
if (!clientGenerator || !clientGenerator.output?.value) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
'Prisma client generator not found. Ensure a generator block exists with name "client" or provider one of: prisma-client-js, @prisma/client, prisma-client.',
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const outputValue = generatorOptions.generator.output?.value
|
|
43
|
+
if (!outputValue) {
|
|
44
|
+
throw new Error('Generator output path not defined.')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const routerDirPath = path.join(outputValue, modelName)
|
|
48
|
+
|
|
49
|
+
return getRelativeImportPath(routerDirPath, clientGenerator.output.value)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function findGuardGenerator(options: GeneratorOptions) {
|
|
53
|
+
const byProvider = options.otherGenerators.find(
|
|
54
|
+
(gen) =>
|
|
55
|
+
!!gen.provider?.value && gen.provider.value.includes('prisma-guard'),
|
|
56
|
+
)
|
|
57
|
+
if (byProvider) return byProvider
|
|
58
|
+
|
|
59
|
+
const byConfig = options.otherGenerators.find(
|
|
60
|
+
(gen) =>
|
|
61
|
+
!!gen.config &&
|
|
62
|
+
('typedGuardShapes' in gen.config ||
|
|
63
|
+
'onInvalidZod' in gen.config ||
|
|
64
|
+
'findUniqueMode' in gen.config),
|
|
65
|
+
)
|
|
66
|
+
return byConfig || null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function getGuardShapesImport(
|
|
70
|
+
options: GeneratorOptions,
|
|
71
|
+
modelName: string,
|
|
72
|
+
): string | null {
|
|
73
|
+
const guard = findGuardGenerator(options)
|
|
74
|
+
if (!guard || !guard.output?.value) return null
|
|
75
|
+
|
|
76
|
+
if (guard.config?.typedGuardShapes === 'false') return null
|
|
77
|
+
|
|
78
|
+
const outputValue = options.generator.output?.value
|
|
79
|
+
if (!outputValue) return null
|
|
80
|
+
|
|
81
|
+
const fromDir = path.join(outputValue, modelName)
|
|
82
|
+
const shapesPath = path.join(guard.output.value, 'shapes')
|
|
83
|
+
return getRelativeImportPath(fromDir, shapesPath)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function getRelativeClientTypeImport(
|
|
87
|
+
options: GeneratorOptions,
|
|
88
|
+
modelName: string,
|
|
89
|
+
): string {
|
|
90
|
+
const client = findClientGenerator(options)
|
|
91
|
+
const entry = client?.provider.value === 'prisma-client' ? 'client' : 'index'
|
|
92
|
+
return getRelativeClientPath(options, modelName) + '/' + entry
|
|
93
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { ImportStyle } from '../utils/resolveImportStyle'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* App-level MCP registry (emitted when generator option `mcp = true`),
|
|
5
|
+
* following the app-level `combinedDocs` / `queryBuilder` precedent.
|
|
6
|
+
*/
|
|
7
|
+
export function generateMcpApp({
|
|
8
|
+
serverName,
|
|
9
|
+
serverVersion,
|
|
10
|
+
importStyle,
|
|
11
|
+
}: {
|
|
12
|
+
serverName: string
|
|
13
|
+
serverVersion: string
|
|
14
|
+
importStyle: ImportStyle
|
|
15
|
+
}): string {
|
|
16
|
+
return `import type { McpServer } from '@modelcontextprotocol/server'
|
|
17
|
+
import { registerMcpTools } from './mcpRuntime'
|
|
18
|
+
import type { McpSharedOptions, McpToolContribution } from './mcpRuntime'
|
|
19
|
+
|
|
20
|
+
export const MCP_SERVER_INFO = { name: ${JSON.stringify(serverName)}, version: ${JSON.stringify(serverVersion)} } as const
|
|
21
|
+
|
|
22
|
+
export type McpRegisterOptions = McpSharedOptions & {
|
|
23
|
+
/** Explicit allowlist: only these model-operation tools are registered. */
|
|
24
|
+
tools: readonly McpToolContribution[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Register the allowed MCP tools on a server instance. Call from the
|
|
29
|
+
* per-request server factory so \`authInfo\` is the verified principal for THIS
|
|
30
|
+
* request. Throws before registering anything when required options are
|
|
31
|
+
* missing, limits are invalid, or the guard is dropped in this environment.
|
|
32
|
+
*/
|
|
33
|
+
export function registerMcpToolsOnServer(
|
|
34
|
+
server: McpServer,
|
|
35
|
+
options: McpRegisterOptions,
|
|
36
|
+
): void {
|
|
37
|
+
registerMcpTools(server, options)
|
|
38
|
+
}
|
|
39
|
+
`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Target-specific Streamable HTTP mount glue (emitted when `mcp = true`).
|
|
44
|
+
* One \`/mcp\` endpoint in the same process as the REST backend.
|
|
45
|
+
*/
|
|
46
|
+
export function generateMcpMount({
|
|
47
|
+
target,
|
|
48
|
+
importStyle,
|
|
49
|
+
}: {
|
|
50
|
+
target: 'express' | 'fastify' | 'hono'
|
|
51
|
+
importStyle: ImportStyle
|
|
52
|
+
}): string {
|
|
53
|
+
if (target === 'express') {
|
|
54
|
+
return `import type { RequestHandler } from 'express'
|
|
55
|
+
import type { McpServer, AuthInfo } from '@modelcontextprotocol/server'
|
|
56
|
+
import { createMcpHandler } from '@modelcontextprotocol/server'
|
|
57
|
+
import { toNodeHandler } from '@modelcontextprotocol/node'
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Build the /mcp handler for Express. Authentication is YOUR middleware: put a
|
|
61
|
+
* verified AuthInfo on req.auth (e.g. requireBearerAuth from
|
|
62
|
+
* @modelcontextprotocol/express) before this handler runs. Without req.auth
|
|
63
|
+
* the request fails closed — registration refuses to run without a verified
|
|
64
|
+
* principal.
|
|
65
|
+
*/
|
|
66
|
+
export function createMcpExpressHandler(
|
|
67
|
+
getServer: (authInfo: AuthInfo) => McpServer,
|
|
68
|
+
): RequestHandler {
|
|
69
|
+
const handler = createMcpHandler((ctx) => getServer(ctx.authInfo as AuthInfo))
|
|
70
|
+
const node = toNodeHandler(handler)
|
|
71
|
+
return (req, res) => void node(req, res, req.body)
|
|
72
|
+
}
|
|
73
|
+
`
|
|
74
|
+
}
|
|
75
|
+
if (target === 'fastify') {
|
|
76
|
+
return `import type { FastifyRequest, FastifyReply } from 'fastify'
|
|
77
|
+
import type { McpServer, AuthInfo } from '@modelcontextprotocol/server'
|
|
78
|
+
import { createMcpHandler } from '@modelcontextprotocol/server'
|
|
79
|
+
import { toNodeHandler } from '@modelcontextprotocol/node'
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Build the /mcp handler for Fastify. Authentication is YOUR hook: attach the
|
|
83
|
+
* verified AuthInfo to request.raw.auth (onRequest) before this handler runs.
|
|
84
|
+
* Without it the request fails closed — registration refuses to run without a
|
|
85
|
+
* verified principal.
|
|
86
|
+
*/
|
|
87
|
+
export function createMcpFastifyHandler(
|
|
88
|
+
getServer: (authInfo: AuthInfo) => McpServer,
|
|
89
|
+
): (request: FastifyRequest, reply: FastifyReply) => Promise<void> {
|
|
90
|
+
const handler = createMcpHandler((ctx) => getServer(ctx.authInfo as AuthInfo))
|
|
91
|
+
const node = toNodeHandler(handler)
|
|
92
|
+
return async (request, reply) => {
|
|
93
|
+
// The Node handler writes the response itself; without hijacking,
|
|
94
|
+
// Fastify would continue its own reply lifecycle after we return.
|
|
95
|
+
reply.hijack()
|
|
96
|
+
await node(request.raw, reply.raw, request.body)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
`
|
|
100
|
+
}
|
|
101
|
+
return `import type { Context, Next } from 'hono'
|
|
102
|
+
import type { McpServer, AuthInfo } from '@modelcontextprotocol/server'
|
|
103
|
+
import { createMcpHandler } from '@modelcontextprotocol/server'
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Build the /mcp handler for Hono (web-standard). Authentication is YOUR
|
|
107
|
+
* middleware: verify the request and set c.set('authInfo', verified) before
|
|
108
|
+
* this handler runs — or use the web-standard requireBearerAuth gate from
|
|
109
|
+
* @modelcontextprotocol/server and pass its AuthInfo to fetch explicitly.
|
|
110
|
+
*/
|
|
111
|
+
export function createMcpHonoHandler(
|
|
112
|
+
getServer: (authInfo: AuthInfo) => McpServer,
|
|
113
|
+
): (c: Context) => Promise<Response> {
|
|
114
|
+
const handler = createMcpHandler((ctx) => getServer(ctx.authInfo as AuthInfo))
|
|
115
|
+
return async (c: Context) => {
|
|
116
|
+
let parsedBody: unknown
|
|
117
|
+
try {
|
|
118
|
+
parsedBody = await c.req.json()
|
|
119
|
+
} catch {
|
|
120
|
+
parsedBody = undefined
|
|
121
|
+
}
|
|
122
|
+
const authInfo = c.get('authInfo') as AuthInfo | undefined
|
|
123
|
+
if (!authInfo) {
|
|
124
|
+
return Response.json(
|
|
125
|
+
{ message: 'unauthenticated: a verified principal is required' },
|
|
126
|
+
{ status: 401 },
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
return handler.fetch(c.req.raw, { parsedBody, authInfo })
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
`
|
|
133
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import type { DMMF } from '@prisma/generator-helper'
|
|
2
|
+
import type { ImportStyle } from '../utils/resolveImportStyle'
|
|
3
|
+
import { importExt } from '../utils/importExt'
|
|
4
|
+
import { OPERATION_METADATA } from '../copy/operationDefinitions'
|
|
5
|
+
import type { WriteStrategy } from '../constants'
|
|
6
|
+
|
|
7
|
+
const MCP_OPS = [
|
|
8
|
+
{ name: 'findMany', coreName: 'findMany' },
|
|
9
|
+
{ name: 'findUnique', coreName: 'findUnique' },
|
|
10
|
+
{ name: 'findFirst', coreName: 'findFirst' },
|
|
11
|
+
{ name: 'count', coreName: 'count' },
|
|
12
|
+
{ name: 'findManyPaginated', coreName: 'findManyPaginated' },
|
|
13
|
+
] as const
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Every guarded write op from the shared metadata. `updateEach` is excluded:
|
|
17
|
+
* it bypasses guard shapes, so it has no MCP factory.
|
|
18
|
+
*/
|
|
19
|
+
const MCP_WRITE_OPS = OPERATION_METADATA.filter(
|
|
20
|
+
(m) => (m.kind === 'write' || m.kind === 'batch') && m.name !== 'updateEach',
|
|
21
|
+
).map((m) => ({ name: m.name, coreName: m.coreName }))
|
|
22
|
+
|
|
23
|
+
/** Matches MAX_PROJECTION_DEPTH in operationSchemas.ts. */
|
|
24
|
+
const MAX_PROJECTION_DEPTH = 4
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The models reachable from `model` through relation fields, to the supported
|
|
28
|
+
* projection depth. Schema narrowing recurses into nested select/include
|
|
29
|
+
* trees through this set — a `User → Post → Comment` projection stays
|
|
30
|
+
* model-aware at `Comment` instead of degrading to an opaque object.
|
|
31
|
+
*/
|
|
32
|
+
function transitiveRelations(
|
|
33
|
+
model: DMMF.Model,
|
|
34
|
+
allModels: readonly DMMF.Model[],
|
|
35
|
+
): string[] {
|
|
36
|
+
// allModels is the EMITTED model set: models turned off with
|
|
37
|
+
/// @generator off have no metadata file, so they are opaque here
|
|
38
|
+
const byName = new Map(allModels.map((m) => [m.name, m]))
|
|
39
|
+
const involved = new Set<string>([model.name])
|
|
40
|
+
let frontier = [model.name]
|
|
41
|
+
for (let depth = 0; depth < MAX_PROJECTION_DEPTH; depth++) {
|
|
42
|
+
const next: string[] = []
|
|
43
|
+
for (const name of frontier) {
|
|
44
|
+
const m = byName.get(name)
|
|
45
|
+
if (!m) continue
|
|
46
|
+
for (const field of m.fields) {
|
|
47
|
+
if (
|
|
48
|
+
field.kind === 'object' &&
|
|
49
|
+
byName.has(field.type) &&
|
|
50
|
+
!involved.has(field.type)
|
|
51
|
+
) {
|
|
52
|
+
involved.add(field.type)
|
|
53
|
+
next.push(field.type)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (next.length === 0) break
|
|
58
|
+
frontier = next
|
|
59
|
+
}
|
|
60
|
+
return [...involved].filter((n) => n !== model.name)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Per-model MCP tool factories: one named export per exposed operation (five
|
|
65
|
+
* reads plus every guarded write), each returning a
|
|
66
|
+
* `McpToolContribution` the application passes to `registerMcpTools`.
|
|
67
|
+
* Importing only the operations you expose is the same static assembly
|
|
68
|
+
* boundary the Hono parts use — unselected operations never enter the module
|
|
69
|
+
* graph, and write tools exist ONLY where application code imports them.
|
|
70
|
+
*
|
|
71
|
+
* Schema narrowing is model-aware and transitive: each tool carries this
|
|
72
|
+
* model's metadata plus the metadata of every model reachable through its
|
|
73
|
+
* relations (to the supported projection depth), so nested `select`/`include`
|
|
74
|
+
* trees stay narrowed at every level, with enum values available throughout.
|
|
75
|
+
*/
|
|
76
|
+
export function generateModelMcp({
|
|
77
|
+
model,
|
|
78
|
+
allModels,
|
|
79
|
+
importStyle,
|
|
80
|
+
writeStrategy,
|
|
81
|
+
}: {
|
|
82
|
+
model: DMMF.Model
|
|
83
|
+
allModels: readonly DMMF.Model[]
|
|
84
|
+
importStyle: ImportStyle
|
|
85
|
+
/** Same value the operation cores were generated with. */
|
|
86
|
+
writeStrategy: WriteStrategy
|
|
87
|
+
}): string {
|
|
88
|
+
const ext = importExt(importStyle)
|
|
89
|
+
const modelName = model.name
|
|
90
|
+
const lower = modelName.charAt(0).toLowerCase() + modelName.slice(1)
|
|
91
|
+
const cap = (s: string) => s.charAt(0).toUpperCase() + s.slice(1)
|
|
92
|
+
|
|
93
|
+
const related = transitiveRelations(model, allModels)
|
|
94
|
+
// Model directories are keyed by the RAW model name for every target
|
|
95
|
+
// (writeFileSafely ignores pathCase for directories), so raw-name imports
|
|
96
|
+
// are correct under every pathCase configuration.
|
|
97
|
+
const relatedImports = related
|
|
98
|
+
.map(
|
|
99
|
+
(t) =>
|
|
100
|
+
`import { MODEL_FIELDS as ${t}Fields, MODEL_ENUMS as ${t}Enums, COMPOUND_ID as ${t}CompoundId, COMPOUND_UNIQUES as ${t}CompoundUniques } from '../${t}/${t}Metadata${ext}'`,
|
|
101
|
+
)
|
|
102
|
+
.join('\n')
|
|
103
|
+
const enumSets = [modelName, ...related]
|
|
104
|
+
.map(
|
|
105
|
+
(t) =>
|
|
106
|
+
`for (const e of ${t === modelName ? 'MODEL_ENUMS' : t + 'Enums'}) {
|
|
107
|
+
enums.set(e.name, e.values.map((v) => v.name))
|
|
108
|
+
}`,
|
|
109
|
+
)
|
|
110
|
+
.join('\n')
|
|
111
|
+
const coreEntries = [modelName, ...related]
|
|
112
|
+
.map((t) => {
|
|
113
|
+
const fields = t === modelName ? 'MODEL_FIELDS' : t + 'Fields'
|
|
114
|
+
const compound =
|
|
115
|
+
t === modelName
|
|
116
|
+
? 'compoundUniques'
|
|
117
|
+
: `(() => {
|
|
118
|
+
const list: Array<{ selector: string; fields: string[] }> = []
|
|
119
|
+
if (${t}CompoundId) {
|
|
120
|
+
list.push({ selector: ${t}CompoundId.selector, fields: [...${t}CompoundId.fields] })
|
|
121
|
+
}
|
|
122
|
+
for (const cu of ${t}CompoundUniques) {
|
|
123
|
+
list.push({ selector: cu.selector, fields: [...cu.fields] })
|
|
124
|
+
}
|
|
125
|
+
return list
|
|
126
|
+
})()`
|
|
127
|
+
return ` ['${t}', {
|
|
128
|
+
name: '${t}',
|
|
129
|
+
fields: ${fields},
|
|
130
|
+
enums,
|
|
131
|
+
uniqueFields: ${fields}.filter((f) => f.isId || f.isUnique).map((f) => f.name),
|
|
132
|
+
compoundUniques: ${compound},
|
|
133
|
+
}]`
|
|
134
|
+
})
|
|
135
|
+
.join(',\n')
|
|
136
|
+
|
|
137
|
+
const writeNames = new Set(MCP_WRITE_OPS.map((op) => op.name))
|
|
138
|
+
const factories = [...MCP_OPS, ...MCP_WRITE_OPS]
|
|
139
|
+
.map(
|
|
140
|
+
(op) => `
|
|
141
|
+
export function ${lower}${cap(op.name)}Tool<TCtx = unknown, TPrisma extends PrismaClientLike = PrismaClientLike>(options: {
|
|
142
|
+
config: ${modelName}RouteConfig<TCtx, TPrisma>
|
|
143
|
+
}): McpToolContribution {
|
|
144
|
+
return ${writeNames.has(op.name) ? 'createMcpWriteTool' : 'createMcpReadTool'}({
|
|
145
|
+
model: '${modelName}',
|
|
146
|
+
operation: '${op.name}',
|
|
147
|
+
config: options.config as unknown as Record<string, unknown>,
|
|
148
|
+
core: core.${op.coreName},
|
|
149
|
+
fields: MODEL_FIELDS,
|
|
150
|
+
enums: enums,
|
|
151
|
+
uniqueFields: MODEL_FIELDS.filter((f) => f.isId || f.isUnique).map(
|
|
152
|
+
(f) => f.name,
|
|
153
|
+
),
|
|
154
|
+
compoundUniques: compoundUniques,
|
|
155
|
+
modelIndex: modelIndex,${
|
|
156
|
+
writeNames.has(op.name)
|
|
157
|
+
? `
|
|
158
|
+
writeStrategy: '${writeStrategy}',`
|
|
159
|
+
: ''
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
`,
|
|
164
|
+
)
|
|
165
|
+
.join('\n')
|
|
166
|
+
|
|
167
|
+
return `import * as core from './${modelName}Core${ext}'
|
|
168
|
+
import type { ${modelName}RouteConfig } from './${modelName}Router${ext}'
|
|
169
|
+
import type { PrismaClientLike } from '../routeConfig.target${ext}'
|
|
170
|
+
import {
|
|
171
|
+
createMcpReadTool,
|
|
172
|
+
createMcpWriteTool,
|
|
173
|
+
type McpToolContribution,
|
|
174
|
+
type SchemaModelMeta,
|
|
175
|
+
} from '../mcpRuntime${ext}'
|
|
176
|
+
import {
|
|
177
|
+
MODEL_FIELDS,
|
|
178
|
+
MODEL_ENUMS,
|
|
179
|
+
COMPOUND_ID,
|
|
180
|
+
COMPOUND_UNIQUES,
|
|
181
|
+
} from './${modelName}Metadata${ext}'
|
|
182
|
+
${relatedImports ? '\n' + relatedImports + '\n' : ''}
|
|
183
|
+
// transitive relation metadata: narrowing recurses through these FULL model
|
|
184
|
+
// metas — compound unique constraints (@@id / @@unique) ride along so nested
|
|
185
|
+
// cursors and nested findUnique surfaces keep their unique selectors.
|
|
186
|
+
const compoundUniques: Array<{ selector: string; fields: string[] }> = []
|
|
187
|
+
if (COMPOUND_ID) {
|
|
188
|
+
compoundUniques.push({ selector: COMPOUND_ID.selector, fields: COMPOUND_ID.fields })
|
|
189
|
+
}
|
|
190
|
+
for (const cu of COMPOUND_UNIQUES) {
|
|
191
|
+
compoundUniques.push({ selector: cu.selector, fields: cu.fields })
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const enums = new Map<string, readonly string[]>()
|
|
195
|
+
${enumSets}
|
|
196
|
+
|
|
197
|
+
// one shared, self-referential metadata index: every related model keeps
|
|
198
|
+
// its full meta (compound uniques included) at every recursion depth
|
|
199
|
+
const modelIndex = new Map<string, SchemaModelMeta>()
|
|
200
|
+
const coreMetas: Array<[string, Omit<SchemaModelMeta, 'modelIndex'>]> = [
|
|
201
|
+
${coreEntries},
|
|
202
|
+
]
|
|
203
|
+
for (const [name, core] of coreMetas) {
|
|
204
|
+
modelIndex.set(name, { ...core, modelIndex })
|
|
205
|
+
}
|
|
206
|
+
${factories}
|
|
207
|
+
`
|
|
208
|
+
}
|
|
@@ -4,16 +4,26 @@ import { importExt } from '../utils/importExt'
|
|
|
4
4
|
|
|
5
5
|
function exampleValueForType(fieldType: string): unknown {
|
|
6
6
|
switch (fieldType) {
|
|
7
|
-
case 'String':
|
|
8
|
-
|
|
9
|
-
case '
|
|
10
|
-
|
|
11
|
-
case '
|
|
12
|
-
|
|
13
|
-
case '
|
|
14
|
-
|
|
15
|
-
case '
|
|
16
|
-
|
|
7
|
+
case 'String':
|
|
8
|
+
return 'example'
|
|
9
|
+
case 'Int':
|
|
10
|
+
return 1
|
|
11
|
+
case 'BigInt':
|
|
12
|
+
return '1'
|
|
13
|
+
case 'Float':
|
|
14
|
+
return 1.0
|
|
15
|
+
case 'Decimal':
|
|
16
|
+
return '1.0'
|
|
17
|
+
case 'Boolean':
|
|
18
|
+
return true
|
|
19
|
+
case 'DateTime':
|
|
20
|
+
return '2025-01-01T00:00:00.000Z'
|
|
21
|
+
case 'Json':
|
|
22
|
+
return {}
|
|
23
|
+
case 'Bytes':
|
|
24
|
+
return 'base64data'
|
|
25
|
+
default:
|
|
26
|
+
return 'example'
|
|
17
27
|
}
|
|
18
28
|
}
|
|
19
29
|
|
|
@@ -23,12 +33,14 @@ export interface GenerateModelMetadataOptions {
|
|
|
23
33
|
importStyle: ImportStyle
|
|
24
34
|
}
|
|
25
35
|
|
|
26
|
-
export function generateModelMetadata(
|
|
36
|
+
export function generateModelMetadata(
|
|
37
|
+
options: GenerateModelMetadataOptions,
|
|
38
|
+
): string {
|
|
27
39
|
const ext = importExt(options.importStyle)
|
|
28
40
|
const { model, enums } = options
|
|
29
41
|
const modelName = model.name
|
|
30
42
|
|
|
31
|
-
const fieldsMeta = model.fields.map((f
|
|
43
|
+
const fieldsMeta = model.fields.map((f) => ({
|
|
32
44
|
name: f.name,
|
|
33
45
|
kind: f.kind,
|
|
34
46
|
type: f.type,
|
|
@@ -43,7 +55,7 @@ export function generateModelMetadata(options: GenerateModelMetadataOptions): st
|
|
|
43
55
|
}))
|
|
44
56
|
|
|
45
57
|
const referencedEnumTypes = new Set(
|
|
46
|
-
model.fields.filter((f
|
|
58
|
+
model.fields.filter((f) => f.kind === 'enum').map((f) => f.type),
|
|
47
59
|
)
|
|
48
60
|
|
|
49
61
|
const enumsMeta = enums
|
|
@@ -54,7 +66,7 @@ export function generateModelMetadata(options: GenerateModelMetadataOptions): st
|
|
|
54
66
|
}))
|
|
55
67
|
|
|
56
68
|
const exampleValues: Record<string, unknown> = {}
|
|
57
|
-
for (const f of model.fields
|
|
69
|
+
for (const f of model.fields) {
|
|
58
70
|
if (f.isId || f.isUnique || f.kind === 'scalar' || f.kind === 'enum') {
|
|
59
71
|
if (f.kind === 'enum') {
|
|
60
72
|
const enumDef = enums.find((e) => e.name === f.type)
|
|
@@ -65,16 +77,22 @@ export function generateModelMetadata(options: GenerateModelMetadataOptions): st
|
|
|
65
77
|
}
|
|
66
78
|
}
|
|
67
79
|
|
|
80
|
+
// the client-input selector key mirrors Prisma's WhereUniqueInput: the
|
|
81
|
+
// constraint's explicit name when given, else the fields joined with '_'
|
|
68
82
|
const compoundId =
|
|
69
83
|
model.primaryKey && model.primaryKey.fields.length > 1
|
|
70
|
-
? {
|
|
84
|
+
? {
|
|
85
|
+
selector: model.primaryKey.name || model.primaryKey.fields.join('_'),
|
|
86
|
+
fields: [...model.primaryKey.fields],
|
|
87
|
+
}
|
|
71
88
|
: null
|
|
72
89
|
|
|
73
|
-
const compoundUniques = (
|
|
74
|
-
.filter((idx
|
|
75
|
-
.map((idx
|
|
90
|
+
const compoundUniques = (model.uniqueIndexes ?? [])
|
|
91
|
+
.filter((idx) => idx.fields && idx.fields.length > 1)
|
|
92
|
+
.map((idx) => ({
|
|
76
93
|
name: idx.name || idx.fields.join('_'),
|
|
77
|
-
|
|
94
|
+
selector: idx.name || idx.fields.join('_'),
|
|
95
|
+
fields: [...idx.fields],
|
|
78
96
|
}))
|
|
79
97
|
|
|
80
98
|
return `import type { FieldMeta, EnumMeta } from '../docsRenderer${ext}'
|
|
@@ -83,10 +101,10 @@ export const MODEL_FIELDS: FieldMeta[] = ${JSON.stringify(fieldsMeta, null, 2)}
|
|
|
83
101
|
|
|
84
102
|
export const MODEL_ENUMS: EnumMeta[] = ${JSON.stringify(enumsMeta, null, 2)}
|
|
85
103
|
|
|
86
|
-
export const COMPOUND_ID: { fields: string[] } | null = ${JSON.stringify(compoundId)}
|
|
104
|
+
export const COMPOUND_ID: { selector: string; fields: string[] } | null = ${JSON.stringify(compoundId)}
|
|
87
105
|
|
|
88
|
-
export const COMPOUND_UNIQUES: { name: string; fields: string[] }[] = ${JSON.stringify(compoundUniques)}
|
|
106
|
+
export const COMPOUND_UNIQUES: { name: string; selector: string; fields: string[] }[] = ${JSON.stringify(compoundUniques)}
|
|
89
107
|
|
|
90
108
|
export const EXAMPLE_VALUES: Record<string, unknown> = ${JSON.stringify(exampleValues, null, 2)}
|
|
91
109
|
`
|
|
92
|
-
}
|
|
110
|
+
}
|