prisma-generator-express 1.59.0 → 1.60.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/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/autoIncludeRuntime.ts +117 -228
- 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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { HttpError, LOG_PREFIX } from './errorMapper'
|
|
1
2
|
import { sanitizeKeys, isPlainObject } from './misc'
|
|
2
3
|
import type {
|
|
3
4
|
ProgressivePatch,
|
|
@@ -59,123 +60,6 @@ export type PrismaClientLike = {
|
|
|
59
60
|
$transaction?: <T>(fn: (tx: PrismaClientLike) => Promise<T>) => Promise<T>
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
type PrismaRawClient = {
|
|
63
|
-
$queryRawUnsafe?: <T = unknown>(sql: string, ...values: unknown[]) => Promise<T>
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export const DISTINCT_COUNT_LIMIT = 100000
|
|
67
|
-
|
|
68
|
-
const IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/
|
|
69
|
-
|
|
70
|
-
export class HttpError extends Error {
|
|
71
|
-
status: number
|
|
72
|
-
constructor(status: number, message: string) {
|
|
73
|
-
super(message)
|
|
74
|
-
this.name = 'HttpError'
|
|
75
|
-
this.status = status
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const PRISMA_ERROR_MAP: Record<string, { status: number; message: string }> = {
|
|
80
|
-
P2000: { status: 400, message: 'Value too long for column' },
|
|
81
|
-
P2001: { status: 404, message: 'Record not found' },
|
|
82
|
-
P2002: { status: 409, message: 'Unique constraint violation' },
|
|
83
|
-
P2003: { status: 400, message: 'Foreign key constraint failed' },
|
|
84
|
-
P2004: { status: 400, message: 'Constraint failed on the database' },
|
|
85
|
-
P2005: { status: 400, message: 'Invalid field value' },
|
|
86
|
-
P2006: { status: 400, message: 'Invalid value provided' },
|
|
87
|
-
P2007: { status: 400, message: 'Data validation error' },
|
|
88
|
-
P2008: { status: 400, message: 'Failed to parse the query' },
|
|
89
|
-
P2009: { status: 400, message: 'Failed to validate the query' },
|
|
90
|
-
P2010: { status: 500, message: 'Raw query failed' },
|
|
91
|
-
P2011: { status: 400, message: 'Null constraint violation' },
|
|
92
|
-
P2012: { status: 400, message: 'Missing required value' },
|
|
93
|
-
P2013: { status: 400, message: 'Missing required argument' },
|
|
94
|
-
P2014: { status: 400, message: 'Required relation violation' },
|
|
95
|
-
P2015: { status: 404, message: 'Related record not found' },
|
|
96
|
-
P2016: { status: 400, message: 'Query interpretation error' },
|
|
97
|
-
P2017: { status: 400, message: 'Records not connected' },
|
|
98
|
-
P2018: { status: 404, message: 'Required connected record not found' },
|
|
99
|
-
P2019: { status: 400, message: 'Input error' },
|
|
100
|
-
P2020: { status: 400, message: 'Value out of range for the field type' },
|
|
101
|
-
P2021: { status: 500, message: 'Table does not exist in the database' },
|
|
102
|
-
P2022: { status: 500, message: 'Column does not exist in the database' },
|
|
103
|
-
P2023: { status: 500, message: 'Inconsistent column data' },
|
|
104
|
-
P2024: { status: 503, message: 'Connection pool timeout' },
|
|
105
|
-
P2025: { status: 404, message: 'Record not found' },
|
|
106
|
-
P2026: { status: 501, message: 'Feature not supported by the current database provider' },
|
|
107
|
-
P2027: { status: 400, message: 'Multiple errors occurred during transaction execution' },
|
|
108
|
-
P2028: { status: 500, message: 'Transaction API error' },
|
|
109
|
-
P2030: { status: 400, message: 'Cannot find a fulltext index for the search' },
|
|
110
|
-
P2033: { status: 400, message: 'Number out of range for the field type' },
|
|
111
|
-
P2034: { status: 409, message: 'Transaction conflict, please retry' },
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
type ErrorShape = {
|
|
115
|
-
name?: string
|
|
116
|
-
code?: string
|
|
117
|
-
message?: string
|
|
118
|
-
issues?: unknown
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function asErrorShape(error: unknown): ErrorShape {
|
|
122
|
-
if (error && typeof error === 'object') return error as ErrorShape
|
|
123
|
-
return {}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function isProduction(): boolean {
|
|
127
|
-
return typeof process !== 'undefined' && process.env && process.env.NODE_ENV === 'production'
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export function mapError(error: unknown): HttpError {
|
|
131
|
-
if (error instanceof HttpError) return error
|
|
132
|
-
const e = asErrorShape(error)
|
|
133
|
-
const isProd = isProduction()
|
|
134
|
-
|
|
135
|
-
if (e.name === 'ShapeError') return new HttpError(400, e.message || 'Shape validation failed')
|
|
136
|
-
if (e.name === 'CallerError') return new HttpError(400, e.message || 'Caller validation failed')
|
|
137
|
-
if (e.name === 'PolicyError') return new HttpError(403, e.message || 'Policy denied')
|
|
138
|
-
if (e.name === 'ZodError') {
|
|
139
|
-
const issues = e.issues
|
|
140
|
-
const message = Array.isArray(issues)
|
|
141
|
-
? (issues as Array<{ message?: string }>).map((i) => i.message ?? '').filter(Boolean).join('; ')
|
|
142
|
-
: (e.message || 'Validation failed')
|
|
143
|
-
return new HttpError(400, message)
|
|
144
|
-
}
|
|
145
|
-
if (typeof e.code === 'string') {
|
|
146
|
-
const mapped = PRISMA_ERROR_MAP[e.code]
|
|
147
|
-
if (mapped) {
|
|
148
|
-
const detail = e.message
|
|
149
|
-
const shouldStripDetail = isProd && mapped.status >= 500
|
|
150
|
-
return new HttpError(
|
|
151
|
-
mapped.status,
|
|
152
|
-
!shouldStripDetail && detail ? mapped.message + ': ' + detail : mapped.message,
|
|
153
|
-
)
|
|
154
|
-
}
|
|
155
|
-
if (e.code.startsWith('P')) {
|
|
156
|
-
const msg = e.message || 'Database operation failed'
|
|
157
|
-
console.warn('[prisma-generator-express] Unmapped Prisma error code:', e.code, msg)
|
|
158
|
-
return new HttpError(500, isProd ? 'Internal server error' : msg)
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
if (typeof e.name === 'string') {
|
|
162
|
-
if (e.name === 'PrismaClientValidationError') return new HttpError(400, e.message || 'Invalid query parameters')
|
|
163
|
-
if (e.name === 'PrismaClientKnownRequestError') return new HttpError(400, e.message || 'Database request error')
|
|
164
|
-
if (e.name === 'PrismaClientInitializationError') {
|
|
165
|
-
return new HttpError(503, isProd ? 'Service unavailable' : (e.message || 'Database connection failed'))
|
|
166
|
-
}
|
|
167
|
-
if (e.name === 'PrismaClientRustPanicError') {
|
|
168
|
-
return new HttpError(500, isProd ? 'Internal server error' : (e.message || 'Internal database engine error'))
|
|
169
|
-
}
|
|
170
|
-
if (e.name === 'PrismaClientUnknownRequestError') {
|
|
171
|
-
return new HttpError(500, isProd ? 'Internal server error' : (e.message || 'Unknown database error'))
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
175
|
-
console.error('[prisma-generator-express] Unhandled error:', error)
|
|
176
|
-
return new HttpError(500, isProd ? 'Internal server error' : (msg || 'Internal server error'))
|
|
177
|
-
}
|
|
178
|
-
|
|
179
63
|
type SpeedExtensionFactory = (opts: { postgres?: unknown; sqlite?: unknown; debug?: boolean }) => unknown
|
|
180
64
|
|
|
181
65
|
let _speedExtension: SpeedExtensionFactory | null = null
|
|
@@ -191,7 +75,7 @@ const _prismasqlReady = (async () => {
|
|
|
191
75
|
} catch (err) {
|
|
192
76
|
const code = (err as { code?: string } | null)?.code
|
|
193
77
|
if (code !== 'MODULE_NOT_FOUND' && code !== 'ERR_MODULE_NOT_FOUND') {
|
|
194
|
-
console.warn('
|
|
78
|
+
console.warn(LOG_PREFIX, 'prisma-sql initialization failed:', err)
|
|
195
79
|
}
|
|
196
80
|
}
|
|
197
81
|
})()
|
|
@@ -207,12 +91,10 @@ export async function getExtendedClient(ctx: OperationContext): Promise<unknown>
|
|
|
207
91
|
if (!_speedExtension) return base
|
|
208
92
|
const connector = (ctx.postgres ?? ctx.sqlite) as object | undefined
|
|
209
93
|
if (!connector) return base
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (cached) return cached
|
|
215
|
-
}
|
|
94
|
+
const innerMap = _extendedClients.get(connector)
|
|
95
|
+
if (innerMap) {
|
|
96
|
+
const cached = innerMap.get(base as unknown as object)
|
|
97
|
+
if (cached) return cached
|
|
216
98
|
}
|
|
217
99
|
try {
|
|
218
100
|
if (typeof base.$extends !== 'function') return base
|
|
@@ -221,14 +103,12 @@ export async function getExtendedClient(ctx: OperationContext): Promise<unknown>
|
|
|
221
103
|
sqlite: ctx.sqlite,
|
|
222
104
|
debug: process.env.DEBUG === 'true',
|
|
223
105
|
}))
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
_extendedClients.set(connector, innerMap)
|
|
229
|
-
}
|
|
230
|
-
innerMap.set(base as unknown as object, extended)
|
|
106
|
+
let map = _extendedClients.get(connector)
|
|
107
|
+
if (!map) {
|
|
108
|
+
map = new WeakMap<object, unknown>()
|
|
109
|
+
_extendedClients.set(connector, map)
|
|
231
110
|
}
|
|
111
|
+
map.set(base as unknown as object, extended)
|
|
232
112
|
return extended
|
|
233
113
|
} catch (error) {
|
|
234
114
|
console.warn('[speedExtension] Failed to initialize, using base client:', error)
|
|
@@ -260,211 +140,6 @@ export function requireBodyField(body: Record<string, unknown>, field: string):
|
|
|
260
140
|
}
|
|
261
141
|
}
|
|
262
142
|
|
|
263
|
-
export function applyPaginationLimits(
|
|
264
|
-
query: Record<string, unknown>,
|
|
265
|
-
config?: PaginationConfig,
|
|
266
|
-
hasGuardShape?: boolean,
|
|
267
|
-
): Record<string, unknown> {
|
|
268
|
-
if (!config) return query
|
|
269
|
-
const result: Record<string, unknown> = { ...query }
|
|
270
|
-
if (!hasGuardShape && result.take === undefined && config.defaultLimit !== undefined) {
|
|
271
|
-
result.take = config.defaultLimit
|
|
272
|
-
}
|
|
273
|
-
if (config.maxLimit !== undefined && result.take !== undefined) {
|
|
274
|
-
const takeNum = Number(result.take)
|
|
275
|
-
if (!Number.isFinite(takeNum)) {
|
|
276
|
-
result.take = config.maxLimit
|
|
277
|
-
} else if (Math.abs(takeNum) > config.maxLimit) {
|
|
278
|
-
result.take = takeNum < 0 ? -config.maxLimit : config.maxLimit
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
return result
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
export function mergePaginationConfig(
|
|
285
|
-
base: PaginationConfig | undefined,
|
|
286
|
-
override: Partial<PaginationConfig> | undefined,
|
|
287
|
-
): PaginationConfig | undefined {
|
|
288
|
-
if (!base && !override) return undefined
|
|
289
|
-
return { ...(base ?? {}), ...(override ?? {}) }
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
export function normalizeDistinct(value: unknown): string[] {
|
|
293
|
-
if (typeof value === 'string') return [value]
|
|
294
|
-
if (Array.isArray(value)) return value.filter((v): v is string => typeof v === 'string')
|
|
295
|
-
return []
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
export function assertGuard(
|
|
299
|
-
delegate: PrismaDelegate,
|
|
300
|
-
): asserts delegate is PrismaDelegate & { guard: NonNullable<PrismaDelegate['guard']> } {
|
|
301
|
-
if (typeof delegate.guard !== 'function') {
|
|
302
|
-
throw new HttpError(
|
|
303
|
-
500,
|
|
304
|
-
'Guard shapes require prisma-guard extension on PrismaClient. Install: npm install prisma-guard, then extend your client with guardExtension().',
|
|
305
|
-
)
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
const GUARD_SHAPE_CONFIG_KEYS = new Set([
|
|
310
|
-
'data', 'create', 'update', 'where', 'include', 'select', 'orderBy',
|
|
311
|
-
'cursor', 'take', 'skip', 'distinct', 'having', '_count', '_avg',
|
|
312
|
-
'_sum', '_min', '_max', 'by',
|
|
313
|
-
])
|
|
314
|
-
|
|
315
|
-
function keepWhereOnly(obj: Record<string, unknown>): Record<string, unknown> {
|
|
316
|
-
const result: Record<string, unknown> = {}
|
|
317
|
-
if ('where' in obj) result.where = obj.where
|
|
318
|
-
return result
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
type ShapeFn = (...args: unknown[]) => Record<string, unknown>
|
|
322
|
-
|
|
323
|
-
export function buildCountShape(
|
|
324
|
-
shape: Record<string, unknown> | ShapeFn,
|
|
325
|
-
): Record<string, unknown> | ShapeFn {
|
|
326
|
-
if (typeof shape === 'function') {
|
|
327
|
-
const fn = shape as ShapeFn
|
|
328
|
-
return (...args: unknown[]) => keepWhereOnly(fn(...args))
|
|
329
|
-
}
|
|
330
|
-
const keys = Object.keys(shape)
|
|
331
|
-
const isSingleShape = keys.length === 0 || keys.every((k) => GUARD_SHAPE_CONFIG_KEYS.has(k))
|
|
332
|
-
if (isSingleShape) return keepWhereOnly(shape)
|
|
333
|
-
const result: Record<string, unknown> = {}
|
|
334
|
-
for (const [key, variant] of Object.entries(shape)) {
|
|
335
|
-
if (typeof variant === 'function') {
|
|
336
|
-
const vfn = variant as ShapeFn
|
|
337
|
-
result[key] = (...args: unknown[]) => keepWhereOnly(vfn(...args))
|
|
338
|
-
} else if (variant && typeof variant === 'object') {
|
|
339
|
-
result[key] = keepWhereOnly(variant as Record<string, unknown>)
|
|
340
|
-
} else {
|
|
341
|
-
result[key] = variant
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
return result
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
function quoteIdent(name: string): string {
|
|
348
|
-
if (!IDENT_RE.test(name)) {
|
|
349
|
-
throw new HttpError(400, 'invalid identifier: ' + name)
|
|
350
|
-
}
|
|
351
|
-
return '"' + name.replace(/"/g, '""') + '"'
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
function buildMaterializedCountFqn(
|
|
355
|
-
source: Extract<PaginationCountSource, { type: 'materializedView' }>,
|
|
356
|
-
): string {
|
|
357
|
-
return source.schema
|
|
358
|
-
? quoteIdent(source.schema) + '.' + quoteIdent(source.relation)
|
|
359
|
-
: quoteIdent(source.relation)
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function buildMaterializedCountWhere(
|
|
363
|
-
where: Record<string, unknown> | undefined,
|
|
364
|
-
): { sql: string; values: unknown[] } {
|
|
365
|
-
if (!where || Object.keys(where).length === 0) {
|
|
366
|
-
return { sql: '', values: [] }
|
|
367
|
-
}
|
|
368
|
-
const values: unknown[] = []
|
|
369
|
-
const clauses: string[] = []
|
|
370
|
-
for (const [key, value] of Object.entries(where)) {
|
|
371
|
-
if (value === null) {
|
|
372
|
-
clauses.push(quoteIdent(key) + ' IS NULL')
|
|
373
|
-
continue
|
|
374
|
-
}
|
|
375
|
-
values.push(value)
|
|
376
|
-
clauses.push(quoteIdent(key) + ' = $' + values.length)
|
|
377
|
-
}
|
|
378
|
-
return { sql: ' WHERE ' + clauses.join(' AND '), values }
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
export async function countFromMaterializedView(
|
|
382
|
-
client: unknown,
|
|
383
|
-
source: Extract<PaginationCountSource, { type: 'materializedView' }>,
|
|
384
|
-
): Promise<number> {
|
|
385
|
-
const raw = client as PrismaRawClient
|
|
386
|
-
if (typeof raw.$queryRawUnsafe !== 'function') {
|
|
387
|
-
throw new HttpError(500, 'Materialized count source requires $queryRawUnsafe on the Prisma client')
|
|
388
|
-
}
|
|
389
|
-
const column = source.column ?? 'total'
|
|
390
|
-
const where = buildMaterializedCountWhere(source.where)
|
|
391
|
-
const sql =
|
|
392
|
-
'SELECT ' +
|
|
393
|
-
quoteIdent(column) +
|
|
394
|
-
' AS "total" FROM ' +
|
|
395
|
-
buildMaterializedCountFqn(source) +
|
|
396
|
-
where.sql +
|
|
397
|
-
' LIMIT 1'
|
|
398
|
-
const rows = await raw.$queryRawUnsafe<Array<{ total: unknown }>>(sql, ...where.values)
|
|
399
|
-
const value = rows[0]?.total
|
|
400
|
-
const total = Number(value)
|
|
401
|
-
if (!Number.isFinite(total)) {
|
|
402
|
-
throw new HttpError(500, 'Materialized count source did not return a numeric total')
|
|
403
|
-
}
|
|
404
|
-
return Math.trunc(total)
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
export async function countForPagination(
|
|
408
|
-
delegate: PrismaDelegate,
|
|
409
|
-
query: Record<string, unknown>,
|
|
410
|
-
shape: Record<string, unknown> | undefined,
|
|
411
|
-
caller: string | undefined,
|
|
412
|
-
distinctCountLimit?: number,
|
|
413
|
-
countSource?: PaginationCountSource,
|
|
414
|
-
rawClient?: unknown,
|
|
415
|
-
): Promise<number> {
|
|
416
|
-
if (
|
|
417
|
-
countSource &&
|
|
418
|
-
countSource.type === 'materializedView' &&
|
|
419
|
-
!shape &&
|
|
420
|
-
!query.where &&
|
|
421
|
-
!query.distinct
|
|
422
|
-
) {
|
|
423
|
-
return countFromMaterializedView(rawClient ?? delegate, countSource)
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
const distinctFields = normalizeDistinct(query.distinct)
|
|
427
|
-
const hasDistinct = distinctFields.length > 0
|
|
428
|
-
const effectiveLimit = distinctCountLimit ?? DISTINCT_COUNT_LIMIT
|
|
429
|
-
const countShape = shape ? buildCountShape(shape) : undefined
|
|
430
|
-
|
|
431
|
-
const runCount = async (): Promise<number> => {
|
|
432
|
-
const countArgs: Record<string, unknown> = {}
|
|
433
|
-
if (query.where) countArgs.where = query.where
|
|
434
|
-
if (countShape) {
|
|
435
|
-
return (await (delegate.guard as NonNullable<PrismaDelegate['guard']>)(
|
|
436
|
-
countShape as Record<string, unknown>,
|
|
437
|
-
caller,
|
|
438
|
-
).count(countArgs)) as number
|
|
439
|
-
}
|
|
440
|
-
return (await delegate.count(countArgs)) as number
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
if (hasDistinct && shape) return runCount()
|
|
444
|
-
|
|
445
|
-
if (hasDistinct) {
|
|
446
|
-
const selectField = distinctFields[0]
|
|
447
|
-
const distinctArgs: Record<string, unknown> = {
|
|
448
|
-
where: query.where,
|
|
449
|
-
distinct: distinctFields,
|
|
450
|
-
select: { [selectField]: true },
|
|
451
|
-
take: effectiveLimit + 1,
|
|
452
|
-
}
|
|
453
|
-
const results = (await delegate.findMany(distinctArgs)) as unknown[]
|
|
454
|
-
if (results.length > effectiveLimit) {
|
|
455
|
-
console.warn(
|
|
456
|
-
'[prisma-generator-express] Distinct count exceeds ' +
|
|
457
|
-
effectiveLimit +
|
|
458
|
-
', falling back to approximate total',
|
|
459
|
-
)
|
|
460
|
-
return runCount()
|
|
461
|
-
}
|
|
462
|
-
return results.length
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
return runCount()
|
|
466
|
-
}
|
|
467
|
-
|
|
468
143
|
export function transformResult(value: unknown): unknown {
|
|
469
144
|
if (value === null || value === undefined) return value
|
|
470
145
|
if (typeof value === 'bigint') return value.toString()
|
|
@@ -494,273 +169,4 @@ export function transformResult(value: unknown): unknown {
|
|
|
494
169
|
return changed ? out : value
|
|
495
170
|
}
|
|
496
171
|
return value
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
export function acceptsEventStream(accept: string | undefined): boolean {
|
|
500
|
-
if (!accept) return false
|
|
501
|
-
return accept
|
|
502
|
-
.toLowerCase()
|
|
503
|
-
.split(',')
|
|
504
|
-
.some((entry) => entry.split(';')[0].trim() === 'text/event-stream')
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
const UNSAFE_PATH_SEGMENTS = new Set(['__proto__', 'constructor', 'prototype'])
|
|
508
|
-
|
|
509
|
-
export function setByPath(target: Record<string, unknown>, path: string, value: unknown): boolean {
|
|
510
|
-
const parts = path.split('.')
|
|
511
|
-
if (parts.length === 0) return false
|
|
512
|
-
for (const p of parts) {
|
|
513
|
-
if (p === '' || UNSAFE_PATH_SEGMENTS.has(p)) return false
|
|
514
|
-
}
|
|
515
|
-
let cursor: Record<string, unknown> = target
|
|
516
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
517
|
-
const part = parts[i]
|
|
518
|
-
const next = cursor[part]
|
|
519
|
-
if (!isPlainObject(next)) {
|
|
520
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
521
|
-
console.warn(
|
|
522
|
-
'[progressive] Dropping patch for "' + path +
|
|
523
|
-
'": cannot traverse non-plain-object at segment "' + part + '"',
|
|
524
|
-
)
|
|
525
|
-
}
|
|
526
|
-
return false
|
|
527
|
-
}
|
|
528
|
-
cursor = next
|
|
529
|
-
}
|
|
530
|
-
cursor[parts[parts.length - 1]] = value
|
|
531
|
-
return true
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
type EventEmitterLike = {
|
|
535
|
-
on?: (event: string, listener: () => void) => unknown
|
|
536
|
-
off?: (event: string, listener: () => void) => unknown
|
|
537
|
-
removeListener?: (event: string, listener: () => void) => unknown
|
|
538
|
-
destroyed?: boolean
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
type SseWritable = {
|
|
542
|
-
statusCode: number
|
|
543
|
-
setHeader: (name: string, value: string) => unknown
|
|
544
|
-
flushHeaders?: () => unknown
|
|
545
|
-
flush?: () => unknown
|
|
546
|
-
write: (chunk: string) => unknown
|
|
547
|
-
end: () => unknown
|
|
548
|
-
writableEnded: boolean
|
|
549
|
-
destroyed: boolean
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
export function removeReqCloseListener(req: EventEmitterLike, listener: () => void): void {
|
|
553
|
-
if (typeof req.off === 'function') {
|
|
554
|
-
req.off('close', listener)
|
|
555
|
-
} else if (typeof req.removeListener === 'function') {
|
|
556
|
-
req.removeListener('close', listener)
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
export function initSSE(res: SseWritable): void {
|
|
561
|
-
res.statusCode = 200
|
|
562
|
-
res.setHeader('Content-Type', 'text/event-stream')
|
|
563
|
-
res.setHeader('Cache-Control', 'no-cache, no-transform')
|
|
564
|
-
res.setHeader('Connection', 'keep-alive')
|
|
565
|
-
res.setHeader('X-Accel-Buffering', 'no')
|
|
566
|
-
if (typeof res.flushHeaders === 'function') res.flushHeaders()
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
export function flushSSE(res: SseWritable): void {
|
|
570
|
-
if (typeof res.flush === 'function') {
|
|
571
|
-
try { res.flush() } catch {}
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
export function sendSSE(res: SseWritable, payload: unknown): boolean {
|
|
576
|
-
if (res.writableEnded || res.destroyed) return false
|
|
577
|
-
try {
|
|
578
|
-
res.write('data: ' + JSON.stringify(transformResult(payload)) + '\n\n')
|
|
579
|
-
flushSSE(res)
|
|
580
|
-
return true
|
|
581
|
-
} catch (err) {
|
|
582
|
-
console.error('[progressive] failed to send SSE event:', err)
|
|
583
|
-
return false
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
export function sendSSEProgress(res: SseWritable, stage: string, completed: number, total: number): boolean {
|
|
588
|
-
return sendSSE(res, { type: 'progress', stage, completed, total })
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
export function sendSSEField(res: SseWritable, key: string, value: unknown): boolean {
|
|
592
|
-
return sendSSE(res, { type: 'field', key, value })
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
export function sendSSEResult(res: SseWritable, data: unknown): boolean {
|
|
596
|
-
return sendSSE(res, { type: 'result', data })
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
export function sendSSERootArray(res: SseWritable, rows: unknown[]): boolean {
|
|
600
|
-
return sendSSE(res, { type: 'rootArray', data: rows })
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
export function sendSSERelationBatch(
|
|
604
|
-
res: SseWritable,
|
|
605
|
-
relationPath: string,
|
|
606
|
-
values: unknown[],
|
|
607
|
-
): boolean {
|
|
608
|
-
return sendSSE(res, { type: 'relationBatch', relationPath, values })
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
export function sendSSENestedRelationBatch(
|
|
612
|
-
res: SseWritable,
|
|
613
|
-
relationPath: string,
|
|
614
|
-
depth: number,
|
|
615
|
-
attachments: Array<{ locator: Array<number | string>; value: unknown }>,
|
|
616
|
-
): boolean {
|
|
617
|
-
return sendSSE(res, { type: 'nestedRelationBatch', relationPath, depth, attachments })
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
export function sendSSEPageMeta(
|
|
621
|
-
res: SseWritable,
|
|
622
|
-
total: number,
|
|
623
|
-
hasMore: boolean,
|
|
624
|
-
): boolean {
|
|
625
|
-
return sendSSE(res, { type: 'pageMeta', total, hasMore })
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
export function sendSSEError(res: SseWritable, message: string): boolean {
|
|
629
|
-
if (res.writableEnded || res.destroyed) return false
|
|
630
|
-
try {
|
|
631
|
-
res.write('data: ' + JSON.stringify({ type: 'error', message }) + '\n\n')
|
|
632
|
-
flushSSE(res)
|
|
633
|
-
return true
|
|
634
|
-
} catch (err) {
|
|
635
|
-
console.error('[progressive] failed to send SSE error event:', err)
|
|
636
|
-
return false
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
type IntervalHandle = ReturnType<typeof setInterval>
|
|
641
|
-
|
|
642
|
-
export function startSSEKeepalive(res: SseWritable, intervalMs: number = 15000): IntervalHandle {
|
|
643
|
-
const handle = setInterval(() => {
|
|
644
|
-
if (res.writableEnded || res.destroyed) return
|
|
645
|
-
try {
|
|
646
|
-
res.write(': keepalive\n\n')
|
|
647
|
-
flushSSE(res)
|
|
648
|
-
} catch {}
|
|
649
|
-
}, intervalMs)
|
|
650
|
-
const maybeUnref = (handle as unknown as { unref?: () => void }).unref
|
|
651
|
-
if (typeof maybeUnref === 'function') maybeUnref.call(handle)
|
|
652
|
-
return handle
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
export function endSSE(res: SseWritable, keepaliveHandle: IntervalHandle | null): void {
|
|
656
|
-
if (keepaliveHandle) {
|
|
657
|
-
try { clearInterval(keepaliveHandle) } catch {}
|
|
658
|
-
}
|
|
659
|
-
if (!res.writableEnded && !res.destroyed) {
|
|
660
|
-
try { res.end() } catch {}
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
export function emitTerminalSSEError(res: SseWritable, message: string): void {
|
|
665
|
-
let keepalive: IntervalHandle | null = null
|
|
666
|
-
try {
|
|
667
|
-
initSSE(res)
|
|
668
|
-
keepalive = startSSEKeepalive(res)
|
|
669
|
-
sendSSEError(res, message)
|
|
670
|
-
} finally {
|
|
671
|
-
endSSE(res, keepalive)
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
export interface RunSingleResultSSEOptions {
|
|
676
|
-
req: EventEmitterLike
|
|
677
|
-
res: SseWritable
|
|
678
|
-
coreQueryFn: () => Promise<unknown>
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
export async function runSingleResultSSE(options: RunSingleResultSSEOptions): Promise<void> {
|
|
682
|
-
const { req, res, coreQueryFn } = options
|
|
683
|
-
let keepalive: IntervalHandle | null = null
|
|
684
|
-
try {
|
|
685
|
-
initSSE(res)
|
|
686
|
-
keepalive = startSSEKeepalive(res)
|
|
687
|
-
if (req.destroyed) return
|
|
688
|
-
const data = await coreQueryFn()
|
|
689
|
-
if (res.writableEnded || res.destroyed) return
|
|
690
|
-
sendSSEResult(res, data)
|
|
691
|
-
} catch (err) {
|
|
692
|
-
console.error('[progressive] single-result error:', err)
|
|
693
|
-
if (!res.writableEnded && !res.destroyed) {
|
|
694
|
-
sendSSEError(res, mapError(err).message)
|
|
695
|
-
}
|
|
696
|
-
} finally {
|
|
697
|
-
endSSE(res, keepalive)
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
function isStopResult(value: unknown): value is ProgressiveStopResult<unknown> {
|
|
702
|
-
return typeof value === 'object' && value !== null && (value as { stop?: unknown }).stop === true
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
export interface RunProgressiveOptions {
|
|
706
|
-
req: EventEmitterLike
|
|
707
|
-
res: SseWritable
|
|
708
|
-
ctx: unknown
|
|
709
|
-
prisma: unknown
|
|
710
|
-
variant: string
|
|
711
|
-
stages: string[]
|
|
712
|
-
stageRegistry: Record<string, ProgressiveStage>
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
export async function runProgressiveEndpoint(options: RunProgressiveOptions): Promise<void> {
|
|
716
|
-
const { req, res, ctx, prisma, variant, stages, stageRegistry } = options
|
|
717
|
-
let keepalive: IntervalHandle | null = null
|
|
718
|
-
const controller = new AbortController()
|
|
719
|
-
const onClose = () => controller.abort()
|
|
720
|
-
if (typeof req.on === 'function') req.on('close', onClose)
|
|
721
|
-
|
|
722
|
-
const accumulated: Record<string, unknown> = {}
|
|
723
|
-
const signal = controller.signal
|
|
724
|
-
|
|
725
|
-
try {
|
|
726
|
-
initSSE(res)
|
|
727
|
-
keepalive = startSSEKeepalive(res)
|
|
728
|
-
sendSSEProgress(res, 'start', 0, stages.length)
|
|
729
|
-
|
|
730
|
-
for (let i = 0; i < stages.length; i++) {
|
|
731
|
-
if (res.writableEnded || res.destroyed || signal.aborted) return
|
|
732
|
-
const stageName = stages[i]
|
|
733
|
-
const stage = stageRegistry[stageName]
|
|
734
|
-
if (!stage) throw new Error('Missing progressive stage: ' + stageName)
|
|
735
|
-
|
|
736
|
-
const result = await stage({ ctx, req, res, prisma, variant, accumulated, signal })
|
|
737
|
-
if (res.writableEnded || res.destroyed) return
|
|
738
|
-
|
|
739
|
-
if (isStopResult(result)) {
|
|
740
|
-
sendSSEResult(res, result.data)
|
|
741
|
-
return
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
const patches = Array.isArray(result) ? result : result ? [result] : []
|
|
745
|
-
for (const patch of patches) {
|
|
746
|
-
if (!patch || typeof patch !== 'object') continue
|
|
747
|
-
const p = patch as ProgressivePatch
|
|
748
|
-
if (typeof p.key !== 'string') continue
|
|
749
|
-
if (!('value' in p)) continue
|
|
750
|
-
const applied = setByPath(accumulated, p.key, p.value)
|
|
751
|
-
if (applied) sendSSEField(res, p.key, p.value)
|
|
752
|
-
}
|
|
753
|
-
sendSSEProgress(res, stageName, i + 1, stages.length)
|
|
754
|
-
}
|
|
755
|
-
if (res.writableEnded || res.destroyed) return
|
|
756
|
-
sendSSEResult(res, accumulated)
|
|
757
|
-
} catch (err) {
|
|
758
|
-
console.error('[progressive] stage error:', err)
|
|
759
|
-
if (!res.writableEnded && !res.destroyed) {
|
|
760
|
-
sendSSEError(res, mapError(err).message)
|
|
761
|
-
}
|
|
762
|
-
} finally {
|
|
763
|
-
removeReqCloseListener(req, onClose)
|
|
764
|
-
endSSE(res, keepalive)
|
|
765
|
-
}
|
|
766
172
|
}
|