prisma-generator-express 1.62.4 → 1.63.1
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 +196 -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 +196 -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,91 @@ 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: unknown = handler(req, res, next)
|
|
309
|
+
if (
|
|
310
|
+
typeof result === 'object' &&
|
|
311
|
+
result !== null &&
|
|
312
|
+
typeof (result as Promise<unknown>).then === 'function'
|
|
313
|
+
) {
|
|
314
|
+
void (result as Promise<unknown>).catch((promiseError) => {
|
|
315
|
+
if (settled) return
|
|
316
|
+
settled = true
|
|
317
|
+
dispatch(promiseError)
|
|
318
|
+
})
|
|
319
|
+
}
|
|
320
|
+
} catch (handlerError) {
|
|
321
|
+
if (settled) return
|
|
322
|
+
settled = true
|
|
323
|
+
dispatch(handlerError)
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
dispatch()
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function variantBeforeDispatcher(opConfig: NormalizedOp): RequestHandler {
|
|
331
|
+
return (req, res, next) => {
|
|
332
|
+
const key = readLocals(res).guardVariantKey
|
|
333
|
+
const hooks =
|
|
334
|
+
key !== undefined
|
|
335
|
+
? (opConfig.variantHooks[key]?.before ?? [])
|
|
336
|
+
: []
|
|
337
|
+
runHandlerSequence(hooks, req, res, next)
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function variantAfterDispatcher(opConfig: NormalizedOp): RequestHandler {
|
|
342
|
+
return (req, res, next) => {
|
|
343
|
+
const key = readLocals(res).guardVariantKey
|
|
344
|
+
const hooks =
|
|
345
|
+
key !== undefined
|
|
346
|
+
? (opConfig.variantHooks[key]?.after ?? [])
|
|
347
|
+
: []
|
|
348
|
+
runHandlerSequence(hooks, req, res, next)
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function requireVariantKey(): RequestHandler {
|
|
353
|
+
return (_req, res, next) => {
|
|
354
|
+
const failure = readLocals(res).guardVariantFailure
|
|
355
|
+
if (!failure) {
|
|
356
|
+
next()
|
|
357
|
+
return
|
|
358
|
+
}
|
|
359
|
+
next(new HttpError(400, formatGuardVariantResolutionError(failure)))
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
218
363
|
export function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(config: ${modelName}RouteConfig<TCtx, TPrisma> = {}) {
|
|
219
364
|
validateCountSourceWhere(config.pagination?.countSource, '${modelName} pagination')
|
|
220
365
|
validateCountSourceWhere(
|
|
@@ -226,6 +371,12 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(config: ${m
|
|
|
226
371
|
|
|
227
372
|
const isEnabled = (value: unknown): boolean => value !== false && !!(config.enableAll || value)
|
|
228
373
|
|
|
374
|
+
const opFor = (key: string): NormalizedOp => {
|
|
375
|
+
const raw = (config as unknown as Record<string, unknown>)[key] as OperationConfigLike | undefined
|
|
376
|
+
validateOperationConfig(raw, '${modelName}.' + key)
|
|
377
|
+
return normalizeExpressOperation(raw)
|
|
378
|
+
}
|
|
379
|
+
|
|
229
380
|
const customPrefix = normalizePrefix(config.customUrlPrefix || '')
|
|
230
381
|
const modelPrefix = config.addModelPrefix !== false ? '/${modelNameLower}' : ''
|
|
231
382
|
const basePath = customPrefix + modelPrefix
|
|
@@ -312,7 +463,7 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(config: ${m
|
|
|
312
463
|
next()
|
|
313
464
|
}
|
|
314
465
|
|
|
315
|
-
const setShape = (opConfig:
|
|
466
|
+
const setShape = (opConfig: NormalizedOp, opKind: OpKind): RequestHandler => {
|
|
316
467
|
return async (req, res, next) => {
|
|
317
468
|
try {
|
|
318
469
|
const locals = readLocals(res)
|
|
@@ -320,17 +471,32 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(config: ${m
|
|
|
320
471
|
if (merged) {
|
|
321
472
|
locals.routeConfig = { pagination: merged }
|
|
322
473
|
}
|
|
474
|
+
|
|
323
475
|
const headerName = config.guard?.variantHeader || 'x-api-variant'
|
|
324
476
|
const headerValue = req.get(headerName)
|
|
325
477
|
const caller = config.guard?.resolveVariant?.(req) ?? headerValue ?? undefined
|
|
326
|
-
if (caller) locals.guardCaller = caller
|
|
327
|
-
|
|
478
|
+
if (typeof caller === 'string') locals.guardCaller = caller
|
|
479
|
+
|
|
480
|
+
const resolution = resolveOperationVariantKey(opConfig.guardRouting, caller)
|
|
481
|
+
if (!resolution.ok) {
|
|
482
|
+
locals.guardVariantFailure = resolution
|
|
483
|
+
next()
|
|
484
|
+
return
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const resolvedKey =
|
|
488
|
+
opConfig.guardRouting.kind === 'named'
|
|
489
|
+
? resolution.key
|
|
490
|
+
: undefined
|
|
491
|
+
if (resolvedKey !== undefined) locals.guardVariantKey = resolvedKey
|
|
492
|
+
|
|
493
|
+
if (opConfig.guardShape) {
|
|
328
494
|
if (!DROP_GUARD) {
|
|
329
|
-
locals.guardShape = opConfig.
|
|
495
|
+
locals.guardShape = opConfig.guardShape
|
|
330
496
|
} else {
|
|
331
497
|
await applyDroppedGuard(
|
|
332
|
-
opConfig.
|
|
333
|
-
|
|
498
|
+
opConfig.guardShape,
|
|
499
|
+
resolvedKey,
|
|
334
500
|
buildResolveContext(req),
|
|
335
501
|
opKind,
|
|
336
502
|
{
|
|
@@ -352,6 +518,7 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(config: ${m
|
|
|
352
518
|
)
|
|
353
519
|
}
|
|
354
520
|
}
|
|
521
|
+
|
|
355
522
|
next()
|
|
356
523
|
} catch (err) {
|
|
357
524
|
next(mapError(err))
|
|
@@ -360,7 +527,7 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(config: ${m
|
|
|
360
527
|
}
|
|
361
528
|
|
|
362
529
|
const maybeProgressiveSSE = (
|
|
363
|
-
opConfig:
|
|
530
|
+
opConfig: NormalizedOp,
|
|
364
531
|
coreFn: (ctx: OperationContext) => Promise<unknown>,
|
|
365
532
|
baseOp: string,
|
|
366
533
|
): RequestHandler => {
|
|
@@ -370,8 +537,9 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(config: ${m
|
|
|
370
537
|
if (!acceptsEventStream(req.headers.accept)) return next()
|
|
371
538
|
|
|
372
539
|
const locals = readLocals(res)
|
|
373
|
-
const variant = locals.guardCaller
|
|
374
|
-
const progressiveConfig =
|
|
540
|
+
const variant = locals.guardVariantKey ?? locals.guardCaller
|
|
541
|
+
const progressiveConfig =
|
|
542
|
+
variant !== undefined ? opConfig.progressive?.[variant] : undefined
|
|
375
543
|
|
|
376
544
|
try {
|
|
377
545
|
if (!progressiveConfig || progressiveConfig.enabled === false) {
|
|
@@ -499,19 +667,20 @@ ${readOpBlocks}
|
|
|
499
667
|
${writeOpBlocks}
|
|
500
668
|
|
|
501
669
|
if (config.updateEach) {
|
|
502
|
-
const
|
|
503
|
-
|
|
670
|
+
const rawUpdateEach = config.updateEach as unknown as OperationConfigLike
|
|
671
|
+
validateUpdateEachConfig(rawUpdateEach, '${modelName}.updateEach')
|
|
672
|
+
const opConfig = normalizeExpressOperation(rawUpdateEach)
|
|
673
|
+
if (opConfig.operationBefore.length === 0 && _env.NODE_ENV !== 'production') {
|
|
504
674
|
console.warn(
|
|
505
675
|
'[${modelName}Router] updateEach is enabled without a before hook. ' +
|
|
506
676
|
'This endpoint bypasses guard shapes and should be protected by authentication middleware.',
|
|
507
677
|
)
|
|
508
678
|
}
|
|
509
|
-
const { before = [], after = [] } = opConfig
|
|
510
679
|
const path = basePath ? \`\${basePath}/each\` : '/each'
|
|
511
680
|
router.post(
|
|
512
681
|
path,
|
|
513
682
|
setShape(opConfig, 'noop'),
|
|
514
|
-
...
|
|
683
|
+
...opConfig.operationBefore,
|
|
515
684
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
516
685
|
try {
|
|
517
686
|
if (!Array.isArray(req.body)) {
|
|
@@ -524,7 +693,7 @@ ${writeOpBlocks}
|
|
|
524
693
|
next(mapError(err))
|
|
525
694
|
}
|
|
526
695
|
},
|
|
527
|
-
...
|
|
696
|
+
...opConfig.operationAfter,
|
|
528
697
|
respond,
|
|
529
698
|
)
|
|
530
699
|
}
|