prisma-generator-express 1.59.0 → 1.61.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +150 -14
- package/dist/client/encodeQueryParams.js +19 -23
- package/dist/client/encodeQueryParams.js.map +1 -1
- package/dist/copy/operationDefinitions.d.ts +37 -0
- package/dist/copy/operationDefinitions.js +412 -0
- package/dist/copy/operationDefinitions.js.map +1 -0
- package/dist/generators/generateFastifyHandler.js +10 -41
- package/dist/generators/generateFastifyHandler.js.map +1 -1
- package/dist/generators/generateHonoHandler.js +10 -41
- package/dist/generators/generateHonoHandler.js.map +1 -1
- package/dist/generators/generateModelMetadata.d.ts +8 -0
- package/dist/generators/generateModelMetadata.js +77 -0
- package/dist/generators/generateModelMetadata.js.map +1 -0
- package/dist/generators/generateOperationCore.js +20 -30
- package/dist/generators/generateOperationCore.js.map +1 -1
- package/dist/generators/generateQueryBuilderHelper.js +1 -1
- package/dist/generators/generateQueryBuilderHelper.js.map +1 -1
- package/dist/generators/generateRouteConfigType.js +11 -57
- package/dist/generators/generateRouteConfigType.js.map +1 -1
- package/dist/generators/generateRouter.js +64 -177
- package/dist/generators/generateRouter.js.map +1 -1
- package/dist/generators/generateRouterFastify.js +59 -169
- package/dist/generators/generateRouterFastify.js.map +1 -1
- package/dist/generators/generateRouterHono.js +52 -152
- package/dist/generators/generateRouterHono.js.map +1 -1
- package/dist/generators/generateUnifiedHandler.js +7 -30
- package/dist/generators/generateUnifiedHandler.js.map +1 -1
- package/dist/generators/generateUnifiedScalarUI.js +9 -74
- package/dist/generators/generateUnifiedScalarUI.js.map +1 -1
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/dist/utils/copyFiles.js +7 -0
- package/dist/utils/copyFiles.js.map +1 -1
- package/dist/utils/writeFileSafely.js +5 -12
- package/dist/utils/writeFileSafely.js.map +1 -1
- package/package.json +12 -2
- package/src/client/encodeQueryParams.ts +23 -36
- package/src/copy/autoIncludePlanner.ts +14 -23
- package/src/copy/autoIncludePlannerGuarded.ts +477 -0
- package/src/copy/autoIncludeRuntime.ts +269 -261
- package/src/copy/autoIncludeRuntimeGuarded.ts +379 -0
- package/src/copy/buildModelOpenApi.ts +248 -628
- package/src/copy/concurrency.ts +20 -0
- package/src/copy/docsRenderer.ts +63 -333
- package/src/copy/errorMapper.ts +126 -0
- package/src/copy/guardHelpers.ts +56 -0
- package/src/copy/materializedCount.ts +68 -0
- package/src/copy/materializedRouter.ts +33 -29
- package/src/copy/operationDefinitions.ts +359 -35
- package/src/copy/operationRuntime.ts +11 -605
- package/src/copy/pagination.ts +151 -0
- package/src/copy/scalarTypes.ts +2 -0
- package/src/copy/sse.ts +296 -0
- package/src/generators/generateFastifyHandler.ts +13 -47
- package/src/generators/generateHonoHandler.ts +13 -47
- package/src/generators/generateModelMetadata.ts +92 -0
- package/src/generators/generateOperationCore.ts +19 -32
- package/src/generators/generateQueryBuilderHelper.ts +1 -1
- package/src/generators/generateRouteConfigType.ts +9 -60
- package/src/generators/generateRouter.ts +88 -180
- package/src/generators/generateRouterFastify.ts +65 -172
- package/src/generators/generateRouterHono.ts +58 -155
- package/src/generators/generateUnifiedHandler.ts +8 -33
- package/src/generators/generateUnifiedScalarUI.ts +9 -91
- package/src/index.ts +13 -1
- package/src/utils/copyFiles.ts +7 -0
- package/src/utils/writeFileSafely.ts +5 -11
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import type { Request, Response } from 'express'
|
|
2
|
+
import { HttpError, LOG_PREFIX, mapError } from './errorMapper'
|
|
3
|
+
import {
|
|
4
|
+
sendSSEField,
|
|
5
|
+
sendSSEResult,
|
|
6
|
+
sendSSEError,
|
|
7
|
+
sendSSEProgress,
|
|
8
|
+
runSingleResultSSE,
|
|
9
|
+
emitTerminalSSEError,
|
|
10
|
+
safeSendError,
|
|
11
|
+
withSSE,
|
|
12
|
+
} from './sse'
|
|
13
|
+
import {
|
|
14
|
+
getDelegate,
|
|
15
|
+
getExtendedClient,
|
|
16
|
+
type OperationContext,
|
|
17
|
+
} from './operationRuntime'
|
|
18
|
+
import { isPlainObject } from './misc'
|
|
19
|
+
import { mapLimited } from './concurrency'
|
|
20
|
+
import {
|
|
21
|
+
planGuardedAutoInclude,
|
|
22
|
+
type GuardedAutoIncludePlan,
|
|
23
|
+
type GuardedAutoIncludeStage,
|
|
24
|
+
} from './autoIncludePlannerGuarded'
|
|
25
|
+
import type { ModelRelationMap } from './autoIncludePlanner'
|
|
26
|
+
import type { AutoIncludeProgressiveVariantConfig } from './routeConfig'
|
|
27
|
+
|
|
28
|
+
const STAGE_CONCURRENCY = 4
|
|
29
|
+
|
|
30
|
+
type GuardedBaseOp =
|
|
31
|
+
| 'findUnique'
|
|
32
|
+
| 'findUniqueOrThrow'
|
|
33
|
+
| 'findFirst'
|
|
34
|
+
| 'findFirstOrThrow'
|
|
35
|
+
|
|
36
|
+
const GUARDED_BASE_OPS = new Set<string>([
|
|
37
|
+
'findUnique',
|
|
38
|
+
'findUniqueOrThrow',
|
|
39
|
+
'findFirst',
|
|
40
|
+
'findFirstOrThrow',
|
|
41
|
+
])
|
|
42
|
+
|
|
43
|
+
export function isGuardedAutoIncludeBaseOp(op: string): op is GuardedBaseOp {
|
|
44
|
+
return GUARDED_BASE_OPS.has(op)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type RunGuardedAutoIncludeOptions = {
|
|
48
|
+
req: Request
|
|
49
|
+
res: Response
|
|
50
|
+
ctx: OperationContext
|
|
51
|
+
args: Record<string, unknown>
|
|
52
|
+
baseOp: GuardedBaseOp
|
|
53
|
+
modelName: string
|
|
54
|
+
delegateKey: string
|
|
55
|
+
models: Record<string, ModelRelationMap>
|
|
56
|
+
variantConfig: AutoIncludeProgressiveVariantConfig
|
|
57
|
+
coreQueryFn: () => Promise<unknown>
|
|
58
|
+
signal?: AbortSignal
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type GuardedDelegate = {
|
|
62
|
+
resolve: (body?: unknown) => {
|
|
63
|
+
shape: Record<string, unknown>
|
|
64
|
+
body: Record<string, unknown>
|
|
65
|
+
effectiveReadBody: Record<string, unknown>
|
|
66
|
+
matchedKey: string
|
|
67
|
+
wasDynamic: boolean
|
|
68
|
+
}
|
|
69
|
+
findFirst: (args?: unknown) => Promise<unknown>
|
|
70
|
+
findFirstOrThrow: (args?: unknown) => Promise<unknown>
|
|
71
|
+
findUnique: (args?: unknown) => Promise<unknown>
|
|
72
|
+
findUniqueOrThrow: (args?: unknown) => Promise<unknown>
|
|
73
|
+
findMany: (args?: unknown) => Promise<unknown>
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function guarded(
|
|
77
|
+
delegate: unknown,
|
|
78
|
+
shape: Record<string, unknown>,
|
|
79
|
+
caller: string | undefined,
|
|
80
|
+
): GuardedDelegate {
|
|
81
|
+
const d = delegate as {
|
|
82
|
+
guard?: (shape: Record<string, unknown>, caller?: string) => unknown
|
|
83
|
+
}
|
|
84
|
+
if (typeof d.guard !== 'function') {
|
|
85
|
+
throw new HttpError(
|
|
86
|
+
500,
|
|
87
|
+
'Guarded auto-include requires prisma-guard extension on PrismaClient.',
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
return d.guard(shape, caller) as GuardedDelegate
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function createClientGoneChecker(res: Response, signal?: AbortSignal): () => boolean {
|
|
94
|
+
return () => signal?.aborted === true || res.writableEnded || res.destroyed
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function stripInternalAtScope(
|
|
98
|
+
target: Record<string, unknown>,
|
|
99
|
+
internalPaths: string[],
|
|
100
|
+
scopePath: string,
|
|
101
|
+
): void {
|
|
102
|
+
const prefix = scopePath === '' ? '' : scopePath + '.'
|
|
103
|
+
for (const fullPath of internalPaths) {
|
|
104
|
+
if (!fullPath.startsWith(prefix)) continue
|
|
105
|
+
const relative = fullPath.slice(prefix.length)
|
|
106
|
+
if (relative === '' || relative.includes('.')) continue
|
|
107
|
+
delete target[relative]
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function buildPublicForStage(
|
|
112
|
+
result: unknown,
|
|
113
|
+
internalFieldPaths: string[],
|
|
114
|
+
scopePath: string,
|
|
115
|
+
): unknown {
|
|
116
|
+
const process = (item: unknown): unknown => {
|
|
117
|
+
if (!isPlainObject(item)) return item
|
|
118
|
+
const copy: Record<string, unknown> = { ...item }
|
|
119
|
+
stripInternalAtScope(copy, internalFieldPaths, scopePath)
|
|
120
|
+
return copy
|
|
121
|
+
}
|
|
122
|
+
if (Array.isArray(result)) return result.map(process)
|
|
123
|
+
return process(result)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function hasClientProjection(body: unknown): boolean {
|
|
127
|
+
if (!isPlainObject(body)) return false
|
|
128
|
+
return 'select' in body || 'include' in body || 'omit' in body
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function handleGuardedFallback(
|
|
132
|
+
options: RunGuardedAutoIncludeOptions,
|
|
133
|
+
message: string,
|
|
134
|
+
): Promise<void> {
|
|
135
|
+
if (options.variantConfig.fallback === 'error') {
|
|
136
|
+
emitTerminalSSEError(options.res, message)
|
|
137
|
+
return Promise.resolve()
|
|
138
|
+
}
|
|
139
|
+
return runSingleResultSSE({
|
|
140
|
+
req: options.req,
|
|
141
|
+
res: options.res,
|
|
142
|
+
coreQueryFn: options.coreQueryFn,
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function runRootQuery(
|
|
147
|
+
extended: unknown,
|
|
148
|
+
delegateKey: string,
|
|
149
|
+
rootShape: Record<string, unknown>,
|
|
150
|
+
rootArgs: Record<string, unknown>,
|
|
151
|
+
baseOp: GuardedBaseOp,
|
|
152
|
+
caller: string | undefined,
|
|
153
|
+
): Promise<unknown> {
|
|
154
|
+
const rootDelegate = getDelegate(extended, delegateKey)
|
|
155
|
+
const g = guarded(rootDelegate, rootShape, caller)
|
|
156
|
+
return g[baseOp](rootArgs)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function runStage(
|
|
160
|
+
extended: unknown,
|
|
161
|
+
models: Record<string, ModelRelationMap>,
|
|
162
|
+
stage: GuardedAutoIncludeStage,
|
|
163
|
+
parentValue: unknown,
|
|
164
|
+
caller: string | undefined,
|
|
165
|
+
): Promise<unknown> {
|
|
166
|
+
const rel = stage.relationField
|
|
167
|
+
const parentKey = rel.parentLinkFields[0]
|
|
168
|
+
const childKey = rel.childLinkFields[0]
|
|
169
|
+
|
|
170
|
+
if (!isPlainObject(parentValue)) {
|
|
171
|
+
return rel.isList ? [] : null
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const linkVal = parentValue[parentKey]
|
|
175
|
+
if (linkVal === undefined || linkVal === null) {
|
|
176
|
+
return rel.isList ? [] : null
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const targetModel = models[rel.type]
|
|
180
|
+
if (!targetModel) {
|
|
181
|
+
throw new HttpError(500, 'Target model not in relation metadata: ' + rel.type)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const stageArgs: Record<string, unknown> = { ...stage.stageArgs }
|
|
185
|
+
|
|
186
|
+
if (stageArgs.where !== undefined && !isPlainObject(stageArgs.where)) {
|
|
187
|
+
throw new HttpError(
|
|
188
|
+
500,
|
|
189
|
+
'Invalid guarded stage where for ' + stage.relationPath,
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const existingWhere = isPlainObject(stageArgs.where) ? stageArgs.where : {}
|
|
194
|
+
stageArgs.where = {
|
|
195
|
+
...existingWhere,
|
|
196
|
+
[childKey]: { in: [linkVal] },
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const stageDelegate = getDelegate(extended, targetModel.delegateKey)
|
|
200
|
+
const g = guarded(stageDelegate, stage.stageShape, caller)
|
|
201
|
+
|
|
202
|
+
if (rel.isList) {
|
|
203
|
+
return g.findMany(stageArgs)
|
|
204
|
+
}
|
|
205
|
+
return g.findFirst(stageArgs)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function runGuardedAutoIncludeSingle(
|
|
209
|
+
options: RunGuardedAutoIncludeOptions,
|
|
210
|
+
plan: GuardedAutoIncludePlan,
|
|
211
|
+
extended: unknown,
|
|
212
|
+
): Promise<void> {
|
|
213
|
+
const { res, ctx, baseOp, delegateKey, models, signal } = options
|
|
214
|
+
const caller = ctx.guardCaller
|
|
215
|
+
const isClientGone = createClientGoneChecker(res, signal)
|
|
216
|
+
|
|
217
|
+
await withSSE({ res, signal, label: 'guarded-single' }, async () => {
|
|
218
|
+
if (isClientGone()) return
|
|
219
|
+
|
|
220
|
+
let rootResult: unknown
|
|
221
|
+
try {
|
|
222
|
+
rootResult = await runRootQuery(
|
|
223
|
+
extended,
|
|
224
|
+
delegateKey,
|
|
225
|
+
plan.rootShape,
|
|
226
|
+
plan.rootArgs,
|
|
227
|
+
baseOp,
|
|
228
|
+
caller,
|
|
229
|
+
)
|
|
230
|
+
} catch (err) {
|
|
231
|
+
if (isClientGone()) return
|
|
232
|
+
console.error(LOG_PREFIX, 'guarded root query failed:', err)
|
|
233
|
+
sendSSEError(res, mapError(err).message)
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (isClientGone()) return
|
|
238
|
+
|
|
239
|
+
if (rootResult === null || !isPlainObject(rootResult)) {
|
|
240
|
+
sendSSEResult(res, rootResult)
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const internal: Record<string, unknown> = { ...rootResult }
|
|
245
|
+
const publicRoot: Record<string, unknown> = { ...rootResult }
|
|
246
|
+
stripInternalAtScope(publicRoot, plan.internalFieldPaths, '')
|
|
247
|
+
|
|
248
|
+
const publicState: Record<string, unknown> = { ...publicRoot }
|
|
249
|
+
for (const [k, v] of Object.entries(publicRoot)) {
|
|
250
|
+
if (isClientGone()) return
|
|
251
|
+
const ok = sendSSEField(res, k, v)
|
|
252
|
+
if (!ok) return
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (isClientGone()) return
|
|
256
|
+
const okStart = sendSSEProgress(res, 'root', 0, plan.stages.length)
|
|
257
|
+
if (!okStart) return
|
|
258
|
+
|
|
259
|
+
let completed = 0
|
|
260
|
+
let stageErrorMessage: string | null = null
|
|
261
|
+
const isAborted = () =>
|
|
262
|
+
stageErrorMessage !== null ||
|
|
263
|
+
signal?.aborted === true ||
|
|
264
|
+
res.writableEnded ||
|
|
265
|
+
res.destroyed
|
|
266
|
+
|
|
267
|
+
await mapLimited(plan.stages, STAGE_CONCURRENCY, async (stage) => {
|
|
268
|
+
if (isAborted()) return
|
|
269
|
+
try {
|
|
270
|
+
const stageResult = await runStage(
|
|
271
|
+
extended,
|
|
272
|
+
models,
|
|
273
|
+
stage,
|
|
274
|
+
internal,
|
|
275
|
+
caller,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
if (isAborted()) return
|
|
279
|
+
|
|
280
|
+
internal[stage.relationName] = stageResult
|
|
281
|
+
const publicVal = buildPublicForStage(
|
|
282
|
+
stageResult,
|
|
283
|
+
plan.internalFieldPaths,
|
|
284
|
+
stage.relationPath,
|
|
285
|
+
)
|
|
286
|
+
publicState[stage.relationName] = publicVal
|
|
287
|
+
|
|
288
|
+
const okField = sendSSEField(res, stage.relationName, publicVal)
|
|
289
|
+
if (!okField) return
|
|
290
|
+
} catch (err) {
|
|
291
|
+
if (isAborted()) return
|
|
292
|
+
console.error(LOG_PREFIX, 'guarded stage failed:', stage.relationPath, err)
|
|
293
|
+
stageErrorMessage = mapError(err).message
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (isAborted()) return
|
|
298
|
+
completed++
|
|
299
|
+
const okProg = sendSSEProgress(res, stage.relationPath, completed, plan.stages.length)
|
|
300
|
+
if (!okProg) return
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
if (isClientGone()) return
|
|
304
|
+
|
|
305
|
+
if (stageErrorMessage) {
|
|
306
|
+
safeSendError(res, stageErrorMessage)
|
|
307
|
+
return
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (res.writableEnded || res.destroyed) return
|
|
311
|
+
sendSSEResult(res, publicState)
|
|
312
|
+
})
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export async function runGuardedAutoIncludeProgressive(
|
|
316
|
+
options: RunGuardedAutoIncludeOptions,
|
|
317
|
+
): Promise<void> {
|
|
318
|
+
if (hasClientProjection(options.args)) {
|
|
319
|
+
return handleGuardedFallback(
|
|
320
|
+
options,
|
|
321
|
+
'guarded auto-progressive fallback: client projection rejected under guarded MVP',
|
|
322
|
+
)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const guardShape = options.ctx.guardShape
|
|
326
|
+
if (!guardShape) {
|
|
327
|
+
return handleGuardedFallback(
|
|
328
|
+
options,
|
|
329
|
+
'guarded auto-progressive fallback: guardShape missing',
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
let extended: unknown
|
|
334
|
+
try {
|
|
335
|
+
extended = await getExtendedClient(options.ctx)
|
|
336
|
+
} catch (err) {
|
|
337
|
+
return handleGuardedFallback(
|
|
338
|
+
options,
|
|
339
|
+
'guarded auto-progressive fallback: getExtendedClient failed: ' + mapError(err).message,
|
|
340
|
+
)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
let resolved: {
|
|
344
|
+
shape: Record<string, unknown>
|
|
345
|
+
body: Record<string, unknown>
|
|
346
|
+
effectiveReadBody: Record<string, unknown>
|
|
347
|
+
}
|
|
348
|
+
try {
|
|
349
|
+
const rootDelegate = getDelegate(extended, options.delegateKey)
|
|
350
|
+
const g = guarded(rootDelegate, guardShape, options.ctx.guardCaller)
|
|
351
|
+
resolved = g.resolve(options.args)
|
|
352
|
+
} catch (err) {
|
|
353
|
+
return handleGuardedFallback(
|
|
354
|
+
options,
|
|
355
|
+
'guarded auto-progressive fallback: resolve failed: ' + mapError(err).message,
|
|
356
|
+
)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const plan = planGuardedAutoInclude({
|
|
360
|
+
rootModelName: options.modelName,
|
|
361
|
+
models: options.models,
|
|
362
|
+
effectiveReadBody: resolved.effectiveReadBody,
|
|
363
|
+
shape: resolved.shape,
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
if (plan.unsupportedReason) {
|
|
367
|
+
return handleGuardedFallback(options, plan.unsupportedReason)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (plan.stages.length === 0) {
|
|
371
|
+
return runSingleResultSSE({
|
|
372
|
+
req: options.req,
|
|
373
|
+
res: options.res,
|
|
374
|
+
coreQueryFn: options.coreQueryFn,
|
|
375
|
+
})
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return runGuardedAutoIncludeSingle(options, plan, extended)
|
|
379
|
+
}
|