prisma-generator-express 1.62.4 → 1.63.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +234 -95
- package/dist/generators/generateRouteConfigType.js +26 -6
- package/dist/generators/generateRouteConfigType.js.map +1 -1
- package/dist/generators/generateRouter.js +192 -27
- package/dist/generators/generateRouter.js.map +1 -1
- package/dist/generators/generateRouterFastify.js +100 -28
- package/dist/generators/generateRouterFastify.js.map +1 -1
- package/dist/generators/generateRouterHono.js +104 -36
- package/dist/generators/generateRouterHono.js.map +1 -1
- package/dist/utils/copyFiles.js +2 -0
- package/dist/utils/copyFiles.js.map +1 -1
- package/package.json +1 -1
- package/src/copy/docsRenderer.ts +1 -0
- package/src/copy/guardVariantError.ts +28 -0
- package/src/copy/guardVariantRouting.ts +109 -0
- package/src/copy/projectionDefaults.ts +5 -6
- package/src/copy/routeConfig.hono.ts +15 -5
- package/src/copy/routeConfig.ts +220 -27
- package/src/generators/generateRouteConfigType.ts +37 -6
- package/src/generators/generateRouter.ts +192 -27
- package/src/generators/generateRouterFastify.ts +100 -28
- package/src/generators/generateRouterHono.ts +104 -36
- package/src/utils/copyFiles.ts +2 -0
package/src/copy/routeConfig.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import { GUARD_SHAPE_CONFIG_KEYS } from './guardHelpers'
|
|
2
|
+
import { isPlainObject } from './misc'
|
|
3
|
+
import {
|
|
4
|
+
resolveGuardVariantKey,
|
|
5
|
+
type GuardVariantResolution,
|
|
6
|
+
} from './guardVariantRouting'
|
|
7
|
+
|
|
1
8
|
export interface QueryBuilderConfig {
|
|
2
9
|
enabled?: boolean
|
|
3
10
|
port?: number
|
|
@@ -87,23 +94,43 @@ export type ProgressiveVariantConfig =
|
|
|
87
94
|
| ManualProgressiveVariantConfig
|
|
88
95
|
| AutoIncludeProgressiveVariantConfig
|
|
89
96
|
|
|
90
|
-
export
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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[]
|
|
94
120
|
pagination?: Partial<PaginationConfig>
|
|
95
121
|
}
|
|
96
122
|
|
|
97
|
-
export interface BaseUpdateEachConfig<
|
|
98
|
-
before?:
|
|
99
|
-
after?:
|
|
123
|
+
export interface BaseUpdateEachConfig<TBefore, TAfter = TBefore> {
|
|
124
|
+
before?: TBefore[]
|
|
125
|
+
after?: TAfter[]
|
|
100
126
|
}
|
|
101
127
|
|
|
102
128
|
export interface BaseRouteConfig<
|
|
103
|
-
|
|
129
|
+
TBefore,
|
|
104
130
|
RequestType,
|
|
105
131
|
TShape = Record<string, unknown>,
|
|
106
132
|
TCtx = unknown,
|
|
133
|
+
TAfter = TBefore,
|
|
107
134
|
> {
|
|
108
135
|
enableAll?: boolean
|
|
109
136
|
addModelPrefix?: boolean
|
|
@@ -125,30 +152,196 @@ export interface BaseRouteConfig<
|
|
|
125
152
|
resolveContext?: (request: RequestType) => TCtx | Promise<TCtx>
|
|
126
153
|
queryBuilder?: QueryBuilderConfig | false
|
|
127
154
|
pagination?: PaginationConfig
|
|
128
|
-
findUnique?: BaseOperationConfig<
|
|
129
|
-
findUniqueOrThrow?: BaseOperationConfig<
|
|
130
|
-
findFirst?: BaseOperationConfig<
|
|
131
|
-
findFirstOrThrow?: BaseOperationConfig<
|
|
132
|
-
findMany?: BaseOperationConfig<
|
|
133
|
-
findManyPaginated?: BaseOperationConfig<
|
|
134
|
-
create?: BaseOperationConfig<
|
|
135
|
-
createMany?: BaseOperationConfig<
|
|
136
|
-
createManyAndReturn?: BaseOperationConfig<
|
|
137
|
-
update?: BaseOperationConfig<
|
|
138
|
-
updateMany?: BaseOperationConfig<
|
|
139
|
-
updateManyAndReturn?: BaseOperationConfig<
|
|
140
|
-
upsert?: BaseOperationConfig<
|
|
141
|
-
delete?: BaseOperationConfig<
|
|
142
|
-
deleteMany?: BaseOperationConfig<
|
|
143
|
-
updateEach?: BaseUpdateEachConfig<
|
|
144
|
-
aggregate?: BaseOperationConfig<
|
|
145
|
-
count?: BaseOperationConfig<
|
|
146
|
-
groupBy?: BaseOperationConfig<
|
|
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
|
|
147
174
|
}
|
|
148
175
|
|
|
149
176
|
export type OperationConfig = BaseOperationConfig<unknown>
|
|
150
177
|
export type RouteConfig = BaseRouteConfig<unknown, unknown>
|
|
151
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
|
+
|
|
152
345
|
export function validateCountSourceWhere(
|
|
153
346
|
cs: PaginationCountSource | undefined,
|
|
154
347
|
location: string,
|
|
@@ -74,25 +74,55 @@ export function generateRouteConfigType(
|
|
|
74
74
|
|
|
75
75
|
const shapeOps = Array.from(new Set(ROUTER_OPERATIONS))
|
|
76
76
|
const opShapeImports = shapeOps
|
|
77
|
-
.
|
|
77
|
+
.flatMap((op) => {
|
|
78
|
+
const prefix = `${m}${capitalize(op)}Shape`
|
|
79
|
+
return [prefix, `${prefix}Input`]
|
|
80
|
+
})
|
|
78
81
|
.join(',\n ')
|
|
79
82
|
|
|
83
|
+
const shapeOrFnAliases = shapeOps
|
|
84
|
+
.map((op) => {
|
|
85
|
+
const prefix = `${m}${capitalize(op)}Shape`
|
|
86
|
+
return (
|
|
87
|
+
`type ${prefix}OrFn<TCtx = unknown> =\n` +
|
|
88
|
+
` | ${prefix}\n` +
|
|
89
|
+
` | ((ctx: TCtx) => ${prefix})`
|
|
90
|
+
)
|
|
91
|
+
})
|
|
92
|
+
.join('\n\n')
|
|
93
|
+
|
|
80
94
|
const overrides = ROUTER_OPERATIONS.map((routerOp) => {
|
|
81
95
|
const c = capitalize(routerOp)
|
|
82
96
|
const isRead = READ_OPERATION_NAMES.has(routerOp)
|
|
83
|
-
const
|
|
97
|
+
const commonLines = [
|
|
84
98
|
` before?: ${beforeRef}[]`,
|
|
85
99
|
` after?: ${afterRef}[]`,
|
|
86
|
-
` shape?: ${m}${c}ShapeInput<TCtx>`,
|
|
87
100
|
` pagination?: Partial<PaginationConfig>`,
|
|
88
101
|
]
|
|
102
|
+
|
|
89
103
|
if (isRead && supportsProgressive) {
|
|
90
|
-
|
|
91
|
-
|
|
104
|
+
commonLines.push(
|
|
105
|
+
` progressive?: Record<string, ProgressiveVariantConfig>`,
|
|
106
|
+
)
|
|
107
|
+
commonLines.push(
|
|
92
108
|
` progressiveStages?: Record<string, ProgressiveStage<TCtx, TPrisma>>`,
|
|
93
109
|
)
|
|
94
110
|
}
|
|
95
|
-
|
|
111
|
+
|
|
112
|
+
const commonConfig = `{\n${commonLines.join('\n')}\n }`
|
|
113
|
+
const variantsConfig =
|
|
114
|
+
`Record<string, {\n` +
|
|
115
|
+
` shape: ${m}${c}ShapeOrFn<TCtx>\n` +
|
|
116
|
+
` before?: ${beforeRef}[]\n` +
|
|
117
|
+
` after?: ${afterRef}[]\n` +
|
|
118
|
+
` }>`
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
` ${routerOp}?: (${commonConfig} & (\n` +
|
|
122
|
+
` | { shape?: ${m}${c}ShapeInput<TCtx>; variants?: never }\n` +
|
|
123
|
+
` | { shape?: never; variants: ${variantsConfig} }\n` +
|
|
124
|
+
` )) | false`
|
|
125
|
+
)
|
|
96
126
|
}).join('\n')
|
|
97
127
|
|
|
98
128
|
const omitKeys = ROUTER_OPERATIONS.map((k) => `'${k}'`).join('\n | ')
|
|
@@ -100,6 +130,7 @@ export function generateRouteConfigType(
|
|
|
100
130
|
return (
|
|
101
131
|
progressiveTypeImport +
|
|
102
132
|
`import type {\n ${opShapeImports}\n} from '${guardShapesImport}${ext}'\n\n` +
|
|
133
|
+
`${shapeOrFnAliases}\n\n` +
|
|
103
134
|
`export type ${m}RouteConfig${generics} = Omit<\n` +
|
|
104
135
|
` ${baseConfig},\n` +
|
|
105
136
|
` | ${omitKeys}\n` +
|
|
@@ -57,15 +57,37 @@ function emitReadOp(
|
|
|
57
57
|
const postReadBlock = meta.supportsPostRead
|
|
58
58
|
? ` if (postReadsEnabled) {
|
|
59
59
|
const postPath = ${meta.name === 'findMany' ? "basePath ? `${basePath}/read` : '/read'" : `path`}
|
|
60
|
-
router.post(
|
|
60
|
+
router.post(
|
|
61
|
+
postPath,
|
|
62
|
+
parseBodyAsQuery,
|
|
63
|
+
setShape(opConfig, '${opKind}'),
|
|
64
|
+
...opConfig.operationBefore,
|
|
65
|
+
requireVariantKey(),
|
|
66
|
+
variantBeforeDispatcher(opConfig),
|
|
67
|
+
${handlerName} as RequestHandler,
|
|
68
|
+
variantAfterDispatcher(opConfig),
|
|
69
|
+
...opConfig.operationAfter,
|
|
70
|
+
respond,
|
|
71
|
+
)
|
|
61
72
|
}`
|
|
62
73
|
: ''
|
|
63
74
|
|
|
64
75
|
return ` if (isEnabled(config.${meta.configKey})) {
|
|
65
|
-
const opConfig
|
|
66
|
-
const { before = [], after = [] } = opConfig
|
|
76
|
+
const opConfig = opFor('${meta.configKey}')
|
|
67
77
|
const path = ${pathValue}
|
|
68
|
-
router.get(
|
|
78
|
+
router.get(
|
|
79
|
+
path,
|
|
80
|
+
parseQuery,
|
|
81
|
+
setShape(opConfig, '${opKind}'),
|
|
82
|
+
...opConfig.operationBefore,
|
|
83
|
+
requireVariantKey(),
|
|
84
|
+
variantBeforeDispatcher(opConfig),
|
|
85
|
+
maybeProgressiveSSE(opConfig, core.${meta.coreName}, '${meta.name}'),
|
|
86
|
+
${handlerName} as RequestHandler,
|
|
87
|
+
variantAfterDispatcher(opConfig),
|
|
88
|
+
...opConfig.operationAfter,
|
|
89
|
+
respond,
|
|
90
|
+
)
|
|
69
91
|
${postReadBlock}
|
|
70
92
|
}`
|
|
71
93
|
}
|
|
@@ -81,10 +103,19 @@ function emitWriteOp(
|
|
|
81
103
|
const opKind = opKindFor(meta.name)
|
|
82
104
|
|
|
83
105
|
return ` if (isEnabled(config.${meta.configKey})) {
|
|
84
|
-
const opConfig
|
|
85
|
-
const { before = [], after = [] } = opConfig
|
|
106
|
+
const opConfig = opFor('${meta.configKey}')
|
|
86
107
|
const path = ${pathValue}
|
|
87
|
-
router.${meta.method}(
|
|
108
|
+
router.${meta.method}(
|
|
109
|
+
path,
|
|
110
|
+
setShape(opConfig, '${opKind}'),
|
|
111
|
+
...opConfig.operationBefore,
|
|
112
|
+
requireVariantKey(),
|
|
113
|
+
variantBeforeDispatcher(opConfig),
|
|
114
|
+
${handlerName} as RequestHandler,
|
|
115
|
+
variantAfterDispatcher(opConfig),
|
|
116
|
+
...opConfig.operationAfter,
|
|
117
|
+
${respondFn},
|
|
118
|
+
)
|
|
88
119
|
}`
|
|
89
120
|
}
|
|
90
121
|
|
|
@@ -146,10 +177,19 @@ import type {
|
|
|
146
177
|
import { parseQueryParams } from '../parseQueryParams${ext}'
|
|
147
178
|
import { sanitizeKeys, normalizePrefix, getEnv, isPlainObject } from '../misc${ext}'
|
|
148
179
|
import { buildModelOpenApi } from '../buildModelOpenApi${ext}'
|
|
149
|
-
import {
|
|
180
|
+
import {
|
|
181
|
+
normalizeOperation,
|
|
182
|
+
resolveOperationVariantKey,
|
|
183
|
+
validateCountSourceWhere,
|
|
184
|
+
validateOperationConfig,
|
|
185
|
+
validateUpdateEachConfig,
|
|
186
|
+
} from '../routeConfig${ext}'
|
|
187
|
+
import type { NormalizedOperationConfig } from '../routeConfig${ext}'
|
|
150
188
|
import type { OperationContext } from '../operationRuntime${ext}'
|
|
151
189
|
import { transformResult } from '../operationRuntime${ext}'
|
|
152
190
|
import { HttpError, mapError } from '../errorMapper${ext}'
|
|
191
|
+
import { formatGuardVariantResolutionError } from '../guardVariantError${ext}'
|
|
192
|
+
import type { GuardVariantResolution } from '../guardVariantRouting${ext}'
|
|
153
193
|
import { mergePaginationConfig } from '../pagination${ext}'
|
|
154
194
|
import {
|
|
155
195
|
acceptsEventStream,
|
|
@@ -173,12 +213,25 @@ const DROP_GUARD = ${dropGuard} || _env.E2E === 'true'
|
|
|
173
213
|
type OperationConfigLike = {
|
|
174
214
|
before?: RequestHandler[]
|
|
175
215
|
after?: RequestHandler[]
|
|
176
|
-
shape?:
|
|
216
|
+
shape?: unknown
|
|
217
|
+
variants?: Record<
|
|
218
|
+
string,
|
|
219
|
+
{
|
|
220
|
+
shape?: unknown
|
|
221
|
+
before?: RequestHandler[]
|
|
222
|
+
after?: RequestHandler[]
|
|
223
|
+
}
|
|
224
|
+
>
|
|
177
225
|
pagination?: Partial<PaginationConfig>
|
|
178
226
|
progressive?: Record<string, ProgressiveVariantConfig>
|
|
179
227
|
progressiveStages?: Record<string, ProgressiveStage<unknown>>
|
|
180
228
|
}
|
|
181
229
|
|
|
230
|
+
type NormalizedOp = NormalizedOperationConfig<RequestHandler, RequestHandler> & {
|
|
231
|
+
progressive?: Record<string, ProgressiveVariantConfig>
|
|
232
|
+
progressiveStages?: Record<string, ProgressiveStage<unknown>>
|
|
233
|
+
}
|
|
234
|
+
|
|
182
235
|
type ExtendedRequest = Request & {
|
|
183
236
|
prisma?: unknown
|
|
184
237
|
postgres?: unknown
|
|
@@ -190,13 +243,20 @@ type LocalsBag = {
|
|
|
190
243
|
routeConfig?: { pagination?: PaginationConfig }
|
|
191
244
|
guardShape?: Record<string, unknown>
|
|
192
245
|
guardCaller?: string
|
|
246
|
+
guardVariantKey?: string
|
|
247
|
+
guardVariantFailure?: Extract<GuardVariantResolution, { ok: false }>
|
|
193
248
|
data?: unknown
|
|
194
249
|
}
|
|
195
250
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
251
|
+
function normalizeExpressOperation(
|
|
252
|
+
config: OperationConfigLike | undefined,
|
|
253
|
+
): NormalizedOp {
|
|
254
|
+
return {
|
|
255
|
+
...normalizeOperation<RequestHandler, RequestHandler>(config),
|
|
256
|
+
progressive: config?.progressive,
|
|
257
|
+
progressiveStages: config?.progressiveStages,
|
|
258
|
+
}
|
|
259
|
+
}
|
|
200
260
|
|
|
201
261
|
function isQueryBuilderEnabled(config: { queryBuilder?: QueryBuilderConfig | false }): boolean {
|
|
202
262
|
if (config.queryBuilder === false) return false
|
|
@@ -215,6 +275,87 @@ function readLocals(res: Response): LocalsBag {
|
|
|
215
275
|
return res.locals as LocalsBag
|
|
216
276
|
}
|
|
217
277
|
|
|
278
|
+
function runHandlerSequence(
|
|
279
|
+
handlers: readonly RequestHandler[],
|
|
280
|
+
req: Request,
|
|
281
|
+
res: Response,
|
|
282
|
+
done: (error?: unknown) => void,
|
|
283
|
+
): void {
|
|
284
|
+
let index = 0
|
|
285
|
+
|
|
286
|
+
const dispatch = (error?: unknown): void => {
|
|
287
|
+
if (error !== undefined) {
|
|
288
|
+
done(error)
|
|
289
|
+
return
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (res.writableEnded || res.destroyed) return
|
|
293
|
+
|
|
294
|
+
const handler = handlers[index++]
|
|
295
|
+
if (!handler) {
|
|
296
|
+
done()
|
|
297
|
+
return
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
let settled = false
|
|
301
|
+
const next = (nextError?: unknown): void => {
|
|
302
|
+
if (settled) return
|
|
303
|
+
settled = true
|
|
304
|
+
dispatch(nextError)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
const result = handler(req, res, next)
|
|
309
|
+
if (result && typeof (result as Promise<unknown>).then === 'function') {
|
|
310
|
+
void (result as Promise<unknown>).catch((promiseError) => {
|
|
311
|
+
if (settled) return
|
|
312
|
+
settled = true
|
|
313
|
+
dispatch(promiseError)
|
|
314
|
+
})
|
|
315
|
+
}
|
|
316
|
+
} catch (handlerError) {
|
|
317
|
+
if (settled) return
|
|
318
|
+
settled = true
|
|
319
|
+
dispatch(handlerError)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
dispatch()
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function variantBeforeDispatcher(opConfig: NormalizedOp): RequestHandler {
|
|
327
|
+
return (req, res, next) => {
|
|
328
|
+
const key = readLocals(res).guardVariantKey
|
|
329
|
+
const hooks =
|
|
330
|
+
key !== undefined
|
|
331
|
+
? (opConfig.variantHooks[key]?.before ?? [])
|
|
332
|
+
: []
|
|
333
|
+
runHandlerSequence(hooks, req, res, next)
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function variantAfterDispatcher(opConfig: NormalizedOp): RequestHandler {
|
|
338
|
+
return (req, res, next) => {
|
|
339
|
+
const key = readLocals(res).guardVariantKey
|
|
340
|
+
const hooks =
|
|
341
|
+
key !== undefined
|
|
342
|
+
? (opConfig.variantHooks[key]?.after ?? [])
|
|
343
|
+
: []
|
|
344
|
+
runHandlerSequence(hooks, req, res, next)
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function requireVariantKey(): RequestHandler {
|
|
349
|
+
return (_req, res, next) => {
|
|
350
|
+
const failure = readLocals(res).guardVariantFailure
|
|
351
|
+
if (!failure) {
|
|
352
|
+
next()
|
|
353
|
+
return
|
|
354
|
+
}
|
|
355
|
+
next(new HttpError(400, formatGuardVariantResolutionError(failure)))
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
218
359
|
export function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(config: ${modelName}RouteConfig<TCtx, TPrisma> = {}) {
|
|
219
360
|
validateCountSourceWhere(config.pagination?.countSource, '${modelName} pagination')
|
|
220
361
|
validateCountSourceWhere(
|
|
@@ -226,6 +367,12 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(config: ${m
|
|
|
226
367
|
|
|
227
368
|
const isEnabled = (value: unknown): boolean => value !== false && !!(config.enableAll || value)
|
|
228
369
|
|
|
370
|
+
const opFor = (key: string): NormalizedOp => {
|
|
371
|
+
const raw = (config as unknown as Record<string, unknown>)[key] as OperationConfigLike | undefined
|
|
372
|
+
validateOperationConfig(raw, '${modelName}.' + key)
|
|
373
|
+
return normalizeExpressOperation(raw)
|
|
374
|
+
}
|
|
375
|
+
|
|
229
376
|
const customPrefix = normalizePrefix(config.customUrlPrefix || '')
|
|
230
377
|
const modelPrefix = config.addModelPrefix !== false ? '/${modelNameLower}' : ''
|
|
231
378
|
const basePath = customPrefix + modelPrefix
|
|
@@ -312,7 +459,7 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(config: ${m
|
|
|
312
459
|
next()
|
|
313
460
|
}
|
|
314
461
|
|
|
315
|
-
const setShape = (opConfig:
|
|
462
|
+
const setShape = (opConfig: NormalizedOp, opKind: OpKind): RequestHandler => {
|
|
316
463
|
return async (req, res, next) => {
|
|
317
464
|
try {
|
|
318
465
|
const locals = readLocals(res)
|
|
@@ -320,17 +467,32 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(config: ${m
|
|
|
320
467
|
if (merged) {
|
|
321
468
|
locals.routeConfig = { pagination: merged }
|
|
322
469
|
}
|
|
470
|
+
|
|
323
471
|
const headerName = config.guard?.variantHeader || 'x-api-variant'
|
|
324
472
|
const headerValue = req.get(headerName)
|
|
325
473
|
const caller = config.guard?.resolveVariant?.(req) ?? headerValue ?? undefined
|
|
326
|
-
if (caller) locals.guardCaller = caller
|
|
327
|
-
|
|
474
|
+
if (typeof caller === 'string') locals.guardCaller = caller
|
|
475
|
+
|
|
476
|
+
const resolution = resolveOperationVariantKey(opConfig.guardRouting, caller)
|
|
477
|
+
if (!resolution.ok) {
|
|
478
|
+
locals.guardVariantFailure = resolution
|
|
479
|
+
next()
|
|
480
|
+
return
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const resolvedKey =
|
|
484
|
+
opConfig.guardRouting.kind === 'named'
|
|
485
|
+
? resolution.key
|
|
486
|
+
: undefined
|
|
487
|
+
if (resolvedKey !== undefined) locals.guardVariantKey = resolvedKey
|
|
488
|
+
|
|
489
|
+
if (opConfig.guardShape) {
|
|
328
490
|
if (!DROP_GUARD) {
|
|
329
|
-
locals.guardShape = opConfig.
|
|
491
|
+
locals.guardShape = opConfig.guardShape
|
|
330
492
|
} else {
|
|
331
493
|
await applyDroppedGuard(
|
|
332
|
-
opConfig.
|
|
333
|
-
|
|
494
|
+
opConfig.guardShape,
|
|
495
|
+
resolvedKey,
|
|
334
496
|
buildResolveContext(req),
|
|
335
497
|
opKind,
|
|
336
498
|
{
|
|
@@ -352,6 +514,7 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(config: ${m
|
|
|
352
514
|
)
|
|
353
515
|
}
|
|
354
516
|
}
|
|
517
|
+
|
|
355
518
|
next()
|
|
356
519
|
} catch (err) {
|
|
357
520
|
next(mapError(err))
|
|
@@ -360,7 +523,7 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(config: ${m
|
|
|
360
523
|
}
|
|
361
524
|
|
|
362
525
|
const maybeProgressiveSSE = (
|
|
363
|
-
opConfig:
|
|
526
|
+
opConfig: NormalizedOp,
|
|
364
527
|
coreFn: (ctx: OperationContext) => Promise<unknown>,
|
|
365
528
|
baseOp: string,
|
|
366
529
|
): RequestHandler => {
|
|
@@ -370,8 +533,9 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(config: ${m
|
|
|
370
533
|
if (!acceptsEventStream(req.headers.accept)) return next()
|
|
371
534
|
|
|
372
535
|
const locals = readLocals(res)
|
|
373
|
-
const variant = locals.guardCaller
|
|
374
|
-
const progressiveConfig =
|
|
536
|
+
const variant = locals.guardVariantKey ?? locals.guardCaller
|
|
537
|
+
const progressiveConfig =
|
|
538
|
+
variant !== undefined ? opConfig.progressive?.[variant] : undefined
|
|
375
539
|
|
|
376
540
|
try {
|
|
377
541
|
if (!progressiveConfig || progressiveConfig.enabled === false) {
|
|
@@ -499,19 +663,20 @@ ${readOpBlocks}
|
|
|
499
663
|
${writeOpBlocks}
|
|
500
664
|
|
|
501
665
|
if (config.updateEach) {
|
|
502
|
-
const
|
|
503
|
-
|
|
666
|
+
const rawUpdateEach = config.updateEach as unknown as OperationConfigLike
|
|
667
|
+
validateUpdateEachConfig(rawUpdateEach, '${modelName}.updateEach')
|
|
668
|
+
const opConfig = normalizeExpressOperation(rawUpdateEach)
|
|
669
|
+
if (opConfig.operationBefore.length === 0 && _env.NODE_ENV !== 'production') {
|
|
504
670
|
console.warn(
|
|
505
671
|
'[${modelName}Router] updateEach is enabled without a before hook. ' +
|
|
506
672
|
'This endpoint bypasses guard shapes and should be protected by authentication middleware.',
|
|
507
673
|
)
|
|
508
674
|
}
|
|
509
|
-
const { before = [], after = [] } = opConfig
|
|
510
675
|
const path = basePath ? \`\${basePath}/each\` : '/each'
|
|
511
676
|
router.post(
|
|
512
677
|
path,
|
|
513
678
|
setShape(opConfig, 'noop'),
|
|
514
|
-
...
|
|
679
|
+
...opConfig.operationBefore,
|
|
515
680
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
516
681
|
try {
|
|
517
682
|
if (!Array.isArray(req.body)) {
|
|
@@ -524,7 +689,7 @@ ${writeOpBlocks}
|
|
|
524
689
|
next(mapError(err))
|
|
525
690
|
}
|
|
526
691
|
},
|
|
527
|
-
...
|
|
692
|
+
...opConfig.operationAfter,
|
|
528
693
|
respond,
|
|
529
694
|
)
|
|
530
695
|
}
|