prisma-generator-express 1.43.0 → 1.45.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/dist/generators/generateFastifyHandler.d.ts +2 -0
- package/dist/generators/generateFastifyHandler.js +4 -2
- package/dist/generators/generateFastifyHandler.js.map +1 -1
- package/dist/generators/generateHonoHandler.d.ts +2 -0
- package/dist/generators/generateHonoHandler.js +4 -2
- package/dist/generators/generateHonoHandler.js.map +1 -1
- package/dist/generators/generateRelationMeta.js +7 -3
- package/dist/generators/generateRelationMeta.js.map +1 -1
- package/dist/generators/generateRouteConfigType.js +9 -1
- package/dist/generators/generateRouteConfigType.js.map +1 -1
- package/dist/generators/generateRouter.js +47 -21
- package/dist/generators/generateRouter.js.map +1 -1
- package/dist/generators/generateUnifiedHandler.d.ts +2 -0
- package/dist/generators/generateUnifiedHandler.js +4 -2
- package/dist/generators/generateUnifiedHandler.js.map +1 -1
- package/dist/generators/generateUnifiedScalarUI.d.ts +2 -0
- package/dist/generators/generateUnifiedScalarUI.js +19 -16
- package/dist/generators/generateUnifiedScalarUI.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/utils/writeFileSafely.js +0 -3
- package/dist/utils/writeFileSafely.js.map +1 -1
- package/package.json +1 -1
- package/src/copy/autoIncludePlanner.ts +64 -9
- package/src/copy/autoIncludeRuntime.ts +91 -36
- package/src/copy/operationRuntime.ts +12 -1
- package/src/generators/generateFastifyHandler.ts +6 -2
- package/src/generators/generateHonoHandler.ts +6 -2
- package/src/generators/generateRelationMeta.ts +8 -2
- package/src/generators/generateRouteConfigType.ts +8 -1
- package/src/generators/generateRouter.ts +47 -21
- package/src/generators/generateUnifiedHandler.ts +6 -2
- package/src/generators/generateUnifiedScalarUI.ts +21 -16
- package/src/index.ts +6 -6
- package/src/utils/writeFileSafely.ts +0 -3
|
@@ -45,12 +45,29 @@ export type AutoIncludePlannerInput = {
|
|
|
45
45
|
export const DEFAULT_AUTO_INCLUDE_MAX_DEPTH = 3
|
|
46
46
|
export const DEFAULT_AUTO_INCLUDE_MAX_STAGES = 20
|
|
47
47
|
|
|
48
|
+
const ALLOWED_TO_ONE_ARGS = new Set(['select', 'include', 'omit'])
|
|
49
|
+
const ALLOWED_TO_MANY_ARGS = new Set([
|
|
50
|
+
'select',
|
|
51
|
+
'include',
|
|
52
|
+
'omit',
|
|
53
|
+
'where',
|
|
54
|
+
'orderBy',
|
|
55
|
+
'take',
|
|
56
|
+
'skip',
|
|
57
|
+
'cursor',
|
|
58
|
+
'distinct',
|
|
59
|
+
])
|
|
60
|
+
|
|
48
61
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
49
62
|
if (value === null || typeof value !== 'object') return false
|
|
50
63
|
if (Array.isArray(value)) return false
|
|
51
64
|
return true
|
|
52
65
|
}
|
|
53
66
|
|
|
67
|
+
function isPubliclySelected(projection: Record<string, unknown>, field: string): boolean {
|
|
68
|
+
return projection[field] === true
|
|
69
|
+
}
|
|
70
|
+
|
|
54
71
|
function hasCountKey(value: unknown): boolean {
|
|
55
72
|
if (!isPlainObject(value)) return false
|
|
56
73
|
return '_count' in value
|
|
@@ -117,11 +134,11 @@ function walk(
|
|
|
117
134
|
parentArgs: Record<string, unknown>,
|
|
118
135
|
depth: number,
|
|
119
136
|
): WalkResult {
|
|
120
|
-
if (depth
|
|
121
|
-
return { unsupportedReason: 'depth
|
|
137
|
+
if (depth >= ctx.maxDepth) {
|
|
138
|
+
return { unsupportedReason: 'nested depth reached maxDepth=' + ctx.maxDepth }
|
|
122
139
|
}
|
|
123
|
-
if (ctx.stages.length
|
|
124
|
-
return { unsupportedReason: 'stages
|
|
140
|
+
if (ctx.stages.length >= ctx.maxStages) {
|
|
141
|
+
return { unsupportedReason: 'stages reached maxStages=' + ctx.maxStages }
|
|
125
142
|
}
|
|
126
143
|
|
|
127
144
|
const model = ctx.models[modelName]
|
|
@@ -154,12 +171,17 @@ function walk(
|
|
|
154
171
|
const isSelectMode = isPlainObject(select)
|
|
155
172
|
const localOmit = isPlainObject(omit) ? omit : null
|
|
156
173
|
const updatedProjection: Record<string, unknown> = {}
|
|
157
|
-
const userFields = new Set(Object.keys(projection))
|
|
158
174
|
|
|
159
175
|
const relationBranches: Array<{ name: string; value: unknown }> = []
|
|
160
176
|
|
|
161
177
|
for (const [key, value] of Object.entries(projection)) {
|
|
162
178
|
if (model.relations[key]) {
|
|
179
|
+
if (value === false || value === null || value === undefined) {
|
|
180
|
+
if (isSelectMode) {
|
|
181
|
+
updatedProjection[key] = value
|
|
182
|
+
}
|
|
183
|
+
continue
|
|
184
|
+
}
|
|
163
185
|
relationBranches.push({ name: key, value })
|
|
164
186
|
} else {
|
|
165
187
|
updatedProjection[key] = value
|
|
@@ -171,6 +193,10 @@ function walk(
|
|
|
171
193
|
}
|
|
172
194
|
|
|
173
195
|
for (const branch of relationBranches) {
|
|
196
|
+
if (ctx.stages.length >= ctx.maxStages) {
|
|
197
|
+
return { unsupportedReason: 'stages reached maxStages=' + ctx.maxStages }
|
|
198
|
+
}
|
|
199
|
+
|
|
174
200
|
const relation = model.relations[branch.name]
|
|
175
201
|
|
|
176
202
|
if (relation.direction === 'implicitM2M') {
|
|
@@ -182,6 +208,19 @@ function walk(
|
|
|
182
208
|
if (relation.parentLinkFields.length !== relation.childLinkFields.length) {
|
|
183
209
|
return { unsupportedReason: 'mismatched link field counts for ' + relation.name }
|
|
184
210
|
}
|
|
211
|
+
if (!ctx.models[relation.type]) {
|
|
212
|
+
return {
|
|
213
|
+
unsupportedReason: 'target model ' + relation.type +
|
|
214
|
+
' not in relation metadata for ' + (parentPath ? parentPath + '.' : '') + branch.name,
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (branch.value !== true && !isPlainObject(branch.value)) {
|
|
219
|
+
return {
|
|
220
|
+
unsupportedReason: 'invalid relation projection value for ' + branch.name +
|
|
221
|
+
' (expected true or plain object)',
|
|
222
|
+
}
|
|
223
|
+
}
|
|
185
224
|
|
|
186
225
|
for (const linkField of relation.parentLinkFields) {
|
|
187
226
|
if (localOmit && localOmit[linkField] === true) {
|
|
@@ -191,16 +230,32 @@ function walk(
|
|
|
191
230
|
}
|
|
192
231
|
|
|
193
232
|
for (const linkField of relation.parentLinkFields) {
|
|
194
|
-
if (isSelectMode && !
|
|
233
|
+
if (isSelectMode && !isPubliclySelected(projection, linkField)) {
|
|
195
234
|
updatedProjection[linkField] = true
|
|
196
235
|
const fullPath = parentPath ? parentPath + '.' + linkField : linkField
|
|
197
236
|
ctx.internalFieldPaths.push(fullPath)
|
|
198
237
|
}
|
|
199
238
|
}
|
|
200
239
|
|
|
201
|
-
const relationArgs: Record<string, unknown> = branch.value === true
|
|
202
|
-
|
|
203
|
-
|
|
240
|
+
const relationArgs: Record<string, unknown> = branch.value === true ? {} : branch.value
|
|
241
|
+
|
|
242
|
+
const allowedArgs = relation.isList ? ALLOWED_TO_MANY_ARGS : ALLOWED_TO_ONE_ARGS
|
|
243
|
+
for (const key of Object.keys(relationArgs)) {
|
|
244
|
+
if (!allowedArgs.has(key)) {
|
|
245
|
+
return {
|
|
246
|
+
unsupportedReason: 'unsupported arg "' + key + '" for ' +
|
|
247
|
+
(relation.isList ? 'to-many' : 'to-one') + ' relation ' + relation.name,
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const targetModel = ctx.models[relation.type]
|
|
253
|
+
if (hasWhereLikeRelationReference(relationArgs, targetModel)) {
|
|
254
|
+
return {
|
|
255
|
+
unsupportedReason: 'nested relation used in where/orderBy/cursor for ' +
|
|
256
|
+
relation.name + ' is not supported in MVP',
|
|
257
|
+
}
|
|
258
|
+
}
|
|
204
259
|
|
|
205
260
|
const relationPath = parentPath ? parentPath + '.' + branch.name : branch.name
|
|
206
261
|
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
sendSSEError,
|
|
9
9
|
sendSSEProgress,
|
|
10
10
|
runSingleResultSSE,
|
|
11
|
+
emitTerminalSSEError,
|
|
11
12
|
setByPath,
|
|
12
13
|
getDelegate,
|
|
13
14
|
getExtendedClient,
|
|
@@ -35,6 +36,7 @@ export type RunAutoIncludeOptions = {
|
|
|
35
36
|
models: Record<string, ModelRelationMap>
|
|
36
37
|
variantConfig: AutoIncludeProgressiveVariantConfig
|
|
37
38
|
coreQueryFn: () => Promise<unknown>
|
|
39
|
+
signal?: AbortSignal
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
function isObject(v: unknown): v is Record<string, unknown> {
|
|
@@ -99,6 +101,29 @@ function emptyResultFor(isList: boolean): unknown {
|
|
|
99
101
|
return isList ? [] : null
|
|
100
102
|
}
|
|
101
103
|
|
|
104
|
+
function buildPublicForStage(
|
|
105
|
+
result: unknown,
|
|
106
|
+
internalFieldPaths: string[],
|
|
107
|
+
scopePath: string,
|
|
108
|
+
): unknown {
|
|
109
|
+
if (Array.isArray(result)) {
|
|
110
|
+
return result.map((item) => {
|
|
111
|
+
if (isObject(item)) {
|
|
112
|
+
const copy: Record<string, unknown> = { ...item }
|
|
113
|
+
stripInternalAtScope(copy, internalFieldPaths, scopePath)
|
|
114
|
+
return copy
|
|
115
|
+
}
|
|
116
|
+
return item
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
if (isObject(result)) {
|
|
120
|
+
const copy: Record<string, unknown> = { ...result }
|
|
121
|
+
stripInternalAtScope(copy, internalFieldPaths, scopePath)
|
|
122
|
+
return copy
|
|
123
|
+
}
|
|
124
|
+
return result
|
|
125
|
+
}
|
|
126
|
+
|
|
102
127
|
async function runOneStage(options: {
|
|
103
128
|
extended: unknown
|
|
104
129
|
models: Record<string, ModelRelationMap>
|
|
@@ -107,32 +132,36 @@ async function runOneStage(options: {
|
|
|
107
132
|
publicState: Record<string, unknown>
|
|
108
133
|
internalFieldPaths: string[]
|
|
109
134
|
res: Response
|
|
135
|
+
isAborted: () => boolean
|
|
110
136
|
}): Promise<void> {
|
|
111
|
-
const { extended, models, stage, internal, publicState, internalFieldPaths, res } = options
|
|
112
|
-
if (
|
|
137
|
+
const { extended, models, stage, internal, publicState, internalFieldPaths, res, isAborted } = options
|
|
138
|
+
if (isAborted()) return
|
|
113
139
|
|
|
114
140
|
const parentRaw = readPath(internal, stage.parentPath)
|
|
115
141
|
if (!isObject(parentRaw)) {
|
|
142
|
+
if (stage.parentPath !== '') {
|
|
143
|
+
return
|
|
144
|
+
}
|
|
116
145
|
const empty = emptyResultFor(stage.relationField.isList)
|
|
117
|
-
setByPath(publicState, stage.relationPath, empty)
|
|
118
|
-
sendSSEField(res, stage.relationPath, empty)
|
|
146
|
+
const applied = setByPath(publicState, stage.relationPath, empty)
|
|
147
|
+
if (applied) sendSSEField(res, stage.relationPath, empty)
|
|
119
148
|
return
|
|
120
149
|
}
|
|
121
150
|
|
|
122
151
|
const linkFilter = buildLinkFilter(stage, parentRaw)
|
|
123
152
|
if (!linkFilter) {
|
|
124
153
|
const empty = emptyResultFor(stage.relationField.isList)
|
|
125
|
-
setByPath(
|
|
126
|
-
|
|
154
|
+
const appliedInternal = setByPath(internal, stage.relationPath, empty)
|
|
155
|
+
const appliedPublic = setByPath(publicState, stage.relationPath, empty)
|
|
156
|
+
if (appliedInternal && appliedPublic) {
|
|
157
|
+
sendSSEField(res, stage.relationPath, empty)
|
|
158
|
+
}
|
|
127
159
|
return
|
|
128
160
|
}
|
|
129
161
|
|
|
130
162
|
const targetModel = models[stage.relationField.type]
|
|
131
163
|
if (!targetModel) {
|
|
132
|
-
|
|
133
|
-
setByPath(publicState, stage.relationPath, empty)
|
|
134
|
-
sendSSEField(res, stage.relationPath, empty)
|
|
135
|
-
return
|
|
164
|
+
throw new Error('Target model not in relation metadata: ' + stage.relationField.type)
|
|
136
165
|
}
|
|
137
166
|
|
|
138
167
|
const finalArgs: Record<string, unknown> = { ...stage.stageArgs }
|
|
@@ -142,20 +171,20 @@ async function runOneStage(options: {
|
|
|
142
171
|
const method: 'findMany' | 'findFirst' = stage.relationField.isList ? 'findMany' : 'findFirst'
|
|
143
172
|
const result = await delegate[method](finalArgs)
|
|
144
173
|
|
|
145
|
-
|
|
174
|
+
if (isAborted()) return
|
|
146
175
|
|
|
147
|
-
|
|
148
|
-
if (
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
176
|
+
const appliedInternal = setByPath(internal, stage.relationPath, result)
|
|
177
|
+
if (!appliedInternal) {
|
|
178
|
+
throw new Error('Failed to apply internal patch for ' + stage.relationPath)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const publicResult = buildPublicForStage(result, internalFieldPaths, stage.relationPath)
|
|
182
|
+
|
|
183
|
+
const appliedPublic = setByPath(publicState, stage.relationPath, publicResult)
|
|
184
|
+
if (!appliedPublic) {
|
|
185
|
+
throw new Error('Failed to apply public patch for ' + stage.relationPath)
|
|
156
186
|
}
|
|
157
187
|
|
|
158
|
-
setByPath(publicState, stage.relationPath, publicResult)
|
|
159
188
|
sendSSEField(res, stage.relationPath, publicResult)
|
|
160
189
|
}
|
|
161
190
|
|
|
@@ -194,9 +223,16 @@ async function runConcurrent<T>(
|
|
|
194
223
|
export async function runAutoIncludeProgressive(
|
|
195
224
|
options: RunAutoIncludeOptions,
|
|
196
225
|
): Promise<void> {
|
|
197
|
-
const { req, res, ctx, args, baseOp, modelName, delegateKey, models, variantConfig, coreQueryFn } = options
|
|
226
|
+
const { req, res, ctx, args, baseOp, modelName, delegateKey, models, variantConfig, coreQueryFn, signal } = options
|
|
227
|
+
|
|
228
|
+
const isClientGone = () =>
|
|
229
|
+
signal?.aborted === true || res.writableEnded || res.destroyed
|
|
198
230
|
|
|
199
231
|
if (ctx.guardShape) {
|
|
232
|
+
if (variantConfig.fallback === 'error') {
|
|
233
|
+
emitTerminalSSEError(res, 'auto-progressive fallback: guard shape disables auto-include')
|
|
234
|
+
return
|
|
235
|
+
}
|
|
200
236
|
return runSingleResultSSE({ req, res, coreQueryFn })
|
|
201
237
|
}
|
|
202
238
|
|
|
@@ -208,14 +244,7 @@ export async function runAutoIncludeProgressive(
|
|
|
208
244
|
|
|
209
245
|
if (plan.unsupportedReason) {
|
|
210
246
|
if (variantConfig.fallback === 'error') {
|
|
211
|
-
|
|
212
|
-
try {
|
|
213
|
-
initSSE(res)
|
|
214
|
-
keepalive = startSSEKeepalive(res)
|
|
215
|
-
sendSSEError(res, plan.unsupportedReason)
|
|
216
|
-
} finally {
|
|
217
|
-
endSSE(res, keepalive)
|
|
218
|
-
}
|
|
247
|
+
emitTerminalSSEError(res, plan.unsupportedReason)
|
|
219
248
|
return
|
|
220
249
|
}
|
|
221
250
|
return runSingleResultSSE({ req, res, coreQueryFn })
|
|
@@ -229,15 +258,18 @@ export async function runAutoIncludeProgressive(
|
|
|
229
258
|
try {
|
|
230
259
|
initSSE(res)
|
|
231
260
|
keepalive = startSSEKeepalive(res)
|
|
232
|
-
if (
|
|
261
|
+
if (isClientGone()) return
|
|
233
262
|
|
|
234
263
|
const extended = await getExtendedClient(ctx)
|
|
264
|
+
if (isClientGone()) return
|
|
265
|
+
|
|
235
266
|
const rootDelegate = getDelegate(extended, delegateKey)
|
|
236
267
|
|
|
237
268
|
let rootResult: unknown
|
|
238
269
|
try {
|
|
239
270
|
rootResult = await rootDelegate[baseOp](plan.rootArgs)
|
|
240
271
|
} catch (err) {
|
|
272
|
+
if (isClientGone()) return
|
|
241
273
|
const code = (err as { code?: string } | null)?.code
|
|
242
274
|
const isOrThrow = baseOp === 'findUniqueOrThrow' || baseOp === 'findFirstOrThrow'
|
|
243
275
|
if (isOrThrow && code === 'P2025') {
|
|
@@ -249,7 +281,7 @@ export async function runAutoIncludeProgressive(
|
|
|
249
281
|
return
|
|
250
282
|
}
|
|
251
283
|
|
|
252
|
-
if (
|
|
284
|
+
if (isClientGone()) return
|
|
253
285
|
|
|
254
286
|
if (rootResult === null || !isObject(rootResult)) {
|
|
255
287
|
sendSSEResult(res, null)
|
|
@@ -262,17 +294,28 @@ export async function runAutoIncludeProgressive(
|
|
|
262
294
|
|
|
263
295
|
const publicState: Record<string, unknown> = { ...publicRoot }
|
|
264
296
|
for (const [k, v] of Object.entries(publicRoot)) {
|
|
297
|
+
if (isClientGone()) return
|
|
265
298
|
sendSSEField(res, k, v)
|
|
266
299
|
}
|
|
267
300
|
|
|
301
|
+
if (isClientGone()) return
|
|
268
302
|
sendSSEProgress(res, 'root', 0, plan.stages.length)
|
|
269
303
|
|
|
270
304
|
const groups = groupStagesByDepth(plan.stages)
|
|
271
305
|
let completed = 0
|
|
306
|
+
let stageErrorMessage: string | null = null
|
|
307
|
+
const isAborted = () =>
|
|
308
|
+
stageErrorMessage !== null ||
|
|
309
|
+
signal?.aborted === true ||
|
|
310
|
+
res.writableEnded ||
|
|
311
|
+
res.destroyed
|
|
272
312
|
|
|
273
313
|
for (const group of groups) {
|
|
274
|
-
if (
|
|
314
|
+
if (isClientGone()) return
|
|
315
|
+
if (stageErrorMessage) break
|
|
316
|
+
|
|
275
317
|
await runConcurrent(group, STAGE_CONCURRENCY, async (stage) => {
|
|
318
|
+
if (isAborted()) return
|
|
276
319
|
try {
|
|
277
320
|
await runOneStage({
|
|
278
321
|
extended,
|
|
@@ -282,21 +325,33 @@ export async function runAutoIncludeProgressive(
|
|
|
282
325
|
publicState,
|
|
283
326
|
internalFieldPaths: plan.internalFieldPaths,
|
|
284
327
|
res,
|
|
328
|
+
isAborted,
|
|
285
329
|
})
|
|
286
330
|
} catch (err) {
|
|
331
|
+
if (isAborted()) return
|
|
287
332
|
console.error('[auto-progressive] stage failed:', stage.relationPath, err)
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
sendSSEField(res, stage.relationPath, empty)
|
|
333
|
+
stageErrorMessage = 'Could not load progressive response'
|
|
334
|
+
return
|
|
291
335
|
}
|
|
336
|
+
if (isAborted()) return
|
|
292
337
|
completed++
|
|
293
338
|
sendSSEProgress(res, stage.relationPath, completed, plan.stages.length)
|
|
294
339
|
})
|
|
295
340
|
}
|
|
296
341
|
|
|
342
|
+
if (isClientGone()) return
|
|
343
|
+
|
|
344
|
+
if (stageErrorMessage) {
|
|
345
|
+
if (!res.writableEnded && !res.destroyed) {
|
|
346
|
+
sendSSEError(res, stageErrorMessage)
|
|
347
|
+
}
|
|
348
|
+
return
|
|
349
|
+
}
|
|
350
|
+
|
|
297
351
|
if (res.writableEnded || res.destroyed) return
|
|
298
352
|
sendSSEResult(res, publicState)
|
|
299
353
|
} catch (err) {
|
|
354
|
+
if (isClientGone()) return
|
|
300
355
|
console.error('[auto-progressive] dispatch error:', err)
|
|
301
356
|
if (!res.writableEnded && !res.destroyed) {
|
|
302
357
|
sendSSEError(res, 'Internal server error')
|
|
@@ -426,7 +426,7 @@ type SseWritable = {
|
|
|
426
426
|
destroyed: boolean
|
|
427
427
|
}
|
|
428
428
|
|
|
429
|
-
function removeReqCloseListener(req: EventEmitterLike, listener: () => void): void {
|
|
429
|
+
export function removeReqCloseListener(req: EventEmitterLike, listener: () => void): void {
|
|
430
430
|
if (typeof req.off === 'function') {
|
|
431
431
|
req.off('close', listener)
|
|
432
432
|
} else if (typeof req.removeListener === 'function') {
|
|
@@ -509,6 +509,17 @@ export function endSSE(res: SseWritable, keepaliveHandle: IntervalHandle | null)
|
|
|
509
509
|
}
|
|
510
510
|
}
|
|
511
511
|
|
|
512
|
+
export function emitTerminalSSEError(res: SseWritable, message: string): void {
|
|
513
|
+
let keepalive: IntervalHandle | null = null
|
|
514
|
+
try {
|
|
515
|
+
initSSE(res)
|
|
516
|
+
keepalive = startSSEKeepalive(res)
|
|
517
|
+
sendSSEError(res, message)
|
|
518
|
+
} finally {
|
|
519
|
+
endSSE(res, keepalive)
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
512
523
|
export interface RunSingleResultSSEOptions {
|
|
513
524
|
req: EventEmitterLike
|
|
514
525
|
res: SseWritable
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { DMMF } from '@prisma/generator-helper'
|
|
2
2
|
import { toCamelCase } from '../utils/strings'
|
|
3
|
+
import { ImportStyle } from '../utils/resolveImportStyle'
|
|
4
|
+
import { importExt } from '../utils/importExt'
|
|
3
5
|
|
|
4
6
|
const CORE_NAME_MAP: Record<string, string> = {
|
|
5
7
|
delete: 'deleteUnique',
|
|
@@ -41,7 +43,9 @@ const CREATED_OPS = new Set([
|
|
|
41
43
|
|
|
42
44
|
export function generateFastifyHandler(options: {
|
|
43
45
|
model: DMMF.Model
|
|
46
|
+
importStyle: ImportStyle
|
|
44
47
|
}): string {
|
|
48
|
+
const ext = importExt(options.importStyle)
|
|
45
49
|
const modelName = options.model.name
|
|
46
50
|
const prefix = toCamelCase(modelName)
|
|
47
51
|
|
|
@@ -75,8 +79,8 @@ export async function ${exportName}(
|
|
|
75
79
|
}).join('\n')
|
|
76
80
|
|
|
77
81
|
return `import type { FastifyRequest, FastifyReply } from 'fastify'
|
|
78
|
-
import * as core from './${modelName}Core'
|
|
79
|
-
import type { OperationContext } from '../operationRuntime'
|
|
82
|
+
import * as core from './${modelName}Core${ext}'
|
|
83
|
+
import type { OperationContext } from '../operationRuntime${ext}'
|
|
80
84
|
|
|
81
85
|
type FastifyExtended = FastifyRequest & {
|
|
82
86
|
prisma?: unknown
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { DMMF } from '@prisma/generator-helper'
|
|
2
2
|
import { toCamelCase } from '../utils/strings'
|
|
3
|
+
import { ImportStyle } from '../utils/resolveImportStyle'
|
|
4
|
+
import { importExt } from '../utils/importExt'
|
|
3
5
|
|
|
4
6
|
const CORE_NAME_MAP: Record<string, string> = {
|
|
5
7
|
delete: 'deleteUnique',
|
|
@@ -41,7 +43,9 @@ const CREATED_OPS = new Set([
|
|
|
41
43
|
|
|
42
44
|
export function generateHonoHandler(options: {
|
|
43
45
|
model: DMMF.Model
|
|
46
|
+
importStyle: ImportStyle
|
|
44
47
|
}): string {
|
|
48
|
+
const ext = importExt(options.importStyle)
|
|
45
49
|
const modelName = options.model.name
|
|
46
50
|
const prefix = toCamelCase(modelName)
|
|
47
51
|
|
|
@@ -68,8 +72,8 @@ export async function ${exportName}(c: Context<HonoEnv>): Promise<void> {
|
|
|
68
72
|
}).join('\n')
|
|
69
73
|
|
|
70
74
|
return `import type { Context } from 'hono'
|
|
71
|
-
import * as core from './${modelName}Core'
|
|
72
|
-
import type { OperationContext } from '../operationRuntime'
|
|
75
|
+
import * as core from './${modelName}Core${ext}'
|
|
76
|
+
import type { OperationContext } from '../operationRuntime${ext}'
|
|
73
77
|
|
|
74
78
|
type HonoVariables = {
|
|
75
79
|
prisma: unknown
|
|
@@ -27,12 +27,18 @@ function findOppositeField(
|
|
|
27
27
|
targetModelName: string,
|
|
28
28
|
relationName: string | undefined,
|
|
29
29
|
selfModelName: string,
|
|
30
|
+
selfFieldName: string,
|
|
30
31
|
): DMMF.Field | null {
|
|
31
32
|
if (!relationName) return null
|
|
32
33
|
const target = models.find((m) => m.name === targetModelName)
|
|
33
34
|
if (!target) return null
|
|
35
|
+
const isSelfRelation = targetModelName === selfModelName
|
|
34
36
|
return target.fields.find(
|
|
35
|
-
(f) =>
|
|
37
|
+
(f) =>
|
|
38
|
+
f.kind === 'object' &&
|
|
39
|
+
f.relationName === relationName &&
|
|
40
|
+
f.type === selfModelName &&
|
|
41
|
+
!(isSelfRelation && f.name === selfFieldName),
|
|
36
42
|
) || null
|
|
37
43
|
}
|
|
38
44
|
|
|
@@ -56,7 +62,7 @@ function computeRelation(
|
|
|
56
62
|
}
|
|
57
63
|
}
|
|
58
64
|
|
|
59
|
-
const opposite = findOppositeField(models, field.type, field.relationName, selfModelName)
|
|
65
|
+
const opposite = findOppositeField(models, field.type, field.relationName, selfModelName, field.name)
|
|
60
66
|
if (opposite) {
|
|
61
67
|
const oppFrom = (opposite.relationFromFields ?? []) as string[]
|
|
62
68
|
const oppTo = (opposite.relationToFields ?? []) as string[]
|
|
@@ -42,6 +42,12 @@ function capitalize(s: string): string {
|
|
|
42
42
|
return s.charAt(0).toUpperCase() + s.slice(1)
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
function requestTypeFor(target: Target): string {
|
|
46
|
+
if (target === 'fastify') return `import('fastify').FastifyRequest`
|
|
47
|
+
if (target === 'hono') return `import('hono').Context`
|
|
48
|
+
return `import('express').Request`
|
|
49
|
+
}
|
|
50
|
+
|
|
45
51
|
export function generateRouteConfigType(
|
|
46
52
|
modelName: string,
|
|
47
53
|
hookHandlerType: string,
|
|
@@ -52,6 +58,7 @@ export function generateRouteConfigType(
|
|
|
52
58
|
const ext = importExt(importStyle)
|
|
53
59
|
const m = modelName
|
|
54
60
|
const supportsProgressive = target === 'express'
|
|
61
|
+
const requestType = requestTypeFor(target)
|
|
55
62
|
|
|
56
63
|
const progressiveTypeImport = supportsProgressive
|
|
57
64
|
? `import type { ProgressiveVariantConfig, ProgressiveStage } from '../routeConfig.target${ext}'\n\n`
|
|
@@ -90,7 +97,7 @@ export function generateRouteConfigType(
|
|
|
90
97
|
` | ${omitKeys}\n` +
|
|
91
98
|
` | 'resolveContext'\n` +
|
|
92
99
|
`> & {\n` +
|
|
93
|
-
` resolveContext?: (request:
|
|
100
|
+
` resolveContext?: (request: ${requestType}) => TCtx | Promise<TCtx>\n` +
|
|
94
101
|
`${overrides}\n}\n`
|
|
95
102
|
)
|
|
96
103
|
}
|
|
@@ -70,7 +70,7 @@ import {
|
|
|
70
70
|
${prefix}GroupBy,
|
|
71
71
|
} from './${modelName}Handlers${ext}'
|
|
72
72
|
import * as core from './${modelName}Core${ext}'
|
|
73
|
-
import type { RouteConfig } from '../routeConfig.target${ext}'
|
|
73
|
+
import type { RouteConfig, QueryBuilderConfig } from '../routeConfig.target${ext}'
|
|
74
74
|
import { parseQueryParams } from '../parseQueryParams${ext}'
|
|
75
75
|
import { sanitizeKeys } from '../misc${ext}'
|
|
76
76
|
import { buildModelOpenApi } from '../buildModelOpenApi${ext}'
|
|
@@ -80,6 +80,10 @@ import {
|
|
|
80
80
|
acceptsEventStream,
|
|
81
81
|
runProgressiveEndpoint,
|
|
82
82
|
runSingleResultSSE,
|
|
83
|
+
emitTerminalSSEError,
|
|
84
|
+
removeReqCloseListener,
|
|
85
|
+
mapError,
|
|
86
|
+
HttpError,
|
|
83
87
|
} from '../operationRuntime${ext}'
|
|
84
88
|
import { relationModels } from '../relationModels${ext}'
|
|
85
89
|
import { runAutoIncludeProgressive } from '../autoIncludeRuntime${ext}'
|
|
@@ -126,14 +130,14 @@ function normalizePrefix(p: string): string {
|
|
|
126
130
|
return result
|
|
127
131
|
}
|
|
128
132
|
|
|
129
|
-
function isQueryBuilderEnabled(config:
|
|
133
|
+
function isQueryBuilderEnabled(config: { queryBuilder?: QueryBuilderConfig | false }): boolean {
|
|
130
134
|
if (config.queryBuilder === false) return false
|
|
131
135
|
if (typeof config.queryBuilder === 'object' && config.queryBuilder.enabled === false) return false
|
|
132
136
|
if (_env.NODE_ENV === 'production') return false
|
|
133
137
|
return true
|
|
134
138
|
}
|
|
135
139
|
|
|
136
|
-
function getQueryBuilderConfig(config:
|
|
140
|
+
function getQueryBuilderConfig(config: { queryBuilder?: QueryBuilderConfig | false }) {
|
|
137
141
|
if (config.queryBuilder === false) return null
|
|
138
142
|
if (typeof config.queryBuilder === 'object') return config.queryBuilder
|
|
139
143
|
return {}
|
|
@@ -215,7 +219,7 @@ export function ${routerFunctionName}<TCtx = unknown>(config: ${modelName}RouteC
|
|
|
215
219
|
}
|
|
216
220
|
}
|
|
217
221
|
|
|
218
|
-
|
|
222
|
+
const maybeProgressiveSSE = (
|
|
219
223
|
opConfig: OperationConfigLike,
|
|
220
224
|
coreFn: (ctx: OperationContext) => Promise<unknown>,
|
|
221
225
|
baseOp: string,
|
|
@@ -246,7 +250,8 @@ export function ${routerFunctionName}<TCtx = unknown>(config: ${modelName}RouteC
|
|
|
246
250
|
|
|
247
251
|
if (!isSingleRecordRead) {
|
|
248
252
|
if (progressiveConfig.fallback === 'error') {
|
|
249
|
-
|
|
253
|
+
emitTerminalSSEError(res, 'auto-progressive fallback: operation not single-record')
|
|
254
|
+
return
|
|
250
255
|
}
|
|
251
256
|
await runSingleResultSSE({
|
|
252
257
|
req,
|
|
@@ -256,25 +261,40 @@ export function ${routerFunctionName}<TCtx = unknown>(config: ${modelName}RouteC
|
|
|
256
261
|
return
|
|
257
262
|
}
|
|
258
263
|
|
|
259
|
-
|
|
264
|
+
const ctx = buildContext(req, res)
|
|
265
|
+
const args = (locals.parsedQuery ?? {}) as Record<string, unknown>
|
|
266
|
+
const controller = new AbortController()
|
|
267
|
+
const onClose = () => controller.abort()
|
|
268
|
+
req.on('close', onClose)
|
|
269
|
+
try {
|
|
270
|
+
await runAutoIncludeProgressive({
|
|
271
|
+
req,
|
|
272
|
+
res,
|
|
273
|
+
ctx,
|
|
274
|
+
args,
|
|
275
|
+
baseOp: baseOp as 'findUnique' | 'findUniqueOrThrow' | 'findFirst' | 'findFirstOrThrow',
|
|
276
|
+
modelName: '${modelName}',
|
|
277
|
+
delegateKey: '${delegateKey}',
|
|
278
|
+
models: relationModels,
|
|
279
|
+
variantConfig: progressiveConfig,
|
|
280
|
+
coreQueryFn: () => coreFn(ctx),
|
|
281
|
+
signal: controller.signal,
|
|
282
|
+
})
|
|
283
|
+
} finally {
|
|
284
|
+
removeReqCloseListener(req, onClose)
|
|
285
|
+
}
|
|
286
|
+
return
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (!Array.isArray(progressiveConfig.stages)) {
|
|
290
|
+
await runSingleResultSSE({
|
|
260
291
|
req,
|
|
261
292
|
res,
|
|
262
|
-
ctx: buildContext(req, res),
|
|
263
|
-
args: locals.parsedQuery ?? {},
|
|
264
|
-
baseOp: baseOp as 'findUnique' | 'findUniqueOrThrow' | 'findFirst' | 'findFirstOrThrow',
|
|
265
|
-
modelName: '${modelName}',
|
|
266
|
-
delegateKey: '${delegateKey}',
|
|
267
|
-
models: relationModels,
|
|
268
|
-
variantConfig: progressiveConfig,
|
|
269
293
|
coreQueryFn: () => coreFn(buildContext(req, res)),
|
|
270
294
|
})
|
|
271
295
|
return
|
|
272
296
|
}
|
|
273
297
|
|
|
274
|
-
if (!Array.isArray(progressiveConfig.stages)) {
|
|
275
|
-
return next({ status: 500, message: 'Progressive endpoint requires stages array' })
|
|
276
|
-
}
|
|
277
|
-
|
|
278
298
|
const stageRegistry = opConfig.progressiveStages ?? {}
|
|
279
299
|
const missingStage = progressiveConfig.stages.find(
|
|
280
300
|
(name: string) => typeof stageRegistry[name] !== 'function',
|
|
@@ -454,10 +474,16 @@ export function ${routerFunctionName}<TCtx = unknown>(config: ${modelName}RouteC
|
|
|
454
474
|
}
|
|
455
475
|
|
|
456
476
|
router.use((err: unknown, _req: Request, res: Response, next: NextFunction) => {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
if (
|
|
477
|
+
let httpError: HttpError
|
|
478
|
+
if (err instanceof HttpError) {
|
|
479
|
+
httpError = err
|
|
480
|
+
} else if (err && typeof err === 'object' && typeof (err as { status?: number }).status === 'number') {
|
|
481
|
+
const e = err as { status: number; message?: string }
|
|
482
|
+
httpError = new HttpError(e.status, e.message || 'Internal server error')
|
|
483
|
+
} else {
|
|
484
|
+
httpError = mapError(err)
|
|
485
|
+
}
|
|
486
|
+
if (!res.headersSent) return res.status(httpError.status).json({ message: httpError.message })
|
|
461
487
|
next(err)
|
|
462
488
|
})
|
|
463
489
|
|