prisma-generator-express 1.45.1 → 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.
- package/dist/client/encodeQueryParams.js +4 -0
- package/dist/client/encodeQueryParams.js.map +1 -1
- package/dist/copy/misc.d.ts +4 -2
- package/dist/copy/misc.js +35 -24
- package/dist/copy/misc.js.map +1 -1
- package/dist/generators/generateFastifyHandler.js +2 -4
- package/dist/generators/generateFastifyHandler.js.map +1 -1
- package/dist/generators/generateHonoHandler.js +2 -4
- package/dist/generators/generateHonoHandler.js.map +1 -1
- package/dist/generators/generateImportPrismaStatement.d.ts +0 -1
- package/dist/generators/generateImportPrismaStatement.js +2 -20
- package/dist/generators/generateImportPrismaStatement.js.map +1 -1
- package/dist/generators/generateOperationCore.js +1 -1
- package/dist/generators/generateQueryBuilderHelper.js +9 -0
- package/dist/generators/generateQueryBuilderHelper.js.map +1 -1
- package/dist/generators/generateRelationMeta.js +0 -10
- package/dist/generators/generateRelationMeta.js.map +1 -1
- package/dist/generators/generateRouteConfigType.js +33 -12
- package/dist/generators/generateRouteConfigType.js.map +1 -1
- package/dist/generators/generateRouter.d.ts +0 -1
- package/dist/generators/generateRouter.js +75 -70
- package/dist/generators/generateRouter.js.map +1 -1
- package/dist/generators/generateRouterFastify.js +83 -89
- package/dist/generators/generateRouterFastify.js.map +1 -1
- package/dist/generators/generateRouterHono.js +257 -237
- package/dist/generators/generateRouterHono.js.map +1 -1
- package/dist/generators/generateUnifiedDocs.d.ts +2 -2
- package/dist/generators/generateUnifiedDocs.js +90 -252
- package/dist/generators/generateUnifiedDocs.js.map +1 -1
- package/dist/generators/generateUnifiedHandler.js +2 -4
- package/dist/generators/generateUnifiedHandler.js.map +1 -1
- package/dist/index.js +16 -8
- package/dist/index.js.map +1 -1
- package/dist/utils/copyFiles.js +3 -2
- package/dist/utils/copyFiles.js.map +1 -1
- package/dist/utils/strings.d.ts +0 -1
- package/dist/utils/strings.js +0 -9
- package/dist/utils/strings.js.map +1 -1
- package/package.json +1 -1
- package/src/client/encodeQueryParams.ts +7 -15
- package/src/copy/autoIncludePlanner.ts +4 -17
- package/src/copy/autoIncludeRuntime.ts +11 -19
- package/src/copy/buildModelOpenApi.ts +11 -14
- package/src/copy/docsRenderer.ts +8 -14
- package/src/copy/misc.ts +28 -23
- package/src/copy/operationRuntime.ts +61 -43
- package/src/copy/parseQueryParams.ts +5 -14
- package/src/copy/routeConfig.express.ts +24 -18
- package/src/copy/routeConfig.fastify.ts +1 -1
- package/src/copy/routeConfig.hono.ts +34 -6
- package/src/copy/routeConfig.ts +2 -2
- package/src/generators/generateFastifyHandler.ts +2 -5
- package/src/generators/generateHonoHandler.ts +2 -5
- package/src/generators/generateImportPrismaStatement.ts +3 -35
- package/src/generators/generateOperationCore.ts +1 -1
- package/src/generators/generateQueryBuilderHelper.ts +9 -0
- package/src/generators/generateRelationMeta.ts +0 -10
- package/src/generators/generateRouteConfigType.ts +34 -10
- package/src/generators/generateRouter.ts +75 -71
- package/src/generators/generateRouterFastify.ts +83 -89
- package/src/generators/generateRouterHono.ts +257 -237
- package/src/generators/generateUnifiedDocs.ts +89 -267
- package/src/generators/generateUnifiedHandler.ts +2 -4
- package/src/index.ts +45 -14
- package/src/utils/copyFiles.ts +2 -2
- package/src/utils/strings.ts +0 -8
- package/src/copy/createOutputValidatorMiddleware.ts +0 -47
- package/src/copy/createValidatorMiddleware.ts +0 -62
- 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 (!
|
|
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 (!
|
|
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 (
|
|
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 (
|
|
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 (!
|
|
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,
|
|
272
|
+
sendSSEError(res, mapError(err).message)
|
|
281
273
|
return
|
|
282
274
|
}
|
|
283
275
|
|
|
284
276
|
if (isClientGone()) return
|
|
285
277
|
|
|
286
|
-
if (rootResult === null || !
|
|
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 =
|
|
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,
|
|
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('{') ||
|
package/src/copy/docsRenderer.ts
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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.
|
|
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
|
|
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
|
|
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 (
|
|
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(
|
|
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 =
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
|
357
|
+
return results.length
|
|
343
358
|
}
|
|
344
359
|
|
|
345
|
-
|
|
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)
|
|
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))
|
|
360
|
-
|
|
361
|
-
const
|
|
362
|
-
|
|
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
|
|
365
|
-
|
|
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
|
|
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 {
|
|
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 {
|
|
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 {
|
|
523
|
+
try { clearInterval(keepaliveHandle) } catch {}
|
|
506
524
|
}
|
|
507
525
|
if (!res.writableEnded && !res.destroyed) {
|
|
508
|
-
try { res.end() } catch {
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
49
|
-
|
|
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<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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<
|
|
51
|
-
|
|
52
|
-
|
|
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<
|
|
19
|
+
) => Promise<unknown> | unknown
|
|
20
20
|
|
|
21
21
|
export type OperationConfig<TShape = Record<string, unknown>> =
|
|
22
22
|
BaseOperationConfig<FastifyHookHandler, TShape>
|