prisma-generator-express 1.64.0 → 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.
- package/dist/bin.js +0 -0
- package/dist/generators/generateRouterHono.js +74 -49
- package/dist/generators/generateRouterHono.js.map +1 -1
- package/package.json +2 -2
- package/src/bin.ts +0 -0
- package/src/copy/projectionDefaults.ts +6 -1
- package/src/copy/routeConfig.ts +572 -361
- package/src/generators/generateRouterHono.ts +101 -49
package/src/copy/routeConfig.ts
CHANGED
|
@@ -1,362 +1,573 @@
|
|
|
1
|
-
import { GUARD_SHAPE_CONFIG_KEYS } from './guardHelpers'
|
|
2
|
-
import { isPlainObject } from './misc'
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
|
64
|
-
| ProgressivePatch
|
|
65
|
-
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
export type
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
| { kind: '
|
|
182
|
-
| { kind: '
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
>
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if (
|
|
221
|
-
if (
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
keys.
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (
|
|
253
|
-
throw new Error(
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
}
|