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.
Files changed (130) hide show
  1. package/README.md +188 -3432
  2. package/dist/client/encodeQueryParams.js +3 -1
  3. package/dist/client/encodeQueryParams.js.map +1 -1
  4. package/dist/copy/misc.d.ts +1 -0
  5. package/dist/copy/misc.js +14 -0
  6. package/dist/copy/misc.js.map +1 -1
  7. package/dist/copy/operationDefinitions.d.ts +4 -1
  8. package/dist/copy/operationDefinitions.js +133 -9
  9. package/dist/copy/operationDefinitions.js.map +1 -1
  10. package/dist/generators/generateFastifyHandler.d.ts +2 -2
  11. package/dist/generators/generateFastifyHandler.js +68 -12
  12. package/dist/generators/generateFastifyHandler.js.map +1 -1
  13. package/dist/generators/generateHonoHandler.d.ts +2 -2
  14. package/dist/generators/generateHonoHandler.js +53 -6
  15. package/dist/generators/generateHonoHandler.js.map +1 -1
  16. package/dist/generators/generateHonoOpenApi.d.ts +13 -0
  17. package/dist/generators/generateHonoOpenApi.js +70 -0
  18. package/dist/generators/generateHonoOpenApi.js.map +1 -0
  19. package/dist/generators/generateImportPrismaStatement.d.ts +1 -0
  20. package/dist/generators/generateImportPrismaStatement.js +6 -0
  21. package/dist/generators/generateImportPrismaStatement.js.map +1 -1
  22. package/dist/generators/generateMcpApp.d.ts +10 -0
  23. package/dist/generators/generateMcpApp.js +113 -0
  24. package/dist/generators/generateMcpApp.js.map +1 -0
  25. package/dist/generators/generateModelMcp.d.ts +9 -0
  26. package/dist/generators/generateModelMcp.js +145 -0
  27. package/dist/generators/generateModelMcp.js.map +1 -0
  28. package/dist/generators/generateModelMetadata.js +29 -15
  29. package/dist/generators/generateModelMetadata.js.map +1 -1
  30. package/dist/generators/generateOperationCore.d.ts +3 -3
  31. package/dist/generators/generateOperationCore.js +51 -16
  32. package/dist/generators/generateOperationCore.js.map +1 -1
  33. package/dist/generators/generateRelationMeta.js +2 -2
  34. package/dist/generators/generateRelationMeta.js.map +1 -1
  35. package/dist/generators/generateRouteConfigType.d.ts +2 -2
  36. package/dist/generators/generateRouteConfigType.js +29 -11
  37. package/dist/generators/generateRouteConfigType.js.map +1 -1
  38. package/dist/generators/generateRouter.d.ts +7 -4
  39. package/dist/generators/generateRouter.js +164 -65
  40. package/dist/generators/generateRouter.js.map +1 -1
  41. package/dist/generators/generateRouterFastify.d.ts +7 -4
  42. package/dist/generators/generateRouterFastify.js +106 -86
  43. package/dist/generators/generateRouterFastify.js.map +1 -1
  44. package/dist/generators/generateRouterHono.d.ts +7 -4
  45. package/dist/generators/generateRouterHono.js +59 -414
  46. package/dist/generators/generateRouterHono.js.map +1 -1
  47. package/dist/generators/generateRouterPartsHono.d.ts +14 -0
  48. package/dist/generators/generateRouterPartsHono.js +180 -0
  49. package/dist/generators/generateRouterPartsHono.js.map +1 -0
  50. package/dist/generators/generateUnifiedDocs.d.ts +1 -1
  51. package/dist/generators/generateUnifiedDocs.js +15 -8
  52. package/dist/generators/generateUnifiedDocs.js.map +1 -1
  53. package/dist/generators/generateUnifiedHandler.d.ts +2 -2
  54. package/dist/generators/generateUnifiedHandler.js +42 -18
  55. package/dist/generators/generateUnifiedHandler.js.map +1 -1
  56. package/dist/guardOptions.js +12 -7
  57. package/dist/guardOptions.js.map +1 -1
  58. package/dist/index.d.ts +3 -1
  59. package/dist/index.js +123 -4
  60. package/dist/index.js.map +1 -1
  61. package/dist/utils/copyFiles.d.ts +1 -1
  62. package/dist/utils/copyFiles.js +42 -7
  63. package/dist/utils/copyFiles.js.map +1 -1
  64. package/dist/utils/importExt.d.ts +1 -1
  65. package/dist/utils/mcpGate.d.ts +10 -0
  66. package/dist/utils/mcpGate.js +118 -0
  67. package/dist/utils/mcpGate.js.map +1 -0
  68. package/dist/utils/pathCasing.d.ts +4 -0
  69. package/dist/utils/pathCasing.js +40 -0
  70. package/dist/utils/pathCasing.js.map +1 -0
  71. package/dist/utils/resolveImportStyle.js +13 -5
  72. package/dist/utils/resolveImportStyle.js.map +1 -1
  73. package/dist/utils/writeFileSafely.js +6 -0
  74. package/dist/utils/writeFileSafely.js.map +1 -1
  75. package/package.json +22 -5
  76. package/src/bin.ts +1 -1
  77. package/src/client/encodeQueryParams.ts +39 -35
  78. package/src/constants.ts +1 -1
  79. package/src/copy/autoIncludePlanner.ts +93 -27
  80. package/src/copy/autoIncludePlannerGuarded.ts +171 -48
  81. package/src/copy/autoIncludeRuntimeGuarded.ts +25 -7
  82. package/src/copy/buildModelOpenApi.ts +229 -176
  83. package/src/copy/concurrency.ts +10 -8
  84. package/src/copy/docsRenderer.ts +14 -15
  85. package/src/copy/errorMapper.ts +68 -19
  86. package/src/copy/guardHelpers.ts +30 -9
  87. package/src/copy/materializedCount.ts +17 -5
  88. package/src/copy/mcpRuntime.ts +1017 -0
  89. package/src/copy/misc.ts +88 -68
  90. package/src/copy/ndjson.ts +72 -0
  91. package/src/copy/operationDefinitions.ts +177 -25
  92. package/src/copy/operationPipeline.ts +267 -0
  93. package/src/copy/operationRuntime.ts +124 -26
  94. package/src/copy/operationSchemas.ts +2773 -0
  95. package/src/copy/pagination.ts +35 -13
  96. package/src/copy/parseQueryParams.ts +47 -48
  97. package/src/copy/projectionDefaults.ts +6 -4
  98. package/src/copy/routeConfig.express.ts +16 -4
  99. package/src/copy/routeConfig.fastify.ts +16 -2
  100. package/src/copy/routeConfig.hono.ts +46 -14
  101. package/src/copy/routeConfig.ts +175 -40
  102. package/src/copy/routerParts.ts +524 -0
  103. package/src/copy/scalarTypes.ts +7 -2
  104. package/src/copy/sse.ts +72 -19
  105. package/src/generators/generateFastifyHandler.ts +77 -20
  106. package/src/generators/generateHonoHandler.ts +62 -14
  107. package/src/generators/generateHonoOpenApi.ts +93 -0
  108. package/src/generators/generateImportPrismaStatement.ts +93 -81
  109. package/src/generators/generateMcpApp.ts +133 -0
  110. package/src/generators/generateModelMcp.ts +208 -0
  111. package/src/generators/generateModelMetadata.ts +40 -22
  112. package/src/generators/generateOperationCore.ts +81 -33
  113. package/src/generators/generateRelationMeta.ts +23 -11
  114. package/src/generators/generateRouteConfigType.ts +39 -13
  115. package/src/generators/generateRouter.ts +171 -68
  116. package/src/generators/generateRouterFastify.ts +117 -89
  117. package/src/generators/generateRouterHono.ts +84 -464
  118. package/src/generators/generateRouterPartsHono.ts +220 -0
  119. package/src/generators/generateUnifiedDocs.ts +29 -8
  120. package/src/generators/generateUnifiedHandler.ts +47 -24
  121. package/src/generators/generateUnifiedScalarUI.ts +1 -1
  122. package/src/guardOptions.ts +105 -92
  123. package/src/index.ts +161 -6
  124. package/src/utils/copyFiles.ts +235 -168
  125. package/src/utils/importExt.ts +2 -2
  126. package/src/utils/mcpGate.ts +157 -0
  127. package/src/utils/pathCasing.ts +42 -0
  128. package/src/utils/resolveImportStyle.ts +37 -15
  129. package/src/utils/strings.ts +2 -2
  130. package/src/utils/writeFileSafely.ts +12 -2
@@ -1,168 +1,235 @@
1
- import { GeneratorOptions } from '@prisma/generator-helper'
2
- import * as fs from 'fs'
3
- import * as path from 'path'
4
- import { Target } from '../constants'
5
- import { ImportStyle } from './resolveImportStyle'
6
- import { importExt } from './importExt'
7
-
8
- const SHARED_FILES = [
9
- 'parseQueryParams.ts',
10
- 'buildModelOpenApi.ts',
11
- 'operationDefinitions.ts',
12
- 'misc.ts',
13
- 'routeConfig.ts',
14
- 'docsRenderer.ts',
15
- 'operationRuntime.ts',
16
- 'errorMapper.ts',
17
- 'sse.ts',
18
- 'pagination.ts',
19
- 'materializedCount.ts',
20
- 'guardHelpers.ts',
21
- 'scalarTypes.ts',
22
- 'concurrency.ts',
23
- 'projectionDefaults.ts',
24
- 'guardVariantRouting.ts',
25
- 'guardVariantError.ts',
26
- ]
27
-
28
- const EXPRESS_ONLY_FILES = [
29
- 'autoIncludePlanner.ts',
30
- 'autoIncludePlannerGuarded.ts',
31
- 'autoIncludeRuntime.ts',
32
- 'autoIncludeRuntimeGuarded.ts',
33
- 'materializedRouter.ts',
34
- ]
35
-
36
- interface CopyFileOptions {
37
- required?: boolean
38
- importRewrites?: Array<{ from: string; to: string }>
39
- destFilename?: string
40
- }
41
-
42
- function resolveTemplateDir(subpath: string): string {
43
- const fromSrc = path.join(__dirname, '..', '..', 'src', subpath)
44
- if (fs.existsSync(fromSrc)) return fromSrc
45
- const fromDist = path.join(__dirname, '..', subpath)
46
- if (fs.existsSync(fromDist)) return fromDist
47
- throw new Error(
48
- 'Template directory "' + subpath + '" not found.\n Searched:\n ' +
49
- fromSrc + '\n ' + fromDist + '\n __dirname: ' + __dirname,
50
- )
51
- }
52
-
53
- function escapeRegex(str: string): string {
54
- return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
55
- }
56
-
57
- const RELATIVE_IMPORT_RE =
58
- /(\b(?:import|export)\b[^'";]*?\bfrom\s+|\bimport\s+|\bimport\s*\(\s*)(['"])(\.{1,2}\/[^'"\n]+)\2/g
59
-
60
- const SKIP_EXTENSION_RE = /\.(m?[jt]sx?|json|css|svg|png|jpe?g|gif|webp|wasm)$/i
61
-
62
- function applyExtension(content: string, style: ImportStyle): string {
63
- if (style === 'none') return content
64
- const ext = importExt(style)
65
- if (!ext) return content
66
- return content.replace(RELATIVE_IMPORT_RE, (_match, prefix, quote, spec) => {
67
- if (SKIP_EXTENSION_RE.test(spec)) return prefix + quote + spec + quote
68
- return prefix + quote + spec + ext + quote
69
- })
70
- }
71
-
72
- function copyFileSync(
73
- srcDir: string,
74
- destDir: string,
75
- filename: string,
76
- importStyle: ImportStyle,
77
- options?: CopyFileOptions,
78
- ): string | null {
79
- const srcPath = path.join(srcDir, filename)
80
- const destFilename = options?.destFilename || filename
81
- const destPath = path.join(destDir, destFilename)
82
-
83
- if (!fs.existsSync(srcPath)) {
84
- if (options?.required) return 'Required file not found: ' + srcPath
85
- return null
86
- }
87
-
88
- const destDirPath = path.dirname(destPath)
89
- if (!fs.existsSync(destDirPath)) fs.mkdirSync(destDirPath, { recursive: true })
90
-
91
- let content = fs.readFileSync(srcPath, 'utf-8')
92
-
93
- if (options?.importRewrites) {
94
- for (const rewrite of options.importRewrites) {
95
- if (rewrite.from === rewrite.to) continue
96
- content = content.replace(
97
- new RegExp('from [\'"]' + escapeRegex(rewrite.from) + '[\'"]', 'g'),
98
- "from '" + rewrite.to + "'",
99
- )
100
- }
101
- }
102
-
103
- const header =
104
- '/**\n' +
105
- ' * Auto-generated by prisma-generator-express\n' +
106
- ' * DO NOT EDIT MANUALLY\n' +
107
- ' */\n\n'
108
-
109
- if (!content.startsWith('/**')) content = header + content
110
-
111
- content = applyExtension(content, importStyle)
112
-
113
- fs.writeFileSync(destPath, content)
114
- return null
115
- }
116
-
117
- export async function copyFiles(
118
- options: GeneratorOptions,
119
- target: Target,
120
- importStyle: ImportStyle,
121
- ): Promise<void> {
122
- const outputPath = options.generator.output?.value
123
- if (!outputPath) return
124
-
125
- const copyBase = resolveTemplateDir('copy')
126
- const errors: string[] = []
127
-
128
- console.log(' Copying utility files to: ' + outputPath + ' (target: ' + target + ')')
129
-
130
- for (const file of SHARED_FILES) {
131
- const err = copyFileSync(copyBase, outputPath, file, importStyle, { required: true })
132
- if (err) errors.push(err)
133
- }
134
-
135
- if (target === 'express') {
136
- for (const file of EXPRESS_ONLY_FILES) {
137
- const err = copyFileSync(copyBase, outputPath, file, importStyle, { required: true })
138
- if (err) errors.push(err)
139
- }
140
- }
141
-
142
- const targetConfigFile = 'routeConfig.' + target + '.ts'
143
- const err = copyFileSync(copyBase, outputPath, targetConfigFile, importStyle, {
144
- required: true,
145
- destFilename: 'routeConfig.target.ts',
146
- })
147
- if (err) errors.push(err)
148
-
149
- const clientDir = path.join(outputPath, 'client')
150
- if (!fs.existsSync(clientDir)) fs.mkdirSync(clientDir, { recursive: true })
151
-
152
- const clientSrcDir = resolveTemplateDir('client')
153
- const clientErr = copyFileSync(clientSrcDir, clientDir, 'encodeQueryParams.ts', importStyle, {
154
- required: true,
155
- importRewrites: [{ from: '../copy/misc', to: '../misc' }],
156
- })
157
- if (clientErr) errors.push(clientErr)
158
-
159
- if (errors.length > 0) {
160
- throw new Error(
161
- 'Failed to copy ' + errors.length + ' required file(s):\n' +
162
- errors.map((e) => ' - ' + e).join('\n') +
163
- '\n copyBase: ' + copyBase + '\n __dirname: ' + __dirname,
164
- )
165
- }
166
-
167
- console.log(' ✓ Utility files copied successfully')
168
- }
1
+ import { GeneratorOptions } from '@prisma/generator-helper'
2
+ import * as fs from 'fs'
3
+ import * as path from 'path'
4
+ import { Target } from '../constants'
5
+ import { ImportStyle } from './resolveImportStyle'
6
+ import { importExt } from './importExt'
7
+
8
+ const SHARED_FILES = [
9
+ 'parseQueryParams.ts',
10
+ 'buildModelOpenApi.ts',
11
+ 'operationDefinitions.ts',
12
+ 'operationSchemas.ts',
13
+ 'misc.ts',
14
+ 'routeConfig.ts',
15
+ 'docsRenderer.ts',
16
+ 'operationRuntime.ts',
17
+ 'operationPipeline.ts',
18
+ 'errorMapper.ts',
19
+ 'sse.ts',
20
+ 'pagination.ts',
21
+ 'materializedCount.ts',
22
+ 'guardHelpers.ts',
23
+ 'scalarTypes.ts',
24
+ 'concurrency.ts',
25
+ 'projectionDefaults.ts',
26
+ 'guardVariantRouting.ts',
27
+ 'guardVariantError.ts',
28
+ ]
29
+
30
+ const EXPRESS_ONLY_FILES = [
31
+ 'autoIncludePlanner.ts',
32
+ 'autoIncludePlannerGuarded.ts',
33
+ 'autoIncludeRuntime.ts',
34
+ 'autoIncludeRuntimeGuarded.ts',
35
+ 'materializedRouter.ts',
36
+ 'ndjson.ts',
37
+ ]
38
+
39
+ interface CopyFileOptions {
40
+ required?: boolean
41
+ importRewrites?: Array<{ from: string; to: string }>
42
+ destFilename?: string
43
+ }
44
+
45
+ function resolveTemplateDir(subpath: string): string {
46
+ const fromSrc = path.join(__dirname, '..', '..', 'src', subpath)
47
+ if (fs.existsSync(fromSrc)) return fromSrc
48
+ const fromDist = path.join(__dirname, '..', subpath)
49
+ if (fs.existsSync(fromDist)) return fromDist
50
+ throw new Error(
51
+ 'Template directory "' +
52
+ subpath +
53
+ '" not found.\n Searched:\n ' +
54
+ fromSrc +
55
+ '\n ' +
56
+ fromDist +
57
+ '\n __dirname: ' +
58
+ __dirname,
59
+ )
60
+ }
61
+
62
+ function escapeRegex(str: string): string {
63
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
64
+ }
65
+
66
+ const RELATIVE_IMPORT_RE =
67
+ /(\b(?:import|export)\b[^'";]*?\bfrom\s+|\bimport\s+|\bimport\s*\(\s*)(['"])(\.{1,2}\/[^'"\n]+)\2/g
68
+
69
+ const SKIP_EXTENSION_RE = /\.(m?[jt]sx?|json|css|svg|png|jpe?g|gif|webp|wasm)$/i
70
+
71
+ function applyExtension(content: string, style: ImportStyle): string {
72
+ if (style === 'none') return content
73
+ const ext = importExt(style)
74
+ if (!ext) return content
75
+ return content.replace(RELATIVE_IMPORT_RE, (_match, prefix, quote, spec) => {
76
+ if (SKIP_EXTENSION_RE.test(spec)) return prefix + quote + spec + quote
77
+ return prefix + quote + spec + ext + quote
78
+ })
79
+ }
80
+
81
+ function copyFileSync(
82
+ srcDir: string,
83
+ destDir: string,
84
+ filename: string,
85
+ importStyle: ImportStyle,
86
+ options?: CopyFileOptions,
87
+ ): string | null {
88
+ const srcPath = path.join(srcDir, filename)
89
+ const destFilename = options?.destFilename || filename
90
+ const destPath = path.join(destDir, destFilename)
91
+
92
+ if (!fs.existsSync(srcPath)) {
93
+ if (options?.required) return 'Required file not found: ' + srcPath
94
+ return null
95
+ }
96
+
97
+ const destDirPath = path.dirname(destPath)
98
+ if (!fs.existsSync(destDirPath))
99
+ fs.mkdirSync(destDirPath, { recursive: true })
100
+
101
+ let content = fs.readFileSync(srcPath, 'utf-8')
102
+
103
+ if (options?.importRewrites) {
104
+ for (const rewrite of options.importRewrites) {
105
+ if (rewrite.from === rewrite.to) continue
106
+ content = content.replace(
107
+ new RegExp('from [\'"]' + escapeRegex(rewrite.from) + '[\'"]', 'g'),
108
+ "from '" + rewrite.to + "'",
109
+ )
110
+ }
111
+ }
112
+
113
+ const header =
114
+ '/**\n' +
115
+ ' * Auto-generated by prisma-generator-express\n' +
116
+ ' * DO NOT EDIT MANUALLY\n' +
117
+ ' */\n\n'
118
+
119
+ if (!content.startsWith('/**')) content = header + content
120
+
121
+ content = applyExtension(content, importStyle)
122
+
123
+ fs.writeFileSync(destPath, content)
124
+ return null
125
+ }
126
+
127
+ export async function copyFiles(
128
+ options: GeneratorOptions,
129
+ target: Target,
130
+ importStyle: ImportStyle,
131
+ mcp = false,
132
+ ): Promise<void> {
133
+ const outputPath = options.generator.output?.value
134
+ if (!outputPath) return
135
+
136
+ const copyBase = resolveTemplateDir('copy')
137
+ const errors: string[] = []
138
+
139
+ console.log(
140
+ ' Copying utility files to: ' + outputPath + ' (target: ' + target + ')',
141
+ )
142
+
143
+ for (const file of SHARED_FILES) {
144
+ const err = copyFileSync(copyBase, outputPath, file, importStyle, {
145
+ required: true,
146
+ })
147
+ if (err) errors.push(err)
148
+ }
149
+
150
+ if (target === 'express') {
151
+ for (const file of EXPRESS_ONLY_FILES) {
152
+ const err = copyFileSync(copyBase, outputPath, file, importStyle, {
153
+ required: true,
154
+ })
155
+ if (err) errors.push(err)
156
+ }
157
+ }
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
+
192
+ const targetConfigFile = 'routeConfig.' + target + '.ts'
193
+ const err = copyFileSync(
194
+ copyBase,
195
+ outputPath,
196
+ targetConfigFile,
197
+ importStyle,
198
+ {
199
+ required: true,
200
+ destFilename: 'routeConfig.target.ts',
201
+ },
202
+ )
203
+ if (err) errors.push(err)
204
+
205
+ const clientDir = path.join(outputPath, 'client')
206
+ if (!fs.existsSync(clientDir)) fs.mkdirSync(clientDir, { recursive: true })
207
+
208
+ const clientSrcDir = resolveTemplateDir('client')
209
+ const clientErr = copyFileSync(
210
+ clientSrcDir,
211
+ clientDir,
212
+ 'encodeQueryParams.ts',
213
+ importStyle,
214
+ {
215
+ required: true,
216
+ importRewrites: [{ from: '../copy/misc', to: '../misc' }],
217
+ },
218
+ )
219
+ if (clientErr) errors.push(clientErr)
220
+
221
+ if (errors.length > 0) {
222
+ throw new Error(
223
+ 'Failed to copy ' +
224
+ errors.length +
225
+ ' required file(s):\n' +
226
+ errors.map((e) => ' - ' + e).join('\n') +
227
+ '\n copyBase: ' +
228
+ copyBase +
229
+ '\n __dirname: ' +
230
+ __dirname,
231
+ )
232
+ }
233
+
234
+ console.log(' ✓ Utility files copied successfully')
235
+ }
@@ -1,7 +1,7 @@
1
- import { ImportStyle } from './resolveImportStyle'
1
+ import type { ImportStyle } from './resolveImportStyle'
2
2
 
3
3
  export function importExt(style: ImportStyle): string {
4
4
  if (style === 'js') return '.js'
5
5
  if (style === 'ts') return '.ts'
6
6
  return ''
7
- }
7
+ }
@@ -0,0 +1,157 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+
4
+ /**
5
+ * MCP is fail-closed at GENERATION time: a guard-dropped artifact must not
6
+ * carry MCP support, because every MCP tool enforces a guard shape and a
7
+ * dropped guard would turn reads and writes into unguarded operations.
8
+ *
9
+ * MCP tool schemas are built against prisma-guard 1.33 semantics (unique
10
+ * selectors, operator configs, forced-value merging). A consumer running an
11
+ * older guard would accept shapes the tool schemas misrepresent, so an
12
+ * mcp=true generation REFUSES to emit MCP support unless prisma-guard
13
+ * >= 1.33.0 is resolvable from the schema's project.
14
+ */
15
+ export function assertMcpGuardCompatibility(
16
+ mcp: boolean,
17
+ dropGuard: boolean,
18
+ ): void {
19
+ if (mcp && dropGuard) {
20
+ throw new Error(
21
+ 'Generator option mcp=true cannot be combined with dropGuard=true. ' +
22
+ 'MCP is fail-closed: every tool enforces a guard shape, so a ' +
23
+ 'guard-dropped generation refuses to emit MCP support.',
24
+ )
25
+ }
26
+ }
27
+
28
+ const MCP_MIN_GUARD = '1.33.0'
29
+
30
+ type Semver = {
31
+ core: [number, number, number]
32
+ pre: string[]
33
+ }
34
+
35
+ /** Semver 2.0.0 ordering, including prerelease precedence. */
36
+ export function parseSemver(v: string): Semver | null {
37
+ // strict semver 2.0.0: no leading-zero numeric identifiers, no empty
38
+ // dot-separated prerelease/build identifiers; build metadata is parsed
39
+ // and ignored for precedence
40
+ const numeric = '0|[1-9]' + '\\d*'
41
+ const ident = '[0-9A-Za-z-]+'
42
+ const dotted = ident + '(?:\\.' + ident + ')*'
43
+ const m = new RegExp(
44
+ `^(${numeric})\\.(${numeric})\\.(${numeric})(?:-(${dotted}))?(?:\\+(${dotted}))?$`,
45
+ ).exec(v.trim())
46
+ if (!m) return null
47
+ const pre = m[4] ? m[4].split('.') : []
48
+ // semver 2.0.0: numeric PRERELEASE identifiers must not carry leading
49
+ // zeros (build-metadata identifiers are exempt)
50
+ for (const ident of pre) {
51
+ if (/^[0-9]+$/.test(ident) && ident.length > 1 && ident.startsWith('0')) {
52
+ return null
53
+ }
54
+ }
55
+ return {
56
+ core: [Number(m[1]), Number(m[2]), Number(m[3])],
57
+ pre,
58
+ }
59
+ }
60
+
61
+ /** `a < b` returns negative, `a > b` positive, equal 0 (semver 2.0.0). */
62
+ export function compareSemver(a: string, b: string): number {
63
+ const va = parseSemver(a)
64
+ const vb = parseSemver(b)
65
+ if (!va || !vb) {
66
+ // unparsable strings sort last so they never satisfy a minimum
67
+ return va ? 1 : vb ? -1 : 0
68
+ }
69
+ for (let i = 0; i < 3; i++) {
70
+ if (va.core[i] !== vb.core[i]) return va.core[i] - vb.core[i]
71
+ }
72
+ // no prerelease > any prerelease
73
+ if (va.pre.length === 0 && vb.pre.length === 0) return 0
74
+ if (va.pre.length === 0) return 1
75
+ if (vb.pre.length === 0) return -1
76
+ const len = Math.max(va.pre.length, vb.pre.length)
77
+ for (let i = 0; i < len; i++) {
78
+ const x = va.pre[i]
79
+ const y = vb.pre[i]
80
+ if (x === undefined) return -1 // shorter set is lower
81
+ if (y === undefined) return 1
82
+ const xn = /^\d+$/.test(x)
83
+ const yn = /^\d+$/.test(y)
84
+ if (xn && yn) {
85
+ if (Number(x) !== Number(y)) return Number(x) - Number(y)
86
+ } else if (xn)
87
+ return -1 // numeric identifiers are lower
88
+ else if (yn) return 1
89
+ else if (x !== y) return x < y ? -1 : 1
90
+ }
91
+ return 0
92
+ }
93
+
94
+ /**
95
+ * Resolve prisma-guard's installed version the way Node resolves it for the
96
+ * generated artifact: walk UP from the schema's directory, checking
97
+ * `<ancestor>/node_modules/prisma-guard` at each level, and stop at the
98
+ * FIRST hit. Returns null when no ancestor carries an install.
99
+ */
100
+ export function resolveInstalledGuardVersion(
101
+ schemaPath: string,
102
+ ): string | null {
103
+ const schemaDir = path.dirname(path.resolve(schemaPath))
104
+ let dir: string | undefined = schemaDir
105
+ while (dir) {
106
+ const pkgPath = path.join(
107
+ dir,
108
+ 'node_modules',
109
+ 'prisma-guard',
110
+ 'package.json',
111
+ )
112
+ try {
113
+ const version = (
114
+ JSON.parse(fs.readFileSync(pkgPath, 'utf8')) as {
115
+ version?: string
116
+ }
117
+ ).version
118
+ if (typeof version === 'string') return version
119
+ } catch {
120
+ // not installed at this ancestor — keep walking
121
+ }
122
+ const parent = path.dirname(dir)
123
+ dir = parent !== dir ? parent : undefined
124
+ }
125
+ return null
126
+ }
127
+
128
+ export function assertMcpGuardVersion(
129
+ mcp: boolean,
130
+ schemaPath: string,
131
+ resolve: (schemaPath: string) => string | null = resolveInstalledGuardVersion,
132
+ ): void {
133
+ if (!mcp) return
134
+ const version = resolve(schemaPath)
135
+ if (version === null) {
136
+ throw new Error(
137
+ 'mcp=true requires prisma-guard >= ' +
138
+ MCP_MIN_GUARD +
139
+ ', but prisma-guard could not be resolved from the schema project. ' +
140
+ 'Install it (npm install prisma-guard@^' +
141
+ MCP_MIN_GUARD +
142
+ ') or turn mcp off.',
143
+ )
144
+ }
145
+ if (compareSemver(version, MCP_MIN_GUARD) < 0) {
146
+ throw new Error(
147
+ 'mcp=true requires prisma-guard >= ' +
148
+ MCP_MIN_GUARD +
149
+ ' (MCP tool schemas mirror 1.33 semantics: unique-selector where, ' +
150
+ 'operator configs, forced-value merging). Found prisma-guard ' +
151
+ version +
152
+ '. Upgrade it (npm install prisma-guard@^' +
153
+ MCP_MIN_GUARD +
154
+ ') or turn mcp off.',
155
+ )
156
+ }
157
+ }
@@ -0,0 +1,42 @@
1
+ export type PathCase = 'lower' | 'kebab' | 'raw'
2
+
3
+ export function parsePathCase(raw: unknown): PathCase {
4
+ const value = String(raw ?? 'lower').toLowerCase()
5
+ if (value === 'lower' || value === 'kebab' || value === 'raw') return value
6
+ throw new Error(
7
+ `Invalid pathCase "${String(raw)}". Expected "lower", "kebab", or "raw".`,
8
+ )
9
+ }
10
+
11
+ export function toKebabCase(name: string): string {
12
+ let out = ''
13
+ for (let i = 0; i < name.length; i++) {
14
+ const ch = name[i]
15
+ const prev = i > 0 ? name[i - 1] : undefined
16
+ const next = name[i + 1]
17
+ const isUpper = ch >= 'A' && ch <= 'Z'
18
+ const prevIsLower = prev !== undefined && prev >= 'a' && prev <= 'z'
19
+ const prevIsDigit = prev !== undefined && prev >= '0' && prev <= '9'
20
+ const nextIsLower = next !== undefined && next >= 'a' && next <= 'z'
21
+ if (
22
+ isUpper &&
23
+ i > 0 &&
24
+ (prevIsLower ||
25
+ prevIsDigit ||
26
+ (prev !== undefined && prev >= 'A' && prev <= 'Z' && nextIsLower))
27
+ ) {
28
+ out += '-'
29
+ }
30
+ out += ch.toLowerCase()
31
+ }
32
+ return out
33
+ }
34
+
35
+ export function modelPathSegment(
36
+ modelName: string,
37
+ pathCase: PathCase,
38
+ ): string {
39
+ if (pathCase === 'raw') return modelName
40
+ if (pathCase === 'kebab') return toKebabCase(modelName)
41
+ return modelName.toLowerCase()
42
+ }