prisma-generator-express 1.41.0 → 1.43.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.js +17 -4
- package/dist/generators/generateFastifyHandler.js.map +1 -1
- package/dist/generators/generateHonoHandler.js +4 -4
- package/dist/generators/generateOperationCore.d.ts +0 -1
- package/dist/generators/generateOperationCore.js +34 -546
- package/dist/generators/generateOperationCore.js.map +1 -1
- package/dist/generators/generateRelationMeta.d.ts +13 -0
- package/dist/generators/generateRelationMeta.js +106 -0
- package/dist/generators/generateRelationMeta.js.map +1 -0
- package/dist/generators/generateRouteConfigType.js +6 -6
- package/dist/generators/generateRouteConfigType.js.map +1 -1
- package/dist/generators/generateRouter.js +141 -60
- package/dist/generators/generateRouter.js.map +1 -1
- package/dist/generators/generateRouterFastify.js +127 -384
- package/dist/generators/generateRouterFastify.js.map +1 -1
- package/dist/generators/generateRouterHono.js +48 -36
- package/dist/generators/generateRouterHono.js.map +1 -1
- package/dist/generators/generateUnifiedHandler.js +24 -8
- package/dist/generators/generateUnifiedHandler.js.map +1 -1
- package/dist/index.js +21 -5
- package/dist/index.js.map +1 -1
- package/dist/utils/copyFiles.js +12 -0
- package/dist/utils/copyFiles.js.map +1 -1
- package/dist/utils/writeFileSafely.js +3 -0
- package/dist/utils/writeFileSafely.js.map +1 -1
- package/package.json +1 -1
- package/src/copy/autoIncludePlanner.ts +299 -0
- package/src/copy/autoIncludeRuntime.ts +307 -0
- package/src/copy/operationRuntime.ts +603 -0
- package/src/copy/routeConfig.express.ts +5 -3
- package/src/copy/routeConfig.fastify.ts +2 -2
- package/src/copy/routeConfig.hono.ts +3 -3
- package/src/copy/routeConfig.ts +20 -9
- package/src/generators/generateFastifyHandler.ts +17 -4
- package/src/generators/generateHonoHandler.ts +4 -4
- package/src/generators/generateOperationCore.ts +34 -546
- package/src/generators/generateRelationMeta.ts +154 -0
- package/src/generators/generateRouteConfigType.ts +7 -7
- package/src/generators/generateRouter.ts +141 -60
- package/src/generators/generateRouterFastify.ts +127 -384
- package/src/generators/generateRouterHono.ts +48 -36
- package/src/generators/generateUnifiedHandler.ts +24 -8
- package/src/index.ts +25 -7
- package/src/utils/copyFiles.ts +13 -0
- package/src/utils/writeFileSafely.ts +3 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
export type ModelRelationDirection = 'parentOwnsFk' | 'childOwnsFk' | 'implicitM2M'
|
|
2
|
+
|
|
3
|
+
export type ModelRelationField = {
|
|
4
|
+
name: string
|
|
5
|
+
type: string
|
|
6
|
+
isList: boolean
|
|
7
|
+
isRequired: boolean
|
|
8
|
+
direction: ModelRelationDirection
|
|
9
|
+
parentLinkFields: string[]
|
|
10
|
+
childLinkFields: string[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type ModelRelationMap = {
|
|
14
|
+
name: string
|
|
15
|
+
delegateKey: string
|
|
16
|
+
scalarFields: string[]
|
|
17
|
+
idFields: string[]
|
|
18
|
+
relations: Record<string, ModelRelationField>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type AutoIncludeStage = {
|
|
22
|
+
relationPath: string
|
|
23
|
+
parentPath: string
|
|
24
|
+
relationName: string
|
|
25
|
+
relationField: ModelRelationField
|
|
26
|
+
stageArgs: Record<string, unknown>
|
|
27
|
+
depth: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type AutoIncludePlan = {
|
|
31
|
+
rootArgs: Record<string, unknown>
|
|
32
|
+
stages: AutoIncludeStage[]
|
|
33
|
+
internalFieldPaths: string[]
|
|
34
|
+
unsupportedReason?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type AutoIncludePlannerInput = {
|
|
38
|
+
rootModelName: string
|
|
39
|
+
models: Record<string, ModelRelationMap>
|
|
40
|
+
args: Record<string, unknown>
|
|
41
|
+
maxDepth?: number
|
|
42
|
+
maxStages?: number
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const DEFAULT_AUTO_INCLUDE_MAX_DEPTH = 3
|
|
46
|
+
export const DEFAULT_AUTO_INCLUDE_MAX_STAGES = 20
|
|
47
|
+
|
|
48
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
49
|
+
if (value === null || typeof value !== 'object') return false
|
|
50
|
+
if (Array.isArray(value)) return false
|
|
51
|
+
return true
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function hasCountKey(value: unknown): boolean {
|
|
55
|
+
if (!isPlainObject(value)) return false
|
|
56
|
+
return '_count' in value
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function hasWhereLikeRelationReference(
|
|
60
|
+
args: Record<string, unknown>,
|
|
61
|
+
model: ModelRelationMap,
|
|
62
|
+
): boolean {
|
|
63
|
+
const checkObject = (obj: unknown): boolean => {
|
|
64
|
+
if (!isPlainObject(obj)) return false
|
|
65
|
+
for (const key of Object.keys(obj)) {
|
|
66
|
+
if (model.relations[key]) return true
|
|
67
|
+
if (key === 'AND' || key === 'OR' || key === 'NOT') {
|
|
68
|
+
const sub = obj[key]
|
|
69
|
+
if (Array.isArray(sub)) {
|
|
70
|
+
for (const item of sub) if (checkObject(item)) return true
|
|
71
|
+
} else if (checkObject(sub)) {
|
|
72
|
+
return true
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return false
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (checkObject(args.where)) return true
|
|
80
|
+
|
|
81
|
+
if (args.orderBy) {
|
|
82
|
+
const obs = Array.isArray(args.orderBy) ? args.orderBy : [args.orderBy]
|
|
83
|
+
for (const ob of obs) {
|
|
84
|
+
if (!isPlainObject(ob)) continue
|
|
85
|
+
for (const key of Object.keys(ob)) {
|
|
86
|
+
if (model.relations[key]) return true
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (isPlainObject(args.cursor)) {
|
|
92
|
+
for (const key of Object.keys(args.cursor)) {
|
|
93
|
+
if (model.relations[key]) return true
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return false
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
type WalkContext = {
|
|
101
|
+
models: Record<string, ModelRelationMap>
|
|
102
|
+
stages: AutoIncludeStage[]
|
|
103
|
+
internalFieldPaths: string[]
|
|
104
|
+
maxDepth: number
|
|
105
|
+
maxStages: number
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
type WalkResult = {
|
|
109
|
+
unsupportedReason?: string
|
|
110
|
+
projectionAfterStrip?: Record<string, unknown>
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function walk(
|
|
114
|
+
ctx: WalkContext,
|
|
115
|
+
modelName: string,
|
|
116
|
+
parentPath: string,
|
|
117
|
+
parentArgs: Record<string, unknown>,
|
|
118
|
+
depth: number,
|
|
119
|
+
): WalkResult {
|
|
120
|
+
if (depth > ctx.maxDepth) {
|
|
121
|
+
return { unsupportedReason: 'depth exceeds maxDepth=' + ctx.maxDepth }
|
|
122
|
+
}
|
|
123
|
+
if (ctx.stages.length > ctx.maxStages) {
|
|
124
|
+
return { unsupportedReason: 'stages exceed maxStages=' + ctx.maxStages }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const model = ctx.models[modelName]
|
|
128
|
+
if (!model) {
|
|
129
|
+
return { unsupportedReason: 'model ' + modelName + ' not in relation metadata' }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const select = parentArgs.select
|
|
133
|
+
const include = parentArgs.include
|
|
134
|
+
const omit = parentArgs.omit
|
|
135
|
+
|
|
136
|
+
if (isPlainObject(select) && isPlainObject(include)) {
|
|
137
|
+
return { unsupportedReason: 'select+include at same level' }
|
|
138
|
+
}
|
|
139
|
+
if (isPlainObject(select) && isPlainObject(omit)) {
|
|
140
|
+
return { unsupportedReason: 'select+omit at same level' }
|
|
141
|
+
}
|
|
142
|
+
if (hasCountKey(select) || hasCountKey(include)) {
|
|
143
|
+
return { unsupportedReason: '_count not supported in MVP' }
|
|
144
|
+
}
|
|
145
|
+
if (hasWhereLikeRelationReference(parentArgs, model)) {
|
|
146
|
+
return { unsupportedReason: 'relation used in where/orderBy/cursor is not supported in MVP' }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const projection = isPlainObject(select) ? select : (isPlainObject(include) ? include : null)
|
|
150
|
+
if (!projection) {
|
|
151
|
+
return {}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const isSelectMode = isPlainObject(select)
|
|
155
|
+
const localOmit = isPlainObject(omit) ? omit : null
|
|
156
|
+
const updatedProjection: Record<string, unknown> = {}
|
|
157
|
+
const userFields = new Set(Object.keys(projection))
|
|
158
|
+
|
|
159
|
+
const relationBranches: Array<{ name: string; value: unknown }> = []
|
|
160
|
+
|
|
161
|
+
for (const [key, value] of Object.entries(projection)) {
|
|
162
|
+
if (model.relations[key]) {
|
|
163
|
+
relationBranches.push({ name: key, value })
|
|
164
|
+
} else {
|
|
165
|
+
updatedProjection[key] = value
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (relationBranches.length === 0) {
|
|
170
|
+
return { projectionAfterStrip: isSelectMode ? updatedProjection : undefined }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
for (const branch of relationBranches) {
|
|
174
|
+
const relation = model.relations[branch.name]
|
|
175
|
+
|
|
176
|
+
if (relation.direction === 'implicitM2M') {
|
|
177
|
+
return { unsupportedReason: 'implicit many-to-many not supported in MVP' }
|
|
178
|
+
}
|
|
179
|
+
if (relation.parentLinkFields.length === 0 || relation.childLinkFields.length === 0) {
|
|
180
|
+
return { unsupportedReason: 'ambiguous relation metadata for ' + relation.name }
|
|
181
|
+
}
|
|
182
|
+
if (relation.parentLinkFields.length !== relation.childLinkFields.length) {
|
|
183
|
+
return { unsupportedReason: 'mismatched link field counts for ' + relation.name }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
for (const linkField of relation.parentLinkFields) {
|
|
187
|
+
if (localOmit && localOmit[linkField] === true) {
|
|
188
|
+
const where = parentPath ? parentPath + '.' : 'root '
|
|
189
|
+
return { unsupportedReason: 'required parent link field omitted: ' + where + linkField }
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
for (const linkField of relation.parentLinkFields) {
|
|
194
|
+
if (isSelectMode && !userFields.has(linkField)) {
|
|
195
|
+
updatedProjection[linkField] = true
|
|
196
|
+
const fullPath = parentPath ? parentPath + '.' + linkField : linkField
|
|
197
|
+
ctx.internalFieldPaths.push(fullPath)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const relationArgs: Record<string, unknown> = branch.value === true
|
|
202
|
+
? {}
|
|
203
|
+
: (isPlainObject(branch.value) ? branch.value : {})
|
|
204
|
+
|
|
205
|
+
const relationPath = parentPath ? parentPath + '.' + branch.name : branch.name
|
|
206
|
+
|
|
207
|
+
const stageArgs: Record<string, unknown> = {}
|
|
208
|
+
for (const [k, v] of Object.entries(relationArgs)) {
|
|
209
|
+
if (k === 'select' || k === 'include' || k === 'omit') continue
|
|
210
|
+
stageArgs[k] = v
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const stageIndex = ctx.stages.length
|
|
214
|
+
|
|
215
|
+
ctx.stages.push({
|
|
216
|
+
relationPath,
|
|
217
|
+
parentPath,
|
|
218
|
+
relationName: branch.name,
|
|
219
|
+
relationField: relation,
|
|
220
|
+
stageArgs,
|
|
221
|
+
depth: depth + 1,
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
const hasNestedProjection =
|
|
225
|
+
isPlainObject(relationArgs.select) ||
|
|
226
|
+
isPlainObject(relationArgs.include) ||
|
|
227
|
+
isPlainObject(relationArgs.omit)
|
|
228
|
+
|
|
229
|
+
if (hasNestedProjection) {
|
|
230
|
+
const stagesBeforeRecursion = ctx.stages.length
|
|
231
|
+
|
|
232
|
+
const nested = walk(ctx, relation.type, relationPath, relationArgs, depth + 1)
|
|
233
|
+
if (nested.unsupportedReason) return nested
|
|
234
|
+
|
|
235
|
+
if (relation.isList && ctx.stages.length > stagesBeforeRecursion) {
|
|
236
|
+
return { unsupportedReason: 'nested relation through to-many parent not supported in MVP' }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (nested.projectionAfterStrip) {
|
|
240
|
+
ctx.stages[stageIndex].stageArgs.select = nested.projectionAfterStrip
|
|
241
|
+
}
|
|
242
|
+
if (isPlainObject(relationArgs.omit)) {
|
|
243
|
+
ctx.stages[stageIndex].stageArgs.omit = relationArgs.omit
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return { projectionAfterStrip: isSelectMode ? updatedProjection : undefined }
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function planAutoInclude(input: AutoIncludePlannerInput): AutoIncludePlan {
|
|
252
|
+
const maxDepth = input.maxDepth ?? DEFAULT_AUTO_INCLUDE_MAX_DEPTH
|
|
253
|
+
const maxStages = input.maxStages ?? DEFAULT_AUTO_INCLUDE_MAX_STAGES
|
|
254
|
+
|
|
255
|
+
const ctx: WalkContext = {
|
|
256
|
+
models: input.models,
|
|
257
|
+
stages: [],
|
|
258
|
+
internalFieldPaths: [],
|
|
259
|
+
maxDepth,
|
|
260
|
+
maxStages,
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const result = walk(ctx, input.rootModelName, '', input.args, 0)
|
|
264
|
+
|
|
265
|
+
if (result.unsupportedReason) {
|
|
266
|
+
return {
|
|
267
|
+
rootArgs: input.args,
|
|
268
|
+
stages: [],
|
|
269
|
+
internalFieldPaths: [],
|
|
270
|
+
unsupportedReason: 'auto-progressive fallback: ' + result.unsupportedReason,
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const rootArgs: Record<string, unknown> = { ...input.args }
|
|
275
|
+
const rootModel = input.models[input.rootModelName]
|
|
276
|
+
|
|
277
|
+
if (result.projectionAfterStrip) {
|
|
278
|
+
rootArgs.select = result.projectionAfterStrip
|
|
279
|
+
delete rootArgs.include
|
|
280
|
+
} else if (rootModel && isPlainObject(input.args.include)) {
|
|
281
|
+
const stripped: Record<string, unknown> = {}
|
|
282
|
+
for (const [k, v] of Object.entries(input.args.include)) {
|
|
283
|
+
if (!rootModel.relations[k]) {
|
|
284
|
+
stripped[k] = v
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (Object.keys(stripped).length > 0) {
|
|
288
|
+
rootArgs.include = stripped
|
|
289
|
+
} else {
|
|
290
|
+
delete rootArgs.include
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
rootArgs,
|
|
296
|
+
stages: ctx.stages,
|
|
297
|
+
internalFieldPaths: ctx.internalFieldPaths,
|
|
298
|
+
}
|
|
299
|
+
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import type { Request, Response } from 'express'
|
|
2
|
+
import {
|
|
3
|
+
initSSE,
|
|
4
|
+
endSSE,
|
|
5
|
+
startSSEKeepalive,
|
|
6
|
+
sendSSEField,
|
|
7
|
+
sendSSEResult,
|
|
8
|
+
sendSSEError,
|
|
9
|
+
sendSSEProgress,
|
|
10
|
+
runSingleResultSSE,
|
|
11
|
+
setByPath,
|
|
12
|
+
getDelegate,
|
|
13
|
+
getExtendedClient,
|
|
14
|
+
type OperationContext,
|
|
15
|
+
type PrismaDelegate,
|
|
16
|
+
} from './operationRuntime'
|
|
17
|
+
import {
|
|
18
|
+
planAutoInclude,
|
|
19
|
+
type ModelRelationMap,
|
|
20
|
+
type AutoIncludeStage,
|
|
21
|
+
} from './autoIncludePlanner'
|
|
22
|
+
import type { AutoIncludeProgressiveVariantConfig } from './routeConfig'
|
|
23
|
+
|
|
24
|
+
const STAGE_CONCURRENCY = 4
|
|
25
|
+
type IntervalHandle = ReturnType<typeof setInterval>
|
|
26
|
+
|
|
27
|
+
export type RunAutoIncludeOptions = {
|
|
28
|
+
req: Request
|
|
29
|
+
res: Response
|
|
30
|
+
ctx: OperationContext
|
|
31
|
+
args: Record<string, unknown>
|
|
32
|
+
baseOp: 'findUnique' | 'findUniqueOrThrow' | 'findFirst' | 'findFirstOrThrow'
|
|
33
|
+
modelName: string
|
|
34
|
+
delegateKey: string
|
|
35
|
+
models: Record<string, ModelRelationMap>
|
|
36
|
+
variantConfig: AutoIncludeProgressiveVariantConfig
|
|
37
|
+
coreQueryFn: () => Promise<unknown>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isObject(v: unknown): v is Record<string, unknown> {
|
|
41
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function readPath(source: Record<string, unknown>, path: string): unknown {
|
|
45
|
+
if (path === '') return source
|
|
46
|
+
const parts = path.split('.')
|
|
47
|
+
let cursor: unknown = source
|
|
48
|
+
for (const part of parts) {
|
|
49
|
+
if (!isObject(cursor)) return undefined
|
|
50
|
+
cursor = cursor[part]
|
|
51
|
+
}
|
|
52
|
+
return cursor
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function stripInternalAtScope(
|
|
56
|
+
target: Record<string, unknown>,
|
|
57
|
+
internalPaths: string[],
|
|
58
|
+
scopePath: string,
|
|
59
|
+
): void {
|
|
60
|
+
for (const fullPath of internalPaths) {
|
|
61
|
+
if (scopePath === '') {
|
|
62
|
+
if (!fullPath.includes('.')) {
|
|
63
|
+
delete target[fullPath]
|
|
64
|
+
}
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
if (!fullPath.startsWith(scopePath + '.')) continue
|
|
68
|
+
const relative = fullPath.slice(scopePath.length + 1)
|
|
69
|
+
if (relative.includes('.')) continue
|
|
70
|
+
delete target[relative]
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function mergeWhere(
|
|
75
|
+
userWhere: unknown,
|
|
76
|
+
linkFilter: Record<string, unknown>,
|
|
77
|
+
): Record<string, unknown> {
|
|
78
|
+
if (!isObject(userWhere) || Object.keys(userWhere).length === 0) return linkFilter
|
|
79
|
+
return { AND: [userWhere, linkFilter] }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function buildLinkFilter(
|
|
83
|
+
stage: AutoIncludeStage,
|
|
84
|
+
parentValue: Record<string, unknown>,
|
|
85
|
+
): Record<string, unknown> | null {
|
|
86
|
+
const filter: Record<string, unknown> = {}
|
|
87
|
+
const rel = stage.relationField
|
|
88
|
+
for (let i = 0; i < rel.parentLinkFields.length; i++) {
|
|
89
|
+
const parentKey = rel.parentLinkFields[i]
|
|
90
|
+
const childKey = rel.childLinkFields[i]
|
|
91
|
+
const value = parentValue[parentKey]
|
|
92
|
+
if (value === undefined || value === null) return null
|
|
93
|
+
filter[childKey] = value
|
|
94
|
+
}
|
|
95
|
+
return filter
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function emptyResultFor(isList: boolean): unknown {
|
|
99
|
+
return isList ? [] : null
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function runOneStage(options: {
|
|
103
|
+
extended: unknown
|
|
104
|
+
models: Record<string, ModelRelationMap>
|
|
105
|
+
stage: AutoIncludeStage
|
|
106
|
+
internal: Record<string, unknown>
|
|
107
|
+
publicState: Record<string, unknown>
|
|
108
|
+
internalFieldPaths: string[]
|
|
109
|
+
res: Response
|
|
110
|
+
}): Promise<void> {
|
|
111
|
+
const { extended, models, stage, internal, publicState, internalFieldPaths, res } = options
|
|
112
|
+
if (res.writableEnded || res.destroyed) return
|
|
113
|
+
|
|
114
|
+
const parentRaw = readPath(internal, stage.parentPath)
|
|
115
|
+
if (!isObject(parentRaw)) {
|
|
116
|
+
const empty = emptyResultFor(stage.relationField.isList)
|
|
117
|
+
setByPath(publicState, stage.relationPath, empty)
|
|
118
|
+
sendSSEField(res, stage.relationPath, empty)
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const linkFilter = buildLinkFilter(stage, parentRaw)
|
|
123
|
+
if (!linkFilter) {
|
|
124
|
+
const empty = emptyResultFor(stage.relationField.isList)
|
|
125
|
+
setByPath(publicState, stage.relationPath, empty)
|
|
126
|
+
sendSSEField(res, stage.relationPath, empty)
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const targetModel = models[stage.relationField.type]
|
|
131
|
+
if (!targetModel) {
|
|
132
|
+
const empty = emptyResultFor(stage.relationField.isList)
|
|
133
|
+
setByPath(publicState, stage.relationPath, empty)
|
|
134
|
+
sendSSEField(res, stage.relationPath, empty)
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const finalArgs: Record<string, unknown> = { ...stage.stageArgs }
|
|
139
|
+
finalArgs.where = mergeWhere(stage.stageArgs.where, linkFilter)
|
|
140
|
+
|
|
141
|
+
const delegate: PrismaDelegate = getDelegate(extended, targetModel.delegateKey)
|
|
142
|
+
const method: 'findMany' | 'findFirst' = stage.relationField.isList ? 'findMany' : 'findFirst'
|
|
143
|
+
const result = await delegate[method](finalArgs)
|
|
144
|
+
|
|
145
|
+
setByPath(internal, stage.relationPath, result)
|
|
146
|
+
|
|
147
|
+
let publicResult: unknown
|
|
148
|
+
if (Array.isArray(result)) {
|
|
149
|
+
publicResult = result
|
|
150
|
+
} else if (isObject(result)) {
|
|
151
|
+
const copy: Record<string, unknown> = { ...result }
|
|
152
|
+
stripInternalAtScope(copy, internalFieldPaths, stage.relationPath)
|
|
153
|
+
publicResult = copy
|
|
154
|
+
} else {
|
|
155
|
+
publicResult = result
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
setByPath(publicState, stage.relationPath, publicResult)
|
|
159
|
+
sendSSEField(res, stage.relationPath, publicResult)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function groupStagesByDepth(stages: AutoIncludeStage[]): AutoIncludeStage[][] {
|
|
163
|
+
const byDepth = new Map<number, AutoIncludeStage[]>()
|
|
164
|
+
for (const s of stages) {
|
|
165
|
+
const arr = byDepth.get(s.depth)
|
|
166
|
+
if (arr) arr.push(s)
|
|
167
|
+
else byDepth.set(s.depth, [s])
|
|
168
|
+
}
|
|
169
|
+
return Array.from(byDepth.keys())
|
|
170
|
+
.sort((a, b) => a - b)
|
|
171
|
+
.map((d) => byDepth.get(d) as AutoIncludeStage[])
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function runConcurrent<T>(
|
|
175
|
+
items: T[],
|
|
176
|
+
limit: number,
|
|
177
|
+
fn: (item: T) => Promise<void>,
|
|
178
|
+
): Promise<void> {
|
|
179
|
+
let index = 0
|
|
180
|
+
const workers: Promise<void>[] = []
|
|
181
|
+
const workerCount = Math.min(limit, items.length)
|
|
182
|
+
for (let w = 0; w < workerCount; w++) {
|
|
183
|
+
workers.push((async () => {
|
|
184
|
+
for (;;) {
|
|
185
|
+
const i = index++
|
|
186
|
+
if (i >= items.length) return
|
|
187
|
+
await fn(items[i])
|
|
188
|
+
}
|
|
189
|
+
})())
|
|
190
|
+
}
|
|
191
|
+
await Promise.all(workers)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function runAutoIncludeProgressive(
|
|
195
|
+
options: RunAutoIncludeOptions,
|
|
196
|
+
): Promise<void> {
|
|
197
|
+
const { req, res, ctx, args, baseOp, modelName, delegateKey, models, variantConfig, coreQueryFn } = options
|
|
198
|
+
|
|
199
|
+
if (ctx.guardShape) {
|
|
200
|
+
return runSingleResultSSE({ req, res, coreQueryFn })
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const plan = planAutoInclude({
|
|
204
|
+
rootModelName: modelName,
|
|
205
|
+
models,
|
|
206
|
+
args,
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
if (plan.unsupportedReason) {
|
|
210
|
+
if (variantConfig.fallback === 'error') {
|
|
211
|
+
let keepalive: IntervalHandle | null = null
|
|
212
|
+
try {
|
|
213
|
+
initSSE(res)
|
|
214
|
+
keepalive = startSSEKeepalive(res)
|
|
215
|
+
sendSSEError(res, plan.unsupportedReason)
|
|
216
|
+
} finally {
|
|
217
|
+
endSSE(res, keepalive)
|
|
218
|
+
}
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
return runSingleResultSSE({ req, res, coreQueryFn })
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (plan.stages.length === 0) {
|
|
225
|
+
return runSingleResultSSE({ req, res, coreQueryFn })
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let keepalive: IntervalHandle | null = null
|
|
229
|
+
try {
|
|
230
|
+
initSSE(res)
|
|
231
|
+
keepalive = startSSEKeepalive(res)
|
|
232
|
+
if (req.destroyed) return
|
|
233
|
+
|
|
234
|
+
const extended = await getExtendedClient(ctx)
|
|
235
|
+
const rootDelegate = getDelegate(extended, delegateKey)
|
|
236
|
+
|
|
237
|
+
let rootResult: unknown
|
|
238
|
+
try {
|
|
239
|
+
rootResult = await rootDelegate[baseOp](plan.rootArgs)
|
|
240
|
+
} catch (err) {
|
|
241
|
+
const code = (err as { code?: string } | null)?.code
|
|
242
|
+
const isOrThrow = baseOp === 'findUniqueOrThrow' || baseOp === 'findFirstOrThrow'
|
|
243
|
+
if (isOrThrow && code === 'P2025') {
|
|
244
|
+
sendSSEError(res, 'Record not found')
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
console.error('[auto-progressive] root query failed:', err)
|
|
248
|
+
sendSSEError(res, 'Root query failed')
|
|
249
|
+
return
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (res.writableEnded || res.destroyed) return
|
|
253
|
+
|
|
254
|
+
if (rootResult === null || !isObject(rootResult)) {
|
|
255
|
+
sendSSEResult(res, null)
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const internal: Record<string, unknown> = { ...rootResult }
|
|
260
|
+
const publicRoot: Record<string, unknown> = { ...rootResult }
|
|
261
|
+
stripInternalAtScope(publicRoot, plan.internalFieldPaths, '')
|
|
262
|
+
|
|
263
|
+
const publicState: Record<string, unknown> = { ...publicRoot }
|
|
264
|
+
for (const [k, v] of Object.entries(publicRoot)) {
|
|
265
|
+
sendSSEField(res, k, v)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
sendSSEProgress(res, 'root', 0, plan.stages.length)
|
|
269
|
+
|
|
270
|
+
const groups = groupStagesByDepth(plan.stages)
|
|
271
|
+
let completed = 0
|
|
272
|
+
|
|
273
|
+
for (const group of groups) {
|
|
274
|
+
if (res.writableEnded || res.destroyed) return
|
|
275
|
+
await runConcurrent(group, STAGE_CONCURRENCY, async (stage) => {
|
|
276
|
+
try {
|
|
277
|
+
await runOneStage({
|
|
278
|
+
extended,
|
|
279
|
+
models,
|
|
280
|
+
stage,
|
|
281
|
+
internal,
|
|
282
|
+
publicState,
|
|
283
|
+
internalFieldPaths: plan.internalFieldPaths,
|
|
284
|
+
res,
|
|
285
|
+
})
|
|
286
|
+
} catch (err) {
|
|
287
|
+
console.error('[auto-progressive] stage failed:', stage.relationPath, err)
|
|
288
|
+
const empty = emptyResultFor(stage.relationField.isList)
|
|
289
|
+
setByPath(publicState, stage.relationPath, empty)
|
|
290
|
+
sendSSEField(res, stage.relationPath, empty)
|
|
291
|
+
}
|
|
292
|
+
completed++
|
|
293
|
+
sendSSEProgress(res, stage.relationPath, completed, plan.stages.length)
|
|
294
|
+
})
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (res.writableEnded || res.destroyed) return
|
|
298
|
+
sendSSEResult(res, publicState)
|
|
299
|
+
} catch (err) {
|
|
300
|
+
console.error('[auto-progressive] dispatch error:', err)
|
|
301
|
+
if (!res.writableEnded && !res.destroyed) {
|
|
302
|
+
sendSSEError(res, 'Internal server error')
|
|
303
|
+
}
|
|
304
|
+
} finally {
|
|
305
|
+
endSSE(res, keepalive)
|
|
306
|
+
}
|
|
307
|
+
}
|