prisma-generator-express 1.51.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 +183 -14
- package/dist/generators/generateRouter.js +6 -5
- package/dist/generators/generateRouter.js.map +1 -1
- package/package.json +1 -1
- package/src/copy/autoIncludePlanner.ts +0 -6
- package/src/copy/autoIncludeRuntime.ts +612 -64
- package/src/copy/operationRuntime.ts +20 -0
- package/src/generators/generateRouter.ts +6 -5
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
|
|
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 single-record
|
|
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
|
|
|
@@ -1558,11 +1558,11 @@ Progressive SSE has two modes:
|
|
|
1558
1558
|
| Mode | Config | Best for |
|
|
1559
1559
|
| ---- | ------ | -------- |
|
|
1560
1560
|
| Manual stages | `{ stages: [...] }` or `{ mode: 'manual', stages: [...] }` | Custom page-level composition where each stage runs its own query and returns patches |
|
|
1561
|
-
| Auto include | `{ mode: 'autoInclude' }` |
|
|
1561
|
+
| Auto include | `{ mode: 'autoInclude' }` | Reads where the client already sends a Prisma `include` or relation `select` tree and you want relation fields streamed progressively |
|
|
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
|
|
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
|
|
|
@@ -1594,14 +1594,18 @@ Manual progressive SSE can be configured on Express GET read operations only:
|
|
|
1594
1594
|
- `aggregate`
|
|
1595
1595
|
- `groupBy`
|
|
1596
1596
|
|
|
1597
|
-
Auto-include progressive SSE
|
|
1597
|
+
Auto-include progressive SSE supports these Express GET read operations:
|
|
1598
1598
|
|
|
1599
1599
|
- `findUnique`
|
|
1600
1600
|
- `findUniqueOrThrow`
|
|
1601
1601
|
- `findFirst`
|
|
1602
1602
|
- `findFirstOrThrow`
|
|
1603
|
+
- `findMany`
|
|
1604
|
+
- `findManyPaginated`
|
|
1603
1605
|
|
|
1604
|
-
|
|
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).
|
|
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`.
|
|
1605
1609
|
|
|
1606
1610
|
Write operations do not support progressive SSE.
|
|
1607
1611
|
|
|
@@ -1627,6 +1631,18 @@ Nested field event:
|
|
|
1627
1631
|
{ "type": "field", "key": "profile.appliedTo", "value": [] }
|
|
1628
1632
|
```
|
|
1629
1633
|
|
|
1634
|
+
Root array event for `findMany` / `findManyPaginated` auto-include:
|
|
1635
|
+
|
|
1636
|
+
```json
|
|
1637
|
+
{ "type": "rootArray", "data": [{ "id": "user-1" }, { "id": "user-2" }] }
|
|
1638
|
+
```
|
|
1639
|
+
|
|
1640
|
+
Relation batch event for a direct root relation in `findMany` / `findManyPaginated` auto-include:
|
|
1641
|
+
|
|
1642
|
+
```json
|
|
1643
|
+
{ "type": "relationBatch", "relationPath": "profile", "values": [{ "id": "profile-1" }, null] }
|
|
1644
|
+
```
|
|
1645
|
+
|
|
1630
1646
|
Final result event:
|
|
1631
1647
|
|
|
1632
1648
|
```json
|
|
@@ -1639,7 +1655,11 @@ Error event:
|
|
|
1639
1655
|
{ "type": "error", "message": "Could not load progressive response" }
|
|
1640
1656
|
```
|
|
1641
1657
|
|
|
1642
|
-
|
|
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.
|
|
1659
|
+
|
|
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 }`.
|
|
1643
1663
|
|
|
1644
1664
|
### Manual staged mode
|
|
1645
1665
|
|
|
@@ -1890,7 +1910,7 @@ const response = await fetch(`/user/unique?${params}`, {
|
|
|
1890
1910
|
})
|
|
1891
1911
|
```
|
|
1892
1912
|
|
|
1893
|
-
|
|
1913
|
+
On single-record reads, auto-include sends root scalar fields first, then sends relation field events as separate relation queries finish:
|
|
1894
1914
|
|
|
1895
1915
|
```json
|
|
1896
1916
|
{ "type": "field", "key": "id", "value": "user-id" }
|
|
@@ -1906,9 +1926,121 @@ Auto-include sends root scalar fields first, then sends relation field events as
|
|
|
1906
1926
|
|
|
1907
1927
|
The final `result` event contains the assembled object.
|
|
1908
1928
|
|
|
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:
|
|
1930
|
+
|
|
1931
|
+
```ts
|
|
1932
|
+
const listConfig = {
|
|
1933
|
+
guard: {
|
|
1934
|
+
variantHeader: 'x-api-variant',
|
|
1935
|
+
},
|
|
1936
|
+
|
|
1937
|
+
findMany: {
|
|
1938
|
+
progressive: {
|
|
1939
|
+
list: {
|
|
1940
|
+
enabled: true,
|
|
1941
|
+
mode: 'autoInclude',
|
|
1942
|
+
fallback: 'singleResult',
|
|
1943
|
+
},
|
|
1944
|
+
},
|
|
1945
|
+
},
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
const params = encodeQueryParams({
|
|
1949
|
+
where: { isActive: true },
|
|
1950
|
+
take: 50,
|
|
1951
|
+
include: {
|
|
1952
|
+
profile: {
|
|
1953
|
+
select: {
|
|
1954
|
+
id: true,
|
|
1955
|
+
displayName: true,
|
|
1956
|
+
},
|
|
1957
|
+
},
|
|
1958
|
+
},
|
|
1959
|
+
})
|
|
1960
|
+
|
|
1961
|
+
const response = await fetch(`/user?${params}`, {
|
|
1962
|
+
headers: {
|
|
1963
|
+
Accept: 'text/event-stream',
|
|
1964
|
+
'x-api-variant': 'list',
|
|
1965
|
+
},
|
|
1966
|
+
})
|
|
1967
|
+
```
|
|
1968
|
+
|
|
1969
|
+
Example shallow `findMany` auto-include event sequence:
|
|
1970
|
+
|
|
1971
|
+
```json
|
|
1972
|
+
{ "type": "rootArray", "data": [{ "id": "user-1" }, { "id": "user-2" }] }
|
|
1973
|
+
```
|
|
1974
|
+
|
|
1975
|
+
```json
|
|
1976
|
+
{ "type": "relationBatch", "relationPath": "profile", "values": [{ "id": "profile-1", "displayName": "Alice" }, null] }
|
|
1977
|
+
```
|
|
1978
|
+
|
|
1979
|
+
```json
|
|
1980
|
+
{ "type": "result", "data": [{ "id": "user-1", "profile": { "id": "profile-1", "displayName": "Alice" } }, { "id": "user-2", "profile": null }] }
|
|
1981
|
+
```
|
|
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
|
+
|
|
1909
2041
|
### Auto-include behavior and limits
|
|
1910
2042
|
|
|
1911
|
-
Auto-include is designed for supported Prisma `include` and relation `select` trees on
|
|
2043
|
+
Auto-include is designed for supported Prisma `include` and relation `select` trees on reads.
|
|
1912
2044
|
|
|
1913
2045
|
Supported root operations:
|
|
1914
2046
|
|
|
@@ -1916,13 +2048,29 @@ Supported root operations:
|
|
|
1916
2048
|
- `findUniqueOrThrow`
|
|
1917
2049
|
- `findFirst`
|
|
1918
2050
|
- `findFirstOrThrow`
|
|
2051
|
+
- `findMany`
|
|
2052
|
+
- `findManyPaginated`
|
|
1919
2053
|
|
|
1920
|
-
Supported relation shapes:
|
|
2054
|
+
Supported single-record relation shapes:
|
|
1921
2055
|
|
|
1922
2056
|
- direct to-one relation includes/selects
|
|
1923
2057
|
- direct to-many relation includes/selects
|
|
1924
2058
|
- to-many relation args such as `where`, `orderBy`, `take`, `skip`, `cursor`, and `distinct`
|
|
1925
|
-
- 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.
|
|
2062
|
+
|
|
2063
|
+
Supported `findMany` and `findManyPaginated` relation shapes:
|
|
2064
|
+
|
|
2065
|
+
- direct and nested to-one relation includes/selects
|
|
2066
|
+
- direct and nested to-many relation includes/selects
|
|
2067
|
+
- relation-level `where` and `orderBy`
|
|
2068
|
+
- single-column link fields only
|
|
2069
|
+
- nested depth up to the configured planner limit
|
|
2070
|
+
|
|
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.
|
|
1926
2074
|
|
|
1927
2075
|
Current MVP fallback cases include:
|
|
1928
2076
|
|
|
@@ -1932,9 +2080,12 @@ Current MVP fallback cases include:
|
|
|
1932
2080
|
- `select` and `omit` at the same level
|
|
1933
2081
|
- relation filters/order/cursor in the root query
|
|
1934
2082
|
- relation filters/order/cursor inside staged relation queries when unsupported
|
|
1935
|
-
- nested relation loading through a to-many parent
|
|
1936
2083
|
- omitted required link fields needed to stitch parent and child records
|
|
1937
2084
|
- planner limits for maximum depth or stage count
|
|
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`
|
|
1938
2089
|
|
|
1939
2090
|
When fallback happens:
|
|
1940
2091
|
|
|
@@ -1943,6 +2094,8 @@ When fallback happens:
|
|
|
1943
2094
|
|
|
1944
2095
|
If `fallback` is omitted, the default behavior is equivalent to `'singleResult'`.
|
|
1945
2096
|
|
|
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.
|
|
2098
|
+
|
|
1946
2099
|
Auto-include does not require `resolveContext` or `progressiveStages`.
|
|
1947
2100
|
|
|
1948
2101
|
### Hooks and guard behavior
|
|
@@ -1980,6 +2133,9 @@ if (!response.body) {
|
|
|
1980
2133
|
const reader = response.body.getReader()
|
|
1981
2134
|
const decoder = new TextDecoder()
|
|
1982
2135
|
let buffer = ''
|
|
2136
|
+
let rows: Array<Record<string, unknown>> = []
|
|
2137
|
+
let fields: Record<string, unknown> = {}
|
|
2138
|
+
let data: unknown = undefined
|
|
1983
2139
|
|
|
1984
2140
|
while (true) {
|
|
1985
2141
|
const { value, done } = await reader.read()
|
|
@@ -1998,12 +2154,23 @@ while (true) {
|
|
|
1998
2154
|
|
|
1999
2155
|
const event = JSON.parse(line.slice('data: '.length))
|
|
2000
2156
|
|
|
2157
|
+
if (event.type === 'rootArray') {
|
|
2158
|
+
rows = event.data
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2001
2161
|
if (event.type === 'field') {
|
|
2002
|
-
|
|
2162
|
+
fields[event.key] = event.value
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
if (event.type === 'relationBatch') {
|
|
2166
|
+
rows = rows.map((row, index) => ({
|
|
2167
|
+
...row,
|
|
2168
|
+
[event.relationPath]: event.values[index],
|
|
2169
|
+
}))
|
|
2003
2170
|
}
|
|
2004
2171
|
|
|
2005
2172
|
if (event.type === 'result') {
|
|
2006
|
-
|
|
2173
|
+
data = event.data
|
|
2007
2174
|
}
|
|
2008
2175
|
}
|
|
2009
2176
|
}
|
|
@@ -2017,6 +2184,8 @@ For React Query, include the variant and mode in the query key:
|
|
|
2017
2184
|
|
|
2018
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`.
|
|
2019
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
|
+
|
|
2020
2189
|
### Runtime notes
|
|
2021
2190
|
|
|
2022
2191
|
The SSE response sets:
|
|
@@ -234,13 +234,14 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(config: ${m
|
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
if (progressiveConfig.mode === 'autoInclude') {
|
|
237
|
-
const
|
|
237
|
+
const isAutoIncludeReadable =
|
|
238
238
|
baseOp === 'findUnique' || baseOp === 'findUniqueOrThrow' ||
|
|
239
|
-
baseOp === 'findFirst' || baseOp === 'findFirstOrThrow'
|
|
239
|
+
baseOp === 'findFirst' || baseOp === 'findFirstOrThrow' ||
|
|
240
|
+
baseOp === 'findMany' || baseOp === 'findManyPaginated'
|
|
240
241
|
|
|
241
|
-
if (!
|
|
242
|
+
if (!isAutoIncludeReadable) {
|
|
242
243
|
if (progressiveConfig.fallback === 'error') {
|
|
243
|
-
emitTerminalSSEError(res, 'auto-progressive fallback: operation not
|
|
244
|
+
emitTerminalSSEError(res, 'auto-progressive fallback: operation not supported by auto-include')
|
|
244
245
|
return
|
|
245
246
|
}
|
|
246
247
|
await runSingleResultSSE({
|
|
@@ -262,7 +263,7 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(config: ${m
|
|
|
262
263
|
res,
|
|
263
264
|
ctx,
|
|
264
265
|
args,
|
|
265
|
-
baseOp: baseOp as 'findUnique' | 'findUniqueOrThrow' | 'findFirst' | 'findFirstOrThrow',
|
|
266
|
+
baseOp: baseOp as 'findUnique' | 'findUniqueOrThrow' | 'findFirst' | 'findFirstOrThrow' | 'findMany' | 'findManyPaginated',
|
|
266
267
|
modelName: '${modelName}',
|
|
267
268
|
delegateKey: '${delegateKey}',
|
|
268
269
|
models: relationModels,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generateRouter.js","sourceRoot":"","sources":["../../src/generators/generateRouter.ts"],"names":[],"mappings":";;AAKA,
|
|
1
|
+
{"version":3,"file":"generateRouter.js","sourceRoot":"","sources":["../../src/generators/generateRouter.ts"],"names":[],"mappings":"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"}
|
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
|
}
|
|
@@ -7,11 +7,16 @@ import {
|
|
|
7
7
|
sendSSEResult,
|
|
8
8
|
sendSSEError,
|
|
9
9
|
sendSSEProgress,
|
|
10
|
+
sendSSERootArray,
|
|
11
|
+
sendSSERelationBatch,
|
|
12
|
+
sendSSEPageMeta,
|
|
10
13
|
runSingleResultSSE,
|
|
11
14
|
emitTerminalSSEError,
|
|
12
15
|
setByPath,
|
|
13
16
|
getDelegate,
|
|
14
17
|
getExtendedClient,
|
|
18
|
+
applyPaginationLimits,
|
|
19
|
+
countForPagination,
|
|
15
20
|
mapError,
|
|
16
21
|
type OperationContext,
|
|
17
22
|
type PrismaDelegate,
|
|
@@ -19,20 +24,31 @@ import {
|
|
|
19
24
|
import { isPlainObject } from './misc'
|
|
20
25
|
import {
|
|
21
26
|
planAutoInclude,
|
|
27
|
+
type AutoIncludePlan,
|
|
22
28
|
type ModelRelationMap,
|
|
23
29
|
type AutoIncludeStage,
|
|
24
30
|
} from './autoIncludePlanner'
|
|
25
31
|
import type { AutoIncludeProgressiveVariantConfig } from './routeConfig'
|
|
26
32
|
|
|
27
33
|
const STAGE_CONCURRENCY = 4
|
|
34
|
+
const MAX_IN_CHUNK = 1000
|
|
35
|
+
|
|
28
36
|
type IntervalHandle = ReturnType<typeof setInterval>
|
|
29
37
|
|
|
38
|
+
export type AutoIncludeBaseOp =
|
|
39
|
+
| 'findUnique'
|
|
40
|
+
| 'findUniqueOrThrow'
|
|
41
|
+
| 'findFirst'
|
|
42
|
+
| 'findFirstOrThrow'
|
|
43
|
+
| 'findMany'
|
|
44
|
+
| 'findManyPaginated'
|
|
45
|
+
|
|
30
46
|
export type RunAutoIncludeOptions = {
|
|
31
47
|
req: Request
|
|
32
48
|
res: Response
|
|
33
49
|
ctx: OperationContext
|
|
34
50
|
args: Record<string, unknown>
|
|
35
|
-
baseOp:
|
|
51
|
+
baseOp: AutoIncludeBaseOp
|
|
36
52
|
modelName: string
|
|
37
53
|
delegateKey: string
|
|
38
54
|
models: Record<string, ModelRelationMap>
|
|
@@ -41,6 +57,11 @@ export type RunAutoIncludeOptions = {
|
|
|
41
57
|
signal?: AbortSignal
|
|
42
58
|
}
|
|
43
59
|
|
|
60
|
+
type RowPair = {
|
|
61
|
+
internal: Record<string, unknown>
|
|
62
|
+
public: Record<string, unknown>
|
|
63
|
+
}
|
|
64
|
+
|
|
44
65
|
function readPath(source: Record<string, unknown>, path: string): unknown {
|
|
45
66
|
if (path === '') return source
|
|
46
67
|
const parts = path.split('.')
|
|
@@ -122,7 +143,130 @@ function buildPublicForStage(
|
|
|
122
143
|
return result
|
|
123
144
|
}
|
|
124
145
|
|
|
125
|
-
|
|
146
|
+
function normalizeKey(v: unknown): string {
|
|
147
|
+
if (v === null || v === undefined) return '\u0000'
|
|
148
|
+
if (typeof v === 'bigint') return 'B' + v.toString()
|
|
149
|
+
if (v instanceof Date) return 'D' + v.getTime().toString()
|
|
150
|
+
if (typeof v === 'object') return 'O' + JSON.stringify(v)
|
|
151
|
+
return typeof v + ':' + String(v)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function groupStagesByDepth(stages: AutoIncludeStage[]): AutoIncludeStage[][] {
|
|
155
|
+
const byDepth = new Map<number, AutoIncludeStage[]>()
|
|
156
|
+
for (const s of stages) {
|
|
157
|
+
const arr = byDepth.get(s.depth)
|
|
158
|
+
if (arr) arr.push(s)
|
|
159
|
+
else byDepth.set(s.depth, [s])
|
|
160
|
+
}
|
|
161
|
+
return Array.from(byDepth.keys())
|
|
162
|
+
.sort((a, b) => a - b)
|
|
163
|
+
.map((d) => byDepth.get(d) as AutoIncludeStage[])
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function runConcurrent<T>(
|
|
167
|
+
items: T[],
|
|
168
|
+
limit: number,
|
|
169
|
+
fn: (item: T) => Promise<void>,
|
|
170
|
+
): Promise<void> {
|
|
171
|
+
let index = 0
|
|
172
|
+
const workers: Promise<void>[] = []
|
|
173
|
+
const workerCount = Math.min(limit, items.length)
|
|
174
|
+
for (let w = 0; w < workerCount; w++) {
|
|
175
|
+
workers.push((async () => {
|
|
176
|
+
for (;;) {
|
|
177
|
+
const i = index++
|
|
178
|
+
if (i >= items.length) return
|
|
179
|
+
await fn(items[i])
|
|
180
|
+
}
|
|
181
|
+
})())
|
|
182
|
+
}
|
|
183
|
+
await Promise.all(workers)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function handleAutoIncludeFallback(
|
|
187
|
+
options: RunAutoIncludeOptions,
|
|
188
|
+
message: string,
|
|
189
|
+
): Promise<void> {
|
|
190
|
+
if (options.variantConfig.fallback === 'error') {
|
|
191
|
+
emitTerminalSSEError(options.res, message)
|
|
192
|
+
return Promise.resolve()
|
|
193
|
+
}
|
|
194
|
+
return runSingleResultSSE({
|
|
195
|
+
req: options.req,
|
|
196
|
+
res: options.res,
|
|
197
|
+
coreQueryFn: options.coreQueryFn,
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function findManyUnsupportedReason(plan: AutoIncludePlan): string | null {
|
|
202
|
+
for (const stage of plan.stages) {
|
|
203
|
+
const rel = stage.relationField
|
|
204
|
+
if (rel.parentLinkFields.length !== 1 || rel.childLinkFields.length !== 1) {
|
|
205
|
+
return 'auto-progressive fallback: composite link fields not supported for findMany batched auto-include'
|
|
206
|
+
}
|
|
207
|
+
if (rel.isList) {
|
|
208
|
+
const sa = stage.stageArgs
|
|
209
|
+
if (
|
|
210
|
+
sa.take !== undefined ||
|
|
211
|
+
sa.skip !== undefined ||
|
|
212
|
+
sa.cursor !== undefined ||
|
|
213
|
+
sa.distinct !== undefined
|
|
214
|
+
) {
|
|
215
|
+
return 'auto-progressive fallback: per-parent take/skip/cursor/distinct on to-many relations not supported for findMany batched auto-include'
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return null
|
|
220
|
+
}
|
|
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
|
+
|
|
269
|
+
async function runOneStageSingle(options: {
|
|
126
270
|
extended: unknown
|
|
127
271
|
models: Record<string, ModelRelationMap>
|
|
128
272
|
stage: AutoIncludeStage
|
|
@@ -186,72 +330,15 @@ async function runOneStage(options: {
|
|
|
186
330
|
sendSSEField(res, stage.relationPath, publicResult)
|
|
187
331
|
}
|
|
188
332
|
|
|
189
|
-
function
|
|
190
|
-
const byDepth = new Map<number, AutoIncludeStage[]>()
|
|
191
|
-
for (const s of stages) {
|
|
192
|
-
const arr = byDepth.get(s.depth)
|
|
193
|
-
if (arr) arr.push(s)
|
|
194
|
-
else byDepth.set(s.depth, [s])
|
|
195
|
-
}
|
|
196
|
-
return Array.from(byDepth.keys())
|
|
197
|
-
.sort((a, b) => a - b)
|
|
198
|
-
.map((d) => byDepth.get(d) as AutoIncludeStage[])
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
async function runConcurrent<T>(
|
|
202
|
-
items: T[],
|
|
203
|
-
limit: number,
|
|
204
|
-
fn: (item: T) => Promise<void>,
|
|
205
|
-
): Promise<void> {
|
|
206
|
-
let index = 0
|
|
207
|
-
const workers: Promise<void>[] = []
|
|
208
|
-
const workerCount = Math.min(limit, items.length)
|
|
209
|
-
for (let w = 0; w < workerCount; w++) {
|
|
210
|
-
workers.push((async () => {
|
|
211
|
-
for (;;) {
|
|
212
|
-
const i = index++
|
|
213
|
-
if (i >= items.length) return
|
|
214
|
-
await fn(items[i])
|
|
215
|
-
}
|
|
216
|
-
})())
|
|
217
|
-
}
|
|
218
|
-
await Promise.all(workers)
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
export async function runAutoIncludeProgressive(
|
|
333
|
+
async function runAutoIncludeSingle(
|
|
222
334
|
options: RunAutoIncludeOptions,
|
|
335
|
+
plan: AutoIncludePlan,
|
|
223
336
|
): Promise<void> {
|
|
224
|
-
const {
|
|
337
|
+
const { res, ctx, baseOp, delegateKey, models, signal } = options
|
|
225
338
|
|
|
226
339
|
const isClientGone = () =>
|
|
227
340
|
signal?.aborted === true || res.writableEnded || res.destroyed
|
|
228
341
|
|
|
229
|
-
if (ctx.guardShape) {
|
|
230
|
-
if (variantConfig.fallback === 'error') {
|
|
231
|
-
emitTerminalSSEError(res, 'auto-progressive fallback: guard shape disables auto-include')
|
|
232
|
-
return
|
|
233
|
-
}
|
|
234
|
-
return runSingleResultSSE({ req, res, coreQueryFn })
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const plan = planAutoInclude({
|
|
238
|
-
rootModelName: modelName,
|
|
239
|
-
models,
|
|
240
|
-
args,
|
|
241
|
-
})
|
|
242
|
-
|
|
243
|
-
if (plan.unsupportedReason) {
|
|
244
|
-
if (variantConfig.fallback === 'error') {
|
|
245
|
-
emitTerminalSSEError(res, plan.unsupportedReason)
|
|
246
|
-
return
|
|
247
|
-
}
|
|
248
|
-
return runSingleResultSSE({ req, res, coreQueryFn })
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if (plan.stages.length === 0) {
|
|
252
|
-
return runSingleResultSSE({ req, res, coreQueryFn })
|
|
253
|
-
}
|
|
254
|
-
|
|
255
342
|
let keepalive: IntervalHandle | null = null
|
|
256
343
|
try {
|
|
257
344
|
initSSE(res)
|
|
@@ -265,7 +352,7 @@ export async function runAutoIncludeProgressive(
|
|
|
265
352
|
|
|
266
353
|
let rootResult: unknown
|
|
267
354
|
try {
|
|
268
|
-
rootResult = await rootDelegate[baseOp](plan.rootArgs)
|
|
355
|
+
rootResult = await rootDelegate[baseOp as Exclude<AutoIncludeBaseOp, 'findMany' | 'findManyPaginated'>](plan.rootArgs)
|
|
269
356
|
} catch (err) {
|
|
270
357
|
if (isClientGone()) return
|
|
271
358
|
console.error('[auto-progressive] root query failed:', err)
|
|
@@ -309,7 +396,7 @@ export async function runAutoIncludeProgressive(
|
|
|
309
396
|
await runConcurrent(group, STAGE_CONCURRENCY, async (stage) => {
|
|
310
397
|
if (isAborted()) return
|
|
311
398
|
try {
|
|
312
|
-
await
|
|
399
|
+
await runOneStageSingle({
|
|
313
400
|
extended,
|
|
314
401
|
models,
|
|
315
402
|
stage,
|
|
@@ -351,4 +438,465 @@ export async function runAutoIncludeProgressive(
|
|
|
351
438
|
} finally {
|
|
352
439
|
endSSE(res, keepalive)
|
|
353
440
|
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function buildStageQueryArgs(
|
|
444
|
+
stage: AutoIncludeStage,
|
|
445
|
+
childKey: string,
|
|
446
|
+
inChunk: unknown[],
|
|
447
|
+
): { args: Record<string, unknown>; injectedChildPath: string | null } {
|
|
448
|
+
const baseSelect = isPlainObject(stage.stageArgs.select)
|
|
449
|
+
? (stage.stageArgs.select as Record<string, unknown>)
|
|
450
|
+
: null
|
|
451
|
+
const baseOmit = isPlainObject(stage.stageArgs.omit)
|
|
452
|
+
? (stage.stageArgs.omit as Record<string, unknown>)
|
|
453
|
+
: null
|
|
454
|
+
|
|
455
|
+
const finalArgs: Record<string, unknown> = { ...stage.stageArgs }
|
|
456
|
+
finalArgs.where = mergeWhere(stage.stageArgs.where, { [childKey]: { in: inChunk } })
|
|
457
|
+
|
|
458
|
+
let injectedChildPath: string | null = null
|
|
459
|
+
|
|
460
|
+
if (baseSelect) {
|
|
461
|
+
if (baseSelect[childKey] !== true) {
|
|
462
|
+
finalArgs.select = { ...baseSelect, [childKey]: true }
|
|
463
|
+
injectedChildPath = stage.relationPath + '.' + childKey
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (baseOmit && baseOmit[childKey] === true) {
|
|
468
|
+
const nextOmit: Record<string, unknown> = {}
|
|
469
|
+
for (const [k, v] of Object.entries(baseOmit)) {
|
|
470
|
+
if (k === childKey) continue
|
|
471
|
+
nextOmit[k] = v
|
|
472
|
+
}
|
|
473
|
+
if (Object.keys(nextOmit).length > 0) {
|
|
474
|
+
finalArgs.omit = nextOmit
|
|
475
|
+
} else {
|
|
476
|
+
delete finalArgs.omit
|
|
477
|
+
}
|
|
478
|
+
injectedChildPath = stage.relationPath + '.' + childKey
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return { args: finalArgs, injectedChildPath }
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function collectDistinctParentValues(
|
|
485
|
+
rows: Record<string, unknown>[],
|
|
486
|
+
parentKey: string,
|
|
487
|
+
): unknown[] {
|
|
488
|
+
const seen = new Set<string>()
|
|
489
|
+
const out: unknown[] = []
|
|
490
|
+
for (const row of rows) {
|
|
491
|
+
const v = row[parentKey]
|
|
492
|
+
if (v === undefined || v === null) continue
|
|
493
|
+
const k = normalizeKey(v)
|
|
494
|
+
if (seen.has(k)) continue
|
|
495
|
+
seen.add(k)
|
|
496
|
+
out.push(v)
|
|
497
|
+
}
|
|
498
|
+
return out
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function groupRelatedRows(
|
|
502
|
+
children: unknown[],
|
|
503
|
+
childKey: string,
|
|
504
|
+
): Map<string, unknown[]> {
|
|
505
|
+
const grouped = new Map<string, unknown[]>()
|
|
506
|
+
for (const child of children) {
|
|
507
|
+
if (!isPlainObject(child)) continue
|
|
508
|
+
const k = child[childKey]
|
|
509
|
+
if (k === undefined || k === null) continue
|
|
510
|
+
const key = normalizeKey(k)
|
|
511
|
+
let arr = grouped.get(key)
|
|
512
|
+
if (!arr) {
|
|
513
|
+
arr = []
|
|
514
|
+
grouped.set(key, arr)
|
|
515
|
+
}
|
|
516
|
+
arr.push(child)
|
|
517
|
+
}
|
|
518
|
+
return grouped
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
async function runOneStageMany(options: {
|
|
522
|
+
extended: unknown
|
|
523
|
+
models: Record<string, ModelRelationMap>
|
|
524
|
+
stage: AutoIncludeStage
|
|
525
|
+
parentPairs: RowPair[]
|
|
526
|
+
internalFieldPaths: string[]
|
|
527
|
+
res: Response
|
|
528
|
+
isAborted: () => boolean
|
|
529
|
+
}): Promise<void> {
|
|
530
|
+
const { extended, models, stage, parentPairs, internalFieldPaths, res, isAborted } = options
|
|
531
|
+
|
|
532
|
+
if (isAborted()) return
|
|
533
|
+
|
|
534
|
+
const rel = stage.relationField
|
|
535
|
+
const parentKey = rel.parentLinkFields[0]
|
|
536
|
+
const childKey = rel.childLinkFields[0]
|
|
537
|
+
|
|
538
|
+
const targetModel = models[rel.type]
|
|
539
|
+
if (!targetModel) {
|
|
540
|
+
throw new Error('Target model not in relation metadata: ' + rel.type)
|
|
541
|
+
}
|
|
542
|
+
|
|
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)
|
|
552
|
+
const delegate: PrismaDelegate = getDelegate(extended, targetModel.delegateKey)
|
|
553
|
+
|
|
554
|
+
const children: unknown[] = []
|
|
555
|
+
let injectedChildPath: string | null = null
|
|
556
|
+
|
|
557
|
+
for (let i = 0; i < distinctValues.length; i += MAX_IN_CHUNK) {
|
|
558
|
+
if (isAborted()) return
|
|
559
|
+
const chunk = distinctValues.slice(i, i + MAX_IN_CHUNK)
|
|
560
|
+
const { args, injectedChildPath: ip } = buildStageQueryArgs(stage, childKey, chunk)
|
|
561
|
+
if (ip) injectedChildPath = ip
|
|
562
|
+
const partial = await delegate.findMany(args)
|
|
563
|
+
if (isAborted()) return
|
|
564
|
+
if (Array.isArray(partial)) {
|
|
565
|
+
for (const c of partial) children.push(c)
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const effectivePaths = injectedChildPath
|
|
570
|
+
? [...internalFieldPaths, injectedChildPath]
|
|
571
|
+
: internalFieldPaths
|
|
572
|
+
|
|
573
|
+
const grouped = groupRelatedRows(children, childKey)
|
|
574
|
+
const publicValues: unknown[] = new Array(parentPairs.length)
|
|
575
|
+
|
|
576
|
+
for (let i = 0; i < parentPairs.length; i++) {
|
|
577
|
+
const pair = parentPairs[i]
|
|
578
|
+
const fkVal = pair.internal[parentKey]
|
|
579
|
+
let internalVal: unknown
|
|
580
|
+
|
|
581
|
+
if (fkVal === undefined || fkVal === null) {
|
|
582
|
+
internalVal = emptyResultFor(rel.isList)
|
|
583
|
+
} else {
|
|
584
|
+
const matches = grouped.get(normalizeKey(fkVal)) ?? []
|
|
585
|
+
if (rel.isList) {
|
|
586
|
+
internalVal = matches
|
|
587
|
+
} else {
|
|
588
|
+
internalVal = matches.length > 0 ? matches[0] : null
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const publicVal = buildPublicForStage(internalVal, effectivePaths, stage.relationPath)
|
|
593
|
+
|
|
594
|
+
pair.internal[stage.relationName] = internalVal
|
|
595
|
+
pair.public[stage.relationName] = publicVal
|
|
596
|
+
publicValues[i] = publicVal
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (isAborted()) return
|
|
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 }
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
async function runAutoIncludeMany(
|
|
715
|
+
options: RunAutoIncludeOptions,
|
|
716
|
+
plan: AutoIncludePlan,
|
|
717
|
+
): Promise<void> {
|
|
718
|
+
const { res, ctx, delegateKey, models, signal } = options
|
|
719
|
+
|
|
720
|
+
const isClientGone = () =>
|
|
721
|
+
signal?.aborted === true || res.writableEnded || res.destroyed
|
|
722
|
+
|
|
723
|
+
let keepalive: IntervalHandle | null = null
|
|
724
|
+
try {
|
|
725
|
+
initSSE(res)
|
|
726
|
+
keepalive = startSSEKeepalive(res)
|
|
727
|
+
if (isClientGone()) return
|
|
728
|
+
|
|
729
|
+
const extended = await getExtendedClient(ctx)
|
|
730
|
+
if (isClientGone()) return
|
|
731
|
+
|
|
732
|
+
const rootDelegate = getDelegate(extended, delegateKey)
|
|
733
|
+
const rootArgs = applyPaginationLimits(plan.rootArgs, ctx.paginationConfig)
|
|
734
|
+
|
|
735
|
+
let rootResult: unknown
|
|
736
|
+
try {
|
|
737
|
+
rootResult = await rootDelegate.findMany(rootArgs)
|
|
738
|
+
} catch (err) {
|
|
739
|
+
if (isClientGone()) return
|
|
740
|
+
console.error('[auto-progressive] root findMany failed:', err)
|
|
741
|
+
sendSSEError(res, mapError(err).message)
|
|
742
|
+
return
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (isClientGone()) return
|
|
746
|
+
|
|
747
|
+
if (!Array.isArray(rootResult)) {
|
|
748
|
+
sendSSEError(res, 'auto-progressive: unexpected non-array root result for findMany')
|
|
749
|
+
return
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const { publicRows, rootPairs } = buildRootPairs(rootResult, plan.internalFieldPaths)
|
|
753
|
+
|
|
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)
|
|
766
|
+
}
|
|
767
|
+
return
|
|
768
|
+
}
|
|
769
|
+
|
|
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
|
+
}
|
|
782
|
+
|
|
783
|
+
async function runAutoIncludePaginated(
|
|
784
|
+
options: RunAutoIncludeOptions,
|
|
785
|
+
plan: AutoIncludePlan,
|
|
786
|
+
): Promise<void> {
|
|
787
|
+
const { res, ctx, delegateKey, models, signal } = options
|
|
788
|
+
|
|
789
|
+
const isClientGone = () =>
|
|
790
|
+
signal?.aborted === true || res.writableEnded || res.destroyed
|
|
791
|
+
|
|
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
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
if (isClientGone()) return
|
|
821
|
+
|
|
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) {
|
|
840
|
+
if (!res.writableEnded && !res.destroyed) {
|
|
841
|
+
sendSSEError(res, stageError)
|
|
842
|
+
}
|
|
843
|
+
return
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
if (res.writableEnded || res.destroyed) return
|
|
847
|
+
sendSSEResult(res, { data: publicRows, total, hasMore })
|
|
848
|
+
} catch (err) {
|
|
849
|
+
if (isClientGone()) return
|
|
850
|
+
console.error('[auto-progressive] paginated dispatch error:', err)
|
|
851
|
+
if (!res.writableEnded && !res.destroyed) {
|
|
852
|
+
sendSSEError(res, mapError(err).message)
|
|
853
|
+
}
|
|
854
|
+
} finally {
|
|
855
|
+
endSSE(res, keepalive)
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
export async function runAutoIncludeProgressive(
|
|
860
|
+
options: RunAutoIncludeOptions,
|
|
861
|
+
): Promise<void> {
|
|
862
|
+
if (options.ctx.guardShape) {
|
|
863
|
+
return handleAutoIncludeFallback(
|
|
864
|
+
options,
|
|
865
|
+
'auto-progressive fallback: guard shape disables auto-include',
|
|
866
|
+
)
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
const plan = planAutoInclude({
|
|
870
|
+
rootModelName: options.modelName,
|
|
871
|
+
models: options.models,
|
|
872
|
+
args: options.args,
|
|
873
|
+
})
|
|
874
|
+
|
|
875
|
+
if (plan.unsupportedReason) {
|
|
876
|
+
return handleAutoIncludeFallback(options, plan.unsupportedReason)
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
if (options.baseOp === 'findMany' || options.baseOp === 'findManyPaginated') {
|
|
880
|
+
const reason = findManyUnsupportedReason(plan)
|
|
881
|
+
if (reason) return handleAutoIncludeFallback(options, reason)
|
|
882
|
+
} else {
|
|
883
|
+
const reason = singleUnsupportedReason(plan)
|
|
884
|
+
if (reason) return handleAutoIncludeFallback(options, reason)
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
if (plan.stages.length === 0) {
|
|
888
|
+
return runSingleResultSSE({
|
|
889
|
+
req: options.req,
|
|
890
|
+
res: options.res,
|
|
891
|
+
coreQueryFn: options.coreQueryFn,
|
|
892
|
+
})
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
if (options.baseOp === 'findMany') {
|
|
896
|
+
return runAutoIncludeMany(options, plan)
|
|
897
|
+
}
|
|
898
|
+
if (options.baseOp === 'findManyPaginated') {
|
|
899
|
+
return runAutoIncludePaginated(options, plan)
|
|
900
|
+
}
|
|
901
|
+
return runAutoIncludeSingle(options, plan)
|
|
354
902
|
}
|
|
@@ -491,6 +491,26 @@ export function sendSSEResult(res: SseWritable, data: unknown): boolean {
|
|
|
491
491
|
return sendSSE(res, { type: 'result', data })
|
|
492
492
|
}
|
|
493
493
|
|
|
494
|
+
export function sendSSERootArray(res: SseWritable, rows: unknown[]): boolean {
|
|
495
|
+
return sendSSE(res, { type: 'rootArray', data: rows })
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
export function sendSSERelationBatch(
|
|
499
|
+
res: SseWritable,
|
|
500
|
+
relationPath: string,
|
|
501
|
+
values: unknown[],
|
|
502
|
+
): boolean {
|
|
503
|
+
return sendSSE(res, { type: 'relationBatch', relationPath, values })
|
|
504
|
+
}
|
|
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
|
+
|
|
494
514
|
export function sendSSEError(res: SseWritable, message: string): boolean {
|
|
495
515
|
if (res.writableEnded || res.destroyed) return false
|
|
496
516
|
try {
|
|
@@ -250,13 +250,14 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(config: ${m
|
|
|
250
250
|
}
|
|
251
251
|
|
|
252
252
|
if (progressiveConfig.mode === 'autoInclude') {
|
|
253
|
-
const
|
|
253
|
+
const isAutoIncludeReadable =
|
|
254
254
|
baseOp === 'findUnique' || baseOp === 'findUniqueOrThrow' ||
|
|
255
|
-
baseOp === 'findFirst' || baseOp === 'findFirstOrThrow'
|
|
255
|
+
baseOp === 'findFirst' || baseOp === 'findFirstOrThrow' ||
|
|
256
|
+
baseOp === 'findMany' || baseOp === 'findManyPaginated'
|
|
256
257
|
|
|
257
|
-
if (!
|
|
258
|
+
if (!isAutoIncludeReadable) {
|
|
258
259
|
if (progressiveConfig.fallback === 'error') {
|
|
259
|
-
emitTerminalSSEError(res, 'auto-progressive fallback: operation not
|
|
260
|
+
emitTerminalSSEError(res, 'auto-progressive fallback: operation not supported by auto-include')
|
|
260
261
|
return
|
|
261
262
|
}
|
|
262
263
|
await runSingleResultSSE({
|
|
@@ -278,7 +279,7 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(config: ${m
|
|
|
278
279
|
res,
|
|
279
280
|
ctx,
|
|
280
281
|
args,
|
|
281
|
-
baseOp: baseOp as 'findUnique' | 'findUniqueOrThrow' | 'findFirst' | 'findFirstOrThrow',
|
|
282
|
+
baseOp: baseOp as 'findUnique' | 'findUniqueOrThrow' | 'findFirst' | 'findFirstOrThrow' | 'findMany' | 'findManyPaginated',
|
|
282
283
|
modelName: '${modelName}',
|
|
283
284
|
delegateKey: '${delegateKey}',
|
|
284
285
|
models: relationModels,
|