prisma-generator-express 1.42.0 → 1.44.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 +21 -6
- package/dist/generators/generateFastifyHandler.js.map +1 -1
- package/dist/generators/generateHonoHandler.d.ts +2 -0
- package/dist/generators/generateHonoHandler.js +8 -6
- package/dist/generators/generateHonoHandler.js.map +1 -1
- 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 +110 -0
- package/dist/generators/generateRelationMeta.js.map +1 -0
- package/dist/generators/generateRouteConfigType.js +13 -5
- package/dist/generators/generateRouteConfigType.js.map +1 -1
- package/dist/generators/generateRouter.js +83 -19
- 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.d.ts +2 -0
- package/dist/generators/generateUnifiedHandler.js +28 -10
- 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 +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 +2 -2
- package/dist/utils/writeFileSafely.js.map +1 -1
- package/package.json +1 -1
- package/src/copy/autoIncludePlanner.ts +354 -0
- package/src/copy/autoIncludeRuntime.ts +362 -0
- package/src/copy/operationRuntime.ts +614 -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 +23 -6
- package/src/generators/generateHonoHandler.ts +10 -6
- package/src/generators/generateOperationCore.ts +34 -546
- package/src/generators/generateRelationMeta.ts +160 -0
- package/src/generators/generateRouteConfigType.ts +13 -6
- package/src/generators/generateRouter.ts +83 -19
- package/src/generators/generateRouterFastify.ts +127 -384
- package/src/generators/generateRouterHono.ts +48 -36
- package/src/generators/generateUnifiedHandler.ts +30 -10
- package/src/generators/generateUnifiedScalarUI.ts +21 -16
- package/src/index.ts +31 -13
- package/src/utils/copyFiles.ts +13 -0
- package/src/utils/writeFileSafely.ts +2 -2
|
@@ -0,0 +1,362 @@
|
|
|
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
|
+
emitTerminalSSEError,
|
|
12
|
+
setByPath,
|
|
13
|
+
getDelegate,
|
|
14
|
+
getExtendedClient,
|
|
15
|
+
type OperationContext,
|
|
16
|
+
type PrismaDelegate,
|
|
17
|
+
} from './operationRuntime'
|
|
18
|
+
import {
|
|
19
|
+
planAutoInclude,
|
|
20
|
+
type ModelRelationMap,
|
|
21
|
+
type AutoIncludeStage,
|
|
22
|
+
} from './autoIncludePlanner'
|
|
23
|
+
import type { AutoIncludeProgressiveVariantConfig } from './routeConfig'
|
|
24
|
+
|
|
25
|
+
const STAGE_CONCURRENCY = 4
|
|
26
|
+
type IntervalHandle = ReturnType<typeof setInterval>
|
|
27
|
+
|
|
28
|
+
export type RunAutoIncludeOptions = {
|
|
29
|
+
req: Request
|
|
30
|
+
res: Response
|
|
31
|
+
ctx: OperationContext
|
|
32
|
+
args: Record<string, unknown>
|
|
33
|
+
baseOp: 'findUnique' | 'findUniqueOrThrow' | 'findFirst' | 'findFirstOrThrow'
|
|
34
|
+
modelName: string
|
|
35
|
+
delegateKey: string
|
|
36
|
+
models: Record<string, ModelRelationMap>
|
|
37
|
+
variantConfig: AutoIncludeProgressiveVariantConfig
|
|
38
|
+
coreQueryFn: () => Promise<unknown>
|
|
39
|
+
signal?: AbortSignal
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isObject(v: unknown): v is Record<string, unknown> {
|
|
43
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readPath(source: Record<string, unknown>, path: string): unknown {
|
|
47
|
+
if (path === '') return source
|
|
48
|
+
const parts = path.split('.')
|
|
49
|
+
let cursor: unknown = source
|
|
50
|
+
for (const part of parts) {
|
|
51
|
+
if (!isObject(cursor)) return undefined
|
|
52
|
+
cursor = cursor[part]
|
|
53
|
+
}
|
|
54
|
+
return cursor
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function stripInternalAtScope(
|
|
58
|
+
target: Record<string, unknown>,
|
|
59
|
+
internalPaths: string[],
|
|
60
|
+
scopePath: string,
|
|
61
|
+
): void {
|
|
62
|
+
for (const fullPath of internalPaths) {
|
|
63
|
+
if (scopePath === '') {
|
|
64
|
+
if (!fullPath.includes('.')) {
|
|
65
|
+
delete target[fullPath]
|
|
66
|
+
}
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
69
|
+
if (!fullPath.startsWith(scopePath + '.')) continue
|
|
70
|
+
const relative = fullPath.slice(scopePath.length + 1)
|
|
71
|
+
if (relative.includes('.')) continue
|
|
72
|
+
delete target[relative]
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function mergeWhere(
|
|
77
|
+
userWhere: unknown,
|
|
78
|
+
linkFilter: Record<string, unknown>,
|
|
79
|
+
): Record<string, unknown> {
|
|
80
|
+
if (!isObject(userWhere) || Object.keys(userWhere).length === 0) return linkFilter
|
|
81
|
+
return { AND: [userWhere, linkFilter] }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function buildLinkFilter(
|
|
85
|
+
stage: AutoIncludeStage,
|
|
86
|
+
parentValue: Record<string, unknown>,
|
|
87
|
+
): Record<string, unknown> | null {
|
|
88
|
+
const filter: Record<string, unknown> = {}
|
|
89
|
+
const rel = stage.relationField
|
|
90
|
+
for (let i = 0; i < rel.parentLinkFields.length; i++) {
|
|
91
|
+
const parentKey = rel.parentLinkFields[i]
|
|
92
|
+
const childKey = rel.childLinkFields[i]
|
|
93
|
+
const value = parentValue[parentKey]
|
|
94
|
+
if (value === undefined || value === null) return null
|
|
95
|
+
filter[childKey] = value
|
|
96
|
+
}
|
|
97
|
+
return filter
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function emptyResultFor(isList: boolean): unknown {
|
|
101
|
+
return isList ? [] : null
|
|
102
|
+
}
|
|
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
|
+
|
|
127
|
+
async function runOneStage(options: {
|
|
128
|
+
extended: unknown
|
|
129
|
+
models: Record<string, ModelRelationMap>
|
|
130
|
+
stage: AutoIncludeStage
|
|
131
|
+
internal: Record<string, unknown>
|
|
132
|
+
publicState: Record<string, unknown>
|
|
133
|
+
internalFieldPaths: string[]
|
|
134
|
+
res: Response
|
|
135
|
+
isAborted: () => boolean
|
|
136
|
+
}): Promise<void> {
|
|
137
|
+
const { extended, models, stage, internal, publicState, internalFieldPaths, res, isAborted } = options
|
|
138
|
+
if (isAborted()) return
|
|
139
|
+
|
|
140
|
+
const parentRaw = readPath(internal, stage.parentPath)
|
|
141
|
+
if (!isObject(parentRaw)) {
|
|
142
|
+
if (stage.parentPath !== '') {
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
const empty = emptyResultFor(stage.relationField.isList)
|
|
146
|
+
const applied = setByPath(publicState, stage.relationPath, empty)
|
|
147
|
+
if (applied) sendSSEField(res, stage.relationPath, empty)
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const linkFilter = buildLinkFilter(stage, parentRaw)
|
|
152
|
+
if (!linkFilter) {
|
|
153
|
+
const empty = emptyResultFor(stage.relationField.isList)
|
|
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
|
+
}
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const targetModel = models[stage.relationField.type]
|
|
163
|
+
if (!targetModel) {
|
|
164
|
+
throw new Error('Target model not in relation metadata: ' + stage.relationField.type)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const finalArgs: Record<string, unknown> = { ...stage.stageArgs }
|
|
168
|
+
finalArgs.where = mergeWhere(stage.stageArgs.where, linkFilter)
|
|
169
|
+
|
|
170
|
+
const delegate: PrismaDelegate = getDelegate(extended, targetModel.delegateKey)
|
|
171
|
+
const method: 'findMany' | 'findFirst' = stage.relationField.isList ? 'findMany' : 'findFirst'
|
|
172
|
+
const result = await delegate[method](finalArgs)
|
|
173
|
+
|
|
174
|
+
if (isAborted()) return
|
|
175
|
+
|
|
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)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
sendSSEField(res, stage.relationPath, publicResult)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function groupStagesByDepth(stages: AutoIncludeStage[]): AutoIncludeStage[][] {
|
|
192
|
+
const byDepth = new Map<number, AutoIncludeStage[]>()
|
|
193
|
+
for (const s of stages) {
|
|
194
|
+
const arr = byDepth.get(s.depth)
|
|
195
|
+
if (arr) arr.push(s)
|
|
196
|
+
else byDepth.set(s.depth, [s])
|
|
197
|
+
}
|
|
198
|
+
return Array.from(byDepth.keys())
|
|
199
|
+
.sort((a, b) => a - b)
|
|
200
|
+
.map((d) => byDepth.get(d) as AutoIncludeStage[])
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function runConcurrent<T>(
|
|
204
|
+
items: T[],
|
|
205
|
+
limit: number,
|
|
206
|
+
fn: (item: T) => Promise<void>,
|
|
207
|
+
): Promise<void> {
|
|
208
|
+
let index = 0
|
|
209
|
+
const workers: Promise<void>[] = []
|
|
210
|
+
const workerCount = Math.min(limit, items.length)
|
|
211
|
+
for (let w = 0; w < workerCount; w++) {
|
|
212
|
+
workers.push((async () => {
|
|
213
|
+
for (;;) {
|
|
214
|
+
const i = index++
|
|
215
|
+
if (i >= items.length) return
|
|
216
|
+
await fn(items[i])
|
|
217
|
+
}
|
|
218
|
+
})())
|
|
219
|
+
}
|
|
220
|
+
await Promise.all(workers)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export async function runAutoIncludeProgressive(
|
|
224
|
+
options: RunAutoIncludeOptions,
|
|
225
|
+
): Promise<void> {
|
|
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
|
|
230
|
+
|
|
231
|
+
if (ctx.guardShape) {
|
|
232
|
+
if (variantConfig.fallback === 'error') {
|
|
233
|
+
emitTerminalSSEError(res, 'auto-progressive fallback: guard shape disables auto-include')
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
return runSingleResultSSE({ req, res, coreQueryFn })
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const plan = planAutoInclude({
|
|
240
|
+
rootModelName: modelName,
|
|
241
|
+
models,
|
|
242
|
+
args,
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
if (plan.unsupportedReason) {
|
|
246
|
+
if (variantConfig.fallback === 'error') {
|
|
247
|
+
emitTerminalSSEError(res, plan.unsupportedReason)
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
return runSingleResultSSE({ req, res, coreQueryFn })
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (plan.stages.length === 0) {
|
|
254
|
+
return runSingleResultSSE({ req, res, coreQueryFn })
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
let keepalive: IntervalHandle | null = null
|
|
258
|
+
try {
|
|
259
|
+
initSSE(res)
|
|
260
|
+
keepalive = startSSEKeepalive(res)
|
|
261
|
+
if (isClientGone()) return
|
|
262
|
+
|
|
263
|
+
const extended = await getExtendedClient(ctx)
|
|
264
|
+
if (isClientGone()) return
|
|
265
|
+
|
|
266
|
+
const rootDelegate = getDelegate(extended, delegateKey)
|
|
267
|
+
|
|
268
|
+
let rootResult: unknown
|
|
269
|
+
try {
|
|
270
|
+
rootResult = await rootDelegate[baseOp](plan.rootArgs)
|
|
271
|
+
} catch (err) {
|
|
272
|
+
if (isClientGone()) return
|
|
273
|
+
const code = (err as { code?: string } | null)?.code
|
|
274
|
+
const isOrThrow = baseOp === 'findUniqueOrThrow' || baseOp === 'findFirstOrThrow'
|
|
275
|
+
if (isOrThrow && code === 'P2025') {
|
|
276
|
+
sendSSEError(res, 'Record not found')
|
|
277
|
+
return
|
|
278
|
+
}
|
|
279
|
+
console.error('[auto-progressive] root query failed:', err)
|
|
280
|
+
sendSSEError(res, 'Root query failed')
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (isClientGone()) return
|
|
285
|
+
|
|
286
|
+
if (rootResult === null || !isObject(rootResult)) {
|
|
287
|
+
sendSSEResult(res, null)
|
|
288
|
+
return
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const internal: Record<string, unknown> = { ...rootResult }
|
|
292
|
+
const publicRoot: Record<string, unknown> = { ...rootResult }
|
|
293
|
+
stripInternalAtScope(publicRoot, plan.internalFieldPaths, '')
|
|
294
|
+
|
|
295
|
+
const publicState: Record<string, unknown> = { ...publicRoot }
|
|
296
|
+
for (const [k, v] of Object.entries(publicRoot)) {
|
|
297
|
+
if (isClientGone()) return
|
|
298
|
+
sendSSEField(res, k, v)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (isClientGone()) return
|
|
302
|
+
sendSSEProgress(res, 'root', 0, plan.stages.length)
|
|
303
|
+
|
|
304
|
+
const groups = groupStagesByDepth(plan.stages)
|
|
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
|
|
312
|
+
|
|
313
|
+
for (const group of groups) {
|
|
314
|
+
if (isClientGone()) return
|
|
315
|
+
if (stageErrorMessage) break
|
|
316
|
+
|
|
317
|
+
await runConcurrent(group, STAGE_CONCURRENCY, async (stage) => {
|
|
318
|
+
if (isAborted()) return
|
|
319
|
+
try {
|
|
320
|
+
await runOneStage({
|
|
321
|
+
extended,
|
|
322
|
+
models,
|
|
323
|
+
stage,
|
|
324
|
+
internal,
|
|
325
|
+
publicState,
|
|
326
|
+
internalFieldPaths: plan.internalFieldPaths,
|
|
327
|
+
res,
|
|
328
|
+
isAborted,
|
|
329
|
+
})
|
|
330
|
+
} catch (err) {
|
|
331
|
+
if (isAborted()) return
|
|
332
|
+
console.error('[auto-progressive] stage failed:', stage.relationPath, err)
|
|
333
|
+
stageErrorMessage = 'Could not load progressive response'
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
if (isAborted()) return
|
|
337
|
+
completed++
|
|
338
|
+
sendSSEProgress(res, stage.relationPath, completed, plan.stages.length)
|
|
339
|
+
})
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (isClientGone()) return
|
|
343
|
+
|
|
344
|
+
if (stageErrorMessage) {
|
|
345
|
+
if (!res.writableEnded && !res.destroyed) {
|
|
346
|
+
sendSSEError(res, stageErrorMessage)
|
|
347
|
+
}
|
|
348
|
+
return
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (res.writableEnded || res.destroyed) return
|
|
352
|
+
sendSSEResult(res, publicState)
|
|
353
|
+
} catch (err) {
|
|
354
|
+
if (isClientGone()) return
|
|
355
|
+
console.error('[auto-progressive] dispatch error:', err)
|
|
356
|
+
if (!res.writableEnded && !res.destroyed) {
|
|
357
|
+
sendSSEError(res, 'Internal server error')
|
|
358
|
+
}
|
|
359
|
+
} finally {
|
|
360
|
+
endSSE(res, keepalive)
|
|
361
|
+
}
|
|
362
|
+
}
|