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