prisma-generator-express 1.52.0 → 1.53.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/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Running `npx prisma generate` produces:
|
|
|
12
12
|
- Handler functions for all Prisma operations (findMany, create, update, delete, etc.)
|
|
13
13
|
- Router generator with middleware support (before/after hooks per operation)
|
|
14
14
|
- POST read endpoints for all read operations (for complex queries exceeding URL length limits)
|
|
15
|
-
- Express-only progressive read streaming over Server-Sent Events (SSE), using manual stages or auto-include splitting for supported relation reads
|
|
15
|
+
- Express-only progressive read streaming over Server-Sent Events (SSE), using manual stages or auto-include splitting for supported relation reads, including deep `findMany` / `findManyPaginated` auto-include paths
|
|
16
16
|
- Express-only standalone materialized view router for read-only access to registered PostgreSQL materialized views
|
|
17
17
|
- OpenAPI 3.1 spec (JSON and YAML endpoints registered automatically per router)
|
|
18
18
|
- Documentation helpers for contract view and Scalar UI (require manual mounting)
|
|
@@ -74,7 +74,7 @@ Some operations require newer versions:
|
|
|
74
74
|
|
|
75
75
|
The Hono target v1 is tested on Node.js runtimes only. See [Cloudflare Workers and edge runtimes](#cloudflare-workers-and-edge-runtimes).
|
|
76
76
|
|
|
77
|
-
Progressive Endpoint Composition over Server-Sent Events is currently supported by the Express target only. Express supports both manual staged streaming and auto-include streaming for supported relation reads, including single-record reads and
|
|
77
|
+
Progressive Endpoint Composition over Server-Sent Events is currently supported by the Express target only. Express supports both manual staged streaming and auto-include streaming for supported relation reads, including single-record reads and deep `findMany` / `findManyPaginated` reads within the configured planner limits. Fastify and Hono continue to support normal JSON read and write routes.
|
|
78
78
|
|
|
79
79
|
### Database provider support
|
|
80
80
|
|
|
@@ -1562,7 +1562,7 @@ Progressive SSE has two modes:
|
|
|
1562
1562
|
|
|
1563
1563
|
Manual mode is explicit staged data loading. You define stages yourself and each stage decides what query to run and which field path to patch.
|
|
1564
1564
|
|
|
1565
|
-
Auto-include mode is generated relation loading. The router keeps the normal GET endpoint, runs the root query first, then loads supported included relations as separate follow-up queries. For single-record reads, relation paths are streamed as field patches. For `findMany`,
|
|
1565
|
+
Auto-include mode is generated relation loading. The router keeps the normal GET endpoint, runs the root query first, then loads supported included relations as separate follow-up queries. For single-record reads, relation paths are streamed as field patches. For `findMany` and `findManyPaginated`, root rows are streamed first, direct root relation stages are streamed as index-aligned relation batches, and deeper nested relation stages are merged into the final result.
|
|
1566
1566
|
|
|
1567
1567
|
### Request format
|
|
1568
1568
|
|
|
@@ -1601,8 +1601,9 @@ Auto-include progressive SSE supports these Express GET read operations:
|
|
|
1601
1601
|
- `findFirst`
|
|
1602
1602
|
- `findFirstOrThrow`
|
|
1603
1603
|
- `findMany`
|
|
1604
|
+
- `findManyPaginated`
|
|
1604
1605
|
|
|
1605
|
-
`findMany` support
|
|
1606
|
+
`findMany` and `findManyPaginated` support deep relation loading by flattening parent rows at each relation path, with fallback cases listed in [Auto-include behavior and limits](#auto-include-behavior-and-limits).
|
|
1606
1607
|
|
|
1607
1608
|
If auto-include is configured on an unsupported operation, the router either falls back to single-result SSE or sends an SSE error depending on `fallback`.
|
|
1608
1609
|
|
|
@@ -1630,13 +1631,13 @@ Nested field event:
|
|
|
1630
1631
|
{ "type": "field", "key": "profile.appliedTo", "value": [] }
|
|
1631
1632
|
```
|
|
1632
1633
|
|
|
1633
|
-
Root array event for `findMany` auto-include:
|
|
1634
|
+
Root array event for `findMany` / `findManyPaginated` auto-include:
|
|
1634
1635
|
|
|
1635
1636
|
```json
|
|
1636
1637
|
{ "type": "rootArray", "data": [{ "id": "user-1" }, { "id": "user-2" }] }
|
|
1637
1638
|
```
|
|
1638
1639
|
|
|
1639
|
-
Relation batch event for `findMany` auto-include:
|
|
1640
|
+
Relation batch event for a direct root relation in `findMany` / `findManyPaginated` auto-include:
|
|
1640
1641
|
|
|
1641
1642
|
```json
|
|
1642
1643
|
{ "type": "relationBatch", "relationPath": "profile", "values": [{ "id": "profile-1" }, null] }
|
|
@@ -1656,7 +1657,9 @@ Error event:
|
|
|
1656
1657
|
|
|
1657
1658
|
For single-record progressive responses, the final `result.data` is the accumulated object built from all applied patches, unless a manual stage returns a stop result.
|
|
1658
1659
|
|
|
1659
|
-
For `findMany` auto-include responses, `rootArray.data` is the source of truth for root row identity and order. Each `relationBatch.values` array is index-aligned with `rootArray.data`, so `values[i]` belongs to `rootArray.data[i]`. The terminal `result.data` is the fully merged array.
|
|
1660
|
+
For `findMany` auto-include responses, `rootArray.data` is the source of truth for root row identity and order. Each depth-1 `relationBatch.values` array is index-aligned with `rootArray.data`, so `values[i]` belongs to `rootArray.data[i]`. Deeper nested stages emit progress events but do not emit relation batches, because their flattened parent indexes are not root-row indexes. The terminal `result.data` is the fully merged array.
|
|
1661
|
+
|
|
1662
|
+
For `findManyPaginated` auto-include responses, `pageMeta` is sent before `rootArray`. The terminal `result.data` has the normal paginated shape: `{ data, total, hasMore }`.
|
|
1660
1663
|
|
|
1661
1664
|
### Manual staged mode
|
|
1662
1665
|
|
|
@@ -1923,7 +1926,7 @@ On single-record reads, auto-include sends root scalar fields first, then sends
|
|
|
1923
1926
|
|
|
1924
1927
|
The final `result` event contains the assembled object.
|
|
1925
1928
|
|
|
1926
|
-
For `findMany`, auto-include sends the root rows first, then sends one relation batch event
|
|
1929
|
+
For `findMany`, auto-include sends the root rows first, then sends one relation batch event for each supported direct root relation stage. Nested relation stages emit progress and are included in the final result:
|
|
1927
1930
|
|
|
1928
1931
|
```ts
|
|
1929
1932
|
const listConfig = {
|
|
@@ -1963,7 +1966,7 @@ const response = await fetch(`/user?${params}`, {
|
|
|
1963
1966
|
})
|
|
1964
1967
|
```
|
|
1965
1968
|
|
|
1966
|
-
Example `findMany` auto-include event sequence:
|
|
1969
|
+
Example shallow `findMany` auto-include event sequence:
|
|
1967
1970
|
|
|
1968
1971
|
```json
|
|
1969
1972
|
{ "type": "rootArray", "data": [{ "id": "user-1" }, { "id": "user-2" }] }
|
|
@@ -1977,6 +1980,64 @@ Example `findMany` auto-include event sequence:
|
|
|
1977
1980
|
{ "type": "result", "data": [{ "id": "user-1", "profile": { "id": "profile-1", "displayName": "Alice" } }, { "id": "user-2", "profile": null }] }
|
|
1978
1981
|
```
|
|
1979
1982
|
|
|
1983
|
+
Deep `findMany` and `findManyPaginated` requests use the same planner. Nested stages are loaded by flattening the parent rows at each path:
|
|
1984
|
+
|
|
1985
|
+
```ts
|
|
1986
|
+
const params = encodeQueryParams({
|
|
1987
|
+
take: 20,
|
|
1988
|
+
include: {
|
|
1989
|
+
companies: {
|
|
1990
|
+
include: {
|
|
1991
|
+
users: {
|
|
1992
|
+
include: {
|
|
1993
|
+
profile: {
|
|
1994
|
+
select: {
|
|
1995
|
+
id: true,
|
|
1996
|
+
displayName: true,
|
|
1997
|
+
},
|
|
1998
|
+
},
|
|
1999
|
+
},
|
|
2000
|
+
},
|
|
2001
|
+
},
|
|
2002
|
+
},
|
|
2003
|
+
},
|
|
2004
|
+
})
|
|
2005
|
+
|
|
2006
|
+
const response = await fetch(`/organization/paginated?${params}`, {
|
|
2007
|
+
headers: {
|
|
2008
|
+
Accept: 'text/event-stream',
|
|
2009
|
+
'x-api-variant': 'list',
|
|
2010
|
+
},
|
|
2011
|
+
})
|
|
2012
|
+
```
|
|
2013
|
+
|
|
2014
|
+
Example deep event sequence:
|
|
2015
|
+
|
|
2016
|
+
```json
|
|
2017
|
+
{ "type": "pageMeta", "total": 120, "hasMore": true }
|
|
2018
|
+
```
|
|
2019
|
+
|
|
2020
|
+
```json
|
|
2021
|
+
{ "type": "rootArray", "data": [{ "id": "org-1" }, { "id": "org-2" }] }
|
|
2022
|
+
```
|
|
2023
|
+
|
|
2024
|
+
```json
|
|
2025
|
+
{ "type": "relationBatch", "relationPath": "companies", "values": [[{ "id": "company-1" }], []] }
|
|
2026
|
+
```
|
|
2027
|
+
|
|
2028
|
+
```json
|
|
2029
|
+
{ "type": "progress", "stage": "companies.users", "completed": 2, "total": 3 }
|
|
2030
|
+
```
|
|
2031
|
+
|
|
2032
|
+
```json
|
|
2033
|
+
{ "type": "progress", "stage": "companies.users.profile", "completed": 3, "total": 3 }
|
|
2034
|
+
```
|
|
2035
|
+
|
|
2036
|
+
```json
|
|
2037
|
+
{ "type": "result", "data": { "data": [{ "id": "org-1", "companies": [{ "id": "company-1", "users": [{ "id": "user-1", "profile": { "id": "profile-1", "displayName": "Alice" } }] }] }, { "id": "org-2", "companies": [] }], "total": 120, "hasMore": true } }
|
|
2038
|
+
```
|
|
2039
|
+
|
|
2040
|
+
|
|
1980
2041
|
### Auto-include behavior and limits
|
|
1981
2042
|
|
|
1982
2043
|
Auto-include is designed for supported Prisma `include` and relation `select` trees on reads.
|
|
@@ -1988,22 +2049,28 @@ Supported root operations:
|
|
|
1988
2049
|
- `findFirst`
|
|
1989
2050
|
- `findFirstOrThrow`
|
|
1990
2051
|
- `findMany`
|
|
2052
|
+
- `findManyPaginated`
|
|
1991
2053
|
|
|
1992
2054
|
Supported single-record relation shapes:
|
|
1993
2055
|
|
|
1994
2056
|
- direct to-one relation includes/selects
|
|
1995
2057
|
- direct to-many relation includes/selects
|
|
1996
2058
|
- to-many relation args such as `where`, `orderBy`, `take`, `skip`, `cursor`, and `distinct`
|
|
1997
|
-
- nested
|
|
2059
|
+
- nested relation loading through to-one parents
|
|
2060
|
+
|
|
2061
|
+
Single-record auto-include falls back when a nested stage crosses a to-many parent. Direct to-many loading is still supported, but nested loading under that array is not handled by the single-record progressive runtime.
|
|
1998
2062
|
|
|
1999
|
-
Supported `findMany` relation shapes:
|
|
2063
|
+
Supported `findMany` and `findManyPaginated` relation shapes:
|
|
2000
2064
|
|
|
2001
|
-
- direct
|
|
2002
|
-
- direct
|
|
2065
|
+
- direct and nested to-one relation includes/selects
|
|
2066
|
+
- direct and nested to-many relation includes/selects
|
|
2003
2067
|
- relation-level `where` and `orderBy`
|
|
2004
2068
|
- single-column link fields only
|
|
2069
|
+
- nested depth up to the configured planner limit
|
|
2005
2070
|
|
|
2006
|
-
`findMany`
|
|
2071
|
+
For `findMany` and `findManyPaginated`, each stage loads children with a batched query over the flattened parent rows at that stage's `parentPath`. Direct root stages can stream `relationBatch` events. Deeper stages are merged into the server-side result and visible in the terminal `result` event.
|
|
2072
|
+
|
|
2073
|
+
`findMany` and `findManyPaginated` auto-include apply configured pagination limits to the root query before loading relation batches. If the client omits `take`, `pagination.defaultLimit` is applied when configured. If the client sends a large `take`, `pagination.maxLimit` is enforced before the root query runs.
|
|
2007
2074
|
|
|
2008
2075
|
Current MVP fallback cases include:
|
|
2009
2076
|
|
|
@@ -2013,13 +2080,12 @@ Current MVP fallback cases include:
|
|
|
2013
2080
|
- `select` and `omit` at the same level
|
|
2014
2081
|
- relation filters/order/cursor in the root query
|
|
2015
2082
|
- relation filters/order/cursor inside staged relation queries when unsupported
|
|
2016
|
-
- nested relation loading through a to-many parent
|
|
2017
2083
|
- omitted required link fields needed to stitch parent and child records
|
|
2018
2084
|
- planner limits for maximum depth or stage count
|
|
2019
|
-
-
|
|
2020
|
-
- `findMany`
|
|
2021
|
-
- `findMany` to-many relation stages using `take`, `skip`, `cursor`, or `distinct`
|
|
2022
|
-
- `
|
|
2085
|
+
- single-record nested relation loading through a to-many parent
|
|
2086
|
+
- `findMany` / `findManyPaginated` relation stages with composite link fields
|
|
2087
|
+
- `findMany` / `findManyPaginated` to-many relation stages using `take`, `skip`, `cursor`, or `distinct`
|
|
2088
|
+
- `createManyAndReturn` and `updateManyAndReturn`
|
|
2023
2089
|
|
|
2024
2090
|
When fallback happens:
|
|
2025
2091
|
|
|
@@ -2028,7 +2094,7 @@ When fallback happens:
|
|
|
2028
2094
|
|
|
2029
2095
|
If `fallback` is omitted, the default behavior is equivalent to `'singleResult'`.
|
|
2030
2096
|
|
|
2031
|
-
`findMany` auto-include
|
|
2097
|
+
`findMany` and `findManyPaginated` auto-include use a batched relation loading strategy. They do not preserve Prisma's per-parent semantics for to-many `take`, `skip`, `cursor`, or `distinct`, so those cases fall back instead of returning silently different data.
|
|
2032
2098
|
|
|
2033
2099
|
Auto-include does not require `resolveContext` or `progressiveStages`.
|
|
2034
2100
|
|
|
@@ -2118,6 +2184,8 @@ For React Query, include the variant and mode in the query key:
|
|
|
2118
2184
|
|
|
2119
2185
|
Do not reuse the same query key as the JSON endpoint because the same URL can return different shapes depending on `x-api-variant`.
|
|
2120
2186
|
|
|
2187
|
+
For deep `findMany` / `findManyPaginated` auto-include, consume the final `result` event as the authoritative nested payload. Only direct root relation stages emit `relationBatch` events.
|
|
2188
|
+
|
|
2121
2189
|
### Runtime notes
|
|
2122
2190
|
|
|
2123
2191
|
The SSE response sets:
|
|
@@ -237,7 +237,7 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(config: ${m
|
|
|
237
237
|
const isAutoIncludeReadable =
|
|
238
238
|
baseOp === 'findUnique' || baseOp === 'findUniqueOrThrow' ||
|
|
239
239
|
baseOp === 'findFirst' || baseOp === 'findFirstOrThrow' ||
|
|
240
|
-
baseOp === 'findMany'
|
|
240
|
+
baseOp === 'findMany' || baseOp === 'findManyPaginated'
|
|
241
241
|
|
|
242
242
|
if (!isAutoIncludeReadable) {
|
|
243
243
|
if (progressiveConfig.fallback === 'error') {
|
|
@@ -263,7 +263,7 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(config: ${m
|
|
|
263
263
|
res,
|
|
264
264
|
ctx,
|
|
265
265
|
args,
|
|
266
|
-
baseOp: baseOp as 'findUnique' | 'findUniqueOrThrow' | 'findFirst' | 'findFirstOrThrow' | 'findMany',
|
|
266
|
+
baseOp: baseOp as 'findUnique' | 'findUniqueOrThrow' | 'findFirst' | 'findFirstOrThrow' | 'findMany' | 'findManyPaginated',
|
|
267
267
|
modelName: '${modelName}',
|
|
268
268
|
delegateKey: '${delegateKey}',
|
|
269
269
|
models: relationModels,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prisma-generator-express",
|
|
3
3
|
"description": "Prisma generator for Express, Fastify, and Hono CRUD APIs with OpenAPI documentation",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.53.0",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"license": "MIT",
|
|
@@ -269,15 +269,9 @@ function walk(
|
|
|
269
269
|
isPlainObject(relationArgs.omit)
|
|
270
270
|
|
|
271
271
|
if (hasNestedProjection) {
|
|
272
|
-
const stagesBeforeRecursion = ctx.stages.length
|
|
273
|
-
|
|
274
272
|
const nested = walk(ctx, relation.type, relationPath, relationArgs, depth + 1)
|
|
275
273
|
if (nested.unsupportedReason) return nested
|
|
276
274
|
|
|
277
|
-
if (relation.isList && ctx.stages.length > stagesBeforeRecursion) {
|
|
278
|
-
return { unsupportedReason: 'nested relation through to-many parent not supported in MVP' }
|
|
279
|
-
}
|
|
280
|
-
|
|
281
275
|
if (nested.projectionAfterStrip) {
|
|
282
276
|
ctx.stages[stageIndex].stageArgs.select = nested.projectionAfterStrip
|
|
283
277
|
}
|
|
@@ -9,12 +9,14 @@ import {
|
|
|
9
9
|
sendSSEProgress,
|
|
10
10
|
sendSSERootArray,
|
|
11
11
|
sendSSERelationBatch,
|
|
12
|
+
sendSSEPageMeta,
|
|
12
13
|
runSingleResultSSE,
|
|
13
14
|
emitTerminalSSEError,
|
|
14
15
|
setByPath,
|
|
15
16
|
getDelegate,
|
|
16
17
|
getExtendedClient,
|
|
17
18
|
applyPaginationLimits,
|
|
19
|
+
countForPagination,
|
|
18
20
|
mapError,
|
|
19
21
|
type OperationContext,
|
|
20
22
|
type PrismaDelegate,
|
|
@@ -39,6 +41,7 @@ export type AutoIncludeBaseOp =
|
|
|
39
41
|
| 'findFirst'
|
|
40
42
|
| 'findFirstOrThrow'
|
|
41
43
|
| 'findMany'
|
|
44
|
+
| 'findManyPaginated'
|
|
42
45
|
|
|
43
46
|
export type RunAutoIncludeOptions = {
|
|
44
47
|
req: Request
|
|
@@ -54,6 +57,11 @@ export type RunAutoIncludeOptions = {
|
|
|
54
57
|
signal?: AbortSignal
|
|
55
58
|
}
|
|
56
59
|
|
|
60
|
+
type RowPair = {
|
|
61
|
+
internal: Record<string, unknown>
|
|
62
|
+
public: Record<string, unknown>
|
|
63
|
+
}
|
|
64
|
+
|
|
57
65
|
function readPath(source: Record<string, unknown>, path: string): unknown {
|
|
58
66
|
if (path === '') return source
|
|
59
67
|
const parts = path.split('.')
|
|
@@ -196,9 +204,6 @@ function findManyUnsupportedReason(plan: AutoIncludePlan): string | null {
|
|
|
196
204
|
if (rel.parentLinkFields.length !== 1 || rel.childLinkFields.length !== 1) {
|
|
197
205
|
return 'auto-progressive fallback: composite link fields not supported for findMany batched auto-include'
|
|
198
206
|
}
|
|
199
|
-
if (stage.depth > 1) {
|
|
200
|
-
return 'auto-progressive fallback: nested relations not supported for findMany batched auto-include'
|
|
201
|
-
}
|
|
202
207
|
if (rel.isList) {
|
|
203
208
|
const sa = stage.stageArgs
|
|
204
209
|
if (
|
|
@@ -214,6 +219,53 @@ function findManyUnsupportedReason(plan: AutoIncludePlan): string | null {
|
|
|
214
219
|
return null
|
|
215
220
|
}
|
|
216
221
|
|
|
222
|
+
function singleUnsupportedReason(plan: AutoIncludePlan): string | null {
|
|
223
|
+
const stagesByPath = new Map(plan.stages.map((s) => [s.relationPath, s]))
|
|
224
|
+
for (const stage of plan.stages) {
|
|
225
|
+
let parentPath = stage.parentPath
|
|
226
|
+
while (parentPath) {
|
|
227
|
+
const parentStage = stagesByPath.get(parentPath)
|
|
228
|
+
if (parentStage?.relationField.isList) {
|
|
229
|
+
return 'auto-progressive fallback: nested relation through to-many parent is not supported for single-result auto-include'
|
|
230
|
+
}
|
|
231
|
+
const dot = parentPath.lastIndexOf('.')
|
|
232
|
+
parentPath = dot === -1 ? '' : parentPath.slice(0, dot)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return null
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function collectParentPairs(
|
|
239
|
+
rootPairs: RowPair[],
|
|
240
|
+
parentPath: string,
|
|
241
|
+
): RowPair[] {
|
|
242
|
+
if (parentPath === '') return rootPairs
|
|
243
|
+
let pairs = rootPairs
|
|
244
|
+
for (const segment of parentPath.split('.')) {
|
|
245
|
+
const next: RowPair[] = []
|
|
246
|
+
for (const pair of pairs) {
|
|
247
|
+
const internalValue = pair.internal[segment]
|
|
248
|
+
const publicValue = pair.public[segment]
|
|
249
|
+
if (Array.isArray(internalValue) && Array.isArray(publicValue)) {
|
|
250
|
+
const length = Math.min(internalValue.length, publicValue.length)
|
|
251
|
+
for (let i = 0; i < length; i++) {
|
|
252
|
+
const internalItem = internalValue[i]
|
|
253
|
+
const publicItem = publicValue[i]
|
|
254
|
+
if (isPlainObject(internalItem) && isPlainObject(publicItem)) {
|
|
255
|
+
next.push({ internal: internalItem, public: publicItem })
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
continue
|
|
259
|
+
}
|
|
260
|
+
if (isPlainObject(internalValue) && isPlainObject(publicValue)) {
|
|
261
|
+
next.push({ internal: internalValue, public: publicValue })
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
pairs = next
|
|
265
|
+
}
|
|
266
|
+
return pairs
|
|
267
|
+
}
|
|
268
|
+
|
|
217
269
|
async function runOneStageSingle(options: {
|
|
218
270
|
extended: unknown
|
|
219
271
|
models: Record<string, ModelRelationMap>
|
|
@@ -300,7 +352,7 @@ async function runAutoIncludeSingle(
|
|
|
300
352
|
|
|
301
353
|
let rootResult: unknown
|
|
302
354
|
try {
|
|
303
|
-
rootResult = await rootDelegate[baseOp as Exclude<AutoIncludeBaseOp, 'findMany'>](plan.rootArgs)
|
|
355
|
+
rootResult = await rootDelegate[baseOp as Exclude<AutoIncludeBaseOp, 'findMany' | 'findManyPaginated'>](plan.rootArgs)
|
|
304
356
|
} catch (err) {
|
|
305
357
|
if (isClientGone()) return
|
|
306
358
|
console.error('[auto-progressive] root query failed:', err)
|
|
@@ -470,16 +522,12 @@ async function runOneStageMany(options: {
|
|
|
470
522
|
extended: unknown
|
|
471
523
|
models: Record<string, ModelRelationMap>
|
|
472
524
|
stage: AutoIncludeStage
|
|
473
|
-
|
|
474
|
-
publicRows: Record<string, unknown>[]
|
|
525
|
+
parentPairs: RowPair[]
|
|
475
526
|
internalFieldPaths: string[]
|
|
476
527
|
res: Response
|
|
477
528
|
isAborted: () => boolean
|
|
478
529
|
}): Promise<void> {
|
|
479
|
-
const {
|
|
480
|
-
extended, models, stage, internalRows, publicRows,
|
|
481
|
-
internalFieldPaths, res, isAborted,
|
|
482
|
-
} = options
|
|
530
|
+
const { extended, models, stage, parentPairs, internalFieldPaths, res, isAborted } = options
|
|
483
531
|
|
|
484
532
|
if (isAborted()) return
|
|
485
533
|
|
|
@@ -492,7 +540,15 @@ async function runOneStageMany(options: {
|
|
|
492
540
|
throw new Error('Target model not in relation metadata: ' + rel.type)
|
|
493
541
|
}
|
|
494
542
|
|
|
495
|
-
|
|
543
|
+
if (parentPairs.length === 0) {
|
|
544
|
+
if (stage.depth === 1) {
|
|
545
|
+
sendSSERelationBatch(res, stage.relationPath, [])
|
|
546
|
+
}
|
|
547
|
+
return
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const internalParents = parentPairs.map((p) => p.internal)
|
|
551
|
+
const distinctValues = collectDistinctParentValues(internalParents, parentKey)
|
|
496
552
|
const delegate: PrismaDelegate = getDelegate(extended, targetModel.delegateKey)
|
|
497
553
|
|
|
498
554
|
const children: unknown[] = []
|
|
@@ -515,11 +571,11 @@ async function runOneStageMany(options: {
|
|
|
515
571
|
: internalFieldPaths
|
|
516
572
|
|
|
517
573
|
const grouped = groupRelatedRows(children, childKey)
|
|
518
|
-
const publicValues: unknown[] = new Array(
|
|
574
|
+
const publicValues: unknown[] = new Array(parentPairs.length)
|
|
519
575
|
|
|
520
|
-
for (let i = 0; i <
|
|
521
|
-
const
|
|
522
|
-
const fkVal =
|
|
576
|
+
for (let i = 0; i < parentPairs.length; i++) {
|
|
577
|
+
const pair = parentPairs[i]
|
|
578
|
+
const fkVal = pair.internal[parentKey]
|
|
523
579
|
let internalVal: unknown
|
|
524
580
|
|
|
525
581
|
if (fkVal === undefined || fkVal === null) {
|
|
@@ -535,13 +591,124 @@ async function runOneStageMany(options: {
|
|
|
535
591
|
|
|
536
592
|
const publicVal = buildPublicForStage(internalVal, effectivePaths, stage.relationPath)
|
|
537
593
|
|
|
538
|
-
|
|
539
|
-
|
|
594
|
+
pair.internal[stage.relationName] = internalVal
|
|
595
|
+
pair.public[stage.relationName] = publicVal
|
|
540
596
|
publicValues[i] = publicVal
|
|
541
597
|
}
|
|
542
598
|
|
|
543
599
|
if (isAborted()) return
|
|
544
|
-
|
|
600
|
+
|
|
601
|
+
if (stage.depth === 1) {
|
|
602
|
+
sendSSERelationBatch(res, stage.relationPath, publicValues)
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function buildRootPairs(
|
|
607
|
+
rootRows: unknown[],
|
|
608
|
+
internalFieldPaths: string[],
|
|
609
|
+
): { publicRows: Record<string, unknown>[]; rootPairs: RowPair[] } {
|
|
610
|
+
const n = rootRows.length
|
|
611
|
+
const publicRows: Record<string, unknown>[] = new Array(n)
|
|
612
|
+
const rootPairs: RowPair[] = new Array(n)
|
|
613
|
+
for (let i = 0; i < n; i++) {
|
|
614
|
+
const row = rootRows[i]
|
|
615
|
+
if (!isPlainObject(row)) {
|
|
616
|
+
const internalEmpty: Record<string, unknown> = {}
|
|
617
|
+
const publicEmpty: Record<string, unknown> = {}
|
|
618
|
+
publicRows[i] = publicEmpty
|
|
619
|
+
rootPairs[i] = { internal: internalEmpty, public: publicEmpty }
|
|
620
|
+
continue
|
|
621
|
+
}
|
|
622
|
+
const internalCopy: Record<string, unknown> = { ...row }
|
|
623
|
+
const publicCopy: Record<string, unknown> = { ...row }
|
|
624
|
+
stripInternalAtScope(publicCopy, internalFieldPaths, '')
|
|
625
|
+
publicRows[i] = publicCopy
|
|
626
|
+
rootPairs[i] = { internal: internalCopy, public: publicCopy }
|
|
627
|
+
}
|
|
628
|
+
return { publicRows, rootPairs }
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
async function processFindManyStages(args: {
|
|
632
|
+
extended: unknown
|
|
633
|
+
models: Record<string, ModelRelationMap>
|
|
634
|
+
plan: AutoIncludePlan
|
|
635
|
+
rootPairs: RowPair[]
|
|
636
|
+
res: Response
|
|
637
|
+
signal?: AbortSignal
|
|
638
|
+
}): Promise<string | null> {
|
|
639
|
+
const { extended, models, plan, rootPairs, res, signal } = args
|
|
640
|
+
const groups = groupStagesByDepth(plan.stages)
|
|
641
|
+
let completed = 0
|
|
642
|
+
let stageErrorMessage: string | null = null
|
|
643
|
+
const isAborted = () =>
|
|
644
|
+
stageErrorMessage !== null ||
|
|
645
|
+
signal?.aborted === true ||
|
|
646
|
+
res.writableEnded ||
|
|
647
|
+
res.destroyed
|
|
648
|
+
|
|
649
|
+
for (const group of groups) {
|
|
650
|
+
if (signal?.aborted === true || res.writableEnded || res.destroyed) return stageErrorMessage
|
|
651
|
+
if (stageErrorMessage) break
|
|
652
|
+
|
|
653
|
+
await runConcurrent(group, STAGE_CONCURRENCY, async (stage) => {
|
|
654
|
+
if (isAborted()) return
|
|
655
|
+
const parentPairs = collectParentPairs(rootPairs, stage.parentPath)
|
|
656
|
+
try {
|
|
657
|
+
await runOneStageMany({
|
|
658
|
+
extended,
|
|
659
|
+
models,
|
|
660
|
+
stage,
|
|
661
|
+
parentPairs,
|
|
662
|
+
internalFieldPaths: plan.internalFieldPaths,
|
|
663
|
+
res,
|
|
664
|
+
isAborted,
|
|
665
|
+
})
|
|
666
|
+
} catch (err) {
|
|
667
|
+
if (isAborted()) return
|
|
668
|
+
console.error('[auto-progressive] stage failed:', stage.relationPath, err)
|
|
669
|
+
stageErrorMessage = mapError(err).message
|
|
670
|
+
return
|
|
671
|
+
}
|
|
672
|
+
if (isAborted()) return
|
|
673
|
+
completed++
|
|
674
|
+
sendSSEProgress(res, stage.relationPath, completed, plan.stages.length)
|
|
675
|
+
})
|
|
676
|
+
}
|
|
677
|
+
return stageErrorMessage
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
async function runPaginatedRoot(
|
|
681
|
+
extended: unknown,
|
|
682
|
+
rootDelegate: PrismaDelegate,
|
|
683
|
+
delegateKey: string,
|
|
684
|
+
rootArgs: Record<string, unknown>,
|
|
685
|
+
distinctCountLimit: number | undefined,
|
|
686
|
+
): Promise<{ data: unknown[]; count: number }> {
|
|
687
|
+
const txClient = extended as {
|
|
688
|
+
$transaction?: <T>(fn: (tx: unknown) => Promise<T>) => Promise<T>
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (typeof txClient.$transaction === 'function') {
|
|
692
|
+
try {
|
|
693
|
+
const result = await txClient.$transaction(async (tx: unknown) => {
|
|
694
|
+
const txDelegate = getDelegate(tx, delegateKey)
|
|
695
|
+
const d = await txDelegate.findMany(rootArgs)
|
|
696
|
+
const t = await countForPagination(txDelegate, rootArgs, undefined, undefined, distinctCountLimit)
|
|
697
|
+
return { d, t }
|
|
698
|
+
})
|
|
699
|
+
return { data: result.d as unknown[], count: result.t }
|
|
700
|
+
} catch (err) {
|
|
701
|
+
const e = err as { code?: string }
|
|
702
|
+
if (e?.code !== 'P2028') throw err
|
|
703
|
+
console.warn('[auto-progressive] Interactive transactions not available, paginated queries are non-atomic')
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const [data, count] = await Promise.all([
|
|
708
|
+
rootDelegate.findMany(rootArgs),
|
|
709
|
+
countForPagination(rootDelegate, rootArgs, undefined, undefined, distinctCountLimit),
|
|
710
|
+
])
|
|
711
|
+
return { data: data as unknown[], count }
|
|
545
712
|
}
|
|
546
713
|
|
|
547
714
|
async function runAutoIncludeMany(
|
|
@@ -582,78 +749,105 @@ async function runAutoIncludeMany(
|
|
|
582
749
|
return
|
|
583
750
|
}
|
|
584
751
|
|
|
585
|
-
const
|
|
586
|
-
const publicRows: Record<string, unknown>[] = new Array(rootResult.length)
|
|
752
|
+
const { publicRows, rootPairs } = buildRootPairs(rootResult, plan.internalFieldPaths)
|
|
587
753
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
754
|
+
sendSSERootArray(res, publicRows)
|
|
755
|
+
sendSSEProgress(res, 'root', 0, plan.stages.length)
|
|
756
|
+
|
|
757
|
+
const stageError = await processFindManyStages({
|
|
758
|
+
extended, models, plan, rootPairs, res, signal,
|
|
759
|
+
})
|
|
760
|
+
|
|
761
|
+
if (isClientGone()) return
|
|
762
|
+
|
|
763
|
+
if (stageError) {
|
|
764
|
+
if (!res.writableEnded && !res.destroyed) {
|
|
765
|
+
sendSSEError(res, stageError)
|
|
594
766
|
}
|
|
595
|
-
|
|
596
|
-
const publicCopy: Record<string, unknown> = { ...row }
|
|
597
|
-
stripInternalAtScope(publicCopy, plan.internalFieldPaths, '')
|
|
598
|
-
internalRows[i] = internalCopy
|
|
599
|
-
publicRows[i] = publicCopy
|
|
767
|
+
return
|
|
600
768
|
}
|
|
601
769
|
|
|
602
|
-
|
|
603
|
-
|
|
770
|
+
if (res.writableEnded || res.destroyed) return
|
|
771
|
+
sendSSEResult(res, publicRows)
|
|
772
|
+
} catch (err) {
|
|
773
|
+
if (isClientGone()) return
|
|
774
|
+
console.error('[auto-progressive] many dispatch error:', err)
|
|
775
|
+
if (!res.writableEnded && !res.destroyed) {
|
|
776
|
+
sendSSEError(res, mapError(err).message)
|
|
777
|
+
}
|
|
778
|
+
} finally {
|
|
779
|
+
endSSE(res, keepalive)
|
|
780
|
+
}
|
|
781
|
+
}
|
|
604
782
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
signal?.aborted === true ||
|
|
611
|
-
res.writableEnded ||
|
|
612
|
-
res.destroyed
|
|
783
|
+
async function runAutoIncludePaginated(
|
|
784
|
+
options: RunAutoIncludeOptions,
|
|
785
|
+
plan: AutoIncludePlan,
|
|
786
|
+
): Promise<void> {
|
|
787
|
+
const { res, ctx, delegateKey, models, signal } = options
|
|
613
788
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
if (stageErrorMessage) break
|
|
789
|
+
const isClientGone = () =>
|
|
790
|
+
signal?.aborted === true || res.writableEnded || res.destroyed
|
|
617
791
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
792
|
+
let keepalive: IntervalHandle | null = null
|
|
793
|
+
try {
|
|
794
|
+
initSSE(res)
|
|
795
|
+
keepalive = startSSEKeepalive(res)
|
|
796
|
+
if (isClientGone()) return
|
|
797
|
+
|
|
798
|
+
const extended = await getExtendedClient(ctx)
|
|
799
|
+
if (isClientGone()) return
|
|
800
|
+
|
|
801
|
+
const rootDelegate = getDelegate(extended, delegateKey)
|
|
802
|
+
const rootArgs = applyPaginationLimits(plan.rootArgs, ctx.paginationConfig)
|
|
803
|
+
const distinctCountLimit = ctx.paginationConfig?.distinctCountLimit
|
|
804
|
+
|
|
805
|
+
let rootRows: unknown[]
|
|
806
|
+
let total: number
|
|
807
|
+
try {
|
|
808
|
+
const { data, count } = await runPaginatedRoot(
|
|
809
|
+
extended, rootDelegate, delegateKey, rootArgs, distinctCountLimit,
|
|
810
|
+
)
|
|
811
|
+
rootRows = data
|
|
812
|
+
total = count
|
|
813
|
+
} catch (err) {
|
|
814
|
+
if (isClientGone()) return
|
|
815
|
+
console.error('[auto-progressive] root findManyPaginated failed:', err)
|
|
816
|
+
sendSSEError(res, mapError(err).message)
|
|
817
|
+
return
|
|
641
818
|
}
|
|
642
819
|
|
|
643
820
|
if (isClientGone()) return
|
|
644
821
|
|
|
645
|
-
|
|
822
|
+
const skip = typeof rootArgs.skip === 'number' ? rootArgs.skip : 0
|
|
823
|
+
const takeRaw = typeof rootArgs.take === 'number' ? rootArgs.take : rootRows.length
|
|
824
|
+
const absTake = Math.abs(takeRaw)
|
|
825
|
+
const hasMore = rootRows.length >= absTake && skip + rootRows.length < total
|
|
826
|
+
|
|
827
|
+
const { publicRows, rootPairs } = buildRootPairs(rootRows, plan.internalFieldPaths)
|
|
828
|
+
|
|
829
|
+
sendSSEPageMeta(res, total, hasMore)
|
|
830
|
+
sendSSERootArray(res, publicRows)
|
|
831
|
+
sendSSEProgress(res, 'root', 0, plan.stages.length)
|
|
832
|
+
|
|
833
|
+
const stageError = await processFindManyStages({
|
|
834
|
+
extended, models, plan, rootPairs, res, signal,
|
|
835
|
+
})
|
|
836
|
+
|
|
837
|
+
if (isClientGone()) return
|
|
838
|
+
|
|
839
|
+
if (stageError) {
|
|
646
840
|
if (!res.writableEnded && !res.destroyed) {
|
|
647
|
-
sendSSEError(res,
|
|
841
|
+
sendSSEError(res, stageError)
|
|
648
842
|
}
|
|
649
843
|
return
|
|
650
844
|
}
|
|
651
845
|
|
|
652
846
|
if (res.writableEnded || res.destroyed) return
|
|
653
|
-
sendSSEResult(res, publicRows)
|
|
847
|
+
sendSSEResult(res, { data: publicRows, total, hasMore })
|
|
654
848
|
} catch (err) {
|
|
655
849
|
if (isClientGone()) return
|
|
656
|
-
console.error('[auto-progressive]
|
|
850
|
+
console.error('[auto-progressive] paginated dispatch error:', err)
|
|
657
851
|
if (!res.writableEnded && !res.destroyed) {
|
|
658
852
|
sendSSEError(res, mapError(err).message)
|
|
659
853
|
}
|
|
@@ -682,9 +876,12 @@ export async function runAutoIncludeProgressive(
|
|
|
682
876
|
return handleAutoIncludeFallback(options, plan.unsupportedReason)
|
|
683
877
|
}
|
|
684
878
|
|
|
685
|
-
if (options.baseOp === 'findMany') {
|
|
879
|
+
if (options.baseOp === 'findMany' || options.baseOp === 'findManyPaginated') {
|
|
686
880
|
const reason = findManyUnsupportedReason(plan)
|
|
687
881
|
if (reason) return handleAutoIncludeFallback(options, reason)
|
|
882
|
+
} else {
|
|
883
|
+
const reason = singleUnsupportedReason(plan)
|
|
884
|
+
if (reason) return handleAutoIncludeFallback(options, reason)
|
|
688
885
|
}
|
|
689
886
|
|
|
690
887
|
if (plan.stages.length === 0) {
|
|
@@ -698,5 +895,8 @@ export async function runAutoIncludeProgressive(
|
|
|
698
895
|
if (options.baseOp === 'findMany') {
|
|
699
896
|
return runAutoIncludeMany(options, plan)
|
|
700
897
|
}
|
|
898
|
+
if (options.baseOp === 'findManyPaginated') {
|
|
899
|
+
return runAutoIncludePaginated(options, plan)
|
|
900
|
+
}
|
|
701
901
|
return runAutoIncludeSingle(options, plan)
|
|
702
902
|
}
|
|
@@ -503,6 +503,14 @@ export function sendSSERelationBatch(
|
|
|
503
503
|
return sendSSE(res, { type: 'relationBatch', relationPath, values })
|
|
504
504
|
}
|
|
505
505
|
|
|
506
|
+
export function sendSSEPageMeta(
|
|
507
|
+
res: SseWritable,
|
|
508
|
+
total: number,
|
|
509
|
+
hasMore: boolean,
|
|
510
|
+
): boolean {
|
|
511
|
+
return sendSSE(res, { type: 'pageMeta', total, hasMore })
|
|
512
|
+
}
|
|
513
|
+
|
|
506
514
|
export function sendSSEError(res: SseWritable, message: string): boolean {
|
|
507
515
|
if (res.writableEnded || res.destroyed) return false
|
|
508
516
|
try {
|
|
@@ -253,7 +253,7 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(config: ${m
|
|
|
253
253
|
const isAutoIncludeReadable =
|
|
254
254
|
baseOp === 'findUnique' || baseOp === 'findUniqueOrThrow' ||
|
|
255
255
|
baseOp === 'findFirst' || baseOp === 'findFirstOrThrow' ||
|
|
256
|
-
baseOp === 'findMany'
|
|
256
|
+
baseOp === 'findMany' || baseOp === 'findManyPaginated'
|
|
257
257
|
|
|
258
258
|
if (!isAutoIncludeReadable) {
|
|
259
259
|
if (progressiveConfig.fallback === 'error') {
|
|
@@ -279,7 +279,7 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(config: ${m
|
|
|
279
279
|
res,
|
|
280
280
|
ctx,
|
|
281
281
|
args,
|
|
282
|
-
baseOp: baseOp as 'findUnique' | 'findUniqueOrThrow' | 'findFirst' | 'findFirstOrThrow' | 'findMany',
|
|
282
|
+
baseOp: baseOp as 'findUnique' | 'findUniqueOrThrow' | 'findFirst' | 'findFirstOrThrow' | 'findMany' | 'findManyPaginated',
|
|
283
283
|
modelName: '${modelName}',
|
|
284
284
|
delegateKey: '${delegateKey}',
|
|
285
285
|
models: relationModels,
|