prisma-generator-express 1.64.1 → 1.64.2

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.
@@ -1,362 +1,573 @@
1
- import { GUARD_SHAPE_CONFIG_KEYS } from './guardHelpers'
2
- import { isPlainObject } from './misc'
3
- import {
4
- resolveGuardVariantKey,
5
- type GuardVariantResolution,
6
- } from './guardVariantRouting'
7
-
8
- export interface QueryBuilderConfig {
9
- enabled?: boolean
10
- port?: number
11
- host?: string
12
- schemaPath?: string
13
- databaseUrl?: string
14
- }
15
-
16
- export interface OpenApiServerConfig {
17
- url: string
18
- description?: string
19
- }
20
-
21
- export interface OpenApiSecuritySchemeConfig {
22
- type: string
23
- scheme?: string
24
- bearerFormat?: string
25
- name?: string
26
- in?: string
27
- description?: string
28
- }
29
-
30
- export type WriteStrategy = 'regular' | 'throwOnNonReturning' | 'forceReturn'
31
-
32
- export type FindManyPaginatedMode = 'transaction' | 'promiseAll'
33
-
34
- export type PaginationCountSource =
35
- | { type?: 'delegate' }
36
- | {
37
- type: 'materializedView'
38
- relation: string
39
- schema?: string
40
- column?: string
41
- where?: Record<string, unknown>
42
- }
43
-
44
- export interface PaginationConfig {
45
- defaultLimit?: number
46
- maxLimit?: number
47
- distinctCountLimit?: number
48
- countSource?: PaginationCountSource
49
- }
50
-
51
- export type ProgressivePatch = {
52
- key: string
53
- value: unknown
54
- }
55
-
56
- export type ProgressiveStopResult<T = unknown> = {
57
- stop: true
58
- data: T
59
- }
60
-
61
- export type ProgressiveStageResult<T = unknown> =
62
- | void
63
- | ProgressivePatch
64
- | ProgressivePatch[]
65
- | ProgressiveStopResult<T>
66
-
67
- export type ProgressiveStageContext<TContext = unknown, TPrisma = any> = {
68
- ctx: TContext
69
- req: unknown
70
- res: unknown
71
- prisma: TPrisma
72
- variant: string
73
- accumulated: Record<string, unknown>
74
- signal: AbortSignal
75
- }
76
-
77
- export type ProgressiveStage<TContext = unknown, TPrisma = any, T = unknown> = (
78
- context: ProgressiveStageContext<TContext, TPrisma>,
79
- ) => Promise<ProgressiveStageResult<T>>
80
-
81
- export type ManualProgressiveVariantConfig = {
82
- enabled?: boolean
83
- mode?: 'manual'
84
- stages: string[]
85
- }
86
-
87
- export type AutoIncludeProgressiveVariantConfig = {
88
- enabled?: boolean
89
- mode: 'autoInclude'
90
- fallback?: 'singleResult' | 'error'
91
- }
92
-
93
- export type ProgressiveVariantConfig =
94
- | ManualProgressiveVariantConfig
95
- | AutoIncludeProgressiveVariantConfig
96
-
97
- export type VariantEntry<TShape, TBefore, TAfter> = {
98
- shape: TShape
99
- before?: TBefore[]
100
- after?: TAfter[]
101
- }
102
-
103
- type OperationShapeConfig<TShape, TBefore, TAfter> =
104
- | {
105
- shape?: TShape
106
- variants?: never
107
- }
108
- | {
109
- shape?: never
110
- variants: Record<string, VariantEntry<TShape, TBefore, TAfter>>
111
- }
112
-
113
- export type BaseOperationConfig<
114
- TBefore,
115
- TShape = Record<string, unknown>,
116
- TAfter = TBefore,
117
- > = OperationShapeConfig<TShape, TBefore, TAfter> & {
118
- before?: TBefore[]
119
- after?: TAfter[]
120
- pagination?: Partial<PaginationConfig>
121
- }
122
-
123
- export interface BaseUpdateEachConfig<TBefore, TAfter = TBefore> {
124
- before?: TBefore[]
125
- after?: TAfter[]
126
- }
127
-
128
- export interface BaseRouteConfig<
129
- TBefore,
130
- RequestType,
131
- TShape = Record<string, unknown>,
132
- TCtx = unknown,
133
- TAfter = TBefore,
134
- > {
135
- enableAll?: boolean
136
- addModelPrefix?: boolean
137
- customUrlPrefix?: string
138
- specBasePath?: string
139
- disableOpenApi?: boolean
140
- disablePostReads?: boolean
141
- scalarCdnUrl?: string
142
- openApiTitle?: string
143
- openApiDescription?: string
144
- openApiVersion?: string
145
- openApiServers?: OpenApiServerConfig[]
146
- openApiSecuritySchemes?: Record<string, OpenApiSecuritySchemeConfig>
147
- openApiSecurity?: Record<string, string[]>[]
148
- guard?: {
149
- resolveVariant?: (request: RequestType) => string | undefined
150
- variantHeader?: string
151
- }
152
- resolveContext?: (request: RequestType) => TCtx | Promise<TCtx>
153
- queryBuilder?: QueryBuilderConfig | false
154
- pagination?: PaginationConfig
155
- findUnique?: BaseOperationConfig<TBefore, TShape, TAfter> | false
156
- findUniqueOrThrow?: BaseOperationConfig<TBefore, TShape, TAfter> | false
157
- findFirst?: BaseOperationConfig<TBefore, TShape, TAfter> | false
158
- findFirstOrThrow?: BaseOperationConfig<TBefore, TShape, TAfter> | false
159
- findMany?: BaseOperationConfig<TBefore, TShape, TAfter> | false
160
- findManyPaginated?: BaseOperationConfig<TBefore, TShape, TAfter> | false
161
- create?: BaseOperationConfig<TBefore, TShape, TAfter> | false
162
- createMany?: BaseOperationConfig<TBefore, TShape, TAfter> | false
163
- createManyAndReturn?: BaseOperationConfig<TBefore, TShape, TAfter> | false
164
- update?: BaseOperationConfig<TBefore, TShape, TAfter> | false
165
- updateMany?: BaseOperationConfig<TBefore, TShape, TAfter> | false
166
- updateManyAndReturn?: BaseOperationConfig<TBefore, TShape, TAfter> | false
167
- upsert?: BaseOperationConfig<TBefore, TShape, TAfter> | false
168
- delete?: BaseOperationConfig<TBefore, TShape, TAfter> | false
169
- deleteMany?: BaseOperationConfig<TBefore, TShape, TAfter> | false
170
- updateEach?: BaseUpdateEachConfig<TBefore, TAfter>
171
- aggregate?: BaseOperationConfig<TBefore, TShape, TAfter> | false
172
- count?: BaseOperationConfig<TBefore, TShape, TAfter> | false
173
- groupBy?: BaseOperationConfig<TBefore, TShape, TAfter> | false
174
- }
175
-
176
- export type OperationConfig = BaseOperationConfig<unknown>
177
- export type RouteConfig = BaseRouteConfig<unknown, unknown>
178
-
179
- export type NormalizedGuardRouting =
180
- | { kind: 'none' }
181
- | { kind: 'single' }
182
- | { kind: 'named'; keys: readonly string[] }
183
-
184
- export type NormalizedVariantHooks<TBefore, TAfter> = Readonly<
185
- Record<
186
- string,
187
- {
188
- before: readonly TBefore[]
189
- after: readonly TAfter[]
190
- }
191
- >
192
- >
193
-
194
- export interface NormalizedOperationConfig<TBefore, TAfter> {
195
- guardShape?: Record<string, unknown>
196
- guardRouting: NormalizedGuardRouting
197
- operationBefore: readonly TBefore[]
198
- operationAfter: readonly TAfter[]
199
- variantHooks: NormalizedVariantHooks<TBefore, TAfter>
200
- pagination?: Readonly<Partial<PaginationConfig>>
201
- }
202
-
203
- type OperationConfigInput<TBefore, TAfter> = {
204
- before?: TBefore[]
205
- after?: TAfter[]
206
- shape?: unknown
207
- variants?: Record<
208
- string,
209
- {
210
- shape?: unknown
211
- before?: TBefore[]
212
- after?: TAfter[]
213
- }
214
- >
215
- pagination?: Partial<PaginationConfig>
216
- }
217
-
218
- function classifyGuardRouting(shape: unknown): NormalizedGuardRouting {
219
- if (shape === undefined) return { kind: 'none' }
220
- if (typeof shape === 'function') return { kind: 'single' }
221
- if (!isPlainObject(shape)) return { kind: 'single' }
222
-
223
- const keys = Object.keys(shape)
224
- const isSingle =
225
- keys.length === 0 ||
226
- keys.every((key) => GUARD_SHAPE_CONFIG_KEYS.has(key))
227
-
228
- return isSingle ? { kind: 'single' } : { kind: 'named', keys }
229
- }
230
-
231
- export function validateOperationConfig(
232
- config: { shape?: unknown; variants?: unknown } | undefined,
233
- location: string,
234
- ): void {
235
- if (!config) return
236
-
237
- const hasShape = config.shape !== undefined
238
- const hasVariants = config.variants !== undefined
239
-
240
- if (hasShape && hasVariants) {
241
- throw new Error(location + ': shape and variants cannot both be defined')
242
- }
243
-
244
- if (!hasVariants) return
245
-
246
- const variants = config.variants
247
- if (!isPlainObject(variants)) {
248
- throw new Error(location + ': variants must be a non-array object')
249
- }
250
-
251
- const entries = Object.entries(variants)
252
- if (entries.length === 0) {
253
- throw new Error(location + ': variants must contain at least one entry')
254
- }
255
-
256
- for (const [key, rawEntry] of entries) {
257
- if (GUARD_SHAPE_CONFIG_KEYS.has(key)) {
258
- throw new Error(
259
- location + ': variant name "' + key +
260
- '" collides with a reserved guard shape key',
261
- )
262
- }
263
-
264
- if (!isPlainObject(rawEntry)) {
265
- throw new Error(
266
- location + ': variant "' + key + '" must be an object with a shape',
267
- )
268
- }
269
-
270
- if (rawEntry.shape === undefined) {
271
- throw new Error(
272
- location + ': variant "' + key + '" is missing "shape"',
273
- )
274
- }
275
- }
276
- }
277
-
278
- export function validateUpdateEachConfig(
279
- config: { variants?: unknown } | undefined,
280
- location: string,
281
- ): void {
282
- if (config?.variants !== undefined) {
283
- throw new Error(location + ': updateEach does not support variants')
284
- }
285
- }
286
-
287
- export function normalizeOperation<TBefore, TAfter>(
288
- config: OperationConfigInput<TBefore, TAfter> | undefined,
289
- ): NormalizedOperationConfig<TBefore, TAfter> {
290
- const operationBefore = config?.before ?? []
291
- const operationAfter = config?.after ?? []
292
-
293
- if (config?.variants !== undefined) {
294
- const entries = Object.entries(config.variants)
295
-
296
- return {
297
- guardShape: Object.fromEntries(
298
- entries.map(([key, entry]) => [key, entry.shape]),
299
- ),
300
- guardRouting: {
301
- kind: 'named',
302
- keys: entries.map(([key]) => key),
303
- },
304
- operationBefore,
305
- operationAfter,
306
- variantHooks: Object.fromEntries(
307
- entries.map(([key, entry]) => [
308
- key,
309
- {
310
- before: entry.before ?? [],
311
- after: entry.after ?? [],
312
- },
313
- ]),
314
- ),
315
- pagination: config.pagination,
316
- }
317
- }
318
-
319
- return {
320
- guardShape: config?.shape as Record<string, unknown> | undefined,
321
- guardRouting: classifyGuardRouting(config?.shape),
322
- operationBefore,
323
- operationAfter,
324
- variantHooks: {},
325
- pagination: config?.pagination,
326
- }
327
- }
328
-
329
- export function resolveOperationVariantKey(
330
- routing: NormalizedGuardRouting,
331
- caller: string | undefined,
332
- ): GuardVariantResolution {
333
- if (routing.kind === 'none' || routing.kind === 'single') {
334
- return resolveGuardVariantKey({ kind: 'single' })
335
- }
336
-
337
- return resolveGuardVariantKey({
338
- kind: 'named',
339
- keys: routing.keys,
340
- caller,
341
- reservedKeys: GUARD_SHAPE_CONFIG_KEYS,
342
- })
343
- }
344
-
345
- export function validateCountSourceWhere(
346
- cs: PaginationCountSource | undefined,
347
- location: string,
348
- ): void {
349
- if (!cs) return
350
- if ((cs as { type?: string }).type !== 'materializedView') return
351
- const where = (cs as { where?: Record<string, unknown> }).where
352
- if (!where) return
353
- for (const [key, value] of Object.entries(where)) {
354
- if (value === null) continue
355
- if (typeof value === 'object') {
356
- throw new Error(
357
- location + ': countSource.where["' + key + '"] must be scalar or null; got ' +
358
- (Array.isArray(value) ? 'array' : 'object'),
359
- )
360
- }
361
- }
1
+ import { GUARD_SHAPE_CONFIG_KEYS } from './guardHelpers'
2
+ import { isPlainObject } from './misc'
3
+ import { resolveShape, type ContextResolver } from './projectionDefaults'
4
+ import {
5
+ resolveGuardVariantKey,
6
+ type GuardVariantResolution,
7
+ } from './guardVariantRouting'
8
+
9
+ export interface QueryBuilderConfig {
10
+ enabled?: boolean
11
+ port?: number
12
+ host?: string
13
+ schemaPath?: string
14
+ databaseUrl?: string
15
+ }
16
+
17
+ export interface OpenApiServerConfig {
18
+ url: string
19
+ description?: string
20
+ }
21
+
22
+ export interface OpenApiSecuritySchemeConfig {
23
+ type: string
24
+ scheme?: string
25
+ bearerFormat?: string
26
+ name?: string
27
+ in?: string
28
+ description?: string
29
+ }
30
+
31
+ export type WriteStrategy = 'regular' | 'throwOnNonReturning' | 'forceReturn'
32
+
33
+ export type FindManyPaginatedMode = 'transaction' | 'promiseAll'
34
+
35
+ export type PaginationCountSource =
36
+ | { type?: 'delegate' }
37
+ | {
38
+ type: 'materializedView'
39
+ relation: string
40
+ schema?: string
41
+ column?: string
42
+ where?: Record<string, unknown>
43
+ }
44
+
45
+ export interface PaginationConfig {
46
+ defaultLimit?: number
47
+ maxLimit?: number
48
+ distinctCountLimit?: number
49
+ countSource?: PaginationCountSource
50
+ }
51
+
52
+ export type ProgressivePatch = {
53
+ key: string
54
+ value: unknown
55
+ }
56
+
57
+ export type ProgressiveStopResult<T = unknown> = {
58
+ stop: true
59
+ data: T
60
+ }
61
+
62
+ export type ProgressiveStageResult<T = unknown> =
63
+ | void
64
+ | ProgressivePatch
65
+ | ProgressivePatch[]
66
+ | ProgressiveStopResult<T>
67
+
68
+ export type ProgressiveStageContext<TContext = unknown, TPrisma = any> = {
69
+ ctx: TContext
70
+ req: unknown
71
+ res: unknown
72
+ prisma: TPrisma
73
+ variant: string
74
+ accumulated: Record<string, unknown>
75
+ signal: AbortSignal
76
+ }
77
+
78
+ export type ProgressiveStage<TContext = unknown, TPrisma = any, T = unknown> = (
79
+ context: ProgressiveStageContext<TContext, TPrisma>,
80
+ ) => Promise<ProgressiveStageResult<T>>
81
+
82
+ export type ManualProgressiveVariantConfig = {
83
+ enabled?: boolean
84
+ mode?: 'manual'
85
+ stages: string[]
86
+ }
87
+
88
+ export type AutoIncludeProgressiveVariantConfig = {
89
+ enabled?: boolean
90
+ mode: 'autoInclude'
91
+ fallback?: 'singleResult' | 'error'
92
+ }
93
+
94
+ export type ProgressiveVariantConfig =
95
+ | ManualProgressiveVariantConfig
96
+ | AutoIncludeProgressiveVariantConfig
97
+
98
+ export type VariantEntry<TShape, TBefore, TAfter> = {
99
+ shape: TShape
100
+ before?: TBefore[]
101
+ after?: TAfter[]
102
+ }
103
+
104
+ type OperationShapeConfig<TShape, TBefore, TAfter> =
105
+ | {
106
+ shape?: TShape
107
+ variants?: never
108
+ }
109
+ | {
110
+ shape?: never
111
+ variants: Record<string, VariantEntry<TShape, TBefore, TAfter>>
112
+ }
113
+
114
+ export type BaseOperationConfig<
115
+ TBefore,
116
+ TShape = Record<string, unknown>,
117
+ TAfter = TBefore,
118
+ > = OperationShapeConfig<TShape, TBefore, TAfter> & {
119
+ before?: TBefore[]
120
+ after?: TAfter[]
121
+ pagination?: Partial<PaginationConfig>
122
+ }
123
+
124
+ export interface BaseUpdateEachConfig<TBefore, TAfter = TBefore> {
125
+ before?: TBefore[]
126
+ after?: TAfter[]
127
+ }
128
+
129
+ export interface BaseRouteConfig<
130
+ TBefore,
131
+ RequestType,
132
+ TShape = Record<string, unknown>,
133
+ TCtx = unknown,
134
+ TAfter = TBefore,
135
+ > {
136
+ enableAll?: boolean
137
+ addModelPrefix?: boolean
138
+ customUrlPrefix?: string
139
+ specBasePath?: string
140
+ disableOpenApi?: boolean
141
+ disablePostReads?: boolean
142
+ scalarCdnUrl?: string
143
+ openApiTitle?: string
144
+ openApiDescription?: string
145
+ openApiVersion?: string
146
+ openApiServers?: OpenApiServerConfig[]
147
+ openApiSecuritySchemes?: Record<string, OpenApiSecuritySchemeConfig>
148
+ openApiSecurity?: Record<string, string[]>[]
149
+ guard?: {
150
+ resolveVariant?: (request: RequestType) => string | undefined
151
+ variantHeader?: string
152
+ }
153
+ resolveContext?: (request: RequestType) => TCtx | Promise<TCtx>
154
+ queryBuilder?: QueryBuilderConfig | false
155
+ pagination?: PaginationConfig
156
+ findUnique?: BaseOperationConfig<TBefore, TShape, TAfter> | false
157
+ findUniqueOrThrow?: BaseOperationConfig<TBefore, TShape, TAfter> | false
158
+ findFirst?: BaseOperationConfig<TBefore, TShape, TAfter> | false
159
+ findFirstOrThrow?: BaseOperationConfig<TBefore, TShape, TAfter> | false
160
+ findMany?: BaseOperationConfig<TBefore, TShape, TAfter> | false
161
+ findManyPaginated?: BaseOperationConfig<TBefore, TShape, TAfter> | false
162
+ create?: BaseOperationConfig<TBefore, TShape, TAfter> | false
163
+ createMany?: BaseOperationConfig<TBefore, TShape, TAfter> | false
164
+ createManyAndReturn?: BaseOperationConfig<TBefore, TShape, TAfter> | false
165
+ update?: BaseOperationConfig<TBefore, TShape, TAfter> | false
166
+ updateMany?: BaseOperationConfig<TBefore, TShape, TAfter> | false
167
+ updateManyAndReturn?: BaseOperationConfig<TBefore, TShape, TAfter> | false
168
+ upsert?: BaseOperationConfig<TBefore, TShape, TAfter> | false
169
+ delete?: BaseOperationConfig<TBefore, TShape, TAfter> | false
170
+ deleteMany?: BaseOperationConfig<TBefore, TShape, TAfter> | false
171
+ updateEach?: BaseUpdateEachConfig<TBefore, TAfter>
172
+ aggregate?: BaseOperationConfig<TBefore, TShape, TAfter> | false
173
+ count?: BaseOperationConfig<TBefore, TShape, TAfter> | false
174
+ groupBy?: BaseOperationConfig<TBefore, TShape, TAfter> | false
175
+ }
176
+
177
+ export type OperationConfig = BaseOperationConfig<unknown>
178
+ export type RouteConfig = BaseRouteConfig<unknown, unknown>
179
+
180
+ export type NormalizedGuardRouting =
181
+ | { kind: 'none' }
182
+ | { kind: 'single' }
183
+ | { kind: 'named'; keys: readonly string[] }
184
+
185
+ export type NormalizedVariantHooks<TBefore, TAfter> = Readonly<
186
+ Record<
187
+ string,
188
+ {
189
+ before: readonly TBefore[]
190
+ after: readonly TAfter[]
191
+ }
192
+ >
193
+ >
194
+
195
+ export interface NormalizedOperationConfig<TBefore, TAfter> {
196
+ guardShape?: Record<string, unknown>
197
+ guardRouting: NormalizedGuardRouting
198
+ operationBefore: readonly TBefore[]
199
+ operationAfter: readonly TAfter[]
200
+ variantHooks: NormalizedVariantHooks<TBefore, TAfter>
201
+ pagination?: Readonly<Partial<PaginationConfig>>
202
+ }
203
+
204
+ type OperationConfigInput<TBefore, TAfter> = {
205
+ before?: TBefore[]
206
+ after?: TAfter[]
207
+ shape?: unknown
208
+ variants?: Record<
209
+ string,
210
+ {
211
+ shape?: unknown
212
+ before?: TBefore[]
213
+ after?: TAfter[]
214
+ }
215
+ >
216
+ pagination?: Partial<PaginationConfig>
217
+ }
218
+
219
+ function classifyGuardRouting(shape: unknown): NormalizedGuardRouting {
220
+ if (shape === undefined) return { kind: 'none' }
221
+ if (typeof shape === 'function') return { kind: 'single' }
222
+ if (!isPlainObject(shape)) return { kind: 'single' }
223
+
224
+ const keys = Object.keys(shape)
225
+ const isSingle =
226
+ keys.length === 0 ||
227
+ keys.every((key) => GUARD_SHAPE_CONFIG_KEYS.has(key))
228
+
229
+ return isSingle ? { kind: 'single' } : { kind: 'named', keys }
230
+ }
231
+
232
+ /**
233
+ * A legacy `shape` must be a shape, and must constrain something.
234
+ *
235
+ * Two shapes that look configured and are not:
236
+ *
237
+ * - `shape: {}` normalizes to a single shape with no constraints. It reads as
238
+ * "guarded" to a reviewer and lets everything through.
239
+ * - `shape: { where: …, admin: … }` mixes a reserved shape key with a
240
+ * non-reserved one. `classifyGuardRouting` decides by whether EVERY key is
241
+ * reserved, so one stray key silently reclassifies the whole operation as
242
+ * variant-routed — after which every request that does not name a variant
243
+ * gets a 400 for a reason no message explains. The commonest cause is a
244
+ * typo: `wheer`, `iclude`, `slect`.
245
+ *
246
+ * A function shape is opaque here and is accepted: it is resolved per request
247
+ * and there is nothing to inspect at construction time.
248
+ */
249
+ function validateShapeConfig(shape: unknown, location: string): void {
250
+ if (typeof shape === 'function') return
251
+
252
+ if (!isPlainObject(shape)) {
253
+ throw new Error(
254
+ location + ': shape must be an object or a function, got ' +
255
+ (shape === null ? 'null' : Array.isArray(shape) ? 'array' : typeof shape),
256
+ )
257
+ }
258
+
259
+ const keys = Object.keys(shape)
260
+ if (keys.length === 0) {
261
+ throw new Error(
262
+ location + ': shape is empty, so it constrains nothing. Give it at least ' +
263
+ 'one guard key, or use variants.',
264
+ )
265
+ }
266
+
267
+ const reserved = keys.filter((key) => GUARD_SHAPE_CONFIG_KEYS.has(key))
268
+ if (reserved.length > 0 && reserved.length !== keys.length) {
269
+ const stray = keys.filter((key) => !GUARD_SHAPE_CONFIG_KEYS.has(key))
270
+ throw new Error(
271
+ location + ': shape mixes guard keys (' + reserved.join(', ') + ') with ' +
272
+ 'non-guard keys (' + stray.join(', ') + '). A shape whose keys are not all ' +
273
+ 'guard keys is read as a VARIANT MAP, which is rarely intended — check for ' +
274
+ 'a typo, or split them into "variants".',
275
+ )
276
+ }
277
+ }
278
+
279
+ /**
280
+ * Every emitted operation must carry a guard, and it must be one that constrains
281
+ * something.
282
+ *
283
+ * An absent configuration used to return here silently, and the emitted handler
284
+ * applies its guard under `if (opConfig.guardShape)` — so an operation nobody
285
+ * configured reached Prisma with whatever `where`, `select` and `include` the
286
+ * request supplied. Nothing warned, and the normalized operation was
287
+ * structurally identical to a configured one apart from an absent shape, so
288
+ * there was nothing to detect it by either.
289
+ *
290
+ * The refusal happens where the configuration is first visible — router
291
+ * construction, which on a Worker is boot. A deployment that forgot a guard
292
+ * fails to start instead of serving unguarded routes, which is the trade this
293
+ * check exists to make.
294
+ */
295
+ export function validateOperationConfig(
296
+ config: { shape?: unknown; variants?: unknown; allowDefaultVariant?: unknown } | undefined,
297
+ location: string,
298
+ ): void {
299
+ if (!config) {
300
+ throw new Error(
301
+ location + ': no guard configured. Every generated operation must define ' +
302
+ '"shape" or "variants"; an unguarded operation would pass the caller\'s ' +
303
+ 'own where/select/include straight to Prisma.',
304
+ )
305
+ }
306
+
307
+ const hasShape = config.shape !== undefined
308
+ const hasVariants = config.variants !== undefined
309
+
310
+ if (hasShape && hasVariants) {
311
+ throw new Error(location + ': shape and variants cannot both be defined')
312
+ }
313
+
314
+ if (!hasShape && !hasVariants) {
315
+ throw new Error(
316
+ location + ': no guard configured. Define "shape" or "variants".',
317
+ )
318
+ }
319
+
320
+ if (hasShape) {
321
+ validateShapeConfig(config.shape, location)
322
+ return
323
+ }
324
+
325
+ const variants = config.variants
326
+ if (!isPlainObject(variants)) {
327
+ throw new Error(location + ': variants must be a non-array object')
328
+ }
329
+
330
+ const entries = Object.entries(variants)
331
+ if (entries.length === 0) {
332
+ throw new Error(location + ': variants must contain at least one entry')
333
+ }
334
+
335
+ /**
336
+ * A variant named `default` catches a missing, blank OR unknown caller, so it
337
+ * turns three fail-closed paths into a pass at once. That is legitimate when
338
+ * `default` is the most restrictive variant and dangerous when it is the most
339
+ * permissive, which is how the word reads — and nothing can tell which from
340
+ * the shape alone.
341
+ *
342
+ * So it must be asked for. `allowDefaultVariant: true` is a sentence the author
343
+ * writes on purpose; a key called `default` is one they may have typed without
344
+ * noticing what it catches.
345
+ */
346
+ if (Object.prototype.hasOwnProperty.call(variants, 'default')) {
347
+ if (config.allowDefaultVariant !== true) {
348
+ throw new Error(
349
+ location + ': a variant named "default" answers every unrecognised, ' +
350
+ 'blank and missing caller. Set allowDefaultVariant: true to confirm ' +
351
+ 'that is intended, and make sure it is the most restrictive variant.',
352
+ )
353
+ }
354
+ } else if (config.allowDefaultVariant !== undefined) {
355
+ throw new Error(
356
+ location + ': allowDefaultVariant is set but no "default" variant exists.',
357
+ )
358
+ }
359
+
360
+ for (const [key, rawEntry] of entries) {
361
+ if (GUARD_SHAPE_CONFIG_KEYS.has(key)) {
362
+ throw new Error(
363
+ location + ': variant name "' + key +
364
+ '" collides with a reserved guard shape key',
365
+ )
366
+ }
367
+
368
+ if (!isPlainObject(rawEntry)) {
369
+ throw new Error(
370
+ location + ': variant "' + key + '" must be an object with a shape',
371
+ )
372
+ }
373
+
374
+ if (rawEntry.shape === undefined) {
375
+ throw new Error(
376
+ location + ': variant "' + key + '" is missing "shape"',
377
+ )
378
+ }
379
+
380
+ /**
381
+ * The SAME validation the legacy `shape` gets. Without this,
382
+ * `variants: { public: { shape: {} } }` emitted a route that constrains
383
+ * nothing — the exact hole the top-level check closes, reachable through a
384
+ * different key. A guard is not more trustworthy for being written inside a
385
+ * variant.
386
+ */
387
+ validateShapeConfig(rawEntry.shape, location + ' variant "' + key + '"')
388
+ }
389
+ }
390
+
391
+ /**
392
+ * What is wrong with a shape that has just been RESOLVED, or `null` if nothing.
393
+ *
394
+ * A function shape is opaque at construction time, so `validateShapeConfig`
395
+ * accepts it and this is the second half of that bargain: whatever it returns is
396
+ * checked before it is used. A function returning `{}`, `undefined`, or a map
397
+ * with stray keys recreates the fail-open at request time, once per request,
398
+ * where no configuration review will ever see it.
399
+ *
400
+ * Returns a description rather than throwing, because the caller is a request
401
+ * handler: this is a 500 (the deployment is misconfigured), not an exception to
402
+ * propagate as-is.
403
+ */
404
+ export function describeResolvedGuardShape(resolved: unknown): string | null {
405
+ if (resolved === null || resolved === undefined) {
406
+ return 'the shape function returned nothing, so the operation would run unguarded'
407
+ }
408
+
409
+ if (!isPlainObject(resolved)) {
410
+ return 'the shape function returned ' +
411
+ (Array.isArray(resolved) ? 'an array' : typeof resolved) +
412
+ ', which is not a guard shape'
413
+ }
414
+
415
+ const keys = Object.keys(resolved)
416
+ if (keys.length === 0) {
417
+ return 'the shape function returned an empty object, which constrains nothing'
418
+ }
419
+
420
+ const stray = keys.filter((key) => !GUARD_SHAPE_CONFIG_KEYS.has(key))
421
+ if (stray.length > 0) {
422
+ return 'the shape function returned non-guard keys (' + stray.join(', ') + ')'
423
+ }
424
+
425
+ return null
426
+ }
427
+
428
+ export function validateUpdateEachConfig(
429
+ config: { variants?: unknown } | undefined,
430
+ location: string,
431
+ ): void {
432
+ if (config?.variants !== undefined) {
433
+ throw new Error(location + ': updateEach does not support variants')
434
+ }
435
+ }
436
+
437
+ export function normalizeOperation<TBefore, TAfter>(
438
+ config: OperationConfigInput<TBefore, TAfter> | undefined,
439
+ ): NormalizedOperationConfig<TBefore, TAfter> {
440
+ const operationBefore = config?.before ?? []
441
+ const operationAfter = config?.after ?? []
442
+
443
+ if (config?.variants !== undefined) {
444
+ const entries = Object.entries(config.variants)
445
+
446
+ return {
447
+ guardShape: Object.fromEntries(
448
+ entries.map(([key, entry]) => [key, entry.shape]),
449
+ ),
450
+ guardRouting: {
451
+ kind: 'named',
452
+ keys: entries.map(([key]) => key),
453
+ },
454
+ operationBefore,
455
+ operationAfter,
456
+ variantHooks: Object.fromEntries(
457
+ entries.map(([key, entry]) => [
458
+ key,
459
+ {
460
+ before: entry.before ?? [],
461
+ after: entry.after ?? [],
462
+ },
463
+ ]),
464
+ ),
465
+ pagination: config.pagination,
466
+ }
467
+ }
468
+
469
+ return {
470
+ guardShape: config?.shape as Record<string, unknown> | undefined,
471
+ guardRouting: classifyGuardRouting(config?.shape),
472
+ operationBefore,
473
+ operationAfter,
474
+ variantHooks: {},
475
+ pagination: config?.pagination,
476
+ }
477
+ }
478
+
479
+ export function resolveOperationVariantKey(
480
+ routing: NormalizedGuardRouting,
481
+ caller: string | undefined,
482
+ ): GuardVariantResolution {
483
+ if (routing.kind === 'none' || routing.kind === 'single') {
484
+ return resolveGuardVariantKey({ kind: 'single' })
485
+ }
486
+
487
+ return resolveGuardVariantKey({
488
+ kind: 'named',
489
+ keys: routing.keys,
490
+ caller,
491
+ reservedKeys: GUARD_SHAPE_CONFIG_KEYS,
492
+ })
493
+ }
494
+
495
+ export type GuardShapeResolution =
496
+ | { ok: true; shape: unknown }
497
+ | { ok: false; problem: string }
498
+
499
+ /**
500
+ * Resolve a dynamic guard shape EXACTLY ONCE, and hand back the value that was
501
+ * validated — never the function that produced it.
502
+ *
503
+ * The first version of this check resolved the shape, validated the result, and
504
+ * then passed the ORIGINAL shape downstream, where prisma-guard resolved it a
505
+ * second time. That is a time-of-check/time-of-use gap with a trivial exploit: a
506
+ * shape function that closes over a counter, a cache, a clock or a request
507
+ * sequence can return `{ where: … }` while it is being inspected and `{}` when
508
+ * it is enforced. Everything about the request would look guarded, and nothing
509
+ * would be. The comment claiming it resolved "once" made it worse by asserting
510
+ * the property it did not have.
511
+ *
512
+ * So the resolved value replaces the function:
513
+ *
514
+ * - a top-level function becomes the resolved static shape;
515
+ * - a function inside a variant map becomes the resolved static shape AT THAT
516
+ * KEY, with the rest of the map untouched — prisma-guard still selects by
517
+ * caller, so routing behaves exactly as it did, and the entries this request
518
+ * did not select are never called.
519
+ *
520
+ * Static shapes are returned as they came: they were validated at construction,
521
+ * and resolving them here would be work with no answer attached.
522
+ *
523
+ * `resolveContext` is invoked by `resolveShape`, once, and only on the dynamic
524
+ * path — so a deployment with no dynamic shapes never pays for it.
525
+ */
526
+ export async function resolveGuardShapeOnce(
527
+ guardShape: unknown,
528
+ resolvedKey: string | undefined,
529
+ resolveContext: ContextResolver | undefined,
530
+ ): Promise<GuardShapeResolution> {
531
+ const selected =
532
+ typeof guardShape === 'function'
533
+ ? guardShape
534
+ : resolvedKey !== undefined && isPlainObject(guardShape)
535
+ ? (guardShape as Record<string, unknown>)[resolvedKey]
536
+ : undefined
537
+
538
+ if (typeof selected !== 'function') return { ok: true, shape: guardShape }
539
+
540
+ const resolved = await resolveShape(guardShape, resolvedKey, resolveContext)
541
+
542
+ const problem = describeResolvedGuardShape(resolved)
543
+ if (problem) return { ok: false, problem }
544
+
545
+ if (typeof guardShape === 'function') return { ok: true, shape: resolved }
546
+
547
+ return {
548
+ ok: true,
549
+ shape: {
550
+ ...(guardShape as Record<string, unknown>),
551
+ [resolvedKey as string]: resolved,
552
+ },
553
+ }
554
+ }
555
+
556
+ export function validateCountSourceWhere(
557
+ cs: PaginationCountSource | undefined,
558
+ location: string,
559
+ ): void {
560
+ if (!cs) return
561
+ if ((cs as { type?: string }).type !== 'materializedView') return
562
+ const where = (cs as { where?: Record<string, unknown> }).where
563
+ if (!where) return
564
+ for (const [key, value] of Object.entries(where)) {
565
+ if (value === null) continue
566
+ if (typeof value === 'object') {
567
+ throw new Error(
568
+ location + ': countSource.where["' + key + '"] must be scalar or null; got ' +
569
+ (Array.isArray(value) ? 'array' : 'object'),
570
+ )
571
+ }
572
+ }
362
573
  }