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.
- package/README.md +131 -12
- package/dist/constants.d.ts +1 -0
- package/dist/generators/generateFastifyHandler.js +3 -1
- package/dist/generators/generateFastifyHandler.js.map +1 -1
- package/dist/generators/generateHonoHandler.js +3 -1
- package/dist/generators/generateHonoHandler.js.map +1 -1
- package/dist/generators/generateOperationCore.d.ts +2 -1
- package/dist/generators/generateOperationCore.js +38 -36
- package/dist/generators/generateOperationCore.js.map +1 -1
- package/dist/generators/generateRouteConfigType.js +2 -1
- package/dist/generators/generateRouteConfigType.js.map +1 -1
- package/dist/generators/generateRouter.d.ts +3 -2
- package/dist/generators/generateRouter.js +16 -5
- package/dist/generators/generateRouter.js.map +1 -1
- package/dist/generators/generateRouterFastify.d.ts +3 -2
- package/dist/generators/generateRouterFastify.js +19 -7
- package/dist/generators/generateRouterFastify.js.map +1 -1
- package/dist/generators/generateRouterHono.d.ts +3 -2
- package/dist/generators/generateRouterHono.js +24 -14
- package/dist/generators/generateRouterHono.js.map +1 -1
- package/dist/index.js +20 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +3 -1
- package/src/copy/autoIncludeRuntime.ts +60 -35
- package/src/copy/docsRenderer.ts +5 -4
- package/src/copy/operationRuntime.ts +94 -9
- package/src/copy/routeConfig.express.ts +6 -0
- package/src/copy/routeConfig.fastify.ts +7 -1
- package/src/copy/routeConfig.hono.ts +8 -2
- package/src/copy/routeConfig.ts +21 -5
- package/src/generators/generateFastifyHandler.ts +3 -1
- package/src/generators/generateHonoHandler.ts +3 -1
- package/src/generators/generateOperationCore.ts +42 -37
- package/src/generators/generateRouteConfigType.ts +2 -2
- package/src/generators/generateRouter.ts +18 -5
- package/src/generators/generateRouterFastify.ts +21 -7
- package/src/generators/generateRouterHono.ts +26 -14
- 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
|
-
|
|
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?:
|
|
35
|
+
routeConfig?: { pagination?: PaginationConfig }
|
|
30
36
|
guardShape?: Record<string, unknown>
|
|
31
37
|
guardCaller?: string
|
|
32
38
|
resultData?: unknown
|
package/src/copy/routeConfig.ts
CHANGED
|
@@ -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
|
|
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
|
|
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 {
|
|
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?:
|
|
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
|
-
|
|
222
|
-
|
|
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 {
|
|
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?:
|
|
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
|
|
143
|
-
if (
|
|
144
|
-
fx.routeConfig = { pagination:
|
|
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
|