prisma-generator-express 1.66.0 → 1.67.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 (102) hide show
  1. package/README.md +37 -3476
  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.js +69 -6
  8. package/dist/copy/operationDefinitions.js.map +1 -1
  9. package/dist/generators/generateFastifyHandler.d.ts +2 -2
  10. package/dist/generators/generateFastifyHandler.js +15 -5
  11. package/dist/generators/generateFastifyHandler.js.map +1 -1
  12. package/dist/generators/generateHonoHandler.d.ts +2 -2
  13. package/dist/generators/generateHonoHandler.js +12 -5
  14. package/dist/generators/generateHonoHandler.js.map +1 -1
  15. package/dist/generators/generateImportPrismaStatement.d.ts +1 -0
  16. package/dist/generators/generateImportPrismaStatement.js +6 -0
  17. package/dist/generators/generateImportPrismaStatement.js.map +1 -1
  18. package/dist/generators/generateModelMetadata.js +20 -10
  19. package/dist/generators/generateModelMetadata.js.map +1 -1
  20. package/dist/generators/generateOperationCore.d.ts +3 -3
  21. package/dist/generators/generateOperationCore.js +51 -16
  22. package/dist/generators/generateOperationCore.js.map +1 -1
  23. package/dist/generators/generateRelationMeta.js +2 -2
  24. package/dist/generators/generateRelationMeta.js.map +1 -1
  25. package/dist/generators/generateRouteConfigType.d.ts +2 -2
  26. package/dist/generators/generateRouteConfigType.js +29 -11
  27. package/dist/generators/generateRouteConfigType.js.map +1 -1
  28. package/dist/generators/generateRouter.d.ts +7 -4
  29. package/dist/generators/generateRouter.js +107 -14
  30. package/dist/generators/generateRouter.js.map +1 -1
  31. package/dist/generators/generateRouterFastify.d.ts +7 -4
  32. package/dist/generators/generateRouterFastify.js +56 -28
  33. package/dist/generators/generateRouterFastify.js.map +1 -1
  34. package/dist/generators/generateRouterHono.d.ts +7 -4
  35. package/dist/generators/generateRouterHono.js +36 -23
  36. package/dist/generators/generateRouterHono.js.map +1 -1
  37. package/dist/generators/generateUnifiedDocs.d.ts +1 -1
  38. package/dist/generators/generateUnifiedDocs.js +15 -8
  39. package/dist/generators/generateUnifiedDocs.js.map +1 -1
  40. package/dist/generators/generateUnifiedHandler.d.ts +2 -2
  41. package/dist/generators/generateUnifiedHandler.js +11 -3
  42. package/dist/generators/generateUnifiedHandler.js.map +1 -1
  43. package/dist/guardOptions.js +12 -7
  44. package/dist/guardOptions.js.map +1 -1
  45. package/dist/index.d.ts +1 -1
  46. package/dist/index.js +22 -1
  47. package/dist/index.js.map +1 -1
  48. package/dist/utils/copyFiles.js +22 -6
  49. package/dist/utils/copyFiles.js.map +1 -1
  50. package/dist/utils/importExt.d.ts +1 -1
  51. package/dist/utils/pathCasing.d.ts +4 -0
  52. package/dist/utils/pathCasing.js +40 -0
  53. package/dist/utils/pathCasing.js.map +1 -0
  54. package/dist/utils/resolveImportStyle.js +13 -5
  55. package/dist/utils/resolveImportStyle.js.map +1 -1
  56. package/dist/utils/writeFileSafely.js.map +1 -1
  57. package/package.json +5 -3
  58. package/src/bin.ts +1 -1
  59. package/src/client/encodeQueryParams.ts +39 -35
  60. package/src/constants.ts +1 -1
  61. package/src/copy/autoIncludePlanner.ts +93 -27
  62. package/src/copy/autoIncludePlannerGuarded.ts +171 -48
  63. package/src/copy/autoIncludeRuntimeGuarded.ts +25 -7
  64. package/src/copy/buildModelOpenApi.ts +153 -50
  65. package/src/copy/concurrency.ts +10 -8
  66. package/src/copy/errorMapper.ts +68 -19
  67. package/src/copy/guardHelpers.ts +30 -9
  68. package/src/copy/materializedCount.ts +17 -5
  69. package/src/copy/misc.ts +88 -68
  70. package/src/copy/ndjson.ts +72 -0
  71. package/src/copy/operationDefinitions.ts +99 -21
  72. package/src/copy/operationRuntime.ts +100 -12
  73. package/src/copy/pagination.ts +25 -11
  74. package/src/copy/parseQueryParams.ts +47 -48
  75. package/src/copy/projectionDefaults.ts +6 -4
  76. package/src/copy/routeConfig.express.ts +16 -4
  77. package/src/copy/routeConfig.fastify.ts +16 -2
  78. package/src/copy/routeConfig.hono.ts +12 -9
  79. package/src/copy/routeConfig.ts +174 -39
  80. package/src/copy/scalarTypes.ts +7 -2
  81. package/src/copy/sse.ts +72 -19
  82. package/src/generators/generateFastifyHandler.ts +24 -13
  83. package/src/generators/generateHonoHandler.ts +21 -13
  84. package/src/generators/generateImportPrismaStatement.ts +93 -81
  85. package/src/generators/generateModelMetadata.ts +24 -12
  86. package/src/generators/generateOperationCore.ts +81 -33
  87. package/src/generators/generateRelationMeta.ts +23 -11
  88. package/src/generators/generateRouteConfigType.ts +39 -13
  89. package/src/generators/generateRouter.ts +114 -17
  90. package/src/generators/generateRouterFastify.ts +67 -31
  91. package/src/generators/generateRouterHono.ts +60 -30
  92. package/src/generators/generateUnifiedDocs.ts +29 -8
  93. package/src/generators/generateUnifiedHandler.ts +16 -8
  94. package/src/generators/generateUnifiedScalarUI.ts +1 -1
  95. package/src/guardOptions.ts +105 -92
  96. package/src/index.ts +46 -4
  97. package/src/utils/copyFiles.ts +199 -168
  98. package/src/utils/importExt.ts +2 -2
  99. package/src/utils/pathCasing.ts +42 -0
  100. package/src/utils/resolveImportStyle.ts +37 -15
  101. package/src/utils/strings.ts +2 -2
  102. package/src/utils/writeFileSafely.ts +6 -2
@@ -4,7 +4,10 @@ import type { PaginationCountSource } from './routeConfig'
4
4
  export const IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/
5
5
 
6
6
  type PrismaRawClient = {
7
- $queryRawUnsafe?: <T = unknown>(sql: string, ...values: unknown[]) => Promise<T>
7
+ $queryRawUnsafe?: <T = unknown>(
8
+ sql: string,
9
+ ...values: unknown[]
10
+ ) => Promise<T>
8
11
  }
9
12
 
10
13
  export function quoteIdent(name: string): string {
@@ -47,7 +50,10 @@ export async function countFromMaterializedView(
47
50
  ): Promise<number> {
48
51
  const raw = client as PrismaRawClient
49
52
  if (typeof raw.$queryRawUnsafe !== 'function') {
50
- throw new HttpError(500, 'Materialized count source requires $queryRawUnsafe on the Prisma client')
53
+ throw new HttpError(
54
+ 500,
55
+ 'Materialized count source requires $queryRawUnsafe on the Prisma client',
56
+ )
51
57
  }
52
58
  const column = source.column ?? 'total'
53
59
  const where = buildMaterializedCountWhere(source.where)
@@ -58,11 +64,17 @@ export async function countFromMaterializedView(
58
64
  buildMaterializedCountFqn(source) +
59
65
  where.sql +
60
66
  ' LIMIT 1'
61
- const rows = await raw.$queryRawUnsafe<Array<{ total: unknown }>>(sql, ...where.values)
67
+ const rows = await raw.$queryRawUnsafe<Array<{ total: unknown }>>(
68
+ sql,
69
+ ...where.values,
70
+ )
62
71
  const value = rows[0]?.total
63
72
  const total = Number(value)
64
73
  if (!Number.isFinite(total)) {
65
- throw new HttpError(500, 'Materialized count source did not return a numeric total')
74
+ throw new HttpError(
75
+ 500,
76
+ 'Materialized count source did not return a numeric total',
77
+ )
66
78
  }
67
79
  return Math.trunc(total)
68
- }
80
+ }
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
+ }
@@ -40,7 +40,17 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
40
40
  errors: [400, 403, 500, 501, 503],
41
41
  responseDesc: 'Array of records',
42
42
  transport: 'GET query params',
43
- argsSchema: ['where', 'select', 'include', 'omit', 'orderBy', 'cursor', 'take', 'skip', 'distinct'],
43
+ argsSchema: [
44
+ 'where',
45
+ 'select',
46
+ 'include',
47
+ 'omit',
48
+ 'orderBy',
49
+ 'cursor',
50
+ 'take',
51
+ 'skip',
52
+ 'distinct',
53
+ ],
44
54
  requiredArgs: [],
45
55
  notes: 'Pagination limits may apply when configured.',
46
56
  },
@@ -100,7 +110,17 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
100
110
  errors: [400, 403, 500, 501, 503],
101
111
  responseDesc: 'Single record or null',
102
112
  transport: 'GET query params',
103
- argsSchema: ['where', 'select', 'include', 'omit', 'orderBy', 'cursor', 'take', 'skip', 'distinct'],
113
+ argsSchema: [
114
+ 'where',
115
+ 'select',
116
+ 'include',
117
+ 'omit',
118
+ 'orderBy',
119
+ 'cursor',
120
+ 'take',
121
+ 'skip',
122
+ 'distinct',
123
+ ],
104
124
  requiredArgs: [],
105
125
  notes: 'Returns null (not 404) when no record matches.',
106
126
  },
@@ -120,7 +140,17 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
120
140
  errors: [400, 403, 404, 500, 501, 503],
121
141
  responseDesc: 'Single record',
122
142
  transport: 'GET query params',
123
- argsSchema: ['where', 'select', 'include', 'omit', 'orderBy', 'cursor', 'take', 'skip', 'distinct'],
143
+ argsSchema: [
144
+ 'where',
145
+ 'select',
146
+ 'include',
147
+ 'omit',
148
+ 'orderBy',
149
+ 'cursor',
150
+ 'take',
151
+ 'skip',
152
+ 'distinct',
153
+ ],
124
154
  requiredArgs: [],
125
155
  notes: 'Returns 404 when no record matches.',
126
156
  },
@@ -140,9 +170,20 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
140
170
  errors: [400, 403, 409, 500, 501, 503],
141
171
  responseDesc: '{ data: Record[], total: number, hasMore: boolean }',
142
172
  transport: 'GET query params',
143
- argsSchema: ['where', 'select', 'include', 'omit', 'orderBy', 'cursor', 'take', 'skip', 'distinct'],
173
+ argsSchema: [
174
+ 'where',
175
+ 'select',
176
+ 'include',
177
+ 'omit',
178
+ 'orderBy',
179
+ 'cursor',
180
+ 'take',
181
+ 'skip',
182
+ 'distinct',
183
+ ],
144
184
  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.',
185
+ notes:
186
+ '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
187
  },
147
188
  {
148
189
  name: 'count',
@@ -162,7 +203,8 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
162
203
  transport: 'GET query params',
163
204
  argsSchema: ['where', 'orderBy', 'cursor', 'take', 'skip', 'select'],
164
205
  requiredArgs: [],
165
- notes: 'select here means count-specific field selection, not record field selection.',
206
+ notes:
207
+ 'select here means count-specific field selection, not record field selection.',
166
208
  },
167
209
  {
168
210
  name: 'aggregate',
@@ -178,9 +220,21 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
178
220
  excludeFromEnableAll: false,
179
221
  writeStrategyAffected: false,
180
222
  errors: [400, 403, 500, 501, 503],
181
- responseDesc: 'Object with requested aggregate fields (_count, _avg, _sum, _min, _max)',
223
+ responseDesc:
224
+ 'Object with requested aggregate fields (_count, _avg, _sum, _min, _max)',
182
225
  transport: 'GET query params',
183
- argsSchema: ['where', 'orderBy', 'cursor', 'take', 'skip', '_count', '_avg', '_sum', '_min', '_max'],
226
+ argsSchema: [
227
+ 'where',
228
+ 'orderBy',
229
+ 'cursor',
230
+ 'take',
231
+ 'skip',
232
+ '_count',
233
+ '_avg',
234
+ '_sum',
235
+ '_min',
236
+ '_max',
237
+ ],
184
238
  requiredArgs: [],
185
239
  notes: '_avg, _sum only apply to numeric fields.',
186
240
  },
@@ -198,11 +252,25 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
198
252
  excludeFromEnableAll: false,
199
253
  writeStrategyAffected: false,
200
254
  errors: [400, 403, 500, 501, 503],
201
- responseDesc: 'Array of objects, each with grouped field values and requested aggregates',
255
+ responseDesc:
256
+ 'Array of objects, each with grouped field values and requested aggregates',
202
257
  transport: 'GET query params',
203
- argsSchema: ['by', 'where', 'orderBy', 'having', 'take', 'skip', '_count', '_avg', '_sum', '_min', '_max'],
258
+ argsSchema: [
259
+ 'by',
260
+ 'where',
261
+ 'orderBy',
262
+ 'having',
263
+ 'take',
264
+ 'skip',
265
+ '_count',
266
+ '_avg',
267
+ '_sum',
268
+ '_min',
269
+ '_max',
270
+ ],
204
271
  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.',
272
+ notes:
273
+ '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
274
  },
207
275
  {
208
276
  name: 'create',
@@ -242,7 +310,8 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
242
310
  transport: 'POST JSON body',
243
311
  argsSchema: ['data', 'skipDuplicates'],
244
312
  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).',
313
+ notes:
314
+ 'data is an array of scalar-only inputs. Nested relation writes are not supported. skipDuplicates silently ignores conflicts (not supported on all providers).',
246
315
  },
247
316
  {
248
317
  name: 'createManyAndReturn',
@@ -262,7 +331,8 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
262
331
  transport: 'POST JSON body',
263
332
  argsSchema: ['data', 'skipDuplicates', 'select', 'include', 'omit'],
264
333
  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.',
334
+ notes:
335
+ '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
336
  },
267
337
  {
268
338
  name: 'update',
@@ -282,7 +352,8 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
282
352
  transport: 'PUT JSON body',
283
353
  argsSchema: ['where', 'data', 'select', 'include', 'omit'],
284
354
  requiredArgs: ['where', 'data'],
285
- notes: '404 when the record to update is not found. 409 on unique constraint violation or transaction conflict.',
355
+ notes:
356
+ '404 when the record to update is not found. 409 on unique constraint violation or transaction conflict.',
286
357
  },
287
358
  {
288
359
  name: 'updateMany',
@@ -302,7 +373,8 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
302
373
  transport: 'PUT JSON body',
303
374
  argsSchema: ['where', 'data'],
304
375
  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.',
376
+ notes:
377
+ 'Updates all matching records with scalar-only data. Nested relation writes are not supported. Returns count, not records. 409 on unique constraint violation.',
306
378
  },
307
379
  {
308
380
  name: 'updateManyAndReturn',
@@ -322,7 +394,8 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
322
394
  transport: 'PUT JSON body',
323
395
  argsSchema: ['where', 'data', 'select', 'include', 'omit'],
324
396
  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.',
397
+ notes:
398
+ '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
399
  },
327
400
  {
328
401
  name: 'upsert',
@@ -398,11 +471,13 @@ export const OPERATION_METADATA: readonly OpMetadata[] = [
398
471
  excludeFromEnableAll: true,
399
472
  writeStrategyAffected: false,
400
473
  errors: [400, 403, 409, 500, 501, 503],
401
- responseDesc: 'Non-atomic: per-row { status, data } / { status, error } array. Atomic: array of records.',
474
+ responseDesc:
475
+ 'Non-atomic: per-row { status, data } / { status, error } array. Atomic: array of records.',
402
476
  transport: 'POST JSON body (array)',
403
477
  argsSchema: [],
404
478
  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.',
479
+ notes:
480
+ '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
481
  },
407
482
  ]
408
483
 
@@ -410,9 +485,12 @@ export const OPERATION_BY_NAME: Record<string, OpMetadata> = Object.fromEntries(
410
485
  OPERATION_METADATA.map((m) => [m.name, m]),
411
486
  )
412
487
 
413
- export const READ_OPERATIONS: readonly OpMetadata[] = OPERATION_METADATA.filter((m) => m.kind === 'read')
488
+ export const READ_OPERATIONS: readonly OpMetadata[] = OPERATION_METADATA.filter(
489
+ (m) => m.kind === 'read',
490
+ )
414
491
 
415
- export const WRITE_OPERATIONS: readonly OpMetadata[] = OPERATION_METADATA.filter((m) => m.kind === 'write' || m.kind === 'batch')
492
+ export const WRITE_OPERATIONS: readonly OpMetadata[] =
493
+ OPERATION_METADATA.filter((m) => m.kind === 'write' || m.kind === 'batch')
416
494
 
417
495
  export const READ_OPERATION_NAMES = new Set(READ_OPERATIONS.map((m) => m.name))
418
496
 
@@ -445,4 +523,4 @@ export function isOperationEnabled(
445
523
  if (config[def.configKey] === false) return false
446
524
  if (def.excludeFromEnableAll) return !!config[def.configKey]
447
525
  return !!(config.enableAll || config[def.configKey])
448
- }
526
+ }
@@ -1,3 +1,5 @@
1
+ import { resolveGuardShapeOnce } from './routeConfig'
2
+ import { assertGuard } from './guardHelpers'
1
3
  import { HttpError, LOG_PREFIX } from './errorMapper'
2
4
  import { sanitizeKeys, isPlainObject } from './misc'
3
5
  import type {
@@ -24,12 +26,15 @@ export type {
24
26
 
25
27
  export interface OperationContext {
26
28
  prisma: unknown
29
+ operationOverride?: RuntimeOperationOverride
30
+ resolveOperationContext?: () => unknown | Promise<unknown>
27
31
  postgres?: unknown
28
32
  sqlite?: unknown
29
33
  parsedQuery?: Record<string, unknown>
30
34
  body?: unknown
31
35
  guardShape?: Record<string, unknown>
32
36
  guardCaller?: string
37
+ guardVariantKey?: string
33
38
  paginationConfig?: PaginationConfig
34
39
  findManyPaginatedMode?: FindManyPaginatedMode
35
40
  }
@@ -60,7 +65,11 @@ export type PrismaClientLike = {
60
65
  $transaction?: <T>(fn: (tx: PrismaClientLike) => Promise<T>) => Promise<T>
61
66
  }
62
67
 
63
- type SpeedExtensionFactory = (opts: { postgres?: unknown; sqlite?: unknown; debug?: boolean }) => unknown
68
+ type SpeedExtensionFactory = (opts: {
69
+ postgres?: unknown
70
+ sqlite?: unknown
71
+ debug?: boolean
72
+ }) => unknown
64
73
 
65
74
  let _speedExtension: SpeedExtensionFactory | null = null
66
75
 
@@ -82,10 +91,15 @@ const _prismasqlReady = (async () => {
82
91
 
83
92
  const _extendedClients = new WeakMap<object, WeakMap<object, unknown>>()
84
93
 
85
- export async function getExtendedClient(ctx: OperationContext): Promise<unknown> {
94
+ export async function getExtendedClient(
95
+ ctx: OperationContext,
96
+ ): Promise<unknown> {
86
97
  const base = ctx.prisma as PrismaClientLike | null | undefined
87
98
  if (!base) {
88
- throw new HttpError(500, 'PrismaClient not found on request. Set req.prisma in middleware.')
99
+ throw new HttpError(
100
+ 500,
101
+ 'PrismaClient not found on request. Set req.prisma in middleware.',
102
+ )
89
103
  }
90
104
  await _prismasqlReady
91
105
  if (!_speedExtension) return base
@@ -98,11 +112,13 @@ export async function getExtendedClient(ctx: OperationContext): Promise<unknown>
98
112
  }
99
113
  try {
100
114
  if (typeof base.$extends !== 'function') return base
101
- const extended = base.$extends(_speedExtension({
102
- postgres: ctx.postgres,
103
- sqlite: ctx.sqlite,
104
- debug: process.env.DEBUG === 'true',
105
- }))
115
+ const extended = base.$extends(
116
+ _speedExtension({
117
+ postgres: ctx.postgres,
118
+ sqlite: ctx.sqlite,
119
+ debug: process.env.DEBUG === 'true',
120
+ }),
121
+ )
106
122
  let map = _extendedClients.get(connector)
107
123
  if (!map) {
108
124
  map = new WeakMap<object, unknown>()
@@ -111,7 +127,10 @@ export async function getExtendedClient(ctx: OperationContext): Promise<unknown>
111
127
  map.set(base as unknown as object, extended)
112
128
  return extended
113
129
  } catch (error) {
114
- console.warn('[speedExtension] Failed to initialize, using base client:', error)
130
+ console.warn(
131
+ '[speedExtension] Failed to initialize, using base client:',
132
+ error,
133
+ )
115
134
  return base
116
135
  }
117
136
  }
@@ -134,7 +153,10 @@ export function validateBody(body: unknown): Record<string, unknown> {
134
153
  return sanitizeKeys(body as Record<string, unknown>)
135
154
  }
136
155
 
137
- export function requireBodyField(body: Record<string, unknown>, field: string): void {
156
+ export function requireBodyField(
157
+ body: Record<string, unknown>,
158
+ field: string,
159
+ ): void {
138
160
  if (!(field in body) || body[field] === undefined) {
139
161
  throw new HttpError(400, 'Missing required field: ' + field)
140
162
  }
@@ -143,7 +165,8 @@ export function requireBodyField(body: Record<string, unknown>, field: string):
143
165
  export function transformResult(value: unknown): unknown {
144
166
  if (value === null || value === undefined) return value
145
167
  if (typeof value === 'bigint') return value.toString()
146
- if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value)) return value.toString('base64')
168
+ if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value))
169
+ return value.toString('base64')
147
170
  if (typeof Buffer !== 'undefined' && value instanceof Uint8Array) {
148
171
  return Buffer.from(value).toString('base64')
149
172
  }
@@ -169,4 +192,69 @@ export function transformResult(value: unknown): unknown {
169
192
  return changed ? out : value
170
193
  }
171
194
  return value
172
- }
195
+ }
196
+
197
+ export const OPERATION_OVERRIDE_VERSION = 1
198
+
199
+ export type OperationOverride<TInput, TResult, TContext, TPrisma> = {
200
+ invoke(options: {
201
+ input: TInput
202
+ args: TInput
203
+ context: TContext
204
+ prisma: TPrisma
205
+ core: () => Promise<TResult>
206
+ }): TResult | Promise<TResult>
207
+ }['invoke']
208
+
209
+ export type RuntimeOperationOverride = OperationOverride<
210
+ Record<string, unknown>,
211
+ unknown,
212
+ unknown,
213
+ Readonly<
214
+ Record<
215
+ string,
216
+ Readonly<Record<string, (...args: never[]) => Promise<unknown>>>
217
+ >
218
+ >
219
+ >
220
+
221
+ export async function executeOperationOverride<TResult>(
222
+ ctx: OperationContext,
223
+ model: string,
224
+ operation: keyof Omit<PrismaDelegate, 'guard'> | 'findManyPaginated',
225
+ input: Record<string, unknown>,
226
+ core: () => Promise<TResult>,
227
+ ): Promise<TResult> {
228
+ if (!ctx.operationOverride) return core()
229
+ if (!ctx.guardShape)
230
+ throw new HttpError(500, 'An operation override requires a guard shape')
231
+ const context = await ctx.resolveOperationContext?.()
232
+ const resolved = await resolveGuardShapeOnce(
233
+ ctx.guardShape,
234
+ ctx.guardVariantKey ?? ctx.guardCaller,
235
+ () => context,
236
+ )
237
+ if (!resolved.ok || !isPlainObject(resolved.shape)) {
238
+ throw new HttpError(
239
+ 500,
240
+ 'An operation override requires a resolved guard shape',
241
+ )
242
+ }
243
+ ctx.guardShape = resolved.shape
244
+ const delegate = getDelegate(await getExtendedClient(ctx), model)
245
+ assertGuard(delegate)
246
+ const guarded = delegate.guard(resolved.shape, ctx.guardCaller)
247
+ const methods =
248
+ operation === 'findManyPaginated'
249
+ ? (['findMany', 'count'] as const)
250
+ : [operation]
251
+ const exposed: Record<string, (...args: never[]) => Promise<unknown>> = {}
252
+ for (const method of methods) exposed[method] = guarded[method].bind(guarded)
253
+ return (await ctx.operationOverride({
254
+ input,
255
+ args: input,
256
+ context,
257
+ prisma: Object.freeze({ [model]: Object.freeze(exposed) }),
258
+ core,
259
+ })) as TResult
260
+ }