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
package/src/copy/misc.ts CHANGED
@@ -1,68 +1,88 @@
1
- export const isObject = (value: unknown): value is Record<string, unknown> => {
2
- return typeof value === 'object' && value !== null && !Array.isArray(value)
3
- }
4
-
5
- export function isPlainObject(value: unknown): value is Record<string, unknown> {
6
- if (value === null || typeof value !== 'object') return false
7
- if (Array.isArray(value)) return false
8
- const proto = Object.getPrototypeOf(value)
9
- return proto === Object.prototype || proto === null
10
- }
11
-
12
- const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype'])
13
-
14
- export function isSafeKey(key: string): boolean {
15
- return !UNSAFE_KEYS.has(key)
16
- }
17
-
18
- export function sanitizeKeys<T>(value: T): T {
19
- if (Array.isArray(value)) {
20
- let mutated = false
21
- const out: unknown[] = new Array(value.length)
22
- for (let i = 0; i < value.length; i++) {
23
- const sanitized = sanitizeKeys(value[i])
24
- if (sanitized !== value[i]) mutated = true
25
- out[i] = sanitized
26
- }
27
- return (mutated ? out : value) as T
28
- }
29
- if (isPlainObject(value)) {
30
- const keys = Object.keys(value)
31
- let hasUnsafe = false
32
- let childrenMutated = false
33
- const sanitizedChildren: Record<string, unknown> = {}
34
- for (const key of keys) {
35
- if (!isSafeKey(key)) {
36
- hasUnsafe = true
37
- continue
38
- }
39
- const original = (value as Record<string, unknown>)[key]
40
- const sanitized = sanitizeKeys(original)
41
- if (sanitized !== original) childrenMutated = true
42
- sanitizedChildren[key] = sanitized
43
- }
44
- if (!hasUnsafe && !childrenMutated) return value
45
- return sanitizedChildren as T
46
- }
47
- return value
48
- }
49
-
50
- export function normalizePrefix(p: string): string {
51
- if (!p) return ''
52
- let result = p
53
- if (!result.startsWith('/')) result = '/' + result
54
- while (result.length > 1 && result.endsWith('/')) result = result.slice(0, -1)
55
- if (result === '/') return ''
56
- return result
57
- }
58
-
59
- export function removeTrailingSlash(path: string): string {
60
- if (path === '/') return ''
61
- return path.endsWith('/') ? path.slice(0, -1) : path
62
- }
63
-
64
- export function getEnv(): Record<string, string | undefined> {
65
- return typeof process !== 'undefined' && process.env
66
- ? process.env
67
- : ({} as Record<string, string | undefined>)
68
- }
1
+ export const isObject = (value: unknown): value is Record<string, unknown> => {
2
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
3
+ }
4
+
5
+ export function isPlainObject(
6
+ value: unknown,
7
+ ): value is Record<string, unknown> {
8
+ if (value === null || typeof value !== 'object') return false
9
+ if (Array.isArray(value)) return false
10
+ const proto = Object.getPrototypeOf(value)
11
+ return proto === Object.prototype || proto === null
12
+ }
13
+
14
+ const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype'])
15
+
16
+ export function isSafeKey(key: string): boolean {
17
+ return !UNSAFE_KEYS.has(key)
18
+ }
19
+
20
+ export function sanitizeKeys<T>(value: T): T {
21
+ if (Array.isArray(value)) {
22
+ let mutated = false
23
+ const out: unknown[] = new Array(value.length)
24
+ for (let i = 0; i < value.length; i++) {
25
+ const sanitized = sanitizeKeys(value[i])
26
+ if (sanitized !== value[i]) mutated = true
27
+ out[i] = sanitized
28
+ }
29
+ return (mutated ? out : value) as T
30
+ }
31
+ if (isPlainObject(value)) {
32
+ const keys = Object.keys(value)
33
+ let hasUnsafe = false
34
+ let childrenMutated = false
35
+ const sanitizedChildren: Record<string, unknown> = {}
36
+ for (const key of keys) {
37
+ if (!isSafeKey(key)) {
38
+ hasUnsafe = true
39
+ continue
40
+ }
41
+ const original = (value as Record<string, unknown>)[key]
42
+ const sanitized = sanitizeKeys(original)
43
+ if (sanitized !== original) childrenMutated = true
44
+ sanitizedChildren[key] = sanitized
45
+ }
46
+ if (!hasUnsafe && !childrenMutated) return value
47
+ return sanitizedChildren as T
48
+ }
49
+ return value
50
+ }
51
+
52
+ export function normalizePrefix(p: string): string {
53
+ if (!p) return ''
54
+ let result = p
55
+ if (!result.startsWith('/')) result = '/' + result
56
+ while (result.length > 1 && result.endsWith('/')) result = result.slice(0, -1)
57
+ if (result === '/') return ''
58
+ return result
59
+ }
60
+
61
+ export function removeTrailingSlash(path: string): string {
62
+ if (path === '/') return ''
63
+ return path.endsWith('/') ? path.slice(0, -1) : path
64
+ }
65
+
66
+ export function getEnv(): Record<string, string | undefined> {
67
+ return typeof process !== 'undefined' && process.env
68
+ ? process.env
69
+ : ({} as Record<string, string | undefined>)
70
+ }
71
+
72
+ let dropGuardDeprecationWarned = false
73
+
74
+ export function resolveDropGuardEnv(
75
+ env: Record<string, string | undefined>,
76
+ ): boolean {
77
+ if (env.PGE_DROP_GUARD === 'true') return true
78
+ if (env.E2E === 'true') {
79
+ if (!dropGuardDeprecationWarned) {
80
+ dropGuardDeprecationWarned = true
81
+ console.warn(
82
+ '[prisma-generator-express] E2E=true guard bypass is deprecated. Use PGE_DROP_GUARD=true.',
83
+ )
84
+ }
85
+ return true
86
+ }
87
+ return false
88
+ }
@@ -0,0 +1,72 @@
1
+ import type { SseWritable } from './sse'
2
+
3
+ /**
4
+ * NDJSON chunked streaming for list reads.
5
+ *
6
+ * Contract: one JSON document per line; the final line is
7
+ * `{"done":true,"rows":N}`. Clients parse by splitting on newlines — there is
8
+ * no event vocabulary and no client-side merge algorithm. The same guard
9
+ * shapes, projection defaults, and pagination limits apply as on the JSON
10
+ * endpoint, because every page goes through the same generated core function.
11
+ */
12
+
13
+ export function acceptsNdjson(accept: string | undefined): boolean {
14
+ if (!accept) return false
15
+ return accept
16
+ .toLowerCase()
17
+ .split(',')
18
+ .some((entry) => entry.split(';')[0].trim() === 'application/x-ndjson')
19
+ }
20
+
21
+ export function startNdjson(res: SseWritable): void {
22
+ res.setHeader('Content-Type', 'application/x-ndjson')
23
+ res.setHeader('Cache-Control', 'no-cache, no-transform')
24
+ res.setHeader('X-Stream-Protocol-Version', '1')
25
+ res.setHeader('X-Accel-Buffering', 'no')
26
+ res.statusCode = 200
27
+ }
28
+
29
+ export interface NdjsonStreamOptions {
30
+ res: SseWritable
31
+ isClosed: () => boolean
32
+ fetchPage: (skip: number, take: number) => Promise<unknown>
33
+ initialSkip: number
34
+ requestedTake?: number
35
+ defaultLimit?: number
36
+ maxLimit: number
37
+ }
38
+
39
+ export async function runNdjsonFindMany(
40
+ options: NdjsonStreamOptions,
41
+ ): Promise<void> {
42
+ startNdjson(options.res)
43
+ const take = Math.min(
44
+ options.requestedTake ?? options.defaultLimit ?? options.maxLimit,
45
+ options.maxLimit,
46
+ )
47
+ let skip = options.initialSkip
48
+ let written = 0
49
+
50
+ while (
51
+ written < options.maxLimit &&
52
+ !options.isClosed() &&
53
+ !options.res.writableEnded
54
+ ) {
55
+ const result = await options.fetchPage(skip, take)
56
+ if (!Array.isArray(result)) {
57
+ throw new Error(
58
+ 'NDJSON streaming requires an array result from the core read',
59
+ )
60
+ }
61
+ for (const row of result) {
62
+ if (written >= options.maxLimit) break
63
+ options.res.write(JSON.stringify(row) + '\n')
64
+ written++
65
+ }
66
+ if (result.length < take) break
67
+ skip += result.length
68
+ }
69
+
70
+ options.res.write(JSON.stringify({ done: true, rows: written }) + '\n')
71
+ options.res.end()
72
+ }
@@ -21,11 +21,25 @@ export interface OpMetadata {
21
21
  argsSchema: readonly string[]
22
22
  requiredArgs: readonly string[]
23
23
  notes: string
24
+ /**
25
+ * EXPLICIT behaviour annotations, never inferred from `kind`. Mapped to MCP
26
+ * tool hints (readOnlyHint / destructiveHint / idempotentHint) and available
27
+ * for any other transport that wants the same vocabulary. MCP semantics:
28
+ * `destructive: false` means ONLY additive updates — anything that can
29
+ * overwrite or remove existing data (update, upsert, delete families) is
30
+ * destructive.
31
+ */
32
+ readOnly: boolean
33
+ destructive: boolean
34
+ idempotent: boolean
24
35
  }
25
36
 
26
37
  export const OPERATION_METADATA: readonly OpMetadata[] = [
27
38
  {
28
39
  name: 'findMany',
40
+ readOnly: true,
41
+ destructive: false,
42
+ idempotent: true,
29
43
  method: 'get',
30
44
  pathSuffix: '',
31
45
  configKey: 'findMany',
@@ -40,12 +54,25 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
40
54
  errors: [400, 403, 500, 501, 503],
41
55
  responseDesc: 'Array of records',
42
56
  transport: 'GET query params',
43
- argsSchema: ['where', 'select', 'include', 'omit', 'orderBy', 'cursor', 'take', 'skip', 'distinct'],
57
+ argsSchema: [
58
+ 'where',
59
+ 'select',
60
+ 'include',
61
+ 'omit',
62
+ 'orderBy',
63
+ 'cursor',
64
+ 'take',
65
+ 'skip',
66
+ 'distinct',
67
+ ],
44
68
  requiredArgs: [],
45
69
  notes: 'Pagination limits may apply when configured.',
46
70
  },
47
71
  {
48
72
  name: 'findUnique',
73
+ readOnly: true,
74
+ destructive: false,
75
+ idempotent: true,
49
76
  method: 'get',
50
77
  pathSuffix: '/unique',
51
78
  configKey: 'findUnique',
@@ -66,6 +93,9 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
66
93
  },
67
94
  {
68
95
  name: 'findUniqueOrThrow',
96
+ readOnly: true,
97
+ destructive: false,
98
+ idempotent: true,
69
99
  method: 'get',
70
100
  pathSuffix: '/unique/strict',
71
101
  configKey: 'findUniqueOrThrow',
@@ -86,6 +116,9 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
86
116
  },
87
117
  {
88
118
  name: 'findFirst',
119
+ readOnly: true,
120
+ destructive: false,
121
+ idempotent: true,
89
122
  method: 'get',
90
123
  pathSuffix: '/first',
91
124
  configKey: 'findFirst',
@@ -100,12 +133,25 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
100
133
  errors: [400, 403, 500, 501, 503],
101
134
  responseDesc: 'Single record or null',
102
135
  transport: 'GET query params',
103
- argsSchema: ['where', 'select', 'include', 'omit', 'orderBy', 'cursor', 'take', 'skip', 'distinct'],
136
+ argsSchema: [
137
+ 'where',
138
+ 'select',
139
+ 'include',
140
+ 'omit',
141
+ 'orderBy',
142
+ 'cursor',
143
+ 'take',
144
+ 'skip',
145
+ 'distinct',
146
+ ],
104
147
  requiredArgs: [],
105
148
  notes: 'Returns null (not 404) when no record matches.',
106
149
  },
107
150
  {
108
151
  name: 'findFirstOrThrow',
152
+ readOnly: true,
153
+ destructive: false,
154
+ idempotent: true,
109
155
  method: 'get',
110
156
  pathSuffix: '/first/strict',
111
157
  configKey: 'findFirstOrThrow',
@@ -120,12 +166,25 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
120
166
  errors: [400, 403, 404, 500, 501, 503],
121
167
  responseDesc: 'Single record',
122
168
  transport: 'GET query params',
123
- argsSchema: ['where', 'select', 'include', 'omit', 'orderBy', 'cursor', 'take', 'skip', 'distinct'],
169
+ argsSchema: [
170
+ 'where',
171
+ 'select',
172
+ 'include',
173
+ 'omit',
174
+ 'orderBy',
175
+ 'cursor',
176
+ 'take',
177
+ 'skip',
178
+ 'distinct',
179
+ ],
124
180
  requiredArgs: [],
125
181
  notes: 'Returns 404 when no record matches.',
126
182
  },
127
183
  {
128
184
  name: 'findManyPaginated',
185
+ readOnly: true,
186
+ destructive: false,
187
+ idempotent: true,
129
188
  method: 'get',
130
189
  pathSuffix: '/paginated',
131
190
  configKey: 'findManyPaginated',
@@ -140,12 +199,26 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
140
199
  errors: [400, 403, 409, 500, 501, 503],
141
200
  responseDesc: '{ data: Record[], total: number, hasMore: boolean }',
142
201
  transport: 'GET query params',
143
- argsSchema: ['where', 'select', 'include', 'omit', 'orderBy', 'cursor', 'take', 'skip', 'distinct'],
202
+ argsSchema: [
203
+ 'where',
204
+ 'select',
205
+ 'include',
206
+ 'omit',
207
+ 'orderBy',
208
+ 'cursor',
209
+ 'take',
210
+ 'skip',
211
+ 'distinct',
212
+ ],
144
213
  requiredArgs: [],
145
- notes: 'Wraps findMany with total count. hasMore is reliable for forward offset pagination (skip + take) only. Distinct count over 100k falls back to approximate total. 409 possible on transaction conflict.',
214
+ notes:
215
+ 'Wraps findMany with total count. hasMore is reliable for forward offset pagination (skip + take) only. Distinct count over 100k falls back to approximate total. 409 possible on transaction conflict.',
146
216
  },
147
217
  {
148
218
  name: 'count',
219
+ readOnly: true,
220
+ destructive: false,
221
+ idempotent: true,
149
222
  method: 'get',
150
223
  pathSuffix: '/count',
151
224
  configKey: 'count',
@@ -162,10 +235,14 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
162
235
  transport: 'GET query params',
163
236
  argsSchema: ['where', 'orderBy', 'cursor', 'take', 'skip', 'select'],
164
237
  requiredArgs: [],
165
- notes: 'select here means count-specific field selection, not record field selection.',
238
+ notes:
239
+ 'select here means count-specific field selection, not record field selection.',
166
240
  },
167
241
  {
168
242
  name: 'aggregate',
243
+ readOnly: true,
244
+ destructive: false,
245
+ idempotent: true,
169
246
  method: 'get',
170
247
  pathSuffix: '/aggregate',
171
248
  configKey: 'aggregate',
@@ -178,14 +255,29 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
178
255
  excludeFromEnableAll: false,
179
256
  writeStrategyAffected: false,
180
257
  errors: [400, 403, 500, 501, 503],
181
- responseDesc: 'Object with requested aggregate fields (_count, _avg, _sum, _min, _max)',
258
+ responseDesc:
259
+ 'Object with requested aggregate fields (_count, _avg, _sum, _min, _max)',
182
260
  transport: 'GET query params',
183
- argsSchema: ['where', 'orderBy', 'cursor', 'take', 'skip', '_count', '_avg', '_sum', '_min', '_max'],
261
+ argsSchema: [
262
+ 'where',
263
+ 'orderBy',
264
+ 'cursor',
265
+ 'take',
266
+ 'skip',
267
+ '_count',
268
+ '_avg',
269
+ '_sum',
270
+ '_min',
271
+ '_max',
272
+ ],
184
273
  requiredArgs: [],
185
274
  notes: '_avg, _sum only apply to numeric fields.',
186
275
  },
187
276
  {
188
277
  name: 'groupBy',
278
+ readOnly: true,
279
+ destructive: false,
280
+ idempotent: true,
189
281
  method: 'get',
190
282
  pathSuffix: '/groupby',
191
283
  configKey: 'groupBy',
@@ -198,14 +290,31 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
198
290
  excludeFromEnableAll: false,
199
291
  writeStrategyAffected: false,
200
292
  errors: [400, 403, 500, 501, 503],
201
- responseDesc: 'Array of objects, each with grouped field values and requested aggregates',
293
+ responseDesc:
294
+ 'Array of objects, each with grouped field values and requested aggregates',
202
295
  transport: 'GET query params',
203
- argsSchema: ['by', 'where', 'orderBy', 'having', 'take', 'skip', '_count', '_avg', '_sum', '_min', '_max'],
296
+ argsSchema: [
297
+ 'by',
298
+ 'where',
299
+ 'orderBy',
300
+ 'having',
301
+ 'take',
302
+ 'skip',
303
+ '_count',
304
+ '_avg',
305
+ '_sum',
306
+ '_min',
307
+ '_max',
308
+ ],
204
309
  requiredArgs: ['by'],
205
- notes: 'by is a JSON-encoded array of scalar field names. orderBy is required when using skip or take. Response contains only the by-fields plus requested aggregates.',
310
+ notes:
311
+ 'by is a JSON-encoded array of scalar field names. orderBy is required when using skip or take. Response contains only the by-fields plus requested aggregates.',
206
312
  },
207
313
  {
208
314
  name: 'create',
315
+ readOnly: false,
316
+ destructive: false,
317
+ idempotent: false,
209
318
  method: 'post',
210
319
  pathSuffix: '',
211
320
  configKey: 'create',
@@ -226,6 +335,9 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
226
335
  },
227
336
  {
228
337
  name: 'createMany',
338
+ readOnly: false,
339
+ destructive: false,
340
+ idempotent: false,
229
341
  method: 'post',
230
342
  pathSuffix: '/many',
231
343
  configKey: 'createMany',
@@ -242,10 +354,14 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
242
354
  transport: 'POST JSON body',
243
355
  argsSchema: ['data', 'skipDuplicates'],
244
356
  requiredArgs: ['data'],
245
- notes: 'data is an array of scalar-only inputs. Nested relation writes are not supported. skipDuplicates silently ignores conflicts (not supported on all providers).',
357
+ notes:
358
+ 'data is an array of scalar-only inputs. Nested relation writes are not supported. skipDuplicates silently ignores conflicts (not supported on all providers).',
246
359
  },
247
360
  {
248
361
  name: 'createManyAndReturn',
362
+ readOnly: false,
363
+ destructive: false,
364
+ idempotent: false,
249
365
  method: 'post',
250
366
  pathSuffix: '/many/return',
251
367
  configKey: 'createManyAndReturn',
@@ -262,10 +378,14 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
262
378
  transport: 'POST JSON body',
263
379
  argsSchema: ['data', 'skipDuplicates', 'select', 'include', 'omit'],
264
380
  requiredArgs: ['data'],
265
- notes: 'Like createMany but returns created records. data items are scalar-only. Requires Prisma 5.14.0+, PostgreSQL/CockroachDB/SQLite only. The order of returned records is not guaranteed.',
381
+ notes:
382
+ 'Like createMany but returns created records. data items are scalar-only. Requires Prisma 5.14.0+, PostgreSQL/CockroachDB/SQLite only. The order of returned records is not guaranteed.',
266
383
  },
267
384
  {
268
385
  name: 'update',
386
+ readOnly: false,
387
+ destructive: true,
388
+ idempotent: false,
269
389
  method: 'put',
270
390
  pathSuffix: '',
271
391
  configKey: 'update',
@@ -282,10 +402,14 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
282
402
  transport: 'PUT JSON body',
283
403
  argsSchema: ['where', 'data', 'select', 'include', 'omit'],
284
404
  requiredArgs: ['where', 'data'],
285
- notes: '404 when the record to update is not found. 409 on unique constraint violation or transaction conflict.',
405
+ notes:
406
+ '404 when the record to update is not found. 409 on unique constraint violation or transaction conflict.',
286
407
  },
287
408
  {
288
409
  name: 'updateMany',
410
+ readOnly: false,
411
+ destructive: true,
412
+ idempotent: false,
289
413
  method: 'put',
290
414
  pathSuffix: '/many',
291
415
  configKey: 'updateMany',
@@ -302,10 +426,14 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
302
426
  transport: 'PUT JSON body',
303
427
  argsSchema: ['where', 'data'],
304
428
  requiredArgs: ['where', 'data'],
305
- notes: 'Updates all matching records with scalar-only data. Nested relation writes are not supported. Returns count, not records. 409 on unique constraint violation.',
429
+ notes:
430
+ 'Updates all matching records with scalar-only data. Nested relation writes are not supported. Returns count, not records. 409 on unique constraint violation.',
306
431
  },
307
432
  {
308
433
  name: 'updateManyAndReturn',
434
+ readOnly: false,
435
+ destructive: true,
436
+ idempotent: false,
309
437
  method: 'put',
310
438
  pathSuffix: '/many/return',
311
439
  configKey: 'updateManyAndReturn',
@@ -322,10 +450,14 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
322
450
  transport: 'PUT JSON body',
323
451
  argsSchema: ['where', 'data', 'select', 'include', 'omit'],
324
452
  requiredArgs: ['where', 'data'],
325
- notes: 'Like updateMany but returns updated records. data is scalar-only. Requires Prisma 6.2.0+, PostgreSQL/CockroachDB/SQLite only. 409 on unique constraint violation.',
453
+ notes:
454
+ 'Like updateMany but returns updated records. data is scalar-only. Requires Prisma 6.2.0+, PostgreSQL/CockroachDB/SQLite only. 409 on unique constraint violation.',
326
455
  },
327
456
  {
328
457
  name: 'upsert',
458
+ readOnly: false,
459
+ destructive: true,
460
+ idempotent: true,
329
461
  method: 'patch',
330
462
  pathSuffix: '',
331
463
  configKey: 'upsert',
@@ -346,6 +478,9 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
346
478
  },
347
479
  {
348
480
  name: 'delete',
481
+ readOnly: false,
482
+ destructive: true,
483
+ idempotent: true,
349
484
  method: 'delete',
350
485
  pathSuffix: '',
351
486
  configKey: 'delete',
@@ -366,6 +501,9 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
366
501
  },
367
502
  {
368
503
  name: 'deleteMany',
504
+ readOnly: false,
505
+ destructive: true,
506
+ idempotent: true,
369
507
  method: 'delete',
370
508
  pathSuffix: '/many',
371
509
  configKey: 'deleteMany',
@@ -386,6 +524,9 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
386
524
  },
387
525
  {
388
526
  name: 'updateEach',
527
+ readOnly: false,
528
+ destructive: true,
529
+ idempotent: false,
389
530
  method: 'post',
390
531
  pathSuffix: '/each',
391
532
  configKey: 'updateEach',
@@ -398,11 +539,13 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
398
539
  excludeFromEnableAll: true,
399
540
  writeStrategyAffected: false,
400
541
  errors: [400, 403, 409, 500, 501, 503],
401
- responseDesc: 'Non-atomic: per-row { status, data } / { status, error } array. Atomic: array of records.',
542
+ responseDesc:
543
+ 'Non-atomic: per-row { status, data } / { status, error } array. Atomic: array of records.',
402
544
  transport: 'POST JSON body (array)',
403
545
  argsSchema: [],
404
546
  requiredArgs: ['array of { where, data }'],
405
- notes: 'Internal batch endpoint. Bypasses guard shapes. Not enabled by enableAll. Non-atomic max 1000 items, atomic max 100 items. Header x-batch-atomic:true switches to transactional mode.',
547
+ notes:
548
+ 'Internal batch endpoint. Bypasses guard shapes. Not enabled by enableAll. Non-atomic max 1000 items, atomic max 100 items. Header x-batch-atomic:true switches to transactional mode.',
406
549
  },
407
550
  ]
408
551
 
@@ -410,9 +553,12 @@ export const OPERATION_BY_NAME: Record<string, OpMetadata> = Object.fromEntries(
410
553
  OPERATION_METADATA.map((m) => [m.name, m]),
411
554
  )
412
555
 
413
- export const READ_OPERATIONS: readonly OpMetadata[] = OPERATION_METADATA.filter((m) => m.kind === 'read')
556
+ export const READ_OPERATIONS: readonly OpMetadata[] = OPERATION_METADATA.filter(
557
+ (m) => m.kind === 'read',
558
+ )
414
559
 
415
- export const WRITE_OPERATIONS: readonly OpMetadata[] = OPERATION_METADATA.filter((m) => m.kind === 'write' || m.kind === 'batch')
560
+ export const WRITE_OPERATIONS: readonly OpMetadata[] =
561
+ OPERATION_METADATA.filter((m) => m.kind === 'write' || m.kind === 'batch')
416
562
 
417
563
  export const READ_OPERATION_NAMES = new Set(READ_OPERATIONS.map((m) => m.name))
418
564
 
@@ -439,10 +585,16 @@ export function getPostReadPathSuffix(opName: string): string {
439
585
  }
440
586
 
441
587
  export function isOperationEnabled(
442
- config: Record<string, any>,
588
+ config: object,
443
589
  def: OperationDef | OpMetadata,
444
590
  ): boolean {
445
- if (config[def.configKey] === false) return false
446
- if (def.excludeFromEnableAll) return !!config[def.configKey]
447
- return !!(config.enableAll || config[def.configKey])
448
- }
591
+ // Route configs are interfaces without index signatures, so the value is
592
+ // reached through one runtime narrowing here at the single point that reads it.
593
+ if (!config || typeof config !== 'object') {
594
+ return false
595
+ }
596
+ const cfg = config as Record<string, unknown>
597
+ if (cfg[def.configKey] === false) return false
598
+ if (def.excludeFromEnableAll) return !!cfg[def.configKey]
599
+ return !!(cfg.enableAll || cfg[def.configKey])
600
+ }