prisma-generator-express 1.45.0 → 1.46.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 (70) hide show
  1. package/README.md +155 -30
  2. package/dist/client/encodeQueryParams.js +4 -0
  3. package/dist/client/encodeQueryParams.js.map +1 -1
  4. package/dist/copy/misc.d.ts +4 -2
  5. package/dist/copy/misc.js +35 -24
  6. package/dist/copy/misc.js.map +1 -1
  7. package/dist/generators/generateFastifyHandler.js +2 -4
  8. package/dist/generators/generateFastifyHandler.js.map +1 -1
  9. package/dist/generators/generateHonoHandler.js +2 -4
  10. package/dist/generators/generateHonoHandler.js.map +1 -1
  11. package/dist/generators/generateImportPrismaStatement.d.ts +0 -1
  12. package/dist/generators/generateImportPrismaStatement.js +2 -20
  13. package/dist/generators/generateImportPrismaStatement.js.map +1 -1
  14. package/dist/generators/generateOperationCore.js +1 -1
  15. package/dist/generators/generateQueryBuilderHelper.js +9 -0
  16. package/dist/generators/generateQueryBuilderHelper.js.map +1 -1
  17. package/dist/generators/generateRelationMeta.js +0 -10
  18. package/dist/generators/generateRelationMeta.js.map +1 -1
  19. package/dist/generators/generateRouteConfigType.js +33 -12
  20. package/dist/generators/generateRouteConfigType.js.map +1 -1
  21. package/dist/generators/generateRouter.d.ts +0 -1
  22. package/dist/generators/generateRouter.js +75 -70
  23. package/dist/generators/generateRouter.js.map +1 -1
  24. package/dist/generators/generateRouterFastify.js +83 -89
  25. package/dist/generators/generateRouterFastify.js.map +1 -1
  26. package/dist/generators/generateRouterHono.js +257 -237
  27. package/dist/generators/generateRouterHono.js.map +1 -1
  28. package/dist/generators/generateUnifiedDocs.d.ts +2 -2
  29. package/dist/generators/generateUnifiedDocs.js +90 -252
  30. package/dist/generators/generateUnifiedDocs.js.map +1 -1
  31. package/dist/generators/generateUnifiedHandler.js +2 -4
  32. package/dist/generators/generateUnifiedHandler.js.map +1 -1
  33. package/dist/index.js +16 -8
  34. package/dist/index.js.map +1 -1
  35. package/dist/utils/copyFiles.js +3 -2
  36. package/dist/utils/copyFiles.js.map +1 -1
  37. package/dist/utils/strings.d.ts +0 -1
  38. package/dist/utils/strings.js +0 -9
  39. package/dist/utils/strings.js.map +1 -1
  40. package/package.json +1 -1
  41. package/src/client/encodeQueryParams.ts +7 -15
  42. package/src/copy/autoIncludePlanner.ts +4 -17
  43. package/src/copy/autoIncludeRuntime.ts +11 -19
  44. package/src/copy/buildModelOpenApi.ts +11 -14
  45. package/src/copy/docsRenderer.ts +8 -14
  46. package/src/copy/misc.ts +28 -23
  47. package/src/copy/operationRuntime.ts +61 -43
  48. package/src/copy/parseQueryParams.ts +5 -14
  49. package/src/copy/routeConfig.express.ts +24 -18
  50. package/src/copy/routeConfig.fastify.ts +1 -1
  51. package/src/copy/routeConfig.hono.ts +34 -6
  52. package/src/copy/routeConfig.ts +2 -2
  53. package/src/generators/generateFastifyHandler.ts +2 -5
  54. package/src/generators/generateHonoHandler.ts +2 -5
  55. package/src/generators/generateImportPrismaStatement.ts +3 -35
  56. package/src/generators/generateOperationCore.ts +1 -1
  57. package/src/generators/generateQueryBuilderHelper.ts +9 -0
  58. package/src/generators/generateRelationMeta.ts +0 -10
  59. package/src/generators/generateRouteConfigType.ts +34 -10
  60. package/src/generators/generateRouter.ts +75 -71
  61. package/src/generators/generateRouterFastify.ts +83 -89
  62. package/src/generators/generateRouterHono.ts +257 -237
  63. package/src/generators/generateUnifiedDocs.ts +89 -267
  64. package/src/generators/generateUnifiedHandler.ts +2 -4
  65. package/src/index.ts +45 -14
  66. package/src/utils/copyFiles.ts +2 -2
  67. package/src/utils/strings.ts +0 -8
  68. package/src/copy/createOutputValidatorMiddleware.ts +0 -47
  69. package/src/copy/createValidatorMiddleware.ts +0 -62
  70. package/src/copy/transformZod.ts +0 -139
@@ -1,20 +1,5 @@
1
1
  import { isObject } from '../copy/misc'
2
2
 
3
- /**
4
- * Frontend query encoder for prisma-generator-express
5
- *
6
- * Encodes complex Prisma query structures as JSON strings in query params.
7
- * Objects and arrays are JSON-stringified. Primitives are encoded directly.
8
- *
9
- * @example
10
- * const params = encodeQueryParams({
11
- * where: { OR: [{ status: 'active' }, { featured: true }] },
12
- * take: 10
13
- * })
14
- * // where=%7B%22OR%22%3A...&take=10
15
- * fetch(`/api/posts?${params}`)
16
- */
17
-
18
3
  function replacer(_key: string, value: unknown): unknown {
19
4
  if (typeof value === 'bigint') {
20
5
  return value.toString()
@@ -40,6 +25,13 @@ export const encodeQueryParams = (params: Record<string, unknown>): string => {
40
25
  continue
41
26
  }
42
27
 
28
+ if (typeof value === 'string') {
29
+ entries.push(
30
+ `${encodeURIComponent(key)}=${encodeURIComponent(JSON.stringify(value))}`,
31
+ )
32
+ continue
33
+ }
34
+
43
35
  if (Array.isArray(value) || isObject(value)) {
44
36
  entries.push(
45
37
  `${encodeURIComponent(key)}=${encodeURIComponent(JSON.stringify(value, replacer))}`,
@@ -1,3 +1,5 @@
1
+ import { isPlainObject } from './misc'
2
+
1
3
  export type ModelRelationDirection = 'parentOwnsFk' | 'childOwnsFk' | 'implicitM2M'
2
4
 
3
5
  export type ModelRelationField = {
@@ -14,7 +16,6 @@ export type ModelRelationMap = {
14
16
  name: string
15
17
  delegateKey: string
16
18
  scalarFields: string[]
17
- idFields: string[]
18
19
  relations: Record<string, ModelRelationField>
19
20
  }
20
21
 
@@ -47,23 +48,9 @@ export const DEFAULT_AUTO_INCLUDE_MAX_STAGES = 20
47
48
 
48
49
  const ALLOWED_TO_ONE_ARGS = new Set(['select', 'include', 'omit'])
49
50
  const ALLOWED_TO_MANY_ARGS = new Set([
50
- 'select',
51
- 'include',
52
- 'omit',
53
- 'where',
54
- 'orderBy',
55
- 'take',
56
- 'skip',
57
- 'cursor',
58
- 'distinct',
51
+ 'select', 'include', 'omit', 'where', 'orderBy', 'take', 'skip', 'cursor', 'distinct',
59
52
  ])
60
53
 
61
- function isPlainObject(value: unknown): value is Record<string, unknown> {
62
- if (value === null || typeof value !== 'object') return false
63
- if (Array.isArray(value)) return false
64
- return true
65
- }
66
-
67
54
  function isPubliclySelected(projection: Record<string, unknown>, field: string): boolean {
68
55
  return projection[field] === true
69
56
  }
@@ -224,7 +211,7 @@ function walk(
224
211
 
225
212
  for (const linkField of relation.parentLinkFields) {
226
213
  if (localOmit && localOmit[linkField] === true) {
227
- const where = parentPath ? parentPath + '.' : 'root '
214
+ const where = parentPath ? parentPath + '.' : 'root.'
228
215
  return { unsupportedReason: 'required parent link field omitted: ' + where + linkField }
229
216
  }
230
217
  }
@@ -12,9 +12,11 @@ import {
12
12
  setByPath,
13
13
  getDelegate,
14
14
  getExtendedClient,
15
+ mapError,
15
16
  type OperationContext,
16
17
  type PrismaDelegate,
17
18
  } from './operationRuntime'
19
+ import { isPlainObject } from './misc'
18
20
  import {
19
21
  planAutoInclude,
20
22
  type ModelRelationMap,
@@ -39,16 +41,12 @@ export type RunAutoIncludeOptions = {
39
41
  signal?: AbortSignal
40
42
  }
41
43
 
42
- function isObject(v: unknown): v is Record<string, unknown> {
43
- return v !== null && typeof v === 'object' && !Array.isArray(v)
44
- }
45
-
46
44
  function readPath(source: Record<string, unknown>, path: string): unknown {
47
45
  if (path === '') return source
48
46
  const parts = path.split('.')
49
47
  let cursor: unknown = source
50
48
  for (const part of parts) {
51
- if (!isObject(cursor)) return undefined
49
+ if (!isPlainObject(cursor)) return undefined
52
50
  cursor = cursor[part]
53
51
  }
54
52
  return cursor
@@ -77,7 +75,7 @@ function mergeWhere(
77
75
  userWhere: unknown,
78
76
  linkFilter: Record<string, unknown>,
79
77
  ): Record<string, unknown> {
80
- if (!isObject(userWhere) || Object.keys(userWhere).length === 0) return linkFilter
78
+ if (!isPlainObject(userWhere) || Object.keys(userWhere).length === 0) return linkFilter
81
79
  return { AND: [userWhere, linkFilter] }
82
80
  }
83
81
 
@@ -108,7 +106,7 @@ function buildPublicForStage(
108
106
  ): unknown {
109
107
  if (Array.isArray(result)) {
110
108
  return result.map((item) => {
111
- if (isObject(item)) {
109
+ if (isPlainObject(item)) {
112
110
  const copy: Record<string, unknown> = { ...item }
113
111
  stripInternalAtScope(copy, internalFieldPaths, scopePath)
114
112
  return copy
@@ -116,7 +114,7 @@ function buildPublicForStage(
116
114
  return item
117
115
  })
118
116
  }
119
- if (isObject(result)) {
117
+ if (isPlainObject(result)) {
120
118
  const copy: Record<string, unknown> = { ...result }
121
119
  stripInternalAtScope(copy, internalFieldPaths, scopePath)
122
120
  return copy
@@ -138,7 +136,7 @@ async function runOneStage(options: {
138
136
  if (isAborted()) return
139
137
 
140
138
  const parentRaw = readPath(internal, stage.parentPath)
141
- if (!isObject(parentRaw)) {
139
+ if (!isPlainObject(parentRaw)) {
142
140
  if (stage.parentPath !== '') {
143
141
  return
144
142
  }
@@ -270,20 +268,14 @@ export async function runAutoIncludeProgressive(
270
268
  rootResult = await rootDelegate[baseOp](plan.rootArgs)
271
269
  } catch (err) {
272
270
  if (isClientGone()) return
273
- const code = (err as { code?: string } | null)?.code
274
- const isOrThrow = baseOp === 'findUniqueOrThrow' || baseOp === 'findFirstOrThrow'
275
- if (isOrThrow && code === 'P2025') {
276
- sendSSEError(res, 'Record not found')
277
- return
278
- }
279
271
  console.error('[auto-progressive] root query failed:', err)
280
- sendSSEError(res, 'Root query failed')
272
+ sendSSEError(res, mapError(err).message)
281
273
  return
282
274
  }
283
275
 
284
276
  if (isClientGone()) return
285
277
 
286
- if (rootResult === null || !isObject(rootResult)) {
278
+ if (rootResult === null || !isPlainObject(rootResult)) {
287
279
  sendSSEResult(res, null)
288
280
  return
289
281
  }
@@ -330,7 +322,7 @@ export async function runAutoIncludeProgressive(
330
322
  } catch (err) {
331
323
  if (isAborted()) return
332
324
  console.error('[auto-progressive] stage failed:', stage.relationPath, err)
333
- stageErrorMessage = 'Could not load progressive response'
325
+ stageErrorMessage = mapError(err).message
334
326
  return
335
327
  }
336
328
  if (isAborted()) return
@@ -354,7 +346,7 @@ export async function runAutoIncludeProgressive(
354
346
  if (isClientGone()) return
355
347
  console.error('[auto-progressive] dispatch error:', err)
356
348
  if (!res.writableEnded && !res.destroyed) {
357
- sendSSEError(res, 'Internal server error')
349
+ sendSSEError(res, mapError(err).message)
358
350
  }
359
351
  } finally {
360
352
  endSSE(res, keepalive)
@@ -1,5 +1,6 @@
1
1
  import type { RouteConfig } from './routeConfig'
2
2
  import { OPERATION_DEFS, isOperationEnabled } from './operationDefinitions'
3
+ import { normalizePrefix, removeTrailingSlash } from './misc'
3
4
 
4
5
  type SchemaObject = {
5
6
  type?: string | string[]
@@ -103,20 +104,6 @@ function addErrorResponses(operation: any, codes: number[]): void {
103
104
  }
104
105
  }
105
106
 
106
- function normalizePrefix(p: string): string {
107
- if (!p) return ''
108
- let result = p
109
- if (!result.startsWith('/')) result = '/' + result
110
- while (result.length > 1 && result.endsWith('/')) result = result.slice(0, -1)
111
- if (result === '/') return ''
112
- return result
113
- }
114
-
115
- function removeTrailingSlash(path: string): string {
116
- if (path === '/') return ''
117
- return path.endsWith('/') ? path.slice(0, -1) : path
118
- }
119
-
120
107
  function queryParam(
121
108
  name: string,
122
109
  description: string,
@@ -1783,6 +1770,11 @@ function yamlEscapeValue(value: unknown, indent: number = 0): string {
1783
1770
  str === '.inf' ||
1784
1771
  str === '-.inf' ||
1785
1772
  str === '.nan' ||
1773
+ str === '-' ||
1774
+ str === '?' ||
1775
+ str.startsWith('- ') ||
1776
+ str.startsWith('? ') ||
1777
+ /^[!&*|>@`]/.test(str) ||
1786
1778
  str.includes(':') ||
1787
1779
  str.includes('#') ||
1788
1780
  str.includes('{') ||
@@ -1837,6 +1829,11 @@ function yamlEscapeKey(key: string): string {
1837
1829
  key === '.inf' ||
1838
1830
  key === '-.inf' ||
1839
1831
  key === '.nan' ||
1832
+ key === '-' ||
1833
+ key === '?' ||
1834
+ key.startsWith('- ') ||
1835
+ key.startsWith('? ') ||
1836
+ /^[!&*|>@`]/.test(key) ||
1840
1837
  key.includes(':') ||
1841
1838
  key.includes('#') ||
1842
1839
  key.includes('{') ||
@@ -1,7 +1,8 @@
1
1
  import type { RouteConfig } from './routeConfig'
2
2
  import { OPERATION_DEFS, isOperationEnabled, READ_OPERATION_NAMES } from './operationDefinitions'
3
+ import { getEnv, normalizePrefix } from './misc'
3
4
 
4
- const _env = typeof process !== 'undefined' && process.env ? process.env : {} as Record<string, string | undefined>
5
+ const _env = getEnv()
5
6
 
6
7
  export interface FieldMeta {
7
8
  name: string
@@ -527,16 +528,9 @@ function anchors(): { id: string; label: string }[] {
527
528
  ]
528
529
  }
529
530
 
530
- function normalizeExamplePrefix(p: string): string {
531
- if (!p) return ''
532
- let result = p.replace(/\/$/, '')
533
- if (result && !result.startsWith('/')) result = '/' + result
534
- return result
535
- }
536
-
537
531
  function buildExampleBasePath(modelName: string, config: DocsConfig): string {
538
532
  const prefixSource = config.specBasePath ?? config.customUrlPrefix ?? ''
539
- const prefix = normalizeExamplePrefix(prefixSource)
533
+ const prefix = normalizePrefix(prefixSource)
540
534
  const modelPrefix = config.addModelPrefix !== false ? '/' + modelName.toLowerCase() : ''
541
535
  return prefix + modelPrefix
542
536
  }
@@ -550,7 +544,7 @@ export function renderDocs(modelName: string, config: DocsConfig, ctx: DocsModel
550
544
  const title = config.docsTitle || modelName + ' API'
551
545
  const generatedAt = new Date().toISOString()
552
546
 
553
- const modelLower = modelName.charAt(0).toLowerCase() + modelName.slice(1)
547
+ const modelLower = modelName.toLowerCase()
554
548
  const exampleBasePath = buildExampleBasePath(modelName, config)
555
549
 
556
550
  const postReadsEnabled = !config.disablePostReads
@@ -1008,15 +1002,15 @@ export function renderDocs(modelName: string, config: DocsConfig, ctx: DocsModel
1008
1002
  '<strong>POST read endpoints:</strong> All read operations accept POST as an alternative transport. The request body is a plain JSON object with the same argument structure as the GET query params — no JSON-string encoding needed. POST reads use native JSON types (numbers, booleans, objects) directly. findMany POST read is at <span class="font-mono">/read</span>; all other read operations use the same path as their GET counterpart. Disable with <span class="font-mono">disablePostReads: true</span> in route config.',
1009
1003
  '<strong>Request body validation:</strong> All write endpoints require a JSON object body. Sending <span class="font-mono">null</span>, arrays, or non-object JSON values returns 400.',
1010
1004
  '<strong>Documentation in production:</strong> Docs endpoints are disabled by default when <span class="font-mono">NODE_ENV=production</span> or <span class="font-mono">DISABLE_OPENAPI=true</span>. To enable in production, set <span class="font-mono">disableOpenApi: false</span> in the route config.',
1011
- '<strong>Paginated query atomicity:</strong> findManyPaginated wraps data + count in a database transaction when available. If interactive transactions are not supported (e.g. some edge adapters), the queries run separately and data/total may be slightly inconsistent under concurrent writes.',
1012
- '<strong>Distinct count approximation:</strong> When findManyPaginated is used with distinct and the number of unique values exceeds 100,000, the total falls back to a non-distinct count which may overcount. The hasMore value is affected accordingly.',
1005
+ '<strong>Paginated query atomicity:</strong> findManyPaginated wraps data + count in a database transaction when available. If interactive transactions are not supported (e.g. some edge adapters), the queries run separately and data/total may be slightly inconsistent under concurrent writes. When a guard shape is configured, the data and count queries run in parallel without a transaction to keep the guard wrapper in scope, so atomicity is not guaranteed in that mode.',
1006
+ '<strong>Distinct count approximation:</strong> When findManyPaginated is used with distinct and the number of unique values exceeds 100,000, the total falls back to a non-distinct count which may overcount. When a guard shape is configured alongside distinct, the total also falls back to a non-distinct count to avoid imposing the public read shape on the internal counting query. The hasMore value is affected accordingly.',
1013
1007
  '<strong>Serialization:</strong> BigInt values are serialized as strings. Bytes/Buffer values are serialized as base64 strings. Decimal values are serialized as strings. DateTime values are serialized as ISO 8601 strings.',
1014
1008
  '<strong>Playground:</strong> The query playground embeds an iframe to a local prisma-query-builder-ui instance. It connects to your real database using the configured DATABASE_URL. It is disabled in production and when queryBuilder is set to false or queryBuilder.enabled is set to false.',
1015
- '<strong>Prototype pollution protection:</strong> All incoming JSON bodies and query parameters are sanitized to reject __proto__, constructor, and prototype keys.',
1009
+ '<strong>Prototype pollution protection:</strong> All incoming JSON bodies and query parameters are sanitized to strip __proto__, constructor, and prototype keys before reaching handlers.',
1016
1010
  '<strong>Batch operation safety:</strong> deleteMany, updateMany, and updateManyAndReturn require a where field in the request body. Requests without where are rejected with 400 to prevent accidental mass operations.',
1017
1011
  '<strong>Bulk write constraints:</strong> createMany, createManyAndReturn, updateMany, and updateManyAndReturn accept scalar-only data inputs. Nested relation writes are not supported in these operations.',
1018
1012
  '<strong>Provider compatibility:</strong> createManyAndReturn requires Prisma 5.14.0+ and is limited to PostgreSQL, CockroachDB, and SQLite. updateManyAndReturn requires Prisma 6.2.0+ with the same provider restrictions. skipDuplicates is not supported on all database providers.',
1019
- '<strong>omit compatibility:</strong> The omit parameter requires Prisma 5.13.0+ (preview) or 6.2.0+ (GA). On older Prisma versions, requests using omit will return 400.',
1013
+ '<strong>omit compatibility:</strong> The omit parameter requires Prisma 6.2.0+. On versions 6.0.x–6.1.x, requests using omit return 400.',
1020
1014
  ]
1021
1015
 
1022
1016
  const noUniqueFieldNote = '<div class="bg-gray-50 border border-gray-200 rounded-xl p-3 text-xs text-gray-500">This model has no unique or id fields suitable for a generated example. Use the unique constraint from your schema.</div>'
package/src/copy/misc.ts CHANGED
@@ -1,29 +1,14 @@
1
- export function isJsonString(str: string | unknown): boolean {
2
- if (typeof str !== 'string') {
3
- return false
4
- }
5
-
6
- try {
7
- JSON.parse(str)
8
- } catch (e: unknown) {
9
- return false
10
- }
11
- return true
12
- }
13
-
14
- export function safeJSONparse<T>(
15
- data: unknown,
16
- ): T | boolean | undefined | null {
17
- if (data === 'false') return false
18
- if (data === 'undefined') return undefined
19
- if (data === 'null') return null
20
- return isJsonString(data) ? JSON.parse(data as string) : data as any
21
- }
22
-
23
1
  export const isObject = (value: unknown): value is Record<string, unknown> => {
24
2
  return typeof value === 'object' && value !== null && !Array.isArray(value)
25
3
  }
26
4
 
5
+ export function isPlainObject(value: unknown): value is Record<string, unknown> {
6
+ if (value === null || typeof value !== 'object') return false
7
+ if (Array.isArray(value)) return false
8
+ const proto = Object.getPrototypeOf(value)
9
+ return proto === Object.prototype || proto === null
10
+ }
11
+
27
12
  const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype'])
28
13
 
29
14
  export function isSafeKey(key: string): boolean {
@@ -34,7 +19,7 @@ export function sanitizeKeys<T>(value: T): T {
34
19
  if (Array.isArray(value)) {
35
20
  return value.map(sanitizeKeys) as T
36
21
  }
37
- if (isObject(value)) {
22
+ if (isPlainObject(value)) {
38
23
  const result: Record<string, unknown> = {}
39
24
  for (const key of Object.keys(value)) {
40
25
  if (!isSafeKey(key)) continue
@@ -43,4 +28,24 @@ export function sanitizeKeys<T>(value: T): T {
43
28
  return result as T
44
29
  }
45
30
  return value
31
+ }
32
+
33
+ export function normalizePrefix(p: string): string {
34
+ if (!p) return ''
35
+ let result = p
36
+ if (!result.startsWith('/')) result = '/' + result
37
+ while (result.length > 1 && result.endsWith('/')) result = result.slice(0, -1)
38
+ if (result === '/') return ''
39
+ return result
40
+ }
41
+
42
+ export function removeTrailingSlash(path: string): string {
43
+ if (path === '/') return ''
44
+ return path.endsWith('/') ? path.slice(0, -1) : path
45
+ }
46
+
47
+ export function getEnv(): Record<string, string | undefined> {
48
+ return typeof process !== 'undefined' && process.env
49
+ ? process.env
50
+ : ({} as Record<string, string | undefined>)
46
51
  }
@@ -1,4 +1,4 @@
1
- import { sanitizeKeys } from './misc'
1
+ import { sanitizeKeys, isPlainObject } from './misc'
2
2
  import type {
3
3
  ProgressivePatch,
4
4
  ProgressiveStopResult,
@@ -264,7 +264,10 @@ export function assertGuard(
264
264
  delegate: PrismaDelegate,
265
265
  ): asserts delegate is PrismaDelegate & { guard: NonNullable<PrismaDelegate['guard']> } {
266
266
  if (typeof delegate.guard !== 'function') {
267
- throw new HttpError(500, 'Guard shapes require prisma-guard extension on PrismaClient.')
267
+ throw new HttpError(
268
+ 500,
269
+ 'Guard shapes require prisma-guard extension on PrismaClient. Install: npm install prisma-guard, then extend your client with guardExtension().',
270
+ )
268
271
  }
269
272
  }
270
273
 
@@ -318,6 +321,22 @@ export async function countForPagination(
318
321
  const effectiveLimit = distinctCountLimit ?? DISTINCT_COUNT_LIMIT
319
322
  const countShape = shape ? buildCountShape(shape) : undefined
320
323
 
324
+ const runCount = async (): Promise<number> => {
325
+ const countArgs: Record<string, unknown> = {}
326
+ if (query.where) countArgs.where = query.where
327
+ if (countShape) {
328
+ return (await (delegate.guard as NonNullable<PrismaDelegate['guard']>)(
329
+ countShape as Record<string, unknown>,
330
+ caller,
331
+ ).count(countArgs)) as number
332
+ }
333
+ return (await delegate.count(countArgs)) as number
334
+ }
335
+
336
+ if (hasDistinct && shape) {
337
+ return runCount()
338
+ }
339
+
321
340
  if (hasDistinct) {
322
341
  const selectField = distinctFields[0]
323
342
  const distinctArgs: Record<string, unknown> = {
@@ -326,63 +345,62 @@ export async function countForPagination(
326
345
  select: { [selectField]: true },
327
346
  take: effectiveLimit + 1,
328
347
  }
329
- const results = shape
330
- ? await (delegate.guard as NonNullable<PrismaDelegate['guard']>)(shape, caller).findMany(distinctArgs)
331
- : await delegate.findMany(distinctArgs)
332
- const resultArray = results as unknown[]
333
- if (resultArray.length > effectiveLimit) {
334
- console.warn('[prisma-generator-express] Distinct count exceeds ' + effectiveLimit + ', falling back to approximate total')
335
- const countArgs: Record<string, unknown> = {}
336
- if (query.where) countArgs.where = query.where
337
- const total = countShape
338
- ? await (delegate.guard as NonNullable<PrismaDelegate['guard']>)(countShape as Record<string, unknown>, caller).count(countArgs)
339
- : await delegate.count(countArgs)
340
- return total as number
348
+ const results = (await delegate.findMany(distinctArgs)) as unknown[]
349
+ if (results.length > effectiveLimit) {
350
+ console.warn(
351
+ '[prisma-generator-express] Distinct count exceeds ' +
352
+ effectiveLimit +
353
+ ', falling back to approximate total',
354
+ )
355
+ return runCount()
341
356
  }
342
- return resultArray.length
357
+ return results.length
343
358
  }
344
359
 
345
- const countArgs: Record<string, unknown> = {}
346
- if (query.where) countArgs.where = query.where
347
- const total = countShape
348
- ? await (delegate.guard as NonNullable<PrismaDelegate['guard']>)(countShape as Record<string, unknown>, caller).count(countArgs)
349
- : await delegate.count(countArgs)
350
- return total as number
360
+ return runCount()
351
361
  }
352
362
 
353
363
  export function transformResult(value: unknown): unknown {
354
364
  if (value === null || value === undefined) return value
355
365
  if (typeof value === 'bigint') return value.toString()
356
366
  if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value)) return value.toString('base64')
357
- if (value instanceof Uint8Array) return Buffer.from(value).toString('base64')
367
+ if (typeof Buffer !== 'undefined' && value instanceof Uint8Array) {
368
+ return Buffer.from(value).toString('base64')
369
+ }
358
370
  if (value instanceof Date) return value
359
- if (Array.isArray(value)) return value.map(transformResult)
360
- if (typeof value === 'object') {
361
- const proto = Object.getPrototypeOf(value)
362
- if (proto !== Object.prototype && proto !== null) return value
371
+ if (Array.isArray(value)) {
372
+ let changed = false
373
+ const out: unknown[] = new Array(value.length)
374
+ for (let i = 0; i < value.length; i++) {
375
+ const t = transformResult(value[i])
376
+ if (t !== value[i]) changed = true
377
+ out[i] = t
378
+ }
379
+ return changed ? out : value
380
+ }
381
+ if (isPlainObject(value)) {
382
+ let changed = false
363
383
  const out: Record<string, unknown> = {}
364
- for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
365
- out[k] = transformResult(v)
384
+ for (const [k, v] of Object.entries(value)) {
385
+ const t = transformResult(v)
386
+ if (t !== v) changed = true
387
+ out[k] = t
366
388
  }
367
- return out
389
+ return changed ? out : value
368
390
  }
369
391
  return value
370
392
  }
371
393
 
372
394
  export function acceptsEventStream(accept: string | undefined): boolean {
373
395
  if (!accept) return false
374
- return accept.toLowerCase().includes('text/event-stream')
396
+ return accept
397
+ .toLowerCase()
398
+ .split(',')
399
+ .some((entry) => entry.split(';')[0].trim() === 'text/event-stream')
375
400
  }
376
401
 
377
402
  const UNSAFE_PATH_SEGMENTS = new Set(['__proto__', 'constructor', 'prototype'])
378
403
 
379
- function isPlainObject(value: unknown): value is Record<string, unknown> {
380
- if (value === null || typeof value !== 'object') return false
381
- if (Array.isArray(value)) return false
382
- const proto = Object.getPrototypeOf(value)
383
- return proto === Object.prototype || proto === null
384
- }
385
-
386
404
  export function setByPath(target: Record<string, unknown>, path: string, value: unknown): boolean {
387
405
  const parts = path.split('.')
388
406
  if (parts.length === 0) return false
@@ -445,7 +463,7 @@ export function initSSE(res: SseWritable): void {
445
463
 
446
464
  export function flushSSE(res: SseWritable): void {
447
465
  if (typeof res.flush === 'function') {
448
- try { res.flush() } catch { /* ignore */ }
466
+ try { res.flush() } catch {}
449
467
  }
450
468
  }
451
469
 
@@ -493,7 +511,7 @@ export function startSSEKeepalive(res: SseWritable, intervalMs: number = 15000):
493
511
  try {
494
512
  res.write(': keepalive\n\n')
495
513
  flushSSE(res)
496
- } catch { /* ignore */ }
514
+ } catch {}
497
515
  }, intervalMs)
498
516
  const maybeUnref = (handle as unknown as { unref?: () => void }).unref
499
517
  if (typeof maybeUnref === 'function') maybeUnref.call(handle)
@@ -502,10 +520,10 @@ export function startSSEKeepalive(res: SseWritable, intervalMs: number = 15000):
502
520
 
503
521
  export function endSSE(res: SseWritable, keepaliveHandle: IntervalHandle | null): void {
504
522
  if (keepaliveHandle) {
505
- try { clearInterval(keepaliveHandle) } catch { /* ignore */ }
523
+ try { clearInterval(keepaliveHandle) } catch {}
506
524
  }
507
525
  if (!res.writableEnded && !res.destroyed) {
508
- try { res.end() } catch { /* ignore */ }
526
+ try { res.end() } catch {}
509
527
  }
510
528
  }
511
529
 
@@ -539,7 +557,7 @@ export async function runSingleResultSSE(options: RunSingleResultSSEOptions): Pr
539
557
  } catch (err) {
540
558
  console.error('[progressive] single-result error:', err)
541
559
  if (!res.writableEnded && !res.destroyed) {
542
- sendSSEError(res, 'Internal server error')
560
+ sendSSEError(res, mapError(err).message)
543
561
  }
544
562
  } finally {
545
563
  endSSE(res, keepalive)
@@ -605,7 +623,7 @@ export async function runProgressiveEndpoint(options: RunProgressiveOptions): Pr
605
623
  } catch (err) {
606
624
  console.error('[progressive] stage error:', err)
607
625
  if (!res.writableEnded && !res.destroyed) {
608
- sendSSEError(res, 'Could not load progressive response')
626
+ sendSSEError(res, mapError(err).message)
609
627
  }
610
628
  } finally {
611
629
  removeReqCloseListener(req, onClose)
@@ -8,16 +8,14 @@ type QueryParams =
8
8
  | undefined
9
9
 
10
10
  const NUMERIC_KEYS = new Set(['take', 'skip'])
11
-
12
11
  const INTEGER_RE = /^-?\d+$/
13
12
 
14
13
  const parseQueryValue = (value: string, key?: string): unknown => {
15
14
  if (value.startsWith('{') || value.startsWith('[') || value.startsWith('"')) {
16
15
  try {
17
16
  const parsed = JSON.parse(value)
18
- return parseQueryParams(sanitizeKeys(parsed) as QueryParams)
17
+ return sanitizeKeys(parsed)
19
18
  } catch {
20
- // fall through
21
19
  }
22
20
  }
23
21
  if (value === 'true') return true
@@ -30,14 +28,10 @@ const parseQueryValue = (value: string, key?: string): unknown => {
30
28
  }
31
29
 
32
30
  export const parseQueryParams = (params: QueryParams): unknown => {
33
- if (typeof params === 'string') {
34
- return parseQueryValue(params)
35
- }
31
+ if (typeof params === 'string') return parseQueryValue(params)
36
32
  if (Array.isArray(params)) {
37
33
  return params.map((item) =>
38
- typeof item === 'string'
39
- ? parseQueryValue(item)
40
- : parseQueryParams(item as QueryParams),
34
+ typeof item === 'string' ? parseQueryValue(item) : sanitizeKeys(item),
41
35
  )
42
36
  }
43
37
  if (isObject(params)) {
@@ -45,11 +39,8 @@ export const parseQueryParams = (params: QueryParams): unknown => {
45
39
  for (const key of Object.keys(params)) {
46
40
  if (!isSafeKey(key)) continue
47
41
  const raw = params[key]
48
- if (typeof raw === 'string') {
49
- parsedParams[key] = parseQueryValue(raw, key)
50
- } else {
51
- parsedParams[key] = parseQueryParams(raw as QueryParams)
52
- }
42
+ parsedParams[key] =
43
+ typeof raw === 'string' ? parseQueryValue(raw, key) : sanitizeKeys(raw)
53
44
  }
54
45
  return parsedParams
55
46
  }
@@ -29,24 +29,30 @@ export type {
29
29
  export type OperationConfig<TShape = Record<string, unknown>> =
30
30
  BaseOperationConfig<RequestHandler, TShape>
31
31
 
32
- export type ReadOperationConfig<TShape = Record<string, unknown>, TCtx = unknown> =
33
- BaseOperationConfig<RequestHandler, TShape> & {
34
- progressive?: Record<string, ProgressiveVariantConfig>
35
- progressiveStages?: Record<string, ProgressiveStage<TCtx>>
36
- }
32
+ export type ReadOperationConfig<
33
+ TShape = Record<string, unknown>,
34
+ TCtx = unknown,
35
+ TPrisma = any,
36
+ > = BaseOperationConfig<RequestHandler, TShape> & {
37
+ progressive?: Record<string, ProgressiveVariantConfig>
38
+ progressiveStages?: Record<string, ProgressiveStage<TCtx, TPrisma>>
39
+ }
37
40
 
38
- type ReadOperationOverrides<TShape, TCtx> = {
39
- findFirst?: ReadOperationConfig<TShape, TCtx>
40
- findFirstOrThrow?: ReadOperationConfig<TShape, TCtx>
41
- findUnique?: ReadOperationConfig<TShape, TCtx>
42
- findUniqueOrThrow?: ReadOperationConfig<TShape, TCtx>
43
- findMany?: ReadOperationConfig<TShape, TCtx>
44
- findManyPaginated?: ReadOperationConfig<TShape, TCtx>
45
- count?: ReadOperationConfig<TShape, TCtx>
46
- aggregate?: ReadOperationConfig<TShape, TCtx>
47
- groupBy?: ReadOperationConfig<TShape, TCtx>
41
+ type ReadOperationOverrides<TShape, TCtx, TPrisma> = {
42
+ findFirst?: ReadOperationConfig<TShape, TCtx, TPrisma>
43
+ findFirstOrThrow?: ReadOperationConfig<TShape, TCtx, TPrisma>
44
+ findUnique?: ReadOperationConfig<TShape, TCtx, TPrisma>
45
+ findUniqueOrThrow?: ReadOperationConfig<TShape, TCtx, TPrisma>
46
+ findMany?: ReadOperationConfig<TShape, TCtx, TPrisma>
47
+ findManyPaginated?: ReadOperationConfig<TShape, TCtx, TPrisma>
48
+ count?: ReadOperationConfig<TShape, TCtx, TPrisma>
49
+ aggregate?: ReadOperationConfig<TShape, TCtx, TPrisma>
50
+ groupBy?: ReadOperationConfig<TShape, TCtx, TPrisma>
48
51
  }
49
52
 
50
- export type RouteConfig<TShape = Record<string, unknown>, TCtx = unknown> =
51
- BaseRouteConfig<RequestHandler, Request, TShape, TCtx> &
52
- ReadOperationOverrides<TShape, TCtx>
53
+ export type RouteConfig<
54
+ TShape = Record<string, unknown>,
55
+ TCtx = unknown,
56
+ TPrisma = any,
57
+ > = BaseRouteConfig<RequestHandler, Request, TShape, TCtx> &
58
+ ReadOperationOverrides<TShape, TCtx, TPrisma>
@@ -16,7 +16,7 @@ export type {
16
16
  export type FastifyHookHandler = (
17
17
  request: FastifyRequest,
18
18
  reply: FastifyReply,
19
- ) => Promise<void> | void
19
+ ) => Promise<unknown> | unknown
20
20
 
21
21
  export type OperationConfig<TShape = Record<string, unknown>> =
22
22
  BaseOperationConfig<FastifyHookHandler, TShape>