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
|
@@ -2,529 +2,6 @@ import { DMMF } from '@prisma/generator-helper'
|
|
|
2
2
|
import { ImportStyle } from '../utils/resolveImportStyle'
|
|
3
3
|
import { importExt } from '../utils/importExt'
|
|
4
4
|
|
|
5
|
-
export function generateOperationRuntime(importStyle: ImportStyle): string {
|
|
6
|
-
const ext = importExt(importStyle)
|
|
7
|
-
return `import { sanitizeKeys } from './misc${ext}'
|
|
8
|
-
import type {
|
|
9
|
-
ProgressivePatch,
|
|
10
|
-
ProgressiveStopResult,
|
|
11
|
-
ProgressiveStageResult,
|
|
12
|
-
ProgressiveStageContext,
|
|
13
|
-
ProgressiveStage,
|
|
14
|
-
} from './routeConfig${ext}'
|
|
15
|
-
|
|
16
|
-
export type {
|
|
17
|
-
ProgressivePatch,
|
|
18
|
-
ProgressiveStopResult,
|
|
19
|
-
ProgressiveStageResult,
|
|
20
|
-
ProgressiveStageContext,
|
|
21
|
-
ProgressiveStage,
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface PaginationConfig {
|
|
25
|
-
defaultLimit?: number
|
|
26
|
-
maxLimit?: number
|
|
27
|
-
distinctCountLimit?: number
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface OperationContext {
|
|
31
|
-
prisma: any
|
|
32
|
-
postgres?: any
|
|
33
|
-
sqlite?: any
|
|
34
|
-
parsedQuery?: Record<string, unknown>
|
|
35
|
-
body?: unknown
|
|
36
|
-
guardShape?: Record<string, unknown>
|
|
37
|
-
guardCaller?: string
|
|
38
|
-
paginationConfig?: PaginationConfig
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export const DISTINCT_COUNT_LIMIT = 100000
|
|
42
|
-
|
|
43
|
-
export class HttpError extends Error {
|
|
44
|
-
status: number
|
|
45
|
-
constructor(status: number, message: string) {
|
|
46
|
-
super(message)
|
|
47
|
-
this.name = 'HttpError'
|
|
48
|
-
this.status = status
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const PRISMA_ERROR_MAP: Record<string, { status: number; message: string }> = {
|
|
53
|
-
P2000: { status: 400, message: 'Value too long for column' },
|
|
54
|
-
P2001: { status: 404, message: 'Record not found' },
|
|
55
|
-
P2002: { status: 409, message: 'Unique constraint violation' },
|
|
56
|
-
P2003: { status: 400, message: 'Foreign key constraint failed' },
|
|
57
|
-
P2004: { status: 400, message: 'Constraint failed on the database' },
|
|
58
|
-
P2005: { status: 400, message: 'Invalid field value' },
|
|
59
|
-
P2006: { status: 400, message: 'Invalid value provided' },
|
|
60
|
-
P2007: { status: 400, message: 'Data validation error' },
|
|
61
|
-
P2008: { status: 400, message: 'Failed to parse the query' },
|
|
62
|
-
P2009: { status: 400, message: 'Failed to validate the query' },
|
|
63
|
-
P2010: { status: 500, message: 'Raw query failed' },
|
|
64
|
-
P2011: { status: 400, message: 'Null constraint violation' },
|
|
65
|
-
P2012: { status: 400, message: 'Missing required value' },
|
|
66
|
-
P2013: { status: 400, message: 'Missing required argument' },
|
|
67
|
-
P2014: { status: 400, message: 'Required relation violation' },
|
|
68
|
-
P2015: { status: 404, message: 'Related record not found' },
|
|
69
|
-
P2016: { status: 400, message: 'Query interpretation error' },
|
|
70
|
-
P2017: { status: 400, message: 'Records not connected' },
|
|
71
|
-
P2018: { status: 404, message: 'Required connected record not found' },
|
|
72
|
-
P2019: { status: 400, message: 'Input error' },
|
|
73
|
-
P2020: { status: 400, message: 'Value out of range for the field type' },
|
|
74
|
-
P2021: { status: 500, message: 'Table does not exist in the database' },
|
|
75
|
-
P2022: { status: 500, message: 'Column does not exist in the database' },
|
|
76
|
-
P2023: { status: 500, message: 'Inconsistent column data' },
|
|
77
|
-
P2024: { status: 503, message: 'Connection pool timeout' },
|
|
78
|
-
P2025: { status: 404, message: 'Record not found' },
|
|
79
|
-
P2026: { status: 501, message: 'Feature not supported by the current database provider' },
|
|
80
|
-
P2028: { status: 500, message: 'Transaction API error' },
|
|
81
|
-
P2030: { status: 400, message: 'Cannot find a fulltext index for the search' },
|
|
82
|
-
P2033: { status: 400, message: 'Number out of range for the field type' },
|
|
83
|
-
P2034: { status: 409, message: 'Transaction conflict, please retry' },
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function mapError(error: unknown): HttpError {
|
|
87
|
-
if (error instanceof HttpError) return error
|
|
88
|
-
if (error && typeof error === 'object' && 'name' in error && (error as any).name === 'ShapeError') {
|
|
89
|
-
return new HttpError(400, (error as any).message)
|
|
90
|
-
}
|
|
91
|
-
if (error && typeof error === 'object' && 'name' in error && (error as any).name === 'CallerError') {
|
|
92
|
-
return new HttpError(400, (error as any).message)
|
|
93
|
-
}
|
|
94
|
-
if (error && typeof error === 'object' && 'name' in error && (error as any).name === 'PolicyError') {
|
|
95
|
-
return new HttpError(403, (error as any).message)
|
|
96
|
-
}
|
|
97
|
-
if (error && typeof error === 'object' && 'issues' in error && 'name' in error && (error as any).name === 'ZodError') {
|
|
98
|
-
const issues = (error as any).issues
|
|
99
|
-
const message = Array.isArray(issues) ? issues.map((i: any) => i.message).join('; ') : (error as any).message
|
|
100
|
-
return new HttpError(400, message)
|
|
101
|
-
}
|
|
102
|
-
if (error && typeof error === 'object' && 'code' in error) {
|
|
103
|
-
const code = (error as any).code as string
|
|
104
|
-
const mapped = PRISMA_ERROR_MAP[code]
|
|
105
|
-
if (mapped) {
|
|
106
|
-
const detail = (error as any).message
|
|
107
|
-
return new HttpError(mapped.status, detail ? mapped.message + ': ' + detail : mapped.message)
|
|
108
|
-
}
|
|
109
|
-
if (typeof code === 'string' && code.startsWith('P')) {
|
|
110
|
-
const msg = (error as any).message || 'Database operation failed'
|
|
111
|
-
console.warn('[prisma-generator-express] Unmapped Prisma error code:', code, msg)
|
|
112
|
-
return new HttpError(500, msg)
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
if (error && typeof error === 'object' && 'name' in error) {
|
|
116
|
-
const name = (error as any).name
|
|
117
|
-
if (name === 'PrismaClientValidationError') return new HttpError(400, (error as any).message || 'Invalid query parameters')
|
|
118
|
-
if (name === 'PrismaClientKnownRequestError') return new HttpError(400, (error as any).message || 'Database request error')
|
|
119
|
-
if (name === 'PrismaClientInitializationError') return new HttpError(503, (error as any).message || 'Database connection failed')
|
|
120
|
-
if (name === 'PrismaClientRustPanicError') return new HttpError(500, (error as any).message || 'Internal database engine error')
|
|
121
|
-
if (name === 'PrismaClientUnknownRequestError') return new HttpError(500, (error as any).message || 'Unknown database error')
|
|
122
|
-
}
|
|
123
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
124
|
-
console.error('[prisma-generator-express] Unhandled error:', error)
|
|
125
|
-
return new HttpError(500, msg || 'Internal server error')
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
let _speedExtension: ((opts: any) => any) | null = null
|
|
129
|
-
|
|
130
|
-
const _prismasqlModule = 'prisma-' + 'sql'
|
|
131
|
-
const _prismasqlReady = (async () => {
|
|
132
|
-
try {
|
|
133
|
-
const mod = await import(_prismasqlModule)
|
|
134
|
-
_speedExtension = mod.speedExtension ?? mod.default?.speedExtension ?? null
|
|
135
|
-
} catch (err: any) {
|
|
136
|
-
const code = err?.code
|
|
137
|
-
if (code !== 'MODULE_NOT_FOUND' && code !== 'ERR_MODULE_NOT_FOUND') {
|
|
138
|
-
console.warn('[prisma-generator-express] prisma-sql initialization failed:', err)
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
})()
|
|
142
|
-
|
|
143
|
-
const _extendedClients = new WeakMap<object, WeakMap<object, any>>()
|
|
144
|
-
|
|
145
|
-
export async function getExtendedClient(ctx: OperationContext): Promise<any> {
|
|
146
|
-
const base = ctx.prisma
|
|
147
|
-
if (!base) {
|
|
148
|
-
throw new HttpError(500, 'PrismaClient not found on request. Set req.prisma in middleware.')
|
|
149
|
-
}
|
|
150
|
-
await _prismasqlReady
|
|
151
|
-
if (!_speedExtension) return base
|
|
152
|
-
const connector = ctx.postgres || ctx.sqlite
|
|
153
|
-
if (!connector) return base
|
|
154
|
-
if (typeof connector === 'object' && connector !== null) {
|
|
155
|
-
const innerMap = _extendedClients.get(connector)
|
|
156
|
-
if (innerMap) {
|
|
157
|
-
const cached = innerMap.get(base)
|
|
158
|
-
if (cached) return cached
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
try {
|
|
162
|
-
const extended = base.$extends(_speedExtension({
|
|
163
|
-
postgres: ctx.postgres,
|
|
164
|
-
sqlite: ctx.sqlite,
|
|
165
|
-
debug: process.env.DEBUG === 'true',
|
|
166
|
-
}))
|
|
167
|
-
if (typeof connector === 'object' && connector !== null) {
|
|
168
|
-
let innerMap = _extendedClients.get(connector)
|
|
169
|
-
if (!innerMap) {
|
|
170
|
-
innerMap = new WeakMap<object, any>()
|
|
171
|
-
_extendedClients.set(connector, innerMap)
|
|
172
|
-
}
|
|
173
|
-
innerMap.set(base, extended)
|
|
174
|
-
}
|
|
175
|
-
return extended
|
|
176
|
-
} catch (error) {
|
|
177
|
-
console.warn('[speedExtension] Failed to initialize, using base client:', error)
|
|
178
|
-
return base
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
export function validateBody(body: unknown): Record<string, any> {
|
|
183
|
-
if (!body || typeof body !== 'object' || Array.isArray(body)) {
|
|
184
|
-
throw new HttpError(400, 'Request body must be a JSON object')
|
|
185
|
-
}
|
|
186
|
-
return sanitizeKeys(body as Record<string, any>)
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
export function requireBodyField(body: Record<string, any>, field: string): void {
|
|
190
|
-
if (!(field in body) || body[field] === undefined) {
|
|
191
|
-
throw new HttpError(400, 'Missing required field: ' + field)
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
export function applyPaginationLimits(query: Record<string, any>, config?: PaginationConfig): Record<string, any> {
|
|
196
|
-
if (!config) return query
|
|
197
|
-
const result = { ...query }
|
|
198
|
-
if (result.take === undefined && config.defaultLimit !== undefined) {
|
|
199
|
-
result.take = config.defaultLimit
|
|
200
|
-
}
|
|
201
|
-
if (config.maxLimit !== undefined && result.take !== undefined) {
|
|
202
|
-
const takeNum = Number(result.take)
|
|
203
|
-
if (Math.abs(takeNum) > config.maxLimit) {
|
|
204
|
-
result.take = takeNum < 0 ? -config.maxLimit : config.maxLimit
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
return result
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
export function normalizeDistinct(value: unknown): string[] {
|
|
211
|
-
if (typeof value === 'string') return [value]
|
|
212
|
-
if (Array.isArray(value)) return value.filter((v): v is string => typeof v === 'string')
|
|
213
|
-
return []
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
export function assertGuard(delegate: any): void {
|
|
217
|
-
if (typeof delegate.guard !== 'function') {
|
|
218
|
-
throw new HttpError(500, 'Guard shapes require prisma-guard extension on PrismaClient.')
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const GUARD_SHAPE_CONFIG_KEYS = new Set([
|
|
223
|
-
'data', 'create', 'update', 'where', 'include', 'select', 'orderBy',
|
|
224
|
-
'cursor', 'take', 'skip', 'distinct', 'having', '_count', '_avg',
|
|
225
|
-
'_sum', '_min', '_max', 'by',
|
|
226
|
-
])
|
|
227
|
-
|
|
228
|
-
function keepWhereOnly(obj: Record<string, any>): Record<string, any> {
|
|
229
|
-
const result: Record<string, any> = {}
|
|
230
|
-
if ('where' in obj) result.where = obj.where
|
|
231
|
-
return result
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
export function buildCountShape(shape: Record<string, any>): Record<string, any> {
|
|
235
|
-
if (typeof shape === 'function') {
|
|
236
|
-
return (...args: any[]) => keepWhereOnly((shape as Function)(...args))
|
|
237
|
-
}
|
|
238
|
-
const keys = Object.keys(shape)
|
|
239
|
-
const isSingleShape = keys.length === 0 || keys.every((k) => GUARD_SHAPE_CONFIG_KEYS.has(k))
|
|
240
|
-
if (isSingleShape) return keepWhereOnly(shape)
|
|
241
|
-
const result: Record<string, any> = {}
|
|
242
|
-
for (const [key, variant] of Object.entries(shape)) {
|
|
243
|
-
if (typeof variant === 'function') {
|
|
244
|
-
result[key] = (...args: any[]) => keepWhereOnly(variant(...args))
|
|
245
|
-
} else if (typeof variant === 'object' && variant !== null) {
|
|
246
|
-
result[key] = keepWhereOnly(variant)
|
|
247
|
-
} else {
|
|
248
|
-
result[key] = variant
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
return result
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
export async function countForPagination(
|
|
255
|
-
delegate: any,
|
|
256
|
-
query: Record<string, any>,
|
|
257
|
-
shape: Record<string, any> | undefined,
|
|
258
|
-
caller: string | undefined,
|
|
259
|
-
distinctCountLimit?: number,
|
|
260
|
-
): Promise<number> {
|
|
261
|
-
const distinctFields = normalizeDistinct(query.distinct)
|
|
262
|
-
const hasDistinct = distinctFields.length > 0
|
|
263
|
-
const effectiveLimit = distinctCountLimit ?? DISTINCT_COUNT_LIMIT
|
|
264
|
-
const countShape = shape ? buildCountShape(shape) : undefined
|
|
265
|
-
|
|
266
|
-
if (hasDistinct) {
|
|
267
|
-
const selectField = distinctFields[0]
|
|
268
|
-
const distinctArgs: Record<string, any> = {
|
|
269
|
-
where: query.where,
|
|
270
|
-
distinct: distinctFields,
|
|
271
|
-
select: { [selectField]: true },
|
|
272
|
-
take: effectiveLimit + 1,
|
|
273
|
-
}
|
|
274
|
-
const results = shape
|
|
275
|
-
? await delegate.guard(shape, caller).findMany(distinctArgs)
|
|
276
|
-
: await delegate.findMany(distinctArgs)
|
|
277
|
-
if (results.length > effectiveLimit) {
|
|
278
|
-
console.warn('[prisma-generator-express] Distinct count exceeds ' + effectiveLimit + ', falling back to approximate total')
|
|
279
|
-
const countArgs: Record<string, any> = {}
|
|
280
|
-
if (query.where) countArgs.where = query.where
|
|
281
|
-
return countShape
|
|
282
|
-
? await delegate.guard(countShape, caller).count(countArgs)
|
|
283
|
-
: await delegate.count(countArgs)
|
|
284
|
-
}
|
|
285
|
-
return results.length
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const countArgs: Record<string, any> = {}
|
|
289
|
-
if (query.where) countArgs.where = query.where
|
|
290
|
-
return countShape
|
|
291
|
-
? await delegate.guard(countShape, caller).count(countArgs)
|
|
292
|
-
: await delegate.count(countArgs)
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
export function transformResult(value: unknown): unknown {
|
|
296
|
-
if (value === null || value === undefined) return value
|
|
297
|
-
if (typeof value === 'bigint') return value.toString()
|
|
298
|
-
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value)) return value.toString('base64')
|
|
299
|
-
if (value instanceof Uint8Array) return Buffer.from(value).toString('base64')
|
|
300
|
-
if (value instanceof Date) return value
|
|
301
|
-
if (Array.isArray(value)) return value.map(transformResult)
|
|
302
|
-
if (typeof value === 'object') {
|
|
303
|
-
const proto = Object.getPrototypeOf(value)
|
|
304
|
-
if (proto !== Object.prototype && proto !== null) return value
|
|
305
|
-
const out: Record<string, unknown> = {}
|
|
306
|
-
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
307
|
-
out[k] = transformResult(v)
|
|
308
|
-
}
|
|
309
|
-
return out
|
|
310
|
-
}
|
|
311
|
-
return value
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
export function acceptsEventStream(accept: string | undefined): boolean {
|
|
315
|
-
if (!accept) return false
|
|
316
|
-
return accept.toLowerCase().includes('text/event-stream')
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const UNSAFE_PATH_SEGMENTS = new Set(['__proto__', 'constructor', 'prototype'])
|
|
320
|
-
|
|
321
|
-
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
322
|
-
if (value === null || typeof value !== 'object') return false
|
|
323
|
-
if (Array.isArray(value)) return false
|
|
324
|
-
const proto = Object.getPrototypeOf(value)
|
|
325
|
-
return proto === Object.prototype || proto === null
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
export function setByPath(target: Record<string, unknown>, path: string, value: unknown): boolean {
|
|
329
|
-
const parts = path.split('.')
|
|
330
|
-
if (parts.length === 0) return false
|
|
331
|
-
for (const p of parts) {
|
|
332
|
-
if (p === '' || UNSAFE_PATH_SEGMENTS.has(p)) return false
|
|
333
|
-
}
|
|
334
|
-
let cursor: Record<string, unknown> = target
|
|
335
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
336
|
-
const part = parts[i]
|
|
337
|
-
const next = cursor[part]
|
|
338
|
-
if (!isPlainObject(next)) {
|
|
339
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
340
|
-
console.warn(
|
|
341
|
-
'[progressive] Dropping patch for "' + path +
|
|
342
|
-
'": cannot traverse non-plain-object at segment "' + part + '"',
|
|
343
|
-
)
|
|
344
|
-
}
|
|
345
|
-
return false
|
|
346
|
-
}
|
|
347
|
-
cursor = next
|
|
348
|
-
}
|
|
349
|
-
cursor[parts[parts.length - 1]] = value
|
|
350
|
-
return true
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function removeReqCloseListener(req: any, listener: () => void): void {
|
|
354
|
-
if (typeof req.off === 'function') {
|
|
355
|
-
req.off('close', listener)
|
|
356
|
-
} else if (typeof req.removeListener === 'function') {
|
|
357
|
-
req.removeListener('close', listener)
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
export function initSSE(res: any): void {
|
|
362
|
-
res.statusCode = 200
|
|
363
|
-
res.setHeader('Content-Type', 'text/event-stream')
|
|
364
|
-
res.setHeader('Cache-Control', 'no-cache, no-transform')
|
|
365
|
-
res.setHeader('Connection', 'keep-alive')
|
|
366
|
-
res.setHeader('X-Accel-Buffering', 'no')
|
|
367
|
-
if (typeof res.flushHeaders === 'function') res.flushHeaders()
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
export function flushSSE(res: any): void {
|
|
371
|
-
if (typeof res.flush === 'function') {
|
|
372
|
-
try { res.flush() } catch {}
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
export function sendSSE(res: any, payload: unknown): boolean {
|
|
377
|
-
if (res.writableEnded || res.destroyed) return false
|
|
378
|
-
try {
|
|
379
|
-
res.write('data: ' + JSON.stringify(transformResult(payload)) + '\\n\\n')
|
|
380
|
-
flushSSE(res)
|
|
381
|
-
return true
|
|
382
|
-
} catch (err) {
|
|
383
|
-
console.error('[progressive] failed to send SSE event:', err)
|
|
384
|
-
return false
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
export function sendSSEProgress(res: any, stage: string, completed: number, total: number): boolean {
|
|
389
|
-
return sendSSE(res, { type: 'progress', stage, completed, total })
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
export function sendSSEField(res: any, key: string, value: unknown): boolean {
|
|
393
|
-
return sendSSE(res, { type: 'field', key, value })
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
export function sendSSEResult(res: any, data: unknown): boolean {
|
|
397
|
-
return sendSSE(res, { type: 'result', data })
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
export function sendSSEError(res: any, message: string): boolean {
|
|
401
|
-
if (res.writableEnded || res.destroyed) return false
|
|
402
|
-
try {
|
|
403
|
-
res.write('data: ' + JSON.stringify({ type: 'error', message }) + '\\n\\n')
|
|
404
|
-
flushSSE(res)
|
|
405
|
-
return true
|
|
406
|
-
} catch (err) {
|
|
407
|
-
console.error('[progressive] failed to send SSE error event:', err)
|
|
408
|
-
return false
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
export function startSSEKeepalive(res: any, intervalMs: number = 15000): any {
|
|
413
|
-
const handle = setInterval(() => {
|
|
414
|
-
if (res.writableEnded || res.destroyed) return
|
|
415
|
-
try {
|
|
416
|
-
res.write(': keepalive\\n\\n')
|
|
417
|
-
flushSSE(res)
|
|
418
|
-
} catch {}
|
|
419
|
-
}, intervalMs)
|
|
420
|
-
if (typeof (handle as any).unref === 'function') (handle as any).unref()
|
|
421
|
-
return handle
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
export function endSSE(res: any, keepaliveHandle: any): void {
|
|
425
|
-
if (keepaliveHandle) {
|
|
426
|
-
try { clearInterval(keepaliveHandle) } catch {}
|
|
427
|
-
}
|
|
428
|
-
if (!res.writableEnded && !res.destroyed) {
|
|
429
|
-
try { res.end() } catch {}
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
export interface RunSingleResultSSEOptions {
|
|
434
|
-
req: any
|
|
435
|
-
res: any
|
|
436
|
-
coreQueryFn: () => Promise<unknown>
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
export async function runSingleResultSSE(options: RunSingleResultSSEOptions): Promise<void> {
|
|
440
|
-
const { req, res, coreQueryFn } = options
|
|
441
|
-
let keepalive: any = null
|
|
442
|
-
try {
|
|
443
|
-
initSSE(res)
|
|
444
|
-
keepalive = startSSEKeepalive(res)
|
|
445
|
-
if (req.destroyed) return
|
|
446
|
-
const data = await coreQueryFn()
|
|
447
|
-
if (res.writableEnded || res.destroyed) return
|
|
448
|
-
sendSSEResult(res, data)
|
|
449
|
-
} catch (err) {
|
|
450
|
-
console.error('[progressive] single-result error:', err)
|
|
451
|
-
if (!res.writableEnded && !res.destroyed) {
|
|
452
|
-
sendSSEError(res, 'Internal server error')
|
|
453
|
-
}
|
|
454
|
-
} finally {
|
|
455
|
-
endSSE(res, keepalive)
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
function isStopResult(value: unknown): value is ProgressiveStopResult<unknown> {
|
|
460
|
-
return typeof value === 'object' && value !== null && (value as any).stop === true
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
export interface RunProgressiveOptions {
|
|
464
|
-
req: any
|
|
465
|
-
res: any
|
|
466
|
-
ctx: unknown
|
|
467
|
-
prisma: any
|
|
468
|
-
variant: string
|
|
469
|
-
stages: string[]
|
|
470
|
-
stageRegistry: Record<string, ProgressiveStage<any, any>>
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
export async function runProgressiveEndpoint(options: RunProgressiveOptions): Promise<void> {
|
|
474
|
-
const { req, res, ctx, prisma, variant, stages, stageRegistry } = options
|
|
475
|
-
let keepalive: any = null
|
|
476
|
-
const controller = new AbortController()
|
|
477
|
-
const onClose = () => controller.abort()
|
|
478
|
-
if (typeof req.on === 'function') req.on('close', onClose)
|
|
479
|
-
|
|
480
|
-
const accumulated: Record<string, unknown> = {}
|
|
481
|
-
const signal = controller.signal
|
|
482
|
-
|
|
483
|
-
try {
|
|
484
|
-
initSSE(res)
|
|
485
|
-
keepalive = startSSEKeepalive(res)
|
|
486
|
-
sendSSEProgress(res, 'start', 0, stages.length)
|
|
487
|
-
|
|
488
|
-
for (let i = 0; i < stages.length; i++) {
|
|
489
|
-
if (res.writableEnded || res.destroyed || signal.aborted) return
|
|
490
|
-
const stageName = stages[i]
|
|
491
|
-
const stage = stageRegistry[stageName]
|
|
492
|
-
if (!stage) throw new Error('Missing progressive stage: ' + stageName)
|
|
493
|
-
|
|
494
|
-
const result = await stage({ ctx, req, res, prisma, variant, accumulated, signal })
|
|
495
|
-
if (res.writableEnded || res.destroyed) return
|
|
496
|
-
|
|
497
|
-
if (isStopResult(result)) {
|
|
498
|
-
sendSSEResult(res, result.data)
|
|
499
|
-
return
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
const patches = Array.isArray(result) ? result : result ? [result] : []
|
|
503
|
-
for (const patch of patches) {
|
|
504
|
-
if (!patch || typeof patch !== 'object') continue
|
|
505
|
-
if (typeof (patch as any).key !== 'string') continue
|
|
506
|
-
if (!('value' in patch)) continue
|
|
507
|
-
const p = patch as ProgressivePatch
|
|
508
|
-
const applied = setByPath(accumulated, p.key, p.value)
|
|
509
|
-
if (applied) sendSSEField(res, p.key, p.value)
|
|
510
|
-
}
|
|
511
|
-
sendSSEProgress(res, stageName, i + 1, stages.length)
|
|
512
|
-
}
|
|
513
|
-
if (res.writableEnded || res.destroyed) return
|
|
514
|
-
sendSSEResult(res, accumulated)
|
|
515
|
-
} catch (err) {
|
|
516
|
-
console.error('[progressive] stage error:', err)
|
|
517
|
-
if (!res.writableEnded && !res.destroyed) {
|
|
518
|
-
sendSSEError(res, 'Could not load progressive response')
|
|
519
|
-
}
|
|
520
|
-
} finally {
|
|
521
|
-
removeReqCloseListener(req, onClose)
|
|
522
|
-
endSSE(res, keepalive)
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
`
|
|
526
|
-
}
|
|
527
|
-
|
|
528
5
|
export interface ModelCoreOptions {
|
|
529
6
|
model: DMMF.Model
|
|
530
7
|
importStyle: ImportStyle
|
|
@@ -545,11 +22,12 @@ export function generateModelCore(options: ModelCoreOptions): string {
|
|
|
545
22
|
export async function ${op}(ctx: OperationContext): Promise<unknown> {
|
|
546
23
|
const query = ctx.parsedQuery || {}
|
|
547
24
|
const extended = await getExtendedClient(ctx)
|
|
25
|
+
const delegate = getDelegate(extended, '${modelNameLower}')
|
|
548
26
|
if (ctx.guardShape) {
|
|
549
|
-
assertGuard(
|
|
550
|
-
return
|
|
27
|
+
assertGuard(delegate)
|
|
28
|
+
return delegate.guard(ctx.guardShape, ctx.guardCaller).${op}(query)
|
|
551
29
|
}
|
|
552
|
-
return
|
|
30
|
+
return delegate.${op}(query)
|
|
553
31
|
}`)
|
|
554
32
|
.join('\n')
|
|
555
33
|
|
|
@@ -572,17 +50,19 @@ export async function ${op.name}(ctx: OperationContext): Promise<unknown> {
|
|
|
572
50
|
const body = validateBody(ctx.body)
|
|
573
51
|
${validationLines}
|
|
574
52
|
const extended = await getExtendedClient(ctx)
|
|
53
|
+
const delegate = getDelegate(extended, '${modelNameLower}')
|
|
575
54
|
if (ctx.guardShape) {
|
|
576
|
-
assertGuard(
|
|
577
|
-
return
|
|
55
|
+
assertGuard(delegate)
|
|
56
|
+
return delegate.guard(ctx.guardShape, ctx.guardCaller).${op.method}(body)
|
|
578
57
|
}
|
|
579
|
-
return
|
|
58
|
+
return delegate.${op.method}(body)
|
|
580
59
|
}`
|
|
581
60
|
}).join('\n')
|
|
582
61
|
|
|
583
62
|
return `import {
|
|
584
63
|
OperationContext,
|
|
585
64
|
getExtendedClient,
|
|
65
|
+
getDelegate,
|
|
586
66
|
validateBody,
|
|
587
67
|
requireBodyField,
|
|
588
68
|
applyPaginationLimits,
|
|
@@ -594,11 +74,12 @@ export async function findMany(ctx: OperationContext): Promise<unknown> {
|
|
|
594
74
|
const rawQuery = ctx.parsedQuery || {}
|
|
595
75
|
const query = applyPaginationLimits(rawQuery, ctx.paginationConfig)
|
|
596
76
|
const extended = await getExtendedClient(ctx)
|
|
77
|
+
const delegate = getDelegate(extended, '${modelNameLower}')
|
|
597
78
|
if (ctx.guardShape) {
|
|
598
|
-
assertGuard(
|
|
599
|
-
return
|
|
79
|
+
assertGuard(delegate)
|
|
80
|
+
return delegate.guard(ctx.guardShape, ctx.guardCaller).findMany(query)
|
|
600
81
|
}
|
|
601
|
-
return
|
|
82
|
+
return delegate.findMany(query)
|
|
602
83
|
}
|
|
603
84
|
${standardReadHandlers}
|
|
604
85
|
${writeHandlers}
|
|
@@ -612,33 +93,39 @@ export async function findManyPaginated(
|
|
|
612
93
|
const shape = ctx.guardShape
|
|
613
94
|
const caller = ctx.guardCaller
|
|
614
95
|
const distinctCountLimit = ctx.paginationConfig?.distinctCountLimit
|
|
615
|
-
const delegate = (extended
|
|
96
|
+
const delegate = getDelegate(extended, '${modelNameLower}')
|
|
616
97
|
|
|
617
98
|
if (shape) assertGuard(delegate)
|
|
618
99
|
|
|
619
|
-
let items:
|
|
100
|
+
let items: unknown[]
|
|
620
101
|
let total: number
|
|
621
102
|
|
|
622
|
-
|
|
103
|
+
const txClient = extended as { $transaction?: <T>(fn: (tx: unknown) => Promise<T>) => Promise<T> }
|
|
104
|
+
|
|
105
|
+
if (shape || typeof txClient.$transaction !== 'function') {
|
|
623
106
|
const [data, count] = await Promise.all([
|
|
624
|
-
shape
|
|
107
|
+
shape
|
|
108
|
+
? (delegate.guard as NonNullable<typeof delegate.guard>)(shape, caller).findMany(query)
|
|
109
|
+
: delegate.findMany(query),
|
|
625
110
|
countForPagination(delegate, query, shape, caller, distinctCountLimit),
|
|
626
111
|
])
|
|
627
|
-
items = data
|
|
112
|
+
items = data as unknown[]
|
|
628
113
|
total = count
|
|
629
114
|
} else {
|
|
630
115
|
try {
|
|
631
|
-
const txResult = await
|
|
632
|
-
const
|
|
633
|
-
const
|
|
116
|
+
const txResult = await txClient.$transaction(async (tx: unknown) => {
|
|
117
|
+
const txDelegate = getDelegate(tx, '${modelNameLower}')
|
|
118
|
+
const d = await txDelegate.findMany(query)
|
|
119
|
+
const t = await countForPagination(txDelegate, query, undefined, undefined, distinctCountLimit)
|
|
634
120
|
return { d, t }
|
|
635
121
|
})
|
|
636
|
-
items = txResult.d
|
|
122
|
+
items = txResult.d as unknown[]
|
|
637
123
|
total = txResult.t
|
|
638
|
-
} catch (txError:
|
|
639
|
-
|
|
124
|
+
} catch (txError: unknown) {
|
|
125
|
+
const txe = txError as { message?: string; code?: string }
|
|
126
|
+
if ((typeof txe?.message === 'string' && txe.message.includes('interactive transactions')) || txe?.code === 'P2028') {
|
|
640
127
|
console.warn('[prisma-generator-express] Interactive transactions not available, pagination queries are non-atomic')
|
|
641
|
-
items = await delegate.findMany(query)
|
|
128
|
+
items = (await delegate.findMany(query)) as unknown[]
|
|
642
129
|
total = await countForPagination(delegate, query, undefined, undefined, distinctCountLimit)
|
|
643
130
|
} else {
|
|
644
131
|
throw txError
|
|
@@ -646,8 +133,9 @@ export async function findManyPaginated(
|
|
|
646
133
|
}
|
|
647
134
|
}
|
|
648
135
|
|
|
649
|
-
const skip = (query.skip
|
|
650
|
-
const
|
|
136
|
+
const skip = (typeof query.skip === 'number' ? query.skip : 0)
|
|
137
|
+
const takeRaw = (typeof query.take === 'number' ? query.take : items.length)
|
|
138
|
+
const absTake = Math.abs(takeRaw)
|
|
651
139
|
const hasMore = items.length >= absTake && skip + items.length < total
|
|
652
140
|
|
|
653
141
|
return { data: items, total, hasMore }
|