prisma-generator-express 1.19.0 → 1.20.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.
@@ -0,0 +1,168 @@
1
+ export function generateUnifiedDocs(models: string[]): string {
2
+ const imports = models
3
+ .map((model) => `import { ${model}Docs } from './${model}/${model}Docs'`)
4
+ .join('\n')
5
+
6
+ return `${imports}
7
+ import { Request, Response, RequestHandler } from 'express'
8
+ import type { RouteConfig } from './routeConfig'
9
+
10
+ const _env = typeof process !== 'undefined' && process.env ? process.env : {} as Record<string, string | undefined>
11
+
12
+ const docsHandlers: Record<string, (config: any) => (req: Request, res: Response) => any> = {
13
+ ${models.map((model) => ` ${model}: ${model}Docs`).join(',\n')}
14
+ }
15
+
16
+ type DocsUI = 'docs' | 'scalar' | 'json' | 'yaml' | 'playground'
17
+
18
+ interface ModelDocsConfig extends RouteConfig {
19
+ docsTitle?: string
20
+ docsUi?: DocsUI
21
+ }
22
+
23
+ export interface CombinedDocsConfig {
24
+ disableOpenApi?: boolean
25
+ title?: string
26
+ description?: string
27
+ basePath?: string
28
+ version?: string
29
+ modelConfigs: {
30
+ [modelName: string]: ModelDocsConfig
31
+ }
32
+ }
33
+
34
+ function escapeHtml(input: string) {
35
+ return input
36
+ .replaceAll('&', '&amp;')
37
+ .replaceAll('<', '&lt;')
38
+ .replaceAll('>', '&gt;')
39
+ .replaceAll('"', '&quot;')
40
+ .replaceAll("'", '&#039;')
41
+ }
42
+
43
+ function removeTrailingSlash(p: string): string {
44
+ if (p === '/') return ''
45
+ return p.endsWith('/') ? p.slice(0, -1) : p
46
+ }
47
+
48
+ function isOpenApiDisabled(disableOpenApi?: boolean) {
49
+ if (disableOpenApi === true) return true
50
+ if (disableOpenApi === false) return false
51
+ return _env.DISABLE_OPENAPI === 'true' || _env.NODE_ENV === 'production'
52
+ }
53
+
54
+ function isPlaygroundAvailable(config?: ModelDocsConfig) {
55
+ if (_env.NODE_ENV === 'production') return false
56
+ if (!config) return true
57
+ if (config.queryBuilder === false) return false
58
+ if (typeof config.queryBuilder === 'object' && config.queryBuilder.enabled === false) return false
59
+ return true
60
+ }
61
+
62
+ export function generateCombinedDocs(config: CombinedDocsConfig) {
63
+ const title = config.title || 'API Documentation'
64
+ const description = config.description || ''
65
+ const version = config.version || ''
66
+
67
+ return (req: Request, res: Response) => {
68
+ const registeredModels = Object.keys(config.modelConfigs).filter((m) => {
69
+ const cfg = config.modelConfigs[m]
70
+ return m in docsHandlers && !isOpenApiDisabled(cfg?.disableOpenApi ?? config.disableOpenApi)
71
+ })
72
+
73
+ if (registeredModels.length === 0) {
74
+ return res.status(404).send('OpenAPI documentation is disabled')
75
+ }
76
+
77
+ const basePath = removeTrailingSlash(config.basePath || '/docs')
78
+ const generatedAt = new Date().toISOString()
79
+
80
+ const html = \`<!DOCTYPE html>
81
+ <html lang="en">
82
+ <head>
83
+ <meta charset="utf-8" />
84
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
85
+ <title>\${escapeHtml(title)}</title>
86
+ <script src="https://cdn.tailwindcss.com"></script>
87
+ </head>
88
+ <body class="m-0 bg-white text-gray-900 font-serif leading-normal">
89
+ <div class="max-w-[980px] mx-auto px-7 pt-10 pb-16">
90
+ <div class="border-b-2 border-gray-900 pb-3.5 mb-[18px]">
91
+ <div class="text-[28px] font-bold tracking-wide">\${escapeHtml(title)}</div>
92
+ \${description ? '<div class="mt-1.5 text-gray-500 text-sm">' + escapeHtml(description) + '</div>' : ''}
93
+ <div class="mt-3 flex gap-x-5 text-[13px] text-gray-500">
94
+ \${version ? '<div>Version: ' + escapeHtml(version) + '</div>' : ''}
95
+ <div>Generated: \${escapeHtml(generatedAt)}</div>
96
+ </div>
97
+ </div>
98
+
99
+ <div class="mt-[22px]">
100
+ <h2 class="m-0 mb-2.5 text-lg border-t border-gray-300 pt-3.5">Models</h2>
101
+ <table class="w-full border-collapse text-[13px]">
102
+ <thead>
103
+ <tr>
104
+ <th class="text-left py-2 px-2 border-b border-gray-300 align-top font-bold">Model</th>
105
+ <th class="text-left py-2 px-2 border-b border-gray-300 align-top font-bold">Documentation</th>
106
+ <th class="text-left py-2 px-2 border-b border-gray-300 align-top font-bold">Views</th>
107
+ </tr>
108
+ </thead>
109
+ <tbody>
110
+ \${registeredModels.map((m) => {
111
+ const lower = m.toLowerCase()
112
+ const docsUrl = \\\`\\\${basePath}/\\\${lower}\\\`
113
+ const scalarUrl = \\\`\\\${basePath}/\\\${lower}?ui=scalar\\\`
114
+ const jsonUrl = \\\`\\\${basePath}/\\\${lower}?ui=json\\\`
115
+ const yamlUrl = \\\`\\\${basePath}/\\\${lower}?ui=yaml\\\`
116
+ const playgroundUrl = \\\`\\\${basePath}/\\\${lower}?ui=playground\\\`
117
+ const modelCfg = config.modelConfigs[m]
118
+ const modelPlayground = isPlaygroundAvailable(modelCfg)
119
+ const playgroundLink = modelPlayground
120
+ ? \\\`, <a href="\\\${playgroundUrl}" class="text-inherit underline">playground</a>\\\`
121
+ : ''
122
+ return \\\`
123
+ <tr>
124
+ <td class="text-left py-2 px-2 border-b border-gray-300 align-top">\\\${escapeHtml(m)}</td>
125
+ <td class="text-left py-2 px-2 border-b border-gray-300 align-top"><a href="\\\${docsUrl}" class="text-inherit underline">\\\${escapeHtml(docsUrl)}</a></td>
126
+ <td class="text-left py-2 px-2 border-b border-gray-300 align-top">
127
+ <a href="\\\${scalarUrl}" class="text-inherit underline">scalar</a>,
128
+ <a href="\\\${jsonUrl}" class="text-inherit underline">json</a>,
129
+ <a href="\\\${yamlUrl}" class="text-inherit underline">yaml</a>\\\${playgroundLink}
130
+ </td>
131
+ </tr>
132
+ \\\`
133
+ }).join('')}
134
+ </tbody>
135
+ </table>
136
+ </div>
137
+ </div>
138
+ </body>
139
+ </html>\`
140
+
141
+ res.type('html').send(html)
142
+ }
143
+ }
144
+
145
+ export function registerModelDocs(
146
+ app: any,
147
+ basePath: string = '/docs',
148
+ configs: CombinedDocsConfig['modelConfigs'] = {},
149
+ options?: { disableOpenApi?: boolean }
150
+ ) {
151
+ const normalizedBase = removeTrailingSlash(basePath)
152
+ const registeredModels = Object.keys(configs).filter((m) => {
153
+ const cfg = configs[m]
154
+ return m in docsHandlers && !isOpenApiDisabled(cfg?.disableOpenApi ?? options?.disableOpenApi)
155
+ })
156
+
157
+ if (registeredModels.length === 0) return
158
+
159
+ registeredModels.forEach((model) => {
160
+ const handler = docsHandlers[model]
161
+ const cfg = configs[model] || {}
162
+ const path = \\\`\\\${normalizedBase}/\\\${model.toLowerCase()}\\\`
163
+ console.log(\\\` Registered docs: \\\${path}\\\`)
164
+ app.get(path, handler(cfg))
165
+ })
166
+ }
167
+ `
168
+ }
@@ -0,0 +1,469 @@
1
+ import { DMMF } from '@prisma/generator-helper'
2
+
3
+ export interface UnifiedHandlerOptions {
4
+ model: DMMF.Model
5
+ prismaImportStatement: string
6
+ }
7
+
8
+ export function generateUnifiedHandler(options: UnifiedHandlerOptions): string {
9
+ const { model, prismaImportStatement } = options
10
+ const modelName = model.name
11
+ const modelNameLower = modelName.charAt(0).toLowerCase() + modelName.slice(1)
12
+ const importPath =
13
+ prismaImportStatement.match(/from ['"](.+?)['"]/)?.[1] || ''
14
+
15
+ return `
16
+ import { Prisma, PrismaClient } from '${importPath}'
17
+ import { Request, Response, NextFunction } from 'express'
18
+ import { sanitizeKeys } from '../misc'
19
+
20
+ let _speedExtension: ((opts: any) => any) | null = null
21
+
22
+ const _prismasqlModule = 'prisma-' + 'sql'
23
+ const _prismasqlReady = (async () => {
24
+ try {
25
+ const mod = await import(_prismasqlModule)
26
+ _speedExtension = mod.speedExtension ?? mod.default?.speedExtension ?? null
27
+ } catch (err: any) {
28
+ const code = err?.code
29
+ if (code !== 'MODULE_NOT_FOUND' && code !== 'ERR_MODULE_NOT_FOUND') {
30
+ console.warn('[prisma-generator-express] prisma-sql initialization failed:', err)
31
+ }
32
+ }
33
+ })()
34
+
35
+ const _extendedClients = new WeakMap<object, WeakMap<object, PrismaClient>>()
36
+
37
+ const DISTINCT_COUNT_LIMIT = 100000
38
+
39
+ export class HttpError extends Error {
40
+ status: number
41
+ constructor(status: number, message: string) {
42
+ super(message)
43
+ this.name = 'HttpError'
44
+ this.status = status
45
+ }
46
+ }
47
+
48
+ const PRISMA_ERROR_MAP: Record<string, { status: number; message: string }> = {
49
+ P2000: { status: 400, message: 'Value too long for column' },
50
+ P2001: { status: 404, message: 'Record not found' },
51
+ P2002: { status: 409, message: 'Unique constraint violation' },
52
+ P2003: { status: 400, message: 'Foreign key constraint failed' },
53
+ P2004: { status: 400, message: 'Constraint failed on the database' },
54
+ P2005: { status: 400, message: 'Invalid field value' },
55
+ P2006: { status: 400, message: 'Invalid value provided' },
56
+ P2007: { status: 400, message: 'Data validation error' },
57
+ P2008: { status: 400, message: 'Failed to parse the query' },
58
+ P2009: { status: 400, message: 'Failed to validate the query' },
59
+ P2010: { status: 500, message: 'Raw query failed' },
60
+ P2011: { status: 400, message: 'Null constraint violation' },
61
+ P2012: { status: 400, message: 'Missing required value' },
62
+ P2013: { status: 400, message: 'Missing required argument' },
63
+ P2014: { status: 400, message: 'Required relation violation' },
64
+ P2015: { status: 404, message: 'Related record not found' },
65
+ P2016: { status: 400, message: 'Query interpretation error' },
66
+ P2017: { status: 400, message: 'Records not connected' },
67
+ P2018: { status: 404, message: 'Required connected record not found' },
68
+ P2019: { status: 400, message: 'Input error' },
69
+ P2020: { status: 400, message: 'Value out of range for the field type' },
70
+ P2021: { status: 500, message: 'Table does not exist in the database' },
71
+ P2022: { status: 500, message: 'Column does not exist in the database' },
72
+ P2023: { status: 500, message: 'Inconsistent column data' },
73
+ P2024: { status: 503, message: 'Connection pool timeout' },
74
+ P2025: { status: 404, message: 'Record not found' },
75
+ P2026: { status: 501, message: 'Feature not supported by the current database provider' },
76
+ P2028: { status: 500, message: 'Transaction API error' },
77
+ P2030: { status: 400, message: 'Cannot find a fulltext index for the search' },
78
+ P2033: { status: 400, message: 'Number out of range for the field type' },
79
+ P2034: { status: 409, message: 'Transaction conflict, please retry' },
80
+ }
81
+
82
+ async function getExtendedClient(req: Request): Promise<PrismaClient> {
83
+ const base = (req as any).prisma as PrismaClient
84
+ if (!base) {
85
+ throw new HttpError(500, 'PrismaClient not found on request. Set req.prisma in middleware.')
86
+ }
87
+
88
+ await _prismasqlReady
89
+
90
+ if (!_speedExtension) return base
91
+
92
+ const connector = (req as any).postgres || (req as any).sqlite
93
+ if (!connector) return base
94
+
95
+ if (typeof connector === 'object' && connector !== null) {
96
+ const innerMap = _extendedClients.get(connector)
97
+ if (innerMap) {
98
+ const cached = innerMap.get(base)
99
+ if (cached) return cached
100
+ }
101
+ }
102
+
103
+ try {
104
+ const extended = base.$extends(_speedExtension({
105
+ postgres: (req as any).postgres,
106
+ sqlite: (req as any).sqlite,
107
+ debug: process.env.DEBUG === 'true'
108
+ })) as unknown as PrismaClient
109
+
110
+ if (typeof connector === 'object' && connector !== null) {
111
+ let innerMap = _extendedClients.get(connector)
112
+ if (!innerMap) {
113
+ innerMap = new WeakMap<object, PrismaClient>()
114
+ _extendedClients.set(connector, innerMap)
115
+ }
116
+ innerMap.set(base, extended)
117
+ }
118
+
119
+ return extended
120
+ } catch (error) {
121
+ console.warn('[speedExtension] Failed to initialize, using base client:', error)
122
+ return base
123
+ }
124
+ }
125
+
126
+ function handleError(error: unknown, next: NextFunction): void {
127
+ if (error instanceof HttpError) {
128
+ next(error)
129
+ return
130
+ }
131
+
132
+ if (error && typeof error === 'object' && 'name' in error && error.name === 'ShapeError') {
133
+ next(new HttpError(400, (error as Error).message))
134
+ return
135
+ }
136
+
137
+ if (error && typeof error === 'object' && 'name' in error && error.name === 'CallerError') {
138
+ next(new HttpError(400, (error as Error).message))
139
+ return
140
+ }
141
+
142
+ if (error && typeof error === 'object' && 'name' in error && error.name === 'PolicyError') {
143
+ next(new HttpError(403, (error as Error).message))
144
+ return
145
+ }
146
+
147
+ if (error && typeof error === 'object' && 'code' in error) {
148
+ const code = (error as any).code as string
149
+ const mapped = PRISMA_ERROR_MAP[code]
150
+ if (mapped) {
151
+ next(new HttpError(mapped.status, mapped.message))
152
+ return
153
+ }
154
+ if (typeof code === 'string' && code.startsWith('P')) {
155
+ next(new HttpError(500, 'Database operation failed'))
156
+ return
157
+ }
158
+ }
159
+
160
+ if (error && typeof error === 'object' && 'name' in error) {
161
+ const name = (error as any).name
162
+ if (name === 'PrismaClientValidationError') {
163
+ next(new HttpError(400, 'Invalid query parameters'))
164
+ return
165
+ }
166
+ }
167
+
168
+ console.error('[prisma-generator-express] Unhandled error:', error)
169
+ next(new HttpError(500, 'Internal server error'))
170
+ }
171
+
172
+ function safeParseBody(req: Request): Record<string, any> {
173
+ const body = req.body
174
+ if (!body || typeof body !== 'object' || Array.isArray(body)) {
175
+ throw new HttpError(400, 'Request body must be a JSON object')
176
+ }
177
+ return sanitizeKeys(body as Record<string, any>)
178
+ }
179
+
180
+ function requireBodyField(body: Record<string, any>, field: string): void {
181
+ if (!(field in body) || body[field] === undefined) {
182
+ throw new HttpError(400, 'Missing required field: ' + field)
183
+ }
184
+ }
185
+
186
+ function applyPaginationLimits(query: Record<string, any>, res: Response): Record<string, any> {
187
+ const routeConfig = res.locals.routeConfig
188
+ const pagination = routeConfig?.pagination
189
+ if (!pagination) return query
190
+
191
+ const result = { ...query }
192
+
193
+ if (result.take === undefined && pagination.defaultLimit !== undefined) {
194
+ result.take = pagination.defaultLimit
195
+ }
196
+
197
+ if (pagination.maxLimit !== undefined && result.take !== undefined) {
198
+ const takeNum = Number(result.take)
199
+ if (Math.abs(takeNum) > pagination.maxLimit) {
200
+ result.take = takeNum < 0 ? -pagination.maxLimit : pagination.maxLimit
201
+ }
202
+ }
203
+
204
+ return result
205
+ }
206
+
207
+ function normalizeDistinct(value: unknown): string[] {
208
+ if (typeof value === 'string') return [value]
209
+ if (Array.isArray(value)) return value.filter((v): v is string => typeof v === 'string')
210
+ return []
211
+ }
212
+
213
+ function assertGuard(delegate: any): void {
214
+ if (typeof delegate.guard !== 'function') {
215
+ throw new HttpError(500, 'Guard shapes require prisma-guard extension on PrismaClient. Install: npm install prisma-guard, then extend your client with guardExtension().')
216
+ }
217
+ }
218
+
219
+ ${generateReadHandlers(modelName, modelNameLower)}
220
+
221
+ ${generateWriteHandlers(modelName, modelNameLower)}
222
+ `
223
+ }
224
+
225
+ function generateReadHandlers(
226
+ modelName: string,
227
+ modelNameLower: string,
228
+ ): string {
229
+ const standardReadOps = [
230
+ 'findFirst',
231
+ 'findUnique',
232
+ 'findUniqueOrThrow',
233
+ 'findFirstOrThrow',
234
+ 'count',
235
+ 'aggregate',
236
+ 'groupBy',
237
+ ]
238
+
239
+ const standardHandlers = standardReadOps
240
+ .map((op) => {
241
+ const functionName = `${modelName}${op.charAt(0).toUpperCase() + op.slice(1)}`
242
+
243
+ return `
244
+ export async function ${functionName}(
245
+ req: Request,
246
+ res: Response,
247
+ next: NextFunction
248
+ ) {
249
+ try {
250
+ const query = res.locals.parsedQuery || {}
251
+ const extended = await getExtendedClient(req)
252
+ const shape = res.locals.guardShape
253
+
254
+ let data
255
+ if (shape) {
256
+ assertGuard(extended.${modelNameLower})
257
+ const caller = res.locals.guardCaller
258
+ data = await extended.${modelNameLower}.guard(shape, caller).${op}(query)
259
+ } else {
260
+ data = await extended.${modelNameLower}.${op}(query)
261
+ }
262
+
263
+ res.locals.data = data
264
+ next()
265
+ } catch (error: unknown) {
266
+ handleError(error, next)
267
+ }
268
+ }
269
+ `
270
+ })
271
+ .join('\n')
272
+
273
+ const findManyHandler = `
274
+ export async function ${modelName}FindMany(
275
+ req: Request,
276
+ res: Response,
277
+ next: NextFunction
278
+ ) {
279
+ try {
280
+ const rawQuery = res.locals.parsedQuery || {}
281
+ const query = applyPaginationLimits(rawQuery, res)
282
+ const extended = await getExtendedClient(req)
283
+ const shape = res.locals.guardShape
284
+
285
+ let data
286
+ if (shape) {
287
+ assertGuard(extended.${modelNameLower})
288
+ const caller = res.locals.guardCaller
289
+ data = await extended.${modelNameLower}.guard(shape, caller).findMany(query)
290
+ } else {
291
+ data = await extended.${modelNameLower}.findMany(query)
292
+ }
293
+
294
+ res.locals.data = data
295
+ next()
296
+ } catch (error: unknown) {
297
+ handleError(error, next)
298
+ }
299
+ }
300
+ `
301
+
302
+ return findManyHandler + '\n' + standardHandlers
303
+ }
304
+
305
+ function generateWriteHandlers(
306
+ modelName: string,
307
+ modelNameLower: string,
308
+ ): string {
309
+ const writeOps: {
310
+ name: string
311
+ method: string
312
+ requiredFields?: string[]
313
+ }[] = [
314
+ { name: 'Create', method: 'create' },
315
+ { name: 'CreateMany', method: 'createMany' },
316
+ { name: 'CreateManyAndReturn', method: 'createManyAndReturn' },
317
+ { name: 'Update', method: 'update' },
318
+ {
319
+ name: 'UpdateMany',
320
+ method: 'updateMany',
321
+ requiredFields: ['where', 'data'],
322
+ },
323
+ {
324
+ name: 'UpdateManyAndReturn',
325
+ method: 'updateManyAndReturn',
326
+ requiredFields: ['where', 'data'],
327
+ },
328
+ { name: 'Delete', method: 'delete' },
329
+ { name: 'DeleteMany', method: 'deleteMany', requiredFields: ['where'] },
330
+ { name: 'Upsert', method: 'upsert' },
331
+ ]
332
+
333
+ return (
334
+ writeOps
335
+ .map((op) => {
336
+ const functionName = `${modelName}${op.name}`
337
+ const validationLines = (op.requiredFields || [])
338
+ .map((field) => ` requireBodyField(body, '${field}')`)
339
+ .join('\n')
340
+
341
+ return `
342
+ export async function ${functionName}(req: Request, res: Response, next: NextFunction) {
343
+ try {
344
+ const body = safeParseBody(req)
345
+ ${validationLines ? validationLines + '\n' : ''} const extended = await getExtendedClient(req)
346
+ const shape = res.locals.guardShape
347
+
348
+ let data
349
+ if (shape) {
350
+ assertGuard(extended.${modelNameLower})
351
+ const caller = res.locals.guardCaller
352
+ data = await extended.${modelNameLower}.guard(shape, caller).${op.method}(body)
353
+ } else {
354
+ data = await extended.${modelNameLower}.${op.method}(body)
355
+ }
356
+
357
+ res.locals.data = data
358
+ next()
359
+ } catch (error: unknown) {
360
+ handleError(error, next)
361
+ }
362
+ }
363
+ `
364
+ })
365
+ .join('\n') +
366
+ `
367
+
368
+ async function countForPagination(
369
+ delegate: any,
370
+ query: Record<string, any>,
371
+ shape: Record<string, any> | undefined,
372
+ caller: string | undefined,
373
+ ): Promise<number> {
374
+ const distinctFields = normalizeDistinct(query.distinct)
375
+ const hasDistinct = distinctFields.length > 0
376
+
377
+ if (hasDistinct) {
378
+ const selectField = distinctFields[0]
379
+ const distinctArgs: Record<string, any> = {
380
+ where: query.where,
381
+ distinct: distinctFields,
382
+ select: { [selectField]: true },
383
+ take: DISTINCT_COUNT_LIMIT + 1,
384
+ }
385
+
386
+ const results = shape
387
+ ? await delegate.guard(shape, caller).findMany(distinctArgs)
388
+ : await delegate.findMany(distinctArgs)
389
+
390
+ if (results.length > DISTINCT_COUNT_LIMIT) {
391
+ console.warn('[prisma-generator-express] Distinct count exceeds ' + DISTINCT_COUNT_LIMIT + ', falling back to approximate total')
392
+ const countArgs: Record<string, any> = {}
393
+ if (query.where) countArgs.where = query.where
394
+ return shape
395
+ ? await delegate.guard(shape, caller).count(countArgs)
396
+ : await delegate.count(countArgs)
397
+ }
398
+
399
+ return results.length
400
+ }
401
+
402
+ const countArgs: Record<string, any> = {}
403
+ if (query.where) countArgs.where = query.where
404
+
405
+ return shape
406
+ ? await delegate.guard(shape, caller).count(countArgs)
407
+ : await delegate.count(countArgs)
408
+ }
409
+
410
+ export async function ${modelName}FindManyPaginated(req: Request, res: Response, next: NextFunction) {
411
+ try {
412
+ const rawQuery = res.locals.parsedQuery || {}
413
+ const query = applyPaginationLimits(rawQuery, res)
414
+ const extended = await getExtendedClient(req)
415
+ const shape = res.locals.guardShape
416
+ const caller = res.locals.guardCaller
417
+
418
+ if (shape) {
419
+ assertGuard(extended.${modelNameLower})
420
+ }
421
+
422
+ let items: any[]
423
+ let total: number
424
+
425
+ if (typeof extended.$transaction === 'function') {
426
+ try {
427
+ const txResult = await extended.$transaction(async (tx: any) => {
428
+ const d = shape
429
+ ? await tx.${modelNameLower}.guard(shape, caller).findMany(query)
430
+ : await tx.${modelNameLower}.findMany(query)
431
+ const t = await countForPagination(tx.${modelNameLower}, query, shape, caller)
432
+ return { d, t }
433
+ })
434
+ items = txResult.d
435
+ total = txResult.t
436
+ } catch (txError: any) {
437
+ if (
438
+ txError?.message?.includes?.('interactive transactions') ||
439
+ txError?.code === 'P2028'
440
+ ) {
441
+ console.warn('[prisma-generator-express] Interactive transactions not available, pagination queries are non-atomic')
442
+ items = shape
443
+ ? await extended.${modelNameLower}.guard(shape, caller).findMany(query)
444
+ : await extended.${modelNameLower}.findMany(query)
445
+ total = await countForPagination(extended.${modelNameLower}, query, shape, caller)
446
+ } else {
447
+ throw txError
448
+ }
449
+ }
450
+ } else {
451
+ items = shape
452
+ ? await extended.${modelNameLower}.guard(shape, caller).findMany(query)
453
+ : await extended.${modelNameLower}.findMany(query)
454
+ total = await countForPagination(extended.${modelNameLower}, query, shape, caller)
455
+ }
456
+
457
+ const skip = (query.skip as number) ?? 0
458
+ const absTake = Math.abs((query.take as number) ?? items.length)
459
+ const hasMore = items.length >= absTake && skip + items.length < total
460
+
461
+ res.locals.data = { data: items, total, hasMore }
462
+ next()
463
+ } catch (error: unknown) {
464
+ handleError(error, next)
465
+ }
466
+ }
467
+ `
468
+ )
469
+ }