prisma-generator-express 1.28.0 → 1.30.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.
Files changed (49) hide show
  1. package/README.md +244 -14
  2. package/dist/constants.d.ts +1 -0
  3. package/dist/generators/generateFastifyHandler.d.ts +4 -0
  4. package/dist/generators/generateFastifyHandler.js +78 -0
  5. package/dist/generators/generateFastifyHandler.js.map +1 -0
  6. package/dist/generators/generateOperationCore.d.ts +6 -0
  7. package/dist/generators/generateOperationCore.js +534 -0
  8. package/dist/generators/generateOperationCore.js.map +1 -0
  9. package/dist/generators/generateQueryBuilderHelper.js +85 -69
  10. package/dist/generators/generateQueryBuilderHelper.js.map +1 -1
  11. package/dist/generators/generateRouter.js +1 -25
  12. package/dist/generators/generateRouter.js.map +1 -1
  13. package/dist/generators/generateRouterFastify.d.ts +5 -0
  14. package/dist/generators/generateRouterFastify.js +512 -0
  15. package/dist/generators/generateRouterFastify.js.map +1 -0
  16. package/dist/generators/generateUnifiedDocs.d.ts +2 -1
  17. package/dist/generators/generateUnifiedDocs.js +147 -82
  18. package/dist/generators/generateUnifiedDocs.js.map +1 -1
  19. package/dist/generators/generateUnifiedHandler.d.ts +0 -1
  20. package/dist/generators/generateUnifiedHandler.js +47 -516
  21. package/dist/generators/generateUnifiedHandler.js.map +1 -1
  22. package/dist/generators/generateUnifiedScalarUI.d.ts +2 -0
  23. package/dist/generators/generateUnifiedScalarUI.js +127 -1324
  24. package/dist/generators/generateUnifiedScalarUI.js.map +1 -1
  25. package/dist/index.js +33 -8
  26. package/dist/index.js.map +1 -1
  27. package/dist/utils/copyFiles.d.ts +2 -1
  28. package/dist/utils/copyFiles.js +73 -39
  29. package/dist/utils/copyFiles.js.map +1 -1
  30. package/dist/utils/writeFileSafely.js +3 -0
  31. package/dist/utils/writeFileSafely.js.map +1 -1
  32. package/package.json +4 -1
  33. package/src/client/encodeQueryParams.ts +1 -1
  34. package/src/constants.ts +2 -0
  35. package/src/copy/createOutputValidatorMiddleware.ts +9 -12
  36. package/src/copy/docsRenderer.ts +1285 -0
  37. package/src/copy/parseQueryParams.ts +4 -8
  38. package/src/copy/routeConfig.ts +10 -4
  39. package/src/generators/generateFastifyHandler.ts +86 -0
  40. package/src/generators/generateOperationCore.ts +545 -0
  41. package/src/generators/generateQueryBuilderHelper.ts +86 -70
  42. package/src/generators/generateRouter.ts +1 -25
  43. package/src/generators/generateRouterFastify.ts +522 -0
  44. package/src/generators/generateUnifiedDocs.ts +164 -81
  45. package/src/generators/generateUnifiedHandler.ts +45 -533
  46. package/src/generators/generateUnifiedScalarUI.ts +134 -1323
  47. package/src/index.ts +45 -9
  48. package/src/utils/copyFiles.ts +88 -44
  49. package/src/utils/writeFileSafely.ts +4 -0
@@ -9,6 +9,8 @@ type QueryParams =
9
9
 
10
10
  const NUMERIC_KEYS = new Set(['take', 'skip'])
11
11
 
12
+ const INTEGER_RE = /^-?\d+$/
13
+
12
14
  const parseQueryValue = (value: string, key?: string): unknown => {
13
15
  if (value.startsWith('{') || value.startsWith('[') || value.startsWith('"')) {
14
16
  try {
@@ -21,14 +23,8 @@ const parseQueryValue = (value: string, key?: string): unknown => {
21
23
  if (value === 'true') return true
22
24
  if (value === 'false') return false
23
25
  if (value === 'null') return null
24
- if (
25
- key &&
26
- NUMERIC_KEYS.has(key) &&
27
- value !== '' &&
28
- !isNaN(Number(value)) &&
29
- isFinite(Number(value))
30
- ) {
31
- return Number(value)
26
+ if (key && NUMERIC_KEYS.has(key) && INTEGER_RE.test(value)) {
27
+ return parseInt(value, 10)
32
28
  }
33
29
  return value
34
30
  }
@@ -1,8 +1,13 @@
1
- import { RequestHandler, Request } from 'express'
1
+ import type { FastifyRequest, FastifyReply } from 'fastify'
2
+
3
+ export type FastifyHookHandler = (
4
+ request: FastifyRequest,
5
+ reply: FastifyReply,
6
+ ) => Promise<void> | void
2
7
 
3
8
  export interface OperationConfig {
4
- before?: RequestHandler[]
5
- after?: RequestHandler[]
9
+ before?: FastifyHookHandler[]
10
+ after?: FastifyHookHandler[]
6
11
  shape?: Record<string, any>
7
12
  }
8
13
 
@@ -44,7 +49,7 @@ export interface RouteConfig {
44
49
  openApiSecurity?: Record<string, string[]>[]
45
50
 
46
51
  guard?: {
47
- resolveVariant?: (req: Request) => string | undefined
52
+ resolveVariant?: (request: FastifyRequest) => string | undefined
48
53
  variantHeader?: string
49
54
  }
50
55
 
@@ -53,6 +58,7 @@ export interface RouteConfig {
53
58
  pagination?: {
54
59
  defaultLimit?: number
55
60
  maxLimit?: number
61
+ distinctCountLimit?: number
56
62
  }
57
63
 
58
64
  findUnique?: OperationConfig
@@ -0,0 +1,86 @@
1
+ import { DMMF } from '@prisma/generator-helper'
2
+
3
+ const READ_OPS = [
4
+ 'findMany',
5
+ 'findFirst',
6
+ 'findFirstOrThrow',
7
+ 'findUnique',
8
+ 'findUniqueOrThrow',
9
+ 'findManyPaginated',
10
+ 'aggregate',
11
+ 'count',
12
+ 'groupBy',
13
+ ]
14
+
15
+ const WRITE_OPS = [
16
+ 'create',
17
+ 'createMany',
18
+ 'createManyAndReturn',
19
+ 'update',
20
+ 'updateMany',
21
+ 'updateManyAndReturn',
22
+ 'upsert',
23
+ 'delete',
24
+ 'deleteMany',
25
+ ]
26
+
27
+ const CREATED_OPS = new Set([
28
+ 'create',
29
+ 'createMany',
30
+ 'createManyAndReturn',
31
+ ])
32
+
33
+ export function generateFastifyHandler(options: {
34
+ model: DMMF.Model
35
+ }): string {
36
+ const modelName = options.model.name
37
+
38
+ const readHandlers = READ_OPS.map((op) => {
39
+ const exportName = `${modelName}${op.charAt(0).toUpperCase() + op.slice(1)}`
40
+
41
+ return `
42
+ export async function ${exportName}(
43
+ request: FastifyRequest,
44
+ _reply: FastifyReply,
45
+ ): Promise<void> {
46
+ const data = await core.${op}(buildContext(request))
47
+ ;(request as any).resultData = data
48
+ }`
49
+ }).join('\n')
50
+
51
+ const writeHandlers = WRITE_OPS.map((op) => {
52
+ const exportName = `${modelName}${op.charAt(0).toUpperCase() + op.slice(1)}`
53
+ const statusCode = CREATED_OPS.has(op) ? 201 : 200
54
+
55
+ return `
56
+ export async function ${exportName}(
57
+ request: FastifyRequest,
58
+ _reply: FastifyReply,
59
+ ): Promise<void> {
60
+ const data = await core.${op}(buildContext(request))
61
+ ;(request as any).resultData = data
62
+ ;(request as any).resultStatus = ${statusCode}
63
+ }`
64
+ }).join('\n')
65
+
66
+ return `import type { FastifyRequest, FastifyReply } from 'fastify'
67
+ import * as core from './${modelName}Core.js'
68
+ import type { OperationContext } from '../operationRuntime.js'
69
+
70
+ function buildContext(request: FastifyRequest): OperationContext {
71
+ const req = request as any
72
+ return {
73
+ prisma: req.prisma,
74
+ postgres: req.postgres,
75
+ sqlite: req.sqlite,
76
+ parsedQuery: req.parsedQuery,
77
+ body: request.body,
78
+ guardShape: req.guardShape,
79
+ guardCaller: req.guardCaller,
80
+ paginationConfig: req.routeConfig?.pagination,
81
+ }
82
+ }
83
+ ${readHandlers}
84
+ ${writeHandlers}
85
+ `
86
+ }
@@ -0,0 +1,545 @@
1
+ import { DMMF } from '@prisma/generator-helper'
2
+
3
+ export function generateOperationRuntime(): string {
4
+ return `import { sanitizeKeys } from './misc.js'
5
+
6
+ export interface PaginationConfig {
7
+ defaultLimit?: number
8
+ maxLimit?: number
9
+ distinctCountLimit?: number
10
+ }
11
+
12
+ export interface OperationContext {
13
+ prisma: any
14
+ postgres?: any
15
+ sqlite?: any
16
+ parsedQuery?: Record<string, unknown>
17
+ body?: unknown
18
+ guardShape?: Record<string, unknown>
19
+ guardCaller?: string
20
+ paginationConfig?: PaginationConfig
21
+ }
22
+
23
+ export const DISTINCT_COUNT_LIMIT = 100000
24
+
25
+ export class HttpError extends Error {
26
+ status: number
27
+ constructor(status: number, message: string) {
28
+ super(message)
29
+ this.name = 'HttpError'
30
+ this.status = status
31
+ }
32
+ }
33
+
34
+ const PRISMA_ERROR_MAP: Record<string, { status: number; message: string }> = {
35
+ P2000: { status: 400, message: 'Value too long for column' },
36
+ P2001: { status: 404, message: 'Record not found' },
37
+ P2002: { status: 409, message: 'Unique constraint violation' },
38
+ P2003: { status: 400, message: 'Foreign key constraint failed' },
39
+ P2004: { status: 400, message: 'Constraint failed on the database' },
40
+ P2005: { status: 400, message: 'Invalid field value' },
41
+ P2006: { status: 400, message: 'Invalid value provided' },
42
+ P2007: { status: 400, message: 'Data validation error' },
43
+ P2008: { status: 400, message: 'Failed to parse the query' },
44
+ P2009: { status: 400, message: 'Failed to validate the query' },
45
+ P2010: { status: 500, message: 'Raw query failed' },
46
+ P2011: { status: 400, message: 'Null constraint violation' },
47
+ P2012: { status: 400, message: 'Missing required value' },
48
+ P2013: { status: 400, message: 'Missing required argument' },
49
+ P2014: { status: 400, message: 'Required relation violation' },
50
+ P2015: { status: 404, message: 'Related record not found' },
51
+ P2016: { status: 400, message: 'Query interpretation error' },
52
+ P2017: { status: 400, message: 'Records not connected' },
53
+ P2018: { status: 404, message: 'Required connected record not found' },
54
+ P2019: { status: 400, message: 'Input error' },
55
+ P2020: { status: 400, message: 'Value out of range for the field type' },
56
+ P2021: { status: 500, message: 'Table does not exist in the database' },
57
+ P2022: { status: 500, message: 'Column does not exist in the database' },
58
+ P2023: { status: 500, message: 'Inconsistent column data' },
59
+ P2024: { status: 503, message: 'Connection pool timeout' },
60
+ P2025: { status: 404, message: 'Record not found' },
61
+ P2026: { status: 501, message: 'Feature not supported by the current database provider' },
62
+ P2028: { status: 500, message: 'Transaction API error' },
63
+ P2030: { status: 400, message: 'Cannot find a fulltext index for the search' },
64
+ P2033: { status: 400, message: 'Number out of range for the field type' },
65
+ P2034: { status: 409, message: 'Transaction conflict, please retry' },
66
+ }
67
+
68
+ export function mapError(error: unknown): HttpError {
69
+ if (error instanceof HttpError) return error
70
+
71
+ if (
72
+ error &&
73
+ typeof error === 'object' &&
74
+ 'name' in error &&
75
+ error.name === 'ShapeError'
76
+ ) {
77
+ return new HttpError(400, (error as any).message)
78
+ }
79
+
80
+ if (
81
+ error &&
82
+ typeof error === 'object' &&
83
+ 'name' in error &&
84
+ error.name === 'CallerError'
85
+ ) {
86
+ return new HttpError(400, (error as any).message)
87
+ }
88
+
89
+ if (
90
+ error &&
91
+ typeof error === 'object' &&
92
+ 'name' in error &&
93
+ error.name === 'PolicyError'
94
+ ) {
95
+ return new HttpError(403, (error as any).message)
96
+ }
97
+
98
+ if (
99
+ error &&
100
+ typeof error === 'object' &&
101
+ 'issues' in error &&
102
+ 'name' in error &&
103
+ (error as any).name === 'ZodError'
104
+ ) {
105
+ const issues = (error as any).issues
106
+ const message = Array.isArray(issues)
107
+ ? issues.map((i: any) => i.message).join('; ')
108
+ : (error as any).message
109
+ return new HttpError(400, message)
110
+ }
111
+
112
+ if (error && typeof error === 'object' && 'code' in error) {
113
+ const code = (error as any).code as string
114
+ const mapped = PRISMA_ERROR_MAP[code]
115
+ if (mapped) {
116
+ return new HttpError(mapped.status, mapped.message)
117
+ }
118
+ if (typeof code === 'string' && code.startsWith('P')) {
119
+ console.warn(
120
+ '[prisma-generator-express] Unmapped Prisma error code:',
121
+ code,
122
+ (error as any).message || '',
123
+ )
124
+ return new HttpError(500, 'Database operation failed')
125
+ }
126
+ }
127
+
128
+ if (error && typeof error === 'object' && 'name' in error) {
129
+ const name = (error as any).name
130
+ if (name === 'PrismaClientValidationError') {
131
+ return new HttpError(400, 'Invalid query parameters')
132
+ }
133
+ }
134
+
135
+ console.error('[prisma-generator-express] Unhandled error:', error)
136
+ return new HttpError(500, 'Internal server error')
137
+ }
138
+
139
+ let _speedExtension: ((opts: any) => any) | null = null
140
+
141
+ const _prismasqlModule = 'prisma-' + 'sql'
142
+ const _prismasqlReady = (async () => {
143
+ try {
144
+ const mod = await import(_prismasqlModule)
145
+ _speedExtension = mod.speedExtension ?? mod.default?.speedExtension ?? null
146
+ } catch (err: any) {
147
+ const code = err?.code
148
+ if (code !== 'MODULE_NOT_FOUND' && code !== 'ERR_MODULE_NOT_FOUND') {
149
+ console.warn('[prisma-generator-express] prisma-sql initialization failed:', err)
150
+ }
151
+ }
152
+ })()
153
+
154
+ const _extendedClients = new WeakMap<object, WeakMap<object, any>>()
155
+
156
+ export async function getExtendedClient(ctx: OperationContext): Promise<any> {
157
+ const base = ctx.prisma
158
+ if (!base) {
159
+ throw new HttpError(500, 'PrismaClient not found on request. Set req.prisma in middleware.')
160
+ }
161
+
162
+ await _prismasqlReady
163
+
164
+ if (!_speedExtension) return base
165
+
166
+ const connector = ctx.postgres || ctx.sqlite
167
+ if (!connector) return base
168
+
169
+ if (typeof connector === 'object' && connector !== null) {
170
+ const innerMap = _extendedClients.get(connector)
171
+ if (innerMap) {
172
+ const cached = innerMap.get(base)
173
+ if (cached) return cached
174
+ }
175
+ }
176
+
177
+ try {
178
+ const extended = base.$extends(_speedExtension({
179
+ postgres: ctx.postgres,
180
+ sqlite: ctx.sqlite,
181
+ debug: process.env.DEBUG === 'true',
182
+ }))
183
+
184
+ if (typeof connector === 'object' && connector !== null) {
185
+ let innerMap = _extendedClients.get(connector)
186
+ if (!innerMap) {
187
+ innerMap = new WeakMap<object, any>()
188
+ _extendedClients.set(connector, innerMap)
189
+ }
190
+ innerMap.set(base, extended)
191
+ }
192
+
193
+ return extended
194
+ } catch (error) {
195
+ console.warn('[speedExtension] Failed to initialize, using base client:', error)
196
+ return base
197
+ }
198
+ }
199
+
200
+ export function validateBody(body: unknown): Record<string, any> {
201
+ if (!body || typeof body !== 'object' || Array.isArray(body)) {
202
+ throw new HttpError(400, 'Request body must be a JSON object')
203
+ }
204
+ return sanitizeKeys(body as Record<string, any>)
205
+ }
206
+
207
+ export function requireBodyField(body: Record<string, any>, field: string): void {
208
+ if (!(field in body) || body[field] === undefined) {
209
+ throw new HttpError(400, 'Missing required field: ' + field)
210
+ }
211
+ }
212
+
213
+ export function applyPaginationLimits(
214
+ query: Record<string, any>,
215
+ config?: PaginationConfig,
216
+ ): Record<string, any> {
217
+ if (!config) return query
218
+
219
+ const result = { ...query }
220
+
221
+ if (result.take === undefined && config.defaultLimit !== undefined) {
222
+ result.take = config.defaultLimit
223
+ }
224
+
225
+ if (config.maxLimit !== undefined && result.take !== undefined) {
226
+ const takeNum = Number(result.take)
227
+ if (Math.abs(takeNum) > config.maxLimit) {
228
+ result.take = takeNum < 0 ? -config.maxLimit : config.maxLimit
229
+ }
230
+ }
231
+
232
+ return result
233
+ }
234
+
235
+ export function normalizeDistinct(value: unknown): string[] {
236
+ if (typeof value === 'string') return [value]
237
+ if (Array.isArray(value)) return value.filter((v): v is string => typeof v === 'string')
238
+ return []
239
+ }
240
+
241
+ export function assertGuard(delegate: any): void {
242
+ if (typeof delegate.guard !== 'function') {
243
+ throw new HttpError(
244
+ 500,
245
+ 'Guard shapes require prisma-guard extension on PrismaClient. Install: npm install prisma-guard, then extend your client with guardExtension().',
246
+ )
247
+ }
248
+ }
249
+
250
+ const GUARD_SHAPE_CONFIG_KEYS = new Set([
251
+ 'data', 'create', 'update', 'where', 'include', 'select', 'orderBy',
252
+ 'cursor', 'take', 'skip', 'distinct', 'having', '_count', '_avg',
253
+ '_sum', '_min', '_max', 'by',
254
+ ])
255
+
256
+ function keepWhereOnly(obj: Record<string, any>): Record<string, any> {
257
+ const result: Record<string, any> = {}
258
+ if ('where' in obj) result.where = obj.where
259
+ return result
260
+ }
261
+
262
+ export function buildCountShape(shape: Record<string, any>): Record<string, any> {
263
+ if (typeof shape === 'function') {
264
+ return (...args: any[]) => keepWhereOnly((shape as Function)(...args))
265
+ }
266
+
267
+ const keys = Object.keys(shape)
268
+ const isSingleShape = keys.length === 0 || keys.every((k) => GUARD_SHAPE_CONFIG_KEYS.has(k))
269
+
270
+ if (isSingleShape) {
271
+ return keepWhereOnly(shape)
272
+ }
273
+
274
+ const result: Record<string, any> = {}
275
+ for (const [key, variant] of Object.entries(shape)) {
276
+ if (typeof variant === 'function') {
277
+ result[key] = (...args: any[]) => keepWhereOnly(variant(...args))
278
+ } else if (typeof variant === 'object' && variant !== null) {
279
+ result[key] = keepWhereOnly(variant)
280
+ } else {
281
+ result[key] = variant
282
+ }
283
+ }
284
+ return result
285
+ }
286
+
287
+ export async function countForPagination(
288
+ delegate: any,
289
+ query: Record<string, any>,
290
+ shape: Record<string, any> | undefined,
291
+ caller: string | undefined,
292
+ distinctCountLimit?: number,
293
+ ): Promise<number> {
294
+ const distinctFields = normalizeDistinct(query.distinct)
295
+ const hasDistinct = distinctFields.length > 0
296
+ const effectiveLimit = distinctCountLimit ?? DISTINCT_COUNT_LIMIT
297
+
298
+ const countShape = shape ? buildCountShape(shape) : undefined
299
+
300
+ if (hasDistinct) {
301
+ const selectField = distinctFields[0]
302
+ const distinctArgs: Record<string, any> = {
303
+ where: query.where,
304
+ distinct: distinctFields,
305
+ select: { [selectField]: true },
306
+ take: effectiveLimit + 1,
307
+ }
308
+
309
+ const results = shape
310
+ ? await delegate.guard(shape, caller).findMany(distinctArgs)
311
+ : await delegate.findMany(distinctArgs)
312
+
313
+ if (results.length > effectiveLimit) {
314
+ console.warn(
315
+ '[prisma-generator-express] Distinct count exceeds ' +
316
+ effectiveLimit +
317
+ ', falling back to approximate total',
318
+ )
319
+ const countArgs: Record<string, any> = {}
320
+ if (query.where) countArgs.where = query.where
321
+ return countShape
322
+ ? await delegate.guard(countShape, caller).count(countArgs)
323
+ : await delegate.count(countArgs)
324
+ }
325
+
326
+ return results.length
327
+ }
328
+
329
+ const countArgs: Record<string, any> = {}
330
+ if (query.where) countArgs.where = query.where
331
+
332
+ return countShape
333
+ ? await delegate.guard(countShape, caller).count(countArgs)
334
+ : await delegate.count(countArgs)
335
+ }
336
+
337
+ export function transformResult(value: unknown): unknown {
338
+ if (value === null || value === undefined) return value
339
+ if (typeof value === 'bigint') return value.toString()
340
+ if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value)) {
341
+ return value.toString('base64')
342
+ }
343
+ if (value instanceof Uint8Array) {
344
+ return Buffer.from(value).toString('base64')
345
+ }
346
+ if (value instanceof Date) return value
347
+ if (Array.isArray(value)) return value.map(transformResult)
348
+ if (typeof value === 'object') {
349
+ const proto = Object.getPrototypeOf(value)
350
+ if (proto !== Object.prototype && proto !== null) return value
351
+ const out: Record<string, unknown> = {}
352
+ for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
353
+ out[k] = transformResult(v)
354
+ }
355
+ return out
356
+ }
357
+ return value
358
+ }
359
+ `
360
+ }
361
+
362
+ export interface ModelCoreOptions {
363
+ model: DMMF.Model
364
+ }
365
+
366
+ export function generateModelCore(options: ModelCoreOptions): string {
367
+ const modelName = options.model.name
368
+ const modelNameLower =
369
+ modelName.charAt(0).toLowerCase() + modelName.slice(1)
370
+
371
+ const standardReadOps = [
372
+ 'findFirst',
373
+ 'findUnique',
374
+ 'findUniqueOrThrow',
375
+ 'findFirstOrThrow',
376
+ 'count',
377
+ 'aggregate',
378
+ 'groupBy',
379
+ ]
380
+
381
+ const standardReadHandlers = standardReadOps
382
+ .map(
383
+ (op) => `
384
+ export async function ${op}(ctx: OperationContext): Promise<unknown> {
385
+ const query = ctx.parsedQuery || {}
386
+ const extended = await getExtendedClient(ctx)
387
+ if (ctx.guardShape) {
388
+ assertGuard((extended as any).${modelNameLower})
389
+ return (extended as any).${modelNameLower}.guard(ctx.guardShape, ctx.guardCaller).${op}(query)
390
+ }
391
+ return (extended as any).${modelNameLower}.${op}(query)
392
+ }`,
393
+ )
394
+ .join('\n')
395
+
396
+ const writeOps = [
397
+ { name: 'create', method: 'create', requiredFields: ['data'] },
398
+ { name: 'createMany', method: 'createMany', requiredFields: ['data'] },
399
+ {
400
+ name: 'createManyAndReturn',
401
+ method: 'createManyAndReturn',
402
+ requiredFields: ['data'],
403
+ },
404
+ {
405
+ name: 'update',
406
+ method: 'update',
407
+ requiredFields: ['where', 'data'],
408
+ },
409
+ {
410
+ name: 'updateMany',
411
+ method: 'updateMany',
412
+ requiredFields: ['where', 'data'],
413
+ },
414
+ {
415
+ name: 'updateManyAndReturn',
416
+ method: 'updateManyAndReturn',
417
+ requiredFields: ['where', 'data'],
418
+ },
419
+ { name: 'delete', method: 'delete', requiredFields: ['where'] },
420
+ {
421
+ name: 'deleteMany',
422
+ method: 'deleteMany',
423
+ requiredFields: ['where'],
424
+ },
425
+ {
426
+ name: 'upsert',
427
+ method: 'upsert',
428
+ requiredFields: ['where', 'create', 'update'],
429
+ },
430
+ ]
431
+
432
+ const writeHandlers = writeOps
433
+ .map((op) => {
434
+ const validationLines = op.requiredFields
435
+ .map((field) => ` requireBodyField(body, '${field}')`)
436
+ .join('\n')
437
+
438
+ return `
439
+ export async function ${op.name}(ctx: OperationContext): Promise<unknown> {
440
+ const body = validateBody(ctx.body)
441
+ ${validationLines}
442
+ const extended = await getExtendedClient(ctx)
443
+ if (ctx.guardShape) {
444
+ assertGuard((extended as any).${modelNameLower})
445
+ return (extended as any).${modelNameLower}.guard(ctx.guardShape, ctx.guardCaller).${op.method}(body)
446
+ }
447
+ return (extended as any).${modelNameLower}.${op.method}(body)
448
+ }`
449
+ })
450
+ .join('\n')
451
+
452
+ return `import {
453
+ OperationContext,
454
+ getExtendedClient,
455
+ validateBody,
456
+ requireBodyField,
457
+ applyPaginationLimits,
458
+ assertGuard,
459
+ countForPagination,
460
+ } from '../operationRuntime.js'
461
+
462
+ export async function findMany(ctx: OperationContext): Promise<unknown> {
463
+ const rawQuery = ctx.parsedQuery || {}
464
+ const query = applyPaginationLimits(rawQuery, ctx.paginationConfig)
465
+ const extended = await getExtendedClient(ctx)
466
+ if (ctx.guardShape) {
467
+ assertGuard((extended as any).${modelNameLower})
468
+ return (extended as any).${modelNameLower}.guard(ctx.guardShape, ctx.guardCaller).findMany(query)
469
+ }
470
+ return (extended as any).${modelNameLower}.findMany(query)
471
+ }
472
+ ${standardReadHandlers}
473
+ ${writeHandlers}
474
+
475
+ export async function findManyPaginated(
476
+ ctx: OperationContext,
477
+ ): Promise<{ data: unknown[]; total: number; hasMore: boolean }> {
478
+ const rawQuery = ctx.parsedQuery || {}
479
+ const query = applyPaginationLimits(rawQuery, ctx.paginationConfig)
480
+ const extended = await getExtendedClient(ctx)
481
+ const shape = ctx.guardShape
482
+ const caller = ctx.guardCaller
483
+ const distinctCountLimit = ctx.paginationConfig?.distinctCountLimit
484
+
485
+ if (shape) {
486
+ assertGuard((extended as any).${modelNameLower})
487
+ }
488
+
489
+ let items: any[]
490
+ let total: number
491
+
492
+ if (typeof extended.$transaction === 'function') {
493
+ try {
494
+ const txResult = await extended.$transaction(async (tx: any) => {
495
+ const d = shape
496
+ ? await tx.${modelNameLower}.guard(shape, caller).findMany(query)
497
+ : await tx.${modelNameLower}.findMany(query)
498
+ const t = await countForPagination(tx.${modelNameLower}, query, shape, caller, distinctCountLimit)
499
+ return { d, t }
500
+ })
501
+ items = txResult.d
502
+ total = txResult.t
503
+ } catch (txError: any) {
504
+ if (
505
+ txError?.message?.includes?.('interactive transactions') ||
506
+ txError?.code === 'P2028'
507
+ ) {
508
+ console.warn(
509
+ '[prisma-generator-express] Interactive transactions not available, pagination queries are non-atomic',
510
+ )
511
+ items = shape
512
+ ? await (extended as any).${modelNameLower}.guard(shape, caller).findMany(query)
513
+ : await (extended as any).${modelNameLower}.findMany(query)
514
+ total = await countForPagination(
515
+ (extended as any).${modelNameLower},
516
+ query,
517
+ shape,
518
+ caller,
519
+ distinctCountLimit,
520
+ )
521
+ } else {
522
+ throw txError
523
+ }
524
+ }
525
+ } else {
526
+ items = shape
527
+ ? await (extended as any).${modelNameLower}.guard(shape, caller).findMany(query)
528
+ : await (extended as any).${modelNameLower}.findMany(query)
529
+ total = await countForPagination(
530
+ (extended as any).${modelNameLower},
531
+ query,
532
+ shape,
533
+ caller,
534
+ distinctCountLimit,
535
+ )
536
+ }
537
+
538
+ const skip = (query.skip as number) ?? 0
539
+ const absTake = Math.abs((query.take as number) ?? items.length)
540
+ const hasMore = items.length >= absTake && skip + items.length < total
541
+
542
+ return { data: items, total, hasMore }
543
+ }
544
+ `
545
+ }