prisma-generator-express 1.55.0 → 1.56.1

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 (39) hide show
  1. package/README.md +131 -12
  2. package/dist/constants.d.ts +1 -0
  3. package/dist/generators/generateFastifyHandler.js +3 -1
  4. package/dist/generators/generateFastifyHandler.js.map +1 -1
  5. package/dist/generators/generateHonoHandler.js +3 -1
  6. package/dist/generators/generateHonoHandler.js.map +1 -1
  7. package/dist/generators/generateOperationCore.d.ts +2 -1
  8. package/dist/generators/generateOperationCore.js +38 -36
  9. package/dist/generators/generateOperationCore.js.map +1 -1
  10. package/dist/generators/generateRouteConfigType.js +2 -1
  11. package/dist/generators/generateRouteConfigType.js.map +1 -1
  12. package/dist/generators/generateRouter.d.ts +3 -2
  13. package/dist/generators/generateRouter.js +16 -5
  14. package/dist/generators/generateRouter.js.map +1 -1
  15. package/dist/generators/generateRouterFastify.d.ts +3 -2
  16. package/dist/generators/generateRouterFastify.js +19 -7
  17. package/dist/generators/generateRouterFastify.js.map +1 -1
  18. package/dist/generators/generateRouterHono.d.ts +3 -2
  19. package/dist/generators/generateRouterHono.js +24 -14
  20. package/dist/generators/generateRouterHono.js.map +1 -1
  21. package/dist/index.js +20 -1
  22. package/dist/index.js.map +1 -1
  23. package/package.json +1 -1
  24. package/src/constants.ts +3 -1
  25. package/src/copy/autoIncludeRuntime.ts +60 -35
  26. package/src/copy/docsRenderer.ts +5 -4
  27. package/src/copy/operationRuntime.ts +94 -9
  28. package/src/copy/routeConfig.express.ts +6 -0
  29. package/src/copy/routeConfig.fastify.ts +7 -1
  30. package/src/copy/routeConfig.hono.ts +8 -2
  31. package/src/copy/routeConfig.ts +21 -5
  32. package/src/generators/generateFastifyHandler.ts +3 -1
  33. package/src/generators/generateHonoHandler.ts +3 -1
  34. package/src/generators/generateOperationCore.ts +42 -37
  35. package/src/generators/generateRouteConfigType.ts +2 -2
  36. package/src/generators/generateRouter.ts +18 -5
  37. package/src/generators/generateRouterFastify.ts +21 -7
  38. package/src/generators/generateRouterHono.ts +26 -14
  39. package/src/index.ts +24 -2
@@ -5,6 +5,9 @@ import type {
5
5
  ProgressiveStageResult,
6
6
  ProgressiveStageContext,
7
7
  ProgressiveStage,
8
+ PaginationConfig,
9
+ PaginationCountSource,
10
+ FindManyPaginatedMode,
8
11
  } from './routeConfig'
9
12
 
10
13
  export type {
@@ -13,12 +16,9 @@ export type {
13
16
  ProgressiveStageResult,
14
17
  ProgressiveStageContext,
15
18
  ProgressiveStage,
16
- }
17
-
18
- export interface PaginationConfig {
19
- defaultLimit?: number
20
- maxLimit?: number
21
- distinctCountLimit?: number
19
+ PaginationConfig,
20
+ PaginationCountSource,
21
+ FindManyPaginatedMode,
22
22
  }
23
23
 
24
24
  export interface OperationContext {
@@ -30,6 +30,7 @@ export interface OperationContext {
30
30
  guardShape?: Record<string, unknown>
31
31
  guardCaller?: string
32
32
  paginationConfig?: PaginationConfig
33
+ findManyPaginatedMode?: FindManyPaginatedMode
33
34
  }
34
35
 
35
36
  export type PrismaDelegate = {
@@ -58,8 +59,14 @@ export type PrismaClientLike = {
58
59
  $transaction?: <T>(fn: (tx: PrismaClientLike) => Promise<T>) => Promise<T>
59
60
  }
60
61
 
62
+ type PrismaRawClient = {
63
+ $queryRawUnsafe?: <T = unknown>(sql: string, ...values: unknown[]) => Promise<T>
64
+ }
65
+
61
66
  export const DISTINCT_COUNT_LIMIT = 100000
62
67
 
68
+ const IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/
69
+
63
70
  export class HttpError extends Error {
64
71
  status: number
65
72
  constructor(status: number, message: string) {
@@ -254,6 +261,14 @@ export function applyPaginationLimits(
254
261
  return result
255
262
  }
256
263
 
264
+ export function mergePaginationConfig(
265
+ base: PaginationConfig | undefined,
266
+ override: Partial<PaginationConfig> | undefined,
267
+ ): PaginationConfig | undefined {
268
+ if (!base && !override) return undefined
269
+ return { ...(base ?? {}), ...(override ?? {}) }
270
+ }
271
+
257
272
  export function normalizeDistinct(value: unknown): string[] {
258
273
  if (typeof value === 'string') return [value]
259
274
  if (Array.isArray(value)) return value.filter((v): v is string => typeof v === 'string')
@@ -309,13 +324,85 @@ export function buildCountShape(
309
324
  return result
310
325
  }
311
326
 
327
+ function quoteIdent(name: string): string {
328
+ if (!IDENT_RE.test(name)) {
329
+ throw new HttpError(400, 'invalid identifier: ' + name)
330
+ }
331
+ return '"' + name.replace(/"/g, '""') + '"'
332
+ }
333
+
334
+ function buildMaterializedCountFqn(
335
+ source: Extract<PaginationCountSource, { type: 'materializedView' }>,
336
+ ): string {
337
+ return source.schema
338
+ ? quoteIdent(source.schema) + '.' + quoteIdent(source.relation)
339
+ : quoteIdent(source.relation)
340
+ }
341
+
342
+ function buildMaterializedCountWhere(
343
+ where: Record<string, unknown> | undefined,
344
+ ): { sql: string; values: unknown[] } {
345
+ if (!where || Object.keys(where).length === 0) {
346
+ return { sql: '', values: [] }
347
+ }
348
+ const values: unknown[] = []
349
+ const clauses: string[] = []
350
+ for (const [key, value] of Object.entries(where)) {
351
+ if (value === null) {
352
+ clauses.push(quoteIdent(key) + ' IS NULL')
353
+ continue
354
+ }
355
+ values.push(value)
356
+ clauses.push(quoteIdent(key) + ' = $' + values.length)
357
+ }
358
+ return { sql: ' WHERE ' + clauses.join(' AND '), values }
359
+ }
360
+
361
+ export async function countFromMaterializedView(
362
+ client: unknown,
363
+ source: Extract<PaginationCountSource, { type: 'materializedView' }>,
364
+ ): Promise<number> {
365
+ const raw = client as PrismaRawClient
366
+ if (typeof raw.$queryRawUnsafe !== 'function') {
367
+ throw new HttpError(500, 'Materialized count source requires $queryRawUnsafe on the Prisma client')
368
+ }
369
+ const column = source.column ?? 'total'
370
+ const where = buildMaterializedCountWhere(source.where)
371
+ const sql =
372
+ 'SELECT ' +
373
+ quoteIdent(column) +
374
+ ' AS "total" FROM ' +
375
+ buildMaterializedCountFqn(source) +
376
+ where.sql +
377
+ ' LIMIT 1'
378
+ const rows = await raw.$queryRawUnsafe<Array<{ total: unknown }>>(sql, ...where.values)
379
+ const value = rows[0]?.total
380
+ const total = Number(value)
381
+ if (!Number.isFinite(total)) {
382
+ throw new HttpError(500, 'Materialized count source did not return a numeric total')
383
+ }
384
+ return Math.trunc(total)
385
+ }
386
+
312
387
  export async function countForPagination(
313
388
  delegate: PrismaDelegate,
314
389
  query: Record<string, unknown>,
315
390
  shape: Record<string, unknown> | undefined,
316
391
  caller: string | undefined,
317
392
  distinctCountLimit?: number,
393
+ countSource?: PaginationCountSource,
394
+ rawClient?: unknown,
318
395
  ): Promise<number> {
396
+ if (
397
+ countSource &&
398
+ countSource.type === 'materializedView' &&
399
+ !shape &&
400
+ !query.where &&
401
+ !query.distinct
402
+ ) {
403
+ return countFromMaterializedView(rawClient ?? delegate, countSource)
404
+ }
405
+
319
406
  const distinctFields = normalizeDistinct(query.distinct)
320
407
  const hasDistinct = distinctFields.length > 0
321
408
  const effectiveLimit = distinctCountLimit ?? DISTINCT_COUNT_LIMIT
@@ -333,9 +420,7 @@ export async function countForPagination(
333
420
  return (await delegate.count(countArgs)) as number
334
421
  }
335
422
 
336
- if (hasDistinct && shape) {
337
- return runCount()
338
- }
423
+ if (hasDistinct && shape) return runCount()
339
424
 
340
425
  if (hasDistinct) {
341
426
  const selectField = distinctFields[0]
@@ -8,6 +8,9 @@ import type {
8
8
  OpenApiServerConfig,
9
9
  OpenApiSecuritySchemeConfig,
10
10
  WriteStrategy,
11
+ FindManyPaginatedMode,
12
+ PaginationConfig,
13
+ PaginationCountSource,
11
14
  } from './routeConfig'
12
15
 
13
16
  export type {
@@ -15,6 +18,9 @@ export type {
15
18
  OpenApiServerConfig,
16
19
  OpenApiSecuritySchemeConfig,
17
20
  WriteStrategy,
21
+ FindManyPaginatedMode,
22
+ PaginationConfig,
23
+ PaginationCountSource,
18
24
  }
19
25
 
20
26
  export type {
@@ -6,13 +6,19 @@ import type {
6
6
  OpenApiServerConfig,
7
7
  OpenApiSecuritySchemeConfig,
8
8
  WriteStrategy,
9
+ FindManyPaginatedMode,
10
+ PaginationConfig,
11
+ PaginationCountSource,
9
12
  } from './routeConfig'
10
13
 
11
14
  export type {
12
15
  QueryBuilderConfig,
13
16
  OpenApiServerConfig,
14
17
  OpenApiSecuritySchemeConfig,
15
- WriteStrategy
18
+ WriteStrategy,
19
+ FindManyPaginatedMode,
20
+ PaginationConfig,
21
+ PaginationCountSource,
16
22
  }
17
23
 
18
24
  export type FastifyHookHandler = (
@@ -6,13 +6,19 @@ import type {
6
6
  OpenApiServerConfig,
7
7
  OpenApiSecuritySchemeConfig,
8
8
  WriteStrategy,
9
+ FindManyPaginatedMode,
10
+ PaginationConfig,
11
+ PaginationCountSource,
9
12
  } from './routeConfig'
10
13
 
11
14
  export type {
12
15
  QueryBuilderConfig,
13
16
  OpenApiServerConfig,
14
17
  OpenApiSecuritySchemeConfig,
15
- WriteStrategy
18
+ WriteStrategy,
19
+ FindManyPaginatedMode,
20
+ PaginationConfig,
21
+ PaginationCountSource,
16
22
  }
17
23
 
18
24
  export type HonoEnvBase = {
@@ -26,7 +32,7 @@ export type HonoInternalVariables = {
26
32
  sqlite?: unknown
27
33
  parsedQuery?: Record<string, unknown>
28
34
  body?: unknown
29
- routeConfig?: { pagination?: unknown }
35
+ routeConfig?: { pagination?: PaginationConfig }
30
36
  guardShape?: Record<string, unknown>
31
37
  guardCaller?: string
32
38
  resultData?: unknown
@@ -22,6 +22,25 @@ export interface OpenApiSecuritySchemeConfig {
22
22
 
23
23
  export type WriteStrategy = 'regular' | 'throwOnNonReturning' | 'forceReturn'
24
24
 
25
+ export type FindManyPaginatedMode = 'transaction' | 'promiseAll'
26
+
27
+ export type PaginationCountSource =
28
+ | { type?: 'delegate' }
29
+ | {
30
+ type: 'materializedView'
31
+ relation: string
32
+ schema?: string
33
+ column?: string
34
+ where?: Record<string, unknown>
35
+ }
36
+
37
+ export interface PaginationConfig {
38
+ defaultLimit?: number
39
+ maxLimit?: number
40
+ distinctCountLimit?: number
41
+ countSource?: PaginationCountSource
42
+ }
43
+
25
44
  export type ProgressivePatch = {
26
45
  key: string
27
46
  value: unknown
@@ -72,6 +91,7 @@ export interface BaseOperationConfig<HookHandler, TShape = Record<string, unknow
72
91
  before?: HookHandler[]
73
92
  after?: HookHandler[]
74
93
  shape?: TShape
94
+ pagination?: Partial<PaginationConfig>
75
95
  }
76
96
 
77
97
  export interface BaseRouteConfig<
@@ -99,11 +119,7 @@ export interface BaseRouteConfig<
99
119
  }
100
120
  resolveContext?: (request: RequestType) => TCtx | Promise<TCtx>
101
121
  queryBuilder?: QueryBuilderConfig | false
102
- pagination?: {
103
- defaultLimit?: number
104
- maxLimit?: number
105
- distinctCountLimit?: number
106
- }
122
+ pagination?: PaginationConfig
107
123
  findUnique?: BaseOperationConfig<HookHandler, TShape>
108
124
  findUniqueOrThrow?: BaseOperationConfig<HookHandler, TShape>
109
125
  findFirst?: BaseOperationConfig<HookHandler, TShape>
@@ -77,7 +77,7 @@ export async function ${exportName}(
77
77
 
78
78
  return `import type { FastifyRequest, FastifyReply } from 'fastify'
79
79
  import * as core from './${modelName}Core${ext}'
80
- import type { OperationContext } from '../operationRuntime${ext}'
80
+ import type { OperationContext, FindManyPaginatedMode } from '../operationRuntime${ext}'
81
81
 
82
82
  type FastifyExtended = FastifyRequest & {
83
83
  prisma?: unknown
@@ -87,6 +87,7 @@ type FastifyExtended = FastifyRequest & {
87
87
  routeConfig?: { pagination?: OperationContext['paginationConfig'] }
88
88
  guardShape?: Record<string, unknown>
89
89
  guardCaller?: string
90
+ findManyPaginatedMode?: FindManyPaginatedMode
90
91
  resultData?: unknown
91
92
  resultStatus?: number
92
93
  }
@@ -102,6 +103,7 @@ function buildContext(request: FastifyRequest): OperationContext {
102
103
  guardShape: req.guardShape,
103
104
  guardCaller: req.guardCaller,
104
105
  paginationConfig: req.routeConfig?.pagination,
106
+ findManyPaginatedMode: req.findManyPaginatedMode,
105
107
  }
106
108
  }
107
109
  ${readHandlers}
@@ -70,7 +70,7 @@ export async function ${exportName}(c: Context<HonoEnv>): Promise<void> {
70
70
 
71
71
  return `import type { Context } from 'hono'
72
72
  import * as core from './${modelName}Core${ext}'
73
- import type { OperationContext } from '../operationRuntime${ext}'
73
+ import type { OperationContext, FindManyPaginatedMode } from '../operationRuntime${ext}'
74
74
 
75
75
  type HonoVariables = {
76
76
  prisma: unknown
@@ -81,6 +81,7 @@ type HonoVariables = {
81
81
  routeConfig?: { pagination?: OperationContext['paginationConfig'] }
82
82
  guardShape?: Record<string, unknown>
83
83
  guardCaller?: string
84
+ findManyPaginatedMode?: FindManyPaginatedMode
84
85
  resultData?: unknown
85
86
  resultStatus?: number
86
87
  }
@@ -97,6 +98,7 @@ function buildContext(c: Context<HonoEnv>): OperationContext {
97
98
  guardShape: c.get('guardShape'),
98
99
  guardCaller: c.get('guardCaller'),
99
100
  paginationConfig: c.get('routeConfig')?.pagination,
101
+ findManyPaginatedMode: c.get('findManyPaginatedMode'),
100
102
  }
101
103
  }
102
104
  ${readHandlers}
@@ -1,12 +1,13 @@
1
1
  import { DMMF } from '@prisma/generator-helper'
2
2
  import { ImportStyle } from '../utils/resolveImportStyle'
3
3
  import { importExt } from '../utils/importExt'
4
- import { WriteStrategy } from '../constants'
4
+ import { WriteStrategy, FindManyPaginatedMode } from '../constants'
5
5
 
6
6
  export interface ModelCoreOptions {
7
7
  model: DMMF.Model
8
8
  importStyle: ImportStyle
9
9
  writeStrategy: WriteStrategy
10
+ findManyPaginatedMode: FindManyPaginatedMode
10
11
  }
11
12
 
12
13
  type WriteOpDecision =
@@ -33,11 +34,49 @@ function decideWriteOp(
33
34
  return { mode: 'normal', method: defaultMethod }
34
35
  }
35
36
 
37
+ function renderPaginatedBody(modelNameLower: string, mode: FindManyPaginatedMode): string {
38
+ if (mode === 'transaction') {
39
+ return `
40
+ const txClient = extended as { $transaction?: <T>(fn: (tx: unknown) => Promise<T>) => Promise<T> }
41
+ if (typeof txClient.$transaction !== 'function') {
42
+ throw new HttpError(500, 'findManyPaginatedMode="transaction" requires transaction support on the Prisma client')
43
+ }
44
+
45
+ const txResult = await txClient.$transaction(async (tx: unknown) => {
46
+ const txDelegate = getDelegate(tx, '${modelNameLower}')
47
+ if (shape) assertGuard(txDelegate)
48
+ const findP = shape
49
+ ? (txDelegate.guard as NonNullable<typeof txDelegate.guard>)(shape, caller).findMany(query)
50
+ : txDelegate.findMany(query)
51
+ const countP = countForPagination(
52
+ txDelegate, query, shape, caller, distinctCountLimit, countSource, tx,
53
+ )
54
+ const [data, count] = await Promise.all([findP, countP])
55
+ return { data, count }
56
+ })
57
+ items = txResult.data as unknown[]
58
+ total = txResult.count`
59
+ }
60
+
61
+ return `
62
+ const delegate = getDelegate(extended, '${modelNameLower}')
63
+ if (shape) assertGuard(delegate)
64
+ const [data, count] = await Promise.all([
65
+ shape
66
+ ? (delegate.guard as NonNullable<typeof delegate.guard>)(shape, caller).findMany(query)
67
+ : delegate.findMany(query),
68
+ countForPagination(delegate, query, shape, caller, distinctCountLimit, countSource, extended),
69
+ ])
70
+ items = data as unknown[]
71
+ total = count`
72
+ }
73
+
36
74
  export function generateModelCore(options: ModelCoreOptions): string {
37
75
  const ext = importExt(options.importStyle)
38
76
  const modelName = options.model.name
39
77
  const modelNameLower = modelName.charAt(0).toLowerCase() + modelName.slice(1)
40
78
  const writeStrategy = options.writeStrategy
79
+ const paginatedBody = renderPaginatedBody(modelNameLower, options.findManyPaginatedMode)
41
80
 
42
81
  const standardReadOps = [
43
82
  'findFirst', 'findUnique', 'findUniqueOrThrow', 'findFirstOrThrow',
@@ -136,45 +175,11 @@ export async function findManyPaginated(
136
175
  const shape = ctx.guardShape
137
176
  const caller = ctx.guardCaller
138
177
  const distinctCountLimit = ctx.paginationConfig?.distinctCountLimit
139
- const delegate = getDelegate(extended, '${modelNameLower}')
140
-
141
- if (shape) assertGuard(delegate)
178
+ const countSource = ctx.paginationConfig?.countSource
142
179
 
143
180
  let items: unknown[]
144
181
  let total: number
145
-
146
- const txClient = extended as { $transaction?: <T>(fn: (tx: unknown) => Promise<T>) => Promise<T> }
147
-
148
- if (shape || typeof txClient.$transaction !== 'function') {
149
- const [data, count] = await Promise.all([
150
- shape
151
- ? (delegate.guard as NonNullable<typeof delegate.guard>)(shape, caller).findMany(query)
152
- : delegate.findMany(query),
153
- countForPagination(delegate, query, shape, caller, distinctCountLimit),
154
- ])
155
- items = data as unknown[]
156
- total = count
157
- } else {
158
- try {
159
- const txResult = await txClient.$transaction(async (tx: unknown) => {
160
- const txDelegate = getDelegate(tx, '${modelNameLower}')
161
- const d = await txDelegate.findMany(query)
162
- const t = await countForPagination(txDelegate, query, undefined, undefined, distinctCountLimit)
163
- return { d, t }
164
- })
165
- items = txResult.d as unknown[]
166
- total = txResult.t
167
- } catch (txError: unknown) {
168
- const txe = txError as { message?: string; code?: string }
169
- if (txe?.code === 'P2028') {
170
- console.warn('[prisma-generator-express] Interactive transactions not available, pagination queries are non-atomic')
171
- items = (await delegate.findMany(query)) as unknown[]
172
- total = await countForPagination(delegate, query, undefined, undefined, distinctCountLimit)
173
- } else {
174
- throw txError
175
- }
176
- }
177
- }
182
+ ${paginatedBody}
178
183
 
179
184
  const skip = (typeof query.skip === 'number' ? query.skip : 0)
180
185
  const takeRaw = (typeof query.take === 'number' ? query.take : items.length)
@@ -39,7 +39,6 @@ const ROUTER_OP_TO_SHAPE_OP: Record<RouterOperation, string> = {
39
39
  deleteMany: 'deleteMany',
40
40
  }
41
41
 
42
-
43
42
  function requestTypeFor(target: Target): string {
44
43
  if (target === 'fastify') return `import('fastify').FastifyRequest`
45
44
  if (target === 'hono') return `import('hono').Context<TEnv>`
@@ -85,7 +84,7 @@ export function generateRouteConfigType(
85
84
  const requestType = requestTypeFor(target)
86
85
 
87
86
  const progressiveTypeImport = supportsProgressive
88
- ? `import type { ProgressiveVariantConfig, ProgressiveStage } from '../routeConfig.target${ext}'\n\n`
87
+ ? `import type { ProgressiveVariantConfig, ProgressiveStage } from '../routeConfig.target${ext}'\n`
89
88
  : ''
90
89
 
91
90
  if (!guardShapesImport) {
@@ -103,6 +102,7 @@ export function generateRouteConfigType(
103
102
  ` before?: ${hookRef}[]`,
104
103
  ` after?: ${hookRef}[]`,
105
104
  ` shape?: ${m}${c}ShapeInput<TCtx>`,
105
+ ` pagination?: Partial<PaginationConfig>`,
106
106
  ]
107
107
  if (isRead && supportsProgressive) {
108
108
  lines.push(` progressive?: Record<string, ProgressiveVariantConfig>`)
@@ -2,7 +2,7 @@ import { DMMF } from '@prisma/generator-helper'
2
2
  import { generateRouteConfigType } from './generateRouteConfigType'
3
3
  import { ImportStyle } from '../utils/resolveImportStyle'
4
4
  import { importExt } from '../utils/importExt'
5
- import { WriteStrategy } from '../constants'
5
+ import { WriteStrategy, FindManyPaginatedMode } from '../constants'
6
6
 
7
7
  export function generateRouterFunction({
8
8
  model,
@@ -10,12 +10,14 @@ export function generateRouterFunction({
10
10
  guardShapesImport,
11
11
  importStyle,
12
12
  writeStrategy,
13
+ findManyPaginatedMode,
13
14
  }: {
14
15
  model: DMMF.Model
15
16
  enums: DMMF.DatamodelEnum[]
16
17
  guardShapesImport: string | null
17
18
  importStyle: ImportStyle
18
19
  writeStrategy: WriteStrategy
20
+ findManyPaginatedMode: FindManyPaginatedMode
19
21
  }): string {
20
22
  const ext = importExt(importStyle)
21
23
  const modelName = model.name
@@ -70,7 +72,13 @@ import {
70
72
  ${modelName}GroupBy,
71
73
  } from './${modelName}Handlers${ext}'
72
74
  import * as core from './${modelName}Core${ext}'
73
- import type { RouteConfig, QueryBuilderConfig, WriteStrategy } from '../routeConfig.target${ext}'
75
+ import type {
76
+ RouteConfig,
77
+ QueryBuilderConfig,
78
+ WriteStrategy,
79
+ FindManyPaginatedMode,
80
+ PaginationConfig,
81
+ } from '../routeConfig.target${ext}'
74
82
  import { parseQueryParams } from '../parseQueryParams${ext}'
75
83
  import { sanitizeKeys, normalizePrefix, getEnv } from '../misc${ext}'
76
84
  import { buildModelOpenApi } from '../buildModelOpenApi${ext}'
@@ -82,6 +90,7 @@ import {
82
90
  runSingleResultSSE,
83
91
  emitTerminalSSEError,
84
92
  removeReqCloseListener,
93
+ mergePaginationConfig,
85
94
  mapError,
86
95
  HttpError,
87
96
  } from '../operationRuntime${ext}'
@@ -92,6 +101,7 @@ ${generateRouteConfigType(modelName, 'RequestHandler', guardShapesImport, import
92
101
  const _env = getEnv()
93
102
 
94
103
  const WRITE_STRATEGY: WriteStrategy = '${writeStrategy}'
104
+ const FIND_MANY_PAGINATED_MODE: FindManyPaginatedMode = '${findManyPaginatedMode}'
95
105
 
96
106
  const MODEL_FIELDS = ${JSON.stringify(fieldsMeta, null, 2)} as const
97
107
  const MODEL_ENUMS = ${JSON.stringify(enumsMeta, null, 2)} as const
@@ -100,6 +110,7 @@ type OperationConfigLike = {
100
110
  before?: RequestHandler[]
101
111
  after?: RequestHandler[]
102
112
  shape?: Record<string, unknown>
113
+ pagination?: Partial<PaginationConfig>
103
114
  progressive?: Record<string, ProgressiveVariantConfig>
104
115
  progressiveStages?: Record<string, ProgressiveStage<unknown>>
105
116
  }
@@ -112,7 +123,7 @@ type ExtendedRequest = Request & {
112
123
 
113
124
  type LocalsBag = {
114
125
  parsedQuery?: Record<string, unknown>
115
- routeConfig?: { pagination?: OperationContext['paginationConfig'] }
126
+ routeConfig?: { pagination?: PaginationConfig }
116
127
  guardShape?: Record<string, unknown>
117
128
  guardCaller?: string
118
129
  data?: unknown
@@ -195,6 +206,7 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(config: ${m
195
206
  guardShape: locals.guardShape,
196
207
  guardCaller: locals.guardCaller,
197
208
  paginationConfig: locals.routeConfig?.pagination,
209
+ findManyPaginatedMode: FIND_MANY_PAGINATED_MODE,
198
210
  }
199
211
  }
200
212
 
@@ -218,8 +230,9 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(config: ${m
218
230
  const setShape = (opConfig: OperationConfigLike): RequestHandler => {
219
231
  return (req, res, next) => {
220
232
  const locals = readLocals(res)
221
- if (config.pagination) {
222
- locals.routeConfig = { pagination: config.pagination }
233
+ const merged = mergePaginationConfig(config.pagination, opConfig.pagination)
234
+ if (merged) {
235
+ locals.routeConfig = { pagination: merged }
223
236
  }
224
237
  const headerName = config.guard?.variantHeader || 'x-api-variant'
225
238
  const headerValue = req.get(headerName)
@@ -2,7 +2,7 @@ import { DMMF } from '@prisma/generator-helper'
2
2
  import { generateRouteConfigType } from './generateRouteConfigType'
3
3
  import { ImportStyle } from '../utils/resolveImportStyle'
4
4
  import { importExt } from '../utils/importExt'
5
- import { WriteStrategy } from '../constants'
5
+ import { WriteStrategy, FindManyPaginatedMode } from '../constants'
6
6
 
7
7
  export function generateFastifyRouterFunction({
8
8
  model,
@@ -10,12 +10,14 @@ export function generateFastifyRouterFunction({
10
10
  guardShapesImport,
11
11
  importStyle,
12
12
  writeStrategy,
13
+ findManyPaginatedMode,
13
14
  }: {
14
15
  model: DMMF.Model
15
16
  enums: DMMF.DatamodelEnum[]
16
17
  guardShapesImport: string | null
17
18
  importStyle: ImportStyle
18
19
  writeStrategy: WriteStrategy
20
+ findManyPaginatedMode: FindManyPaginatedMode
19
21
  }): string {
20
22
  const ext = importExt(importStyle)
21
23
  const modelName = model.name
@@ -67,16 +69,23 @@ import {
67
69
  ${modelName}Count,
68
70
  ${modelName}GroupBy,
69
71
  } from './${modelName}Handlers${ext}'
70
- import type { RouteConfig, FastifyHookHandler, WriteStrategy } from '../routeConfig.target${ext}'
72
+ import type {
73
+ RouteConfig,
74
+ FastifyHookHandler,
75
+ WriteStrategy,
76
+ FindManyPaginatedMode,
77
+ PaginationConfig,
78
+ } from '../routeConfig.target${ext}'
71
79
  import { parseQueryParams } from '../parseQueryParams${ext}'
72
80
  import { sanitizeKeys, normalizePrefix, getEnv } from '../misc${ext}'
73
81
  import { buildModelOpenApi } from '../buildModelOpenApi${ext}'
74
- import { mapError, transformResult, HttpError, type OperationContext } from '../operationRuntime${ext}'
82
+ import { mapError, transformResult, mergePaginationConfig, HttpError, type OperationContext } from '../operationRuntime${ext}'
75
83
 
76
84
  ${generateRouteConfigType(modelName, 'FastifyHookHandler', guardShapesImport, importStyle, 'fastify')}
77
85
  const _env = getEnv()
78
86
 
79
87
  const WRITE_STRATEGY: WriteStrategy = '${writeStrategy}'
88
+ const FIND_MANY_PAGINATED_MODE: FindManyPaginatedMode = '${findManyPaginatedMode}'
80
89
 
81
90
  const MODEL_FIELDS = ${JSON.stringify(fieldsMeta, null, 2)} as const
82
91
 
@@ -86,6 +95,7 @@ type OperationConfigLike = {
86
95
  before?: FastifyHookHandler[]
87
96
  after?: FastifyHookHandler[]
88
97
  shape?: Record<string, unknown>
98
+ pagination?: Partial<PaginationConfig>
89
99
  }
90
100
 
91
101
  type FastifyExtended = FastifyRequest & {
@@ -93,7 +103,7 @@ type FastifyExtended = FastifyRequest & {
93
103
  postgres?: unknown
94
104
  sqlite?: unknown
95
105
  parsedQuery?: Record<string, unknown>
96
- routeConfig?: { pagination?: OperationContext['paginationConfig'] }
106
+ routeConfig?: { pagination?: PaginationConfig }
97
107
  guardShape?: Record<string, unknown>
98
108
  guardCaller?: string
99
109
  resultData?: unknown
@@ -139,9 +149,9 @@ function makeShapeHook(
139
149
  ): (request: FastifyRequest) => void {
140
150
  return (request: FastifyRequest) => {
141
151
  const fx = request as FastifyExtended
142
- const paginationConfig = (config as { pagination?: OperationContext['paginationConfig'] }).pagination
143
- if (paginationConfig) {
144
- fx.routeConfig = { pagination: paginationConfig }
152
+ const merged = mergePaginationConfig(config.pagination, opConfig.pagination)
153
+ if (merged) {
154
+ fx.routeConfig = { pagination: merged }
145
155
  }
146
156
  const headerName = (config.guard?.variantHeader || 'x-api-variant').toLowerCase()
147
157
  const headerValue = request.headers[headerName]
@@ -233,6 +243,10 @@ export async function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(
233
243
  }
234
244
  }
235
245
 
246
+ fastify.addHook('onRequest', async (request: FastifyRequest) => {
247
+ (request as FastifyExtended & { findManyPaginatedMode?: FindManyPaginatedMode }).findManyPaginatedMode = FIND_MANY_PAGINATED_MODE
248
+ })
249
+
236
250
  fastify.setErrorHandler((error: FastifyError, _request: FastifyRequest, reply: FastifyReply) => {
237
251
  const e = error as { status?: number; statusCode?: number; message?: string }
238
252
  const status = e.status ?? e.statusCode ?? 500