prisma-generator-express 1.64.1 → 1.64.3

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,701 @@
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
+ * Confirms that a variant literally named `default` is meant to answer every
124
+ * unrecognised, blank and missing caller.
125
+ *
126
+ * Only consulted when `requireDefaultVariantOptIn` is on. Otherwise the key
127
+ * behaves as it always has, so this is inert for existing configurations.
128
+ */
129
+ allowDefaultVariant?: boolean
130
+ }
131
+
132
+ export interface BaseUpdateEachConfig<TBefore, TAfter = TBefore> {
133
+ before?: TBefore[]
134
+ after?: TAfter[]
135
+ }
136
+
137
+ export interface BaseRouteConfig<
138
+ TBefore,
139
+ RequestType,
140
+ TShape = Record<string, unknown>,
141
+ TCtx = unknown,
142
+ TAfter = TBefore,
143
+ > {
144
+ enableAll?: boolean
145
+ addModelPrefix?: boolean
146
+ customUrlPrefix?: string
147
+ specBasePath?: string
148
+ disableOpenApi?: boolean
149
+ disablePostReads?: boolean
150
+ scalarCdnUrl?: string
151
+ openApiTitle?: string
152
+ openApiDescription?: string
153
+ openApiVersion?: string
154
+ openApiServers?: OpenApiServerConfig[]
155
+ openApiSecuritySchemes?: Record<string, OpenApiSecuritySchemeConfig>
156
+ openApiSecurity?: Record<string, string[]>[]
157
+ guard?: {
158
+ resolveVariant?: (request: RequestType) => string | undefined
159
+ variantHeader?: string
160
+ }
161
+ resolveContext?: (request: RequestType) => TCtx | Promise<TCtx>
162
+ queryBuilder?: QueryBuilderConfig | false
163
+ pagination?: PaginationConfig
164
+ findUnique?: BaseOperationConfig<TBefore, TShape, TAfter> | false
165
+ findUniqueOrThrow?: BaseOperationConfig<TBefore, TShape, TAfter> | false
166
+ findFirst?: BaseOperationConfig<TBefore, TShape, TAfter> | false
167
+ findFirstOrThrow?: BaseOperationConfig<TBefore, TShape, TAfter> | false
168
+ findMany?: BaseOperationConfig<TBefore, TShape, TAfter> | false
169
+ findManyPaginated?: BaseOperationConfig<TBefore, TShape, TAfter> | false
170
+ create?: BaseOperationConfig<TBefore, TShape, TAfter> | false
171
+ createMany?: BaseOperationConfig<TBefore, TShape, TAfter> | false
172
+ createManyAndReturn?: BaseOperationConfig<TBefore, TShape, TAfter> | false
173
+ update?: BaseOperationConfig<TBefore, TShape, TAfter> | false
174
+ updateMany?: BaseOperationConfig<TBefore, TShape, TAfter> | false
175
+ updateManyAndReturn?: BaseOperationConfig<TBefore, TShape, TAfter> | false
176
+ upsert?: BaseOperationConfig<TBefore, TShape, TAfter> | false
177
+ delete?: BaseOperationConfig<TBefore, TShape, TAfter> | false
178
+ deleteMany?: BaseOperationConfig<TBefore, TShape, TAfter> | false
179
+ updateEach?: BaseUpdateEachConfig<TBefore, TAfter>
180
+ aggregate?: BaseOperationConfig<TBefore, TShape, TAfter> | false
181
+ count?: BaseOperationConfig<TBefore, TShape, TAfter> | false
182
+ groupBy?: BaseOperationConfig<TBefore, TShape, TAfter> | false
183
+ }
184
+
185
+ export type OperationConfig = BaseOperationConfig<unknown>
186
+ export type RouteConfig = BaseRouteConfig<unknown, unknown>
187
+
188
+ export type NormalizedGuardRouting =
189
+ | { kind: 'none' }
190
+ | { kind: 'single' }
191
+ | { kind: 'named'; keys: readonly string[] }
192
+
193
+ export type NormalizedVariantHooks<TBefore, TAfter> = Readonly<
194
+ Record<
195
+ string,
196
+ {
197
+ before: readonly TBefore[]
198
+ after: readonly TAfter[]
199
+ }
200
+ >
201
+ >
202
+
203
+ export interface NormalizedOperationConfig<TBefore, TAfter> {
204
+ guardShape?: Record<string, unknown>
205
+ guardRouting: NormalizedGuardRouting
206
+ operationBefore: readonly TBefore[]
207
+ operationAfter: readonly TAfter[]
208
+ variantHooks: NormalizedVariantHooks<TBefore, TAfter>
209
+ pagination?: Readonly<Partial<PaginationConfig>>
210
+ }
211
+
212
+ type OperationConfigInput<TBefore, TAfter> = {
213
+ before?: TBefore[]
214
+ after?: TAfter[]
215
+ shape?: unknown
216
+ variants?: Record<
217
+ string,
218
+ {
219
+ shape?: unknown
220
+ before?: TBefore[]
221
+ after?: TAfter[]
222
+ }
223
+ >
224
+ pagination?: Partial<PaginationConfig>
225
+ }
226
+
227
+ /**
228
+ * The seven guard controls, resolved — the RUNTIME half.
229
+ *
230
+ * This lives here, not in the compiled `src/guardOptions.ts`, because a generated
231
+ * project gets these files copied in and must run with no dependency on this
232
+ * package. The two cannot import each other across that boundary, so the values
233
+ * appear twice and `guardOptionIndependence.test.ts` asserts they agree — against
234
+ * this resolver, which is the one the router actually runs.
235
+ *
236
+ * The machine-readable METADATA is compiled and public: see src/guardOptions.ts.
237
+ * Defining it here would put it in a file that never reaches `dist`.
238
+ */
239
+ export type GuardPolicy = {
240
+ requireGuardShape: boolean
241
+ validateGuardShapes: boolean
242
+ requireDefaultVariantOptIn: boolean
243
+ enableUpdateEach: boolean
244
+ guardResolutionOrder: 'after-hooks' | 'before-hooks'
245
+ allowE2EGuardBypass: boolean
246
+ validateResolvedShapes: boolean
247
+ }
248
+
249
+ /**
250
+ * PRE-1.64.2 BEHAVIOUR, exactly. Changing any value here changes what an
251
+ * unconfigured consumer gets on upgrade, which is the thing that must not happen.
252
+ */
253
+ export const UPSTREAM_GUARD_DEFAULTS: Readonly<GuardPolicy> = Object.freeze({
254
+ requireGuardShape: false,
255
+ validateGuardShapes: false,
256
+ requireDefaultVariantOptIn: false,
257
+ enableUpdateEach: true,
258
+ guardResolutionOrder: 'after-hooks',
259
+ allowE2EGuardBypass: true,
260
+ validateResolvedShapes: false,
261
+ })
262
+
263
+ /**
264
+ * Every control at its most restrictive — a SHORTCUT, not a mode.
265
+ *
266
+ * Spread it, edit what you disagree with, and store the result. There is
267
+ * deliberately no `preset: 'hardened'` option: a stored preset name would mean a
268
+ * future version of this package could silently change what a consumer's
269
+ * configuration does, which is the same class of mistake as 1.64.2 itself.
270
+ */
271
+ export const HARDENED_GUARD_PROFILE: Readonly<GuardPolicy> = Object.freeze({
272
+ requireGuardShape: true,
273
+ validateGuardShapes: true,
274
+ requireDefaultVariantOptIn: true,
275
+ enableUpdateEach: false,
276
+ guardResolutionOrder: 'before-hooks',
277
+ allowE2EGuardBypass: false,
278
+ validateResolvedShapes: true,
279
+ })
280
+
281
+ /** Fill in the defaults. Absent means upstream, never an opinion. */
282
+ export function resolveGuardPolicy(config: Partial<GuardPolicy> | undefined): GuardPolicy {
283
+ return {
284
+ requireGuardShape: config?.requireGuardShape ?? UPSTREAM_GUARD_DEFAULTS.requireGuardShape,
285
+ validateGuardShapes: config?.validateGuardShapes ?? UPSTREAM_GUARD_DEFAULTS.validateGuardShapes,
286
+ requireDefaultVariantOptIn:
287
+ config?.requireDefaultVariantOptIn ?? UPSTREAM_GUARD_DEFAULTS.requireDefaultVariantOptIn,
288
+ enableUpdateEach: config?.enableUpdateEach ?? UPSTREAM_GUARD_DEFAULTS.enableUpdateEach,
289
+ guardResolutionOrder:
290
+ config?.guardResolutionOrder ?? UPSTREAM_GUARD_DEFAULTS.guardResolutionOrder,
291
+ allowE2EGuardBypass:
292
+ config?.allowE2EGuardBypass ?? UPSTREAM_GUARD_DEFAULTS.allowE2EGuardBypass,
293
+ validateResolvedShapes:
294
+ config?.validateResolvedShapes ?? UPSTREAM_GUARD_DEFAULTS.validateResolvedShapes,
295
+ }
296
+ }
297
+
298
+ function classifyGuardRouting(shape: unknown): NormalizedGuardRouting {
299
+ if (shape === undefined) return { kind: 'none' }
300
+ if (typeof shape === 'function') return { kind: 'single' }
301
+ if (!isPlainObject(shape)) return { kind: 'single' }
302
+
303
+ const keys = Object.keys(shape)
304
+ const isSingle =
305
+ keys.length === 0 ||
306
+ keys.every((key) => GUARD_SHAPE_CONFIG_KEYS.has(key))
307
+
308
+ return isSingle ? { kind: 'single' } : { kind: 'named', keys }
309
+ }
310
+
311
+ /**
312
+ * A legacy `shape` must be a shape, and must constrain something.
313
+ *
314
+ * Two shapes that look configured and are not:
315
+ *
316
+ * - `shape: {}` normalizes to a single shape with no constraints. It reads as
317
+ * "guarded" to a reviewer and lets everything through.
318
+ * - `shape: { where: …, admin: … }` mixes a reserved shape key with a
319
+ * non-reserved one. `classifyGuardRouting` decides by whether EVERY key is
320
+ * reserved, so one stray key silently reclassifies the whole operation as
321
+ * variant-routed — after which every request that does not name a variant
322
+ * gets a 400 for a reason no message explains. The commonest cause is a
323
+ * typo: `wheer`, `iclude`, `slect`.
324
+ *
325
+ * A function shape is opaque here and is accepted: it is resolved per request
326
+ * and there is nothing to inspect at construction time.
327
+ */
328
+ function validateShapeConfig(shape: unknown, location: string): void {
329
+ if (typeof shape === 'function') return
330
+
331
+ if (!isPlainObject(shape)) {
332
+ throw new Error(
333
+ location + ': shape must be an object or a function, got ' +
334
+ (shape === null ? 'null' : Array.isArray(shape) ? 'array' : typeof shape),
335
+ )
336
+ }
337
+
338
+ const keys = Object.keys(shape)
339
+ if (keys.length === 0) {
340
+ throw new Error(
341
+ location + ': shape is empty, so it constrains nothing. Give it at least ' +
342
+ 'one guard key, or use variants.',
343
+ )
344
+ }
345
+
346
+ const reserved = keys.filter((key) => GUARD_SHAPE_CONFIG_KEYS.has(key))
347
+ if (reserved.length > 0 && reserved.length !== keys.length) {
348
+ const stray = keys.filter((key) => !GUARD_SHAPE_CONFIG_KEYS.has(key))
349
+ throw new Error(
350
+ location + ': shape mixes guard keys (' + reserved.join(', ') + ') with ' +
351
+ 'non-guard keys (' + stray.join(', ') + '). A shape whose keys are not all ' +
352
+ 'guard keys is read as a VARIANT MAP, which is rarely intended — check for ' +
353
+ 'a typo, or split them into "variants".',
354
+ )
355
+ }
356
+ }
357
+
358
+ /**
359
+ * Every emitted operation must carry a guard, and it must be one that constrains
360
+ * something.
361
+ *
362
+ * An absent configuration used to return here silently, and the emitted handler
363
+ * applies its guard under `if (opConfig.guardShape)` — so an operation nobody
364
+ * configured reached Prisma with whatever `where`, `select` and `include` the
365
+ * request supplied. Nothing warned, and the normalized operation was
366
+ * structurally identical to a configured one apart from an absent shape, so
367
+ * there was nothing to detect it by either.
368
+ *
369
+ * The refusal happens where the configuration is first visible — router
370
+ * construction, which on a Worker is boot. A deployment that forgot a guard
371
+ * fails to start instead of serving unguarded routes, which is the trade this
372
+ * check exists to make.
373
+ */
374
+ export function validateOperationConfig(
375
+ config: { shape?: unknown; variants?: unknown; allowDefaultVariant?: unknown } | undefined,
376
+ location: string,
377
+ policy: Partial<GuardPolicy> = {},
378
+ ): void {
379
+ const { requireGuardShape, validateGuardShapes, requireDefaultVariantOptIn } =
380
+ resolveGuardPolicy(policy)
381
+
382
+ /**
383
+ * THE CHECKS THAT PREDATE 1.64.2 RUN UNCONDITIONALLY.
384
+ *
385
+ * Everything below them is gated per option. Note each gate stands alone: a
386
+ * consumer who enables `validateGuardShapes` gets shape validation and NOT a
387
+ * missing-guard refusal, and vice versa. That independence is the point, and
388
+ * `guardOptionIndependence.test.ts` asserts it option by option.
389
+ */
390
+ if (!config) {
391
+ if (requireGuardShape) {
392
+ throw new Error(
393
+ location + ': no guard configured. Every generated operation must define ' +
394
+ '"shape" or "variants"; an unguarded operation would pass the caller\'s ' +
395
+ 'own where/select/include straight to Prisma.',
396
+ )
397
+ }
398
+ return
399
+ }
400
+
401
+ const hasShape = config.shape !== undefined
402
+ const hasVariants = config.variants !== undefined
403
+
404
+ // Pre-1.64.2. Not gated.
405
+ if (hasShape && hasVariants) {
406
+ throw new Error(location + ': shape and variants cannot both be defined')
407
+ }
408
+
409
+ if (!hasShape && !hasVariants) {
410
+ if (requireGuardShape) {
411
+ throw new Error(
412
+ location + ': no guard configured. Define "shape" or "variants".',
413
+ )
414
+ }
415
+ return
416
+ }
417
+
418
+ if (hasShape) {
419
+ if (validateGuardShapes) validateShapeConfig(config.shape, location)
420
+ return
421
+ }
422
+
423
+ validateVariantMap(
424
+ config.variants,
425
+ location,
426
+ { validateGuardShapes, requireDefaultVariantOptIn },
427
+ config.allowDefaultVariant,
428
+ )
429
+ }
430
+
431
+ /**
432
+ * The variant-map checks.
433
+ *
434
+ * The four that predate 1.64.2 — non-array object, at least one entry, no
435
+ * reserved-key collision, every entry an object carrying a `shape` — run
436
+ * ALWAYS, because they are not new and removing them would be its own breaking
437
+ * change.
438
+ *
439
+ * The two the hardening added are gated on their OWN options, independently:
440
+ * `requireDefaultVariantOptIn` confirms a `default` variant,
441
+ * `validateGuardShapes` validates each variant's shape the way a top-level
442
+ * `shape` is validated. Enabling either does not enable the other.
443
+ */
444
+ function validateVariantMap(
445
+ variants: unknown,
446
+ location: string,
447
+ flags: { validateGuardShapes: boolean; requireDefaultVariantOptIn: boolean },
448
+ allowDefaultVariant?: unknown,
449
+ ): void {
450
+ if (!isPlainObject(variants)) {
451
+ throw new Error(location + ': variants must be a non-array object')
452
+ }
453
+
454
+ const entries = Object.entries(variants)
455
+ if (entries.length === 0) {
456
+ throw new Error(location + ': variants must contain at least one entry')
457
+ }
458
+
459
+ /**
460
+ * A variant named `default` catches a missing, blank OR unknown caller, so it
461
+ * turns three fail-closed paths into a pass at once. That is legitimate when
462
+ * `default` is the most restrictive variant and dangerous when it is the most
463
+ * permissive, which is how the word reads — and nothing can tell which from
464
+ * the shape alone.
465
+ *
466
+ * So it must be asked for. `allowDefaultVariant: true` is a sentence the author
467
+ * writes on purpose; a key called `default` is one they may have typed without
468
+ * noticing what it catches.
469
+ */
470
+ if (flags.requireDefaultVariantOptIn) {
471
+ if (Object.prototype.hasOwnProperty.call(variants, 'default')) {
472
+ if (allowDefaultVariant !== true) {
473
+ throw new Error(
474
+ location + ': a variant named "default" answers every unrecognised, ' +
475
+ 'blank and missing caller. Set allowDefaultVariant: true to confirm ' +
476
+ 'that is intended, and make sure it is the most restrictive variant.',
477
+ )
478
+ }
479
+ } else if (allowDefaultVariant !== undefined) {
480
+ throw new Error(
481
+ location + ': allowDefaultVariant is set but no "default" variant exists.',
482
+ )
483
+ }
484
+ }
485
+
486
+ for (const [key, rawEntry] of entries) {
487
+ if (GUARD_SHAPE_CONFIG_KEYS.has(key)) {
488
+ throw new Error(
489
+ location + ': variant name "' + key +
490
+ '" collides with a reserved guard shape key',
491
+ )
492
+ }
493
+
494
+ if (!isPlainObject(rawEntry)) {
495
+ throw new Error(
496
+ location + ': variant "' + key + '" must be an object with a shape',
497
+ )
498
+ }
499
+
500
+ if (rawEntry.shape === undefined) {
501
+ throw new Error(
502
+ location + ': variant "' + key + '" is missing "shape"',
503
+ )
504
+ }
505
+
506
+ /**
507
+ * The SAME validation the legacy `shape` gets. Without this,
508
+ * `variants: { public: { shape: {} } }` emitted a route that constrains
509
+ * nothing — the exact hole the top-level check closes, reachable through a
510
+ * different key. A guard is not more trustworthy for being written inside a
511
+ * variant.
512
+ */
513
+ if (flags.validateGuardShapes) {
514
+ validateShapeConfig(rawEntry.shape, location + ' variant "' + key + '"')
515
+ }
516
+ }
517
+ }
518
+
519
+ /**
520
+ * What is wrong with a shape that has just been RESOLVED, or `null` if nothing.
521
+ *
522
+ * A function shape is opaque at construction time, so `validateShapeConfig`
523
+ * accepts it and this is the second half of that bargain: whatever it returns is
524
+ * checked before it is used. A function returning `{}`, `undefined`, or a map
525
+ * with stray keys recreates the fail-open at request time, once per request,
526
+ * where no configuration review will ever see it.
527
+ *
528
+ * Returns a description rather than throwing, because the caller is a request
529
+ * handler: this is a 500 (the deployment is misconfigured), not an exception to
530
+ * propagate as-is.
531
+ */
532
+ export function describeResolvedGuardShape(resolved: unknown): string | null {
533
+ if (resolved === null || resolved === undefined) {
534
+ return 'the shape function returned nothing, so the operation would run unguarded'
535
+ }
536
+
537
+ if (!isPlainObject(resolved)) {
538
+ return 'the shape function returned ' +
539
+ (Array.isArray(resolved) ? 'an array' : typeof resolved) +
540
+ ', which is not a guard shape'
541
+ }
542
+
543
+ const keys = Object.keys(resolved)
544
+ if (keys.length === 0) {
545
+ return 'the shape function returned an empty object, which constrains nothing'
546
+ }
547
+
548
+ const stray = keys.filter((key) => !GUARD_SHAPE_CONFIG_KEYS.has(key))
549
+ if (stray.length > 0) {
550
+ return 'the shape function returned non-guard keys (' + stray.join(', ') + ')'
551
+ }
552
+
553
+ return null
554
+ }
555
+
556
+ export function validateUpdateEachConfig(
557
+ config: { variants?: unknown } | undefined,
558
+ location: string,
559
+ ): void {
560
+ if (config?.variants !== undefined) {
561
+ throw new Error(location + ': updateEach does not support variants')
562
+ }
563
+ }
564
+
565
+ export function normalizeOperation<TBefore, TAfter>(
566
+ config: OperationConfigInput<TBefore, TAfter> | undefined,
567
+ ): NormalizedOperationConfig<TBefore, TAfter> {
568
+ const operationBefore = config?.before ?? []
569
+ const operationAfter = config?.after ?? []
570
+
571
+ if (config?.variants !== undefined) {
572
+ const entries = Object.entries(config.variants)
573
+
574
+ return {
575
+ guardShape: Object.fromEntries(
576
+ entries.map(([key, entry]) => [key, entry.shape]),
577
+ ),
578
+ guardRouting: {
579
+ kind: 'named',
580
+ keys: entries.map(([key]) => key),
581
+ },
582
+ operationBefore,
583
+ operationAfter,
584
+ variantHooks: Object.fromEntries(
585
+ entries.map(([key, entry]) => [
586
+ key,
587
+ {
588
+ before: entry.before ?? [],
589
+ after: entry.after ?? [],
590
+ },
591
+ ]),
592
+ ),
593
+ pagination: config.pagination,
594
+ }
595
+ }
596
+
597
+ return {
598
+ guardShape: config?.shape as Record<string, unknown> | undefined,
599
+ guardRouting: classifyGuardRouting(config?.shape),
600
+ operationBefore,
601
+ operationAfter,
602
+ variantHooks: {},
603
+ pagination: config?.pagination,
604
+ }
605
+ }
606
+
607
+ export function resolveOperationVariantKey(
608
+ routing: NormalizedGuardRouting,
609
+ caller: string | undefined,
610
+ ): GuardVariantResolution {
611
+ if (routing.kind === 'none' || routing.kind === 'single') {
612
+ return resolveGuardVariantKey({ kind: 'single' })
613
+ }
614
+
615
+ return resolveGuardVariantKey({
616
+ kind: 'named',
617
+ keys: routing.keys,
618
+ caller,
619
+ reservedKeys: GUARD_SHAPE_CONFIG_KEYS,
620
+ })
621
+ }
622
+
623
+ export type GuardShapeResolution =
624
+ | { ok: true; shape: unknown }
625
+ | { ok: false; problem: string }
626
+
627
+ /**
628
+ * Resolve a dynamic guard shape EXACTLY ONCE, and hand back the value that was
629
+ * validated — never the function that produced it.
630
+ *
631
+ * The first version of this check resolved the shape, validated the result, and
632
+ * then passed the ORIGINAL shape downstream, where prisma-guard resolved it a
633
+ * second time. That is a time-of-check/time-of-use gap with a trivial exploit: a
634
+ * shape function that closes over a counter, a cache, a clock or a request
635
+ * sequence can return `{ where: … }` while it is being inspected and `{}` when
636
+ * it is enforced. Everything about the request would look guarded, and nothing
637
+ * would be. The comment claiming it resolved "once" made it worse by asserting
638
+ * the property it did not have.
639
+ *
640
+ * So the resolved value replaces the function:
641
+ *
642
+ * - a top-level function becomes the resolved static shape;
643
+ * - a function inside a variant map becomes the resolved static shape AT THAT
644
+ * KEY, with the rest of the map untouched — prisma-guard still selects by
645
+ * caller, so routing behaves exactly as it did, and the entries this request
646
+ * did not select are never called.
647
+ *
648
+ * Static shapes are returned as they came: they were validated at construction,
649
+ * and resolving them here would be work with no answer attached.
650
+ *
651
+ * `resolveContext` is invoked by `resolveShape`, once, and only on the dynamic
652
+ * path — so a deployment with no dynamic shapes never pays for it.
653
+ */
654
+ export async function resolveGuardShapeOnce(
655
+ guardShape: unknown,
656
+ resolvedKey: string | undefined,
657
+ resolveContext: ContextResolver | undefined,
658
+ ): Promise<GuardShapeResolution> {
659
+ const selected =
660
+ typeof guardShape === 'function'
661
+ ? guardShape
662
+ : resolvedKey !== undefined && isPlainObject(guardShape)
663
+ ? (guardShape as Record<string, unknown>)[resolvedKey]
664
+ : undefined
665
+
666
+ if (typeof selected !== 'function') return { ok: true, shape: guardShape }
667
+
668
+ const resolved = await resolveShape(guardShape, resolvedKey, resolveContext)
669
+
670
+ const problem = describeResolvedGuardShape(resolved)
671
+ if (problem) return { ok: false, problem }
672
+
673
+ if (typeof guardShape === 'function') return { ok: true, shape: resolved }
674
+
675
+ return {
676
+ ok: true,
677
+ shape: {
678
+ ...(guardShape as Record<string, unknown>),
679
+ [resolvedKey as string]: resolved,
680
+ },
681
+ }
682
+ }
683
+
684
+ export function validateCountSourceWhere(
685
+ cs: PaginationCountSource | undefined,
686
+ location: string,
687
+ ): void {
688
+ if (!cs) return
689
+ if ((cs as { type?: string }).type !== 'materializedView') return
690
+ const where = (cs as { where?: Record<string, unknown> }).where
691
+ if (!where) return
692
+ for (const [key, value] of Object.entries(where)) {
693
+ if (value === null) continue
694
+ if (typeof value === 'object') {
695
+ throw new Error(
696
+ location + ': countSource.where["' + key + '"] must be scalar or null; got ' +
697
+ (Array.isArray(value) ? 'array' : 'object'),
698
+ )
699
+ }
700
+ }
362
701
  }