prisma-generator-express 1.67.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.
Files changed (68) hide show
  1. package/README.md +223 -28
  2. package/dist/copy/operationDefinitions.d.ts +4 -1
  3. package/dist/copy/operationDefinitions.js +64 -3
  4. package/dist/copy/operationDefinitions.js.map +1 -1
  5. package/dist/generators/generateFastifyHandler.js +55 -9
  6. package/dist/generators/generateFastifyHandler.js.map +1 -1
  7. package/dist/generators/generateHonoHandler.js +44 -4
  8. package/dist/generators/generateHonoHandler.js.map +1 -1
  9. package/dist/generators/generateHonoOpenApi.d.ts +13 -0
  10. package/dist/generators/generateHonoOpenApi.js +70 -0
  11. package/dist/generators/generateHonoOpenApi.js.map +1 -0
  12. package/dist/generators/generateMcpApp.d.ts +10 -0
  13. package/dist/generators/generateMcpApp.js +113 -0
  14. package/dist/generators/generateMcpApp.js.map +1 -0
  15. package/dist/generators/generateModelMcp.d.ts +9 -0
  16. package/dist/generators/generateModelMcp.js +145 -0
  17. package/dist/generators/generateModelMcp.js.map +1 -0
  18. package/dist/generators/generateModelMetadata.js +9 -5
  19. package/dist/generators/generateModelMetadata.js.map +1 -1
  20. package/dist/generators/generateRouter.js +62 -56
  21. package/dist/generators/generateRouter.js.map +1 -1
  22. package/dist/generators/generateRouterFastify.js +56 -64
  23. package/dist/generators/generateRouterFastify.js.map +1 -1
  24. package/dist/generators/generateRouterHono.js +43 -411
  25. package/dist/generators/generateRouterHono.js.map +1 -1
  26. package/dist/generators/generateRouterPartsHono.d.ts +14 -0
  27. package/dist/generators/generateRouterPartsHono.js +180 -0
  28. package/dist/generators/generateRouterPartsHono.js.map +1 -0
  29. package/dist/generators/generateUnifiedHandler.js +36 -20
  30. package/dist/generators/generateUnifiedHandler.js.map +1 -1
  31. package/dist/index.d.ts +2 -0
  32. package/dist/index.js +101 -3
  33. package/dist/index.js.map +1 -1
  34. package/dist/utils/copyFiles.d.ts +1 -1
  35. package/dist/utils/copyFiles.js +20 -1
  36. package/dist/utils/copyFiles.js.map +1 -1
  37. package/dist/utils/mcpGate.d.ts +10 -0
  38. package/dist/utils/mcpGate.js +118 -0
  39. package/dist/utils/mcpGate.js.map +1 -0
  40. package/dist/utils/writeFileSafely.js +6 -0
  41. package/dist/utils/writeFileSafely.js.map +1 -1
  42. package/package.json +18 -3
  43. package/src/copy/buildModelOpenApi.ts +76 -126
  44. package/src/copy/docsRenderer.ts +14 -15
  45. package/src/copy/mcpRuntime.ts +1017 -0
  46. package/src/copy/operationDefinitions.ts +78 -4
  47. package/src/copy/operationPipeline.ts +267 -0
  48. package/src/copy/operationRuntime.ts +24 -14
  49. package/src/copy/operationSchemas.ts +2773 -0
  50. package/src/copy/pagination.ts +10 -2
  51. package/src/copy/routeConfig.hono.ts +34 -5
  52. package/src/copy/routeConfig.ts +1 -1
  53. package/src/copy/routerParts.ts +524 -0
  54. package/src/generators/generateFastifyHandler.ts +55 -9
  55. package/src/generators/generateHonoHandler.ts +44 -4
  56. package/src/generators/generateHonoOpenApi.ts +93 -0
  57. package/src/generators/generateMcpApp.ts +133 -0
  58. package/src/generators/generateModelMcp.ts +208 -0
  59. package/src/generators/generateModelMetadata.ts +16 -10
  60. package/src/generators/generateRouter.ts +62 -56
  61. package/src/generators/generateRouterFastify.ts +56 -64
  62. package/src/generators/generateRouterHono.ts +49 -459
  63. package/src/generators/generateRouterPartsHono.ts +220 -0
  64. package/src/generators/generateUnifiedHandler.ts +36 -21
  65. package/src/index.ts +116 -3
  66. package/src/utils/copyFiles.ts +36 -0
  67. package/src/utils/mcpGate.ts +157 -0
  68. package/src/utils/writeFileSafely.ts +6 -0
@@ -24,8 +24,24 @@ export async function ${exportName}(
24
24
  request: FastifyRequest,
25
25
  _reply: FastifyReply,
26
26
  ): Promise<void> {
27
- const data = await core.${meta.coreName}(buildContext(request))
28
- ;(request as FastifyExtended).resultData = data
27
+ const fx = request as FastifyExtended
28
+ const data = await executeOperation(
29
+ { variantKey: fx.guardVariantKey, caller: fx.guardCaller },
30
+ { guardShape: fx.guardShape },
31
+ {
32
+ core: core.${meta.coreName},
33
+ args: queryChannel(request),
34
+ body: request.body,
35
+ prisma: requirePrisma(fx.prisma),
36
+ postgres: fx.postgres,
37
+ sqlite: fx.sqlite,
38
+ pagination: fx.routeConfig?.pagination,
39
+ override: fx.operationOverride,
40
+ getContext: fx.getContext,
41
+ findManyPaginatedMode: fx.findManyPaginatedMode,
42
+ },
43
+ )
44
+ fx.resultData = data
29
45
  }`
30
46
  })
31
47
  .join('\n')
@@ -38,10 +54,24 @@ export async function ${exportName}(
38
54
  request: FastifyRequest,
39
55
  _reply: FastifyReply,
40
56
  ): Promise<void> {
41
- const data = await core.${meta.coreName}(buildContext(request))
42
- const ext = request as FastifyExtended
43
- ext.resultData = data
44
- ext.resultStatus = ${meta.successStatus}
57
+ const fx = request as FastifyExtended
58
+ const data = await executeOperation(
59
+ { variantKey: fx.guardVariantKey, caller: fx.guardCaller },
60
+ { guardShape: fx.guardShape },
61
+ {
62
+ core: core.${meta.coreName},
63
+ args: queryChannel(request),
64
+ body: request.body,
65
+ prisma: requirePrisma(fx.prisma),
66
+ postgres: fx.postgres,
67
+ sqlite: fx.sqlite,
68
+ pagination: fx.routeConfig?.pagination,
69
+ override: fx.operationOverride,
70
+ getContext: fx.getContext,
71
+ },
72
+ )
73
+ fx.resultData = data
74
+ fx.resultStatus = ${meta.successStatus}
45
75
  }`
46
76
  })
47
77
  .join('\n')
@@ -60,28 +90,44 @@ export async function ${updateEachExportName}(
60
90
  return `import type { FastifyRequest, FastifyReply } from 'fastify'
61
91
  import * as core from './${modelName}Core${ext}'
62
92
  import type { RuntimeOperationOverride, OperationContext, FindManyPaginatedMode } from '../operationRuntime${ext}'
93
+ import type { PaginationConfig } from '../routeConfig${ext}'
94
+ import {
95
+ executeOperation,
96
+ requirePrisma,
97
+ type ArgsChannel,
98
+ } from '../operationPipeline${ext}'
63
99
 
64
100
  type FastifyExtended = FastifyRequest & {
65
101
  prisma?: unknown
66
102
  postgres?: unknown
67
103
  sqlite?: unknown
68
104
  parsedQuery?: Record<string, unknown>
69
- routeConfig?: { pagination?: OperationContext['paginationConfig'] }
105
+ routeConfig?: { pagination?: PaginationConfig }
70
106
  guardShape?: Record<string, unknown>
71
107
  guardCaller?: string
72
108
  guardVariantKey?: string
73
109
  operationOverride?: RuntimeOperationOverride
74
- resolveOperationContext?: () => unknown | Promise<unknown>
110
+ getContext?: () => Promise<unknown>
75
111
  findManyPaginatedMode?: FindManyPaginatedMode
76
112
  resultData?: unknown
77
113
  resultStatus?: number
78
114
  }
79
115
 
116
+ function queryChannel(request: FastifyRequest): ArgsChannel {
117
+ const fx = request as FastifyExtended
118
+ return {
119
+ read: () => fx.parsedQuery,
120
+ write: (next) => {
121
+ fx.parsedQuery = next
122
+ },
123
+ }
124
+ }
125
+
80
126
  function buildContext(request: FastifyRequest): OperationContext {
81
127
  const req = request as FastifyExtended
82
128
  return {
83
129
  operationOverride: req.operationOverride,
84
- resolveOperationContext: req.resolveOperationContext,
130
+ resolveOperationContext: req.getContext,
85
131
  prisma: req.prisma,
86
132
  postgres: req.postgres,
87
133
  sqlite: req.sqlite,
@@ -21,7 +21,21 @@ export function generateHonoHandler(options: {
21
21
  const exportName = `${modelName}${meta.name.charAt(0).toUpperCase() + meta.name.slice(1)}`
22
22
  return `
23
23
  export async function ${exportName}(c: HandlerContext): Promise<void> {
24
- const data = await core.${meta.coreName}(buildContext(c))
24
+ const data = await executeOperation(
25
+ { variantKey: c.get('guardVariantKey'), caller: c.get('guardCaller') },
26
+ { guardShape: c.get('guardShape') },
27
+ {
28
+ core: core.${meta.coreName},
29
+ args: queryChannel(c),
30
+ body: c.get('body'),
31
+ prisma: requirePrisma(c.get('prisma')),
32
+ postgres: c.get('postgres'),
33
+ sqlite: c.get('sqlite'),
34
+ pagination: c.get('routeConfig')?.pagination,
35
+ override: c.get('operationOverride'),
36
+ getContext: c.get('getContext'),
37
+ },
38
+ )
25
39
  c.set('resultData', data)
26
40
  }`
27
41
  })
@@ -32,7 +46,21 @@ export async function ${exportName}(c: HandlerContext): Promise<void> {
32
46
  const exportName = `${modelName}${meta.name.charAt(0).toUpperCase() + meta.name.slice(1)}`
33
47
  return `
34
48
  export async function ${exportName}(c: HandlerContext): Promise<void> {
35
- const data = await core.${meta.coreName}(buildContext(c))
49
+ const data = await executeOperation(
50
+ { variantKey: c.get('guardVariantKey'), caller: c.get('guardCaller') },
51
+ { guardShape: c.get('guardShape') },
52
+ {
53
+ core: core.${meta.coreName},
54
+ args: queryChannel(c),
55
+ body: c.get('body'),
56
+ prisma: requirePrisma(c.get('prisma')),
57
+ postgres: c.get('postgres'),
58
+ sqlite: c.get('sqlite'),
59
+ pagination: c.get('routeConfig')?.pagination,
60
+ override: c.get('operationOverride'),
61
+ getContext: c.get('getContext'),
62
+ },
63
+ )
36
64
  c.set('resultData', data)
37
65
  c.set('resultStatus', ${meta.successStatus})
38
66
  }`
@@ -49,15 +77,27 @@ export async function ${updateEachExportName}(c: HandlerContext): Promise<void>
49
77
 
50
78
  return `import type { Context } from 'hono'
51
79
  import * as core from './${modelName}Core${ext}'
52
- import type { RuntimeOperationOverride, OperationContext } from '../operationRuntime${ext}'
80
+ import type { OperationContext } from '../operationRuntime${ext}'
81
+ import {
82
+ executeOperation,
83
+ requirePrisma,
84
+ type ArgsChannel,
85
+ } from '../operationPipeline${ext}'
53
86
  import type { HonoInternalVariables } from '../routeConfig.target${ext}'
54
87
 
55
88
  type HandlerContext = Context<{ Variables: HonoInternalVariables }>
56
89
 
90
+ function queryChannel(c: HandlerContext): ArgsChannel {
91
+ return {
92
+ read: () => c.get('parsedQuery'),
93
+ write: (next) => c.set('parsedQuery', next),
94
+ }
95
+ }
96
+
57
97
  function buildContext(c: HandlerContext): OperationContext {
58
98
  return {
59
99
  operationOverride: c.get('operationOverride'),
60
- resolveOperationContext: c.get('resolveOperationContext'),
100
+ resolveOperationContext: c.get('getContext'),
61
101
  prisma: c.get('prisma'),
62
102
  postgres: c.get('postgres'),
63
103
  sqlite: c.get('sqlite'),
@@ -0,0 +1,93 @@
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
+
8
+ /**
9
+ * The per-model OpenAPI part, in a module of its own so the phase 9 boundary
10
+ * holds by construction: a bundle that does not import this file has no path
11
+ * to `buildModelOpenApi` or `yaml`, regardless of bundler side-effect
12
+ * heuristics.
13
+ */
14
+ export function generateHonoOpenApiRoutes({
15
+ model,
16
+ enums,
17
+ guardShapesImport,
18
+ clientImport,
19
+ importStyle,
20
+ writeStrategy,
21
+ pathCase,
22
+ }: {
23
+ model: DMMF.Model
24
+ enums: DMMF.DatamodelEnum[]
25
+ guardShapesImport: string | null
26
+ clientImport?: string
27
+ importStyle: ImportStyle
28
+ writeStrategy: WriteStrategy
29
+ pathCase: PathCase
30
+ }): string {
31
+ const ext = importExt(importStyle)
32
+ const modelName = model.name
33
+ const modelSegment = modelPathSegment(modelName, pathCase)
34
+ const lower = modelName.charAt(0).toLowerCase() + modelName.slice(1)
35
+
36
+ return `import { Hono } from 'hono'
37
+ import type {
38
+ RouteConfig,
39
+ HonoEnvBase,
40
+ GeneratedHonoEnv,
41
+ PrismaClientLike,
42
+ } from '../routeConfig.target${ext}'
43
+ import { normalizePrefix, getEnv } from '../misc${ext}'
44
+ import { registerOpenApiRoutes } from '../routerParts${ext}'
45
+ import { buildModelOpenApi } from '../buildModelOpenApi${ext}'
46
+ import { MODEL_FIELDS, MODEL_ENUMS } from './${modelName}Metadata${ext}'
47
+
48
+ ${generateRouteConfigType(modelName, 'HonoBeforeHook', guardShapesImport, importStyle, 'hono', clientImport)}
49
+
50
+ export function ${lower}OpenApi<TCtx = unknown, TPrisma extends PrismaClientLike = PrismaClientLike, TEnv extends HonoEnvBase = HonoEnvBase>(config: ${modelName}RouteConfig<TCtx, TPrisma, TEnv> = {}): Hono<GeneratedHonoEnv<TEnv>> {
51
+ const _env = getEnv()
52
+ const customPrefix = normalizePrefix(config.customUrlPrefix || '')
53
+ const modelPrefix = config.addModelPrefix !== false ? '/${modelSegment}' : ''
54
+ const basePath = customPrefix + modelPrefix
55
+
56
+ const openApiDisabled = config.disableOpenApi === true
57
+ || (config.disableOpenApi !== false && (
58
+ _env.NODE_ENV === 'production'
59
+ || _env.DISABLE_OPENAPI === 'true'
60
+ ))
61
+
62
+ const app = new Hono<GeneratedHonoEnv<TEnv>>()
63
+ if (!openApiDisabled) {
64
+ let jsonCache: unknown = undefined
65
+ let yamlCache: string | undefined = undefined
66
+ registerOpenApiRoutes(app, basePath, () => {
67
+ if (jsonCache === undefined) {
68
+ jsonCache = buildModelOpenApi(
69
+ '${modelName}',
70
+ MODEL_FIELDS as unknown as Parameters<typeof buildModelOpenApi>[1],
71
+ MODEL_ENUMS as unknown as Parameters<typeof buildModelOpenApi>[2],
72
+ config as unknown as Parameters<typeof buildModelOpenApi>[3],
73
+ { format: 'json', writeStrategy: '${writeStrategy}', pathSegment: '${modelSegment}' },
74
+ )
75
+ }
76
+ return jsonCache
77
+ }, () => {
78
+ if (yamlCache === undefined) {
79
+ yamlCache = buildModelOpenApi(
80
+ '${modelName}',
81
+ MODEL_FIELDS as unknown as Parameters<typeof buildModelOpenApi>[1],
82
+ MODEL_ENUMS as unknown as Parameters<typeof buildModelOpenApi>[2],
83
+ config as unknown as Parameters<typeof buildModelOpenApi>[3],
84
+ { format: 'yaml', writeStrategy: '${writeStrategy}', pathSegment: '${modelSegment}' },
85
+ ) as string
86
+ }
87
+ return yamlCache
88
+ })
89
+ }
90
+ return app
91
+ }
92
+ `
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
+ }
@@ -40,7 +40,7 @@ export function generateModelMetadata(
40
40
  const { model, enums } = options
41
41
  const modelName = model.name
42
42
 
43
- const fieldsMeta = model.fields.map((f: any) => ({
43
+ const fieldsMeta = model.fields.map((f) => ({
44
44
  name: f.name,
45
45
  kind: f.kind,
46
46
  type: f.type,
@@ -55,7 +55,7 @@ export function generateModelMetadata(
55
55
  }))
56
56
 
57
57
  const referencedEnumTypes = new Set(
58
- model.fields.filter((f: any) => f.kind === 'enum').map((f: any) => f.type),
58
+ model.fields.filter((f) => f.kind === 'enum').map((f) => f.type),
59
59
  )
60
60
 
61
61
  const enumsMeta = enums
@@ -66,7 +66,7 @@ export function generateModelMetadata(
66
66
  }))
67
67
 
68
68
  const exampleValues: Record<string, unknown> = {}
69
- for (const f of model.fields as any[]) {
69
+ for (const f of model.fields) {
70
70
  if (f.isId || f.isUnique || f.kind === 'scalar' || f.kind === 'enum') {
71
71
  if (f.kind === 'enum') {
72
72
  const enumDef = enums.find((e) => e.name === f.type)
@@ -77,16 +77,22 @@ export function generateModelMetadata(
77
77
  }
78
78
  }
79
79
 
80
+ // the client-input selector key mirrors Prisma's WhereUniqueInput: the
81
+ // constraint's explicit name when given, else the fields joined with '_'
80
82
  const compoundId =
81
83
  model.primaryKey && model.primaryKey.fields.length > 1
82
- ? { fields: model.primaryKey.fields }
84
+ ? {
85
+ selector: model.primaryKey.name || model.primaryKey.fields.join('_'),
86
+ fields: [...model.primaryKey.fields],
87
+ }
83
88
  : null
84
89
 
85
- const compoundUniques = ((model as any).uniqueIndexes || [])
86
- .filter((idx: any) => idx.fields && idx.fields.length > 1)
87
- .map((idx: any) => ({
90
+ const compoundUniques = (model.uniqueIndexes ?? [])
91
+ .filter((idx) => idx.fields && idx.fields.length > 1)
92
+ .map((idx) => ({
88
93
  name: idx.name || idx.fields.join('_'),
89
- fields: idx.fields,
94
+ selector: idx.name || idx.fields.join('_'),
95
+ fields: [...idx.fields],
90
96
  }))
91
97
 
92
98
  return `import type { FieldMeta, EnumMeta } from '../docsRenderer${ext}'
@@ -95,9 +101,9 @@ export const MODEL_FIELDS: FieldMeta[] = ${JSON.stringify(fieldsMeta, null, 2)}
95
101
 
96
102
  export const MODEL_ENUMS: EnumMeta[] = ${JSON.stringify(enumsMeta, null, 2)}
97
103
 
98
- export const COMPOUND_ID: { fields: string[] } | null = ${JSON.stringify(compoundId)}
104
+ export const COMPOUND_ID: { selector: string; fields: string[] } | null = ${JSON.stringify(compoundId)}
99
105
 
100
- 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)}
101
107
 
102
108
  export const EXAMPLE_VALUES: Record<string, unknown> = ${JSON.stringify(exampleValues, null, 2)}
103
109
  `