prisma-generator-express 1.65.2 → 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.
- package/README.md +51 -3480
- package/dist/client/encodeQueryParams.js +3 -1
- package/dist/client/encodeQueryParams.js.map +1 -1
- package/dist/copy/misc.d.ts +1 -0
- package/dist/copy/misc.js +14 -0
- package/dist/copy/misc.js.map +1 -1
- package/dist/copy/operationDefinitions.js +69 -6
- package/dist/copy/operationDefinitions.js.map +1 -1
- package/dist/generators/generateFastifyHandler.d.ts +2 -2
- package/dist/generators/generateFastifyHandler.js +15 -5
- package/dist/generators/generateFastifyHandler.js.map +1 -1
- package/dist/generators/generateHonoHandler.d.ts +2 -2
- package/dist/generators/generateHonoHandler.js +12 -5
- package/dist/generators/generateHonoHandler.js.map +1 -1
- package/dist/generators/generateImportPrismaStatement.d.ts +1 -0
- package/dist/generators/generateImportPrismaStatement.js +6 -0
- package/dist/generators/generateImportPrismaStatement.js.map +1 -1
- package/dist/generators/generateModelMetadata.js +20 -10
- package/dist/generators/generateModelMetadata.js.map +1 -1
- package/dist/generators/generateOperationCore.d.ts +3 -3
- package/dist/generators/generateOperationCore.js +51 -16
- package/dist/generators/generateOperationCore.js.map +1 -1
- package/dist/generators/generateQueryBuilderHelper.d.ts +1 -1
- package/dist/generators/generateQueryBuilderHelper.js +2 -4
- package/dist/generators/generateQueryBuilderHelper.js.map +1 -1
- package/dist/generators/generateRelationMeta.js +2 -2
- package/dist/generators/generateRelationMeta.js.map +1 -1
- package/dist/generators/generateRouteConfigType.d.ts +2 -2
- package/dist/generators/generateRouteConfigType.js +29 -11
- package/dist/generators/generateRouteConfigType.js.map +1 -1
- package/dist/generators/generateRouter.d.ts +7 -4
- package/dist/generators/generateRouter.js +107 -14
- package/dist/generators/generateRouter.js.map +1 -1
- package/dist/generators/generateRouterFastify.d.ts +7 -4
- package/dist/generators/generateRouterFastify.js +56 -28
- package/dist/generators/generateRouterFastify.js.map +1 -1
- package/dist/generators/generateRouterHono.d.ts +7 -4
- package/dist/generators/generateRouterHono.js +84 -53
- package/dist/generators/generateRouterHono.js.map +1 -1
- package/dist/generators/generateUnifiedDocs.d.ts +1 -1
- package/dist/generators/generateUnifiedDocs.js +15 -8
- package/dist/generators/generateUnifiedDocs.js.map +1 -1
- package/dist/generators/generateUnifiedHandler.d.ts +2 -2
- package/dist/generators/generateUnifiedHandler.js +11 -3
- package/dist/generators/generateUnifiedHandler.js.map +1 -1
- package/dist/guardOptions.js +12 -7
- package/dist/guardOptions.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +22 -1
- package/dist/index.js.map +1 -1
- package/dist/utils/copyFiles.js +22 -6
- package/dist/utils/copyFiles.js.map +1 -1
- package/dist/utils/importExt.d.ts +1 -1
- package/dist/utils/pathCasing.d.ts +4 -0
- package/dist/utils/pathCasing.js +40 -0
- package/dist/utils/pathCasing.js.map +1 -0
- package/dist/utils/resolveImportStyle.js +13 -5
- package/dist/utils/resolveImportStyle.js.map +1 -1
- package/dist/utils/writeFileSafely.js.map +1 -1
- package/package.json +16 -3
- package/src/bin.ts +1 -1
- package/src/client/encodeQueryParams.ts +39 -35
- package/src/constants.ts +1 -1
- package/src/copy/autoIncludePlanner.ts +93 -27
- package/src/copy/autoIncludePlannerGuarded.ts +171 -48
- package/src/copy/autoIncludeRuntimeGuarded.ts +25 -7
- package/src/copy/buildModelOpenApi.ts +153 -50
- package/src/copy/concurrency.ts +10 -8
- package/src/copy/errorMapper.ts +68 -19
- package/src/copy/guardHelpers.ts +30 -9
- package/src/copy/materializedCount.ts +17 -5
- package/src/copy/misc.ts +88 -68
- package/src/copy/ndjson.ts +72 -0
- package/src/copy/operationDefinitions.ts +99 -21
- package/src/copy/operationRuntime.ts +100 -12
- package/src/copy/pagination.ts +25 -11
- package/src/copy/parseQueryParams.ts +47 -48
- package/src/copy/projectionDefaults.ts +6 -4
- package/src/copy/routeConfig.express.ts +16 -4
- package/src/copy/routeConfig.fastify.ts +16 -2
- package/src/copy/routeConfig.hono.ts +19 -9
- package/src/copy/routeConfig.ts +174 -39
- package/src/copy/scalarTypes.ts +7 -2
- package/src/copy/sse.ts +72 -19
- package/src/generators/generateFastifyHandler.ts +24 -13
- package/src/generators/generateHonoHandler.ts +21 -13
- package/src/generators/generateImportPrismaStatement.ts +93 -81
- package/src/generators/generateModelMetadata.ts +24 -12
- package/src/generators/generateOperationCore.ts +81 -33
- package/src/generators/generateQueryBuilderHelper.ts +20 -4
- package/src/generators/generateRelationMeta.ts +23 -11
- package/src/generators/generateRouteConfigType.ts +39 -13
- package/src/generators/generateRouter.ts +114 -17
- package/src/generators/generateRouterFastify.ts +67 -31
- package/src/generators/generateRouterHono.ts +108 -60
- package/src/generators/generateUnifiedDocs.ts +29 -8
- package/src/generators/generateUnifiedHandler.ts +16 -8
- package/src/generators/generateUnifiedScalarUI.ts +1 -1
- package/src/guardOptions.ts +105 -92
- package/src/index.ts +46 -4
- package/src/utils/copyFiles.ts +199 -168
- package/src/utils/importExt.ts +2 -2
- package/src/utils/pathCasing.ts +42 -0
- package/src/utils/resolveImportStyle.ts +37 -15
- package/src/utils/strings.ts +2 -2
- package/src/utils/writeFileSafely.ts +6 -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(
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (!
|
|
54
|
-
|
|
55
|
-
if (result
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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: [
|
|
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: [
|
|
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: [
|
|
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: [
|
|
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:
|
|
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:
|
|
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:
|
|
223
|
+
responseDesc:
|
|
224
|
+
'Object with requested aggregate fields (_count, _avg, _sum, _min, _max)',
|
|
182
225
|
transport: 'GET query params',
|
|
183
|
-
argsSchema: [
|
|
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:
|
|
255
|
+
responseDesc:
|
|
256
|
+
'Array of objects, each with grouped field values and requested aggregates',
|
|
202
257
|
transport: 'GET query params',
|
|
203
|
-
argsSchema: [
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
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[] =
|
|
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: {
|
|
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(
|
|
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(
|
|
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(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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(
|
|
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(
|
|
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))
|
|
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
|
+
}
|