prisma-generator-express 1.51.0 → 1.52.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
|
|
15
|
+
- Express-only progressive read streaming over Server-Sent Events (SSE), using manual stages or auto-include splitting for supported relation reads
|
|
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 limited `findMany` reads. 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`, direct root relations are loaded in batches and streamed as index-aligned relation batches.
|
|
1566
1566
|
|
|
1567
1567
|
### Request format
|
|
1568
1568
|
|
|
@@ -1594,14 +1594,17 @@ 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
|
+
|
|
1605
|
+
`findMany` support is intentionally narrower than single-record support. It supports direct root relation loading only, with additional fallback cases listed in [Auto-include behavior and limits](#auto-include-behavior-and-limits).
|
|
1603
1606
|
|
|
1604
|
-
If auto-include is configured on
|
|
1607
|
+
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
1608
|
|
|
1606
1609
|
Write operations do not support progressive SSE.
|
|
1607
1610
|
|
|
@@ -1627,6 +1630,18 @@ Nested field event:
|
|
|
1627
1630
|
{ "type": "field", "key": "profile.appliedTo", "value": [] }
|
|
1628
1631
|
```
|
|
1629
1632
|
|
|
1633
|
+
Root array event for `findMany` auto-include:
|
|
1634
|
+
|
|
1635
|
+
```json
|
|
1636
|
+
{ "type": "rootArray", "data": [{ "id": "user-1" }, { "id": "user-2" }] }
|
|
1637
|
+
```
|
|
1638
|
+
|
|
1639
|
+
Relation batch event for `findMany` auto-include:
|
|
1640
|
+
|
|
1641
|
+
```json
|
|
1642
|
+
{ "type": "relationBatch", "relationPath": "profile", "values": [{ "id": "profile-1" }, null] }
|
|
1643
|
+
```
|
|
1644
|
+
|
|
1630
1645
|
Final result event:
|
|
1631
1646
|
|
|
1632
1647
|
```json
|
|
@@ -1639,7 +1654,9 @@ Error event:
|
|
|
1639
1654
|
{ "type": "error", "message": "Could not load progressive response" }
|
|
1640
1655
|
```
|
|
1641
1656
|
|
|
1642
|
-
|
|
1657
|
+
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
|
+
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.
|
|
1643
1660
|
|
|
1644
1661
|
### Manual staged mode
|
|
1645
1662
|
|
|
@@ -1890,7 +1907,7 @@ const response = await fetch(`/user/unique?${params}`, {
|
|
|
1890
1907
|
})
|
|
1891
1908
|
```
|
|
1892
1909
|
|
|
1893
|
-
|
|
1910
|
+
On single-record reads, auto-include sends root scalar fields first, then sends relation field events as separate relation queries finish:
|
|
1894
1911
|
|
|
1895
1912
|
```json
|
|
1896
1913
|
{ "type": "field", "key": "id", "value": "user-id" }
|
|
@@ -1906,9 +1923,63 @@ Auto-include sends root scalar fields first, then sends relation field events as
|
|
|
1906
1923
|
|
|
1907
1924
|
The final `result` event contains the assembled object.
|
|
1908
1925
|
|
|
1926
|
+
For `findMany`, auto-include sends the root rows first, then sends one relation batch event per supported relation stage:
|
|
1927
|
+
|
|
1928
|
+
```ts
|
|
1929
|
+
const listConfig = {
|
|
1930
|
+
guard: {
|
|
1931
|
+
variantHeader: 'x-api-variant',
|
|
1932
|
+
},
|
|
1933
|
+
|
|
1934
|
+
findMany: {
|
|
1935
|
+
progressive: {
|
|
1936
|
+
list: {
|
|
1937
|
+
enabled: true,
|
|
1938
|
+
mode: 'autoInclude',
|
|
1939
|
+
fallback: 'singleResult',
|
|
1940
|
+
},
|
|
1941
|
+
},
|
|
1942
|
+
},
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
const params = encodeQueryParams({
|
|
1946
|
+
where: { isActive: true },
|
|
1947
|
+
take: 50,
|
|
1948
|
+
include: {
|
|
1949
|
+
profile: {
|
|
1950
|
+
select: {
|
|
1951
|
+
id: true,
|
|
1952
|
+
displayName: true,
|
|
1953
|
+
},
|
|
1954
|
+
},
|
|
1955
|
+
},
|
|
1956
|
+
})
|
|
1957
|
+
|
|
1958
|
+
const response = await fetch(`/user?${params}`, {
|
|
1959
|
+
headers: {
|
|
1960
|
+
Accept: 'text/event-stream',
|
|
1961
|
+
'x-api-variant': 'list',
|
|
1962
|
+
},
|
|
1963
|
+
})
|
|
1964
|
+
```
|
|
1965
|
+
|
|
1966
|
+
Example `findMany` auto-include event sequence:
|
|
1967
|
+
|
|
1968
|
+
```json
|
|
1969
|
+
{ "type": "rootArray", "data": [{ "id": "user-1" }, { "id": "user-2" }] }
|
|
1970
|
+
```
|
|
1971
|
+
|
|
1972
|
+
```json
|
|
1973
|
+
{ "type": "relationBatch", "relationPath": "profile", "values": [{ "id": "profile-1", "displayName": "Alice" }, null] }
|
|
1974
|
+
```
|
|
1975
|
+
|
|
1976
|
+
```json
|
|
1977
|
+
{ "type": "result", "data": [{ "id": "user-1", "profile": { "id": "profile-1", "displayName": "Alice" } }, { "id": "user-2", "profile": null }] }
|
|
1978
|
+
```
|
|
1979
|
+
|
|
1909
1980
|
### Auto-include behavior and limits
|
|
1910
1981
|
|
|
1911
|
-
Auto-include is designed for supported Prisma `include` and relation `select` trees on
|
|
1982
|
+
Auto-include is designed for supported Prisma `include` and relation `select` trees on reads.
|
|
1912
1983
|
|
|
1913
1984
|
Supported root operations:
|
|
1914
1985
|
|
|
@@ -1916,14 +1987,24 @@ Supported root operations:
|
|
|
1916
1987
|
- `findUniqueOrThrow`
|
|
1917
1988
|
- `findFirst`
|
|
1918
1989
|
- `findFirstOrThrow`
|
|
1990
|
+
- `findMany`
|
|
1919
1991
|
|
|
1920
|
-
Supported relation shapes:
|
|
1992
|
+
Supported single-record relation shapes:
|
|
1921
1993
|
|
|
1922
1994
|
- direct to-one relation includes/selects
|
|
1923
1995
|
- direct to-many relation includes/selects
|
|
1924
1996
|
- to-many relation args such as `where`, `orderBy`, `take`, `skip`, `cursor`, and `distinct`
|
|
1925
1997
|
- nested to-one relation loading through to-one parents
|
|
1926
1998
|
|
|
1999
|
+
Supported `findMany` relation shapes:
|
|
2000
|
+
|
|
2001
|
+
- direct root to-one relation includes/selects
|
|
2002
|
+
- direct root to-many relation includes/selects
|
|
2003
|
+
- relation-level `where` and `orderBy`
|
|
2004
|
+
- single-column link fields only
|
|
2005
|
+
|
|
2006
|
+
`findMany` auto-include applies 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
|
+
|
|
1927
2008
|
Current MVP fallback cases include:
|
|
1928
2009
|
|
|
1929
2010
|
- `_count` in `select` or `include`
|
|
@@ -1935,6 +2016,10 @@ Current MVP fallback cases include:
|
|
|
1935
2016
|
- nested relation loading through a to-many parent
|
|
1936
2017
|
- omitted required link fields needed to stitch parent and child records
|
|
1937
2018
|
- planner limits for maximum depth or stage count
|
|
2019
|
+
- `findMany` relation stages with composite link fields
|
|
2020
|
+
- `findMany` nested relation stages deeper than direct root relations
|
|
2021
|
+
- `findMany` to-many relation stages using `take`, `skip`, `cursor`, or `distinct`
|
|
2022
|
+
- `findManyPaginated`, `createManyAndReturn`, and `updateManyAndReturn`
|
|
1938
2023
|
|
|
1939
2024
|
When fallback happens:
|
|
1940
2025
|
|
|
@@ -1943,6 +2028,8 @@ When fallback happens:
|
|
|
1943
2028
|
|
|
1944
2029
|
If `fallback` is omitted, the default behavior is equivalent to `'singleResult'`.
|
|
1945
2030
|
|
|
2031
|
+
`findMany` auto-include uses a batched relation loading strategy. It does 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
|
+
|
|
1946
2033
|
Auto-include does not require `resolveContext` or `progressiveStages`.
|
|
1947
2034
|
|
|
1948
2035
|
### Hooks and guard behavior
|
|
@@ -1980,6 +2067,9 @@ if (!response.body) {
|
|
|
1980
2067
|
const reader = response.body.getReader()
|
|
1981
2068
|
const decoder = new TextDecoder()
|
|
1982
2069
|
let buffer = ''
|
|
2070
|
+
let rows: Array<Record<string, unknown>> = []
|
|
2071
|
+
let fields: Record<string, unknown> = {}
|
|
2072
|
+
let data: unknown = undefined
|
|
1983
2073
|
|
|
1984
2074
|
while (true) {
|
|
1985
2075
|
const { value, done } = await reader.read()
|
|
@@ -1998,12 +2088,23 @@ while (true) {
|
|
|
1998
2088
|
|
|
1999
2089
|
const event = JSON.parse(line.slice('data: '.length))
|
|
2000
2090
|
|
|
2091
|
+
if (event.type === 'rootArray') {
|
|
2092
|
+
rows = event.data
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2001
2095
|
if (event.type === 'field') {
|
|
2002
|
-
|
|
2096
|
+
fields[event.key] = event.value
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
if (event.type === 'relationBatch') {
|
|
2100
|
+
rows = rows.map((row, index) => ({
|
|
2101
|
+
...row,
|
|
2102
|
+
[event.relationPath]: event.values[index],
|
|
2103
|
+
}))
|
|
2003
2104
|
}
|
|
2004
2105
|
|
|
2005
2106
|
if (event.type === 'result') {
|
|
2006
|
-
|
|
2107
|
+
data = event.data
|
|
2007
2108
|
}
|
|
2008
2109
|
}
|
|
2009
2110
|
}
|
|
@@ -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'
|
|
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',
|
|
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.52.0",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"license": "MIT",
|
|
@@ -7,11 +7,14 @@ import {
|
|
|
7
7
|
sendSSEResult,
|
|
8
8
|
sendSSEError,
|
|
9
9
|
sendSSEProgress,
|
|
10
|
+
sendSSERootArray,
|
|
11
|
+
sendSSERelationBatch,
|
|
10
12
|
runSingleResultSSE,
|
|
11
13
|
emitTerminalSSEError,
|
|
12
14
|
setByPath,
|
|
13
15
|
getDelegate,
|
|
14
16
|
getExtendedClient,
|
|
17
|
+
applyPaginationLimits,
|
|
15
18
|
mapError,
|
|
16
19
|
type OperationContext,
|
|
17
20
|
type PrismaDelegate,
|
|
@@ -19,20 +22,30 @@ import {
|
|
|
19
22
|
import { isPlainObject } from './misc'
|
|
20
23
|
import {
|
|
21
24
|
planAutoInclude,
|
|
25
|
+
type AutoIncludePlan,
|
|
22
26
|
type ModelRelationMap,
|
|
23
27
|
type AutoIncludeStage,
|
|
24
28
|
} from './autoIncludePlanner'
|
|
25
29
|
import type { AutoIncludeProgressiveVariantConfig } from './routeConfig'
|
|
26
30
|
|
|
27
31
|
const STAGE_CONCURRENCY = 4
|
|
32
|
+
const MAX_IN_CHUNK = 1000
|
|
33
|
+
|
|
28
34
|
type IntervalHandle = ReturnType<typeof setInterval>
|
|
29
35
|
|
|
36
|
+
export type AutoIncludeBaseOp =
|
|
37
|
+
| 'findUnique'
|
|
38
|
+
| 'findUniqueOrThrow'
|
|
39
|
+
| 'findFirst'
|
|
40
|
+
| 'findFirstOrThrow'
|
|
41
|
+
| 'findMany'
|
|
42
|
+
|
|
30
43
|
export type RunAutoIncludeOptions = {
|
|
31
44
|
req: Request
|
|
32
45
|
res: Response
|
|
33
46
|
ctx: OperationContext
|
|
34
47
|
args: Record<string, unknown>
|
|
35
|
-
baseOp:
|
|
48
|
+
baseOp: AutoIncludeBaseOp
|
|
36
49
|
modelName: string
|
|
37
50
|
delegateKey: string
|
|
38
51
|
models: Record<string, ModelRelationMap>
|
|
@@ -122,7 +135,86 @@ function buildPublicForStage(
|
|
|
122
135
|
return result
|
|
123
136
|
}
|
|
124
137
|
|
|
125
|
-
|
|
138
|
+
function normalizeKey(v: unknown): string {
|
|
139
|
+
if (v === null || v === undefined) return '\u0000'
|
|
140
|
+
if (typeof v === 'bigint') return 'B' + v.toString()
|
|
141
|
+
if (v instanceof Date) return 'D' + v.getTime().toString()
|
|
142
|
+
if (typeof v === 'object') return 'O' + JSON.stringify(v)
|
|
143
|
+
return typeof v + ':' + String(v)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function groupStagesByDepth(stages: AutoIncludeStage[]): AutoIncludeStage[][] {
|
|
147
|
+
const byDepth = new Map<number, AutoIncludeStage[]>()
|
|
148
|
+
for (const s of stages) {
|
|
149
|
+
const arr = byDepth.get(s.depth)
|
|
150
|
+
if (arr) arr.push(s)
|
|
151
|
+
else byDepth.set(s.depth, [s])
|
|
152
|
+
}
|
|
153
|
+
return Array.from(byDepth.keys())
|
|
154
|
+
.sort((a, b) => a - b)
|
|
155
|
+
.map((d) => byDepth.get(d) as AutoIncludeStage[])
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function runConcurrent<T>(
|
|
159
|
+
items: T[],
|
|
160
|
+
limit: number,
|
|
161
|
+
fn: (item: T) => Promise<void>,
|
|
162
|
+
): Promise<void> {
|
|
163
|
+
let index = 0
|
|
164
|
+
const workers: Promise<void>[] = []
|
|
165
|
+
const workerCount = Math.min(limit, items.length)
|
|
166
|
+
for (let w = 0; w < workerCount; w++) {
|
|
167
|
+
workers.push((async () => {
|
|
168
|
+
for (;;) {
|
|
169
|
+
const i = index++
|
|
170
|
+
if (i >= items.length) return
|
|
171
|
+
await fn(items[i])
|
|
172
|
+
}
|
|
173
|
+
})())
|
|
174
|
+
}
|
|
175
|
+
await Promise.all(workers)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function handleAutoIncludeFallback(
|
|
179
|
+
options: RunAutoIncludeOptions,
|
|
180
|
+
message: string,
|
|
181
|
+
): Promise<void> {
|
|
182
|
+
if (options.variantConfig.fallback === 'error') {
|
|
183
|
+
emitTerminalSSEError(options.res, message)
|
|
184
|
+
return Promise.resolve()
|
|
185
|
+
}
|
|
186
|
+
return runSingleResultSSE({
|
|
187
|
+
req: options.req,
|
|
188
|
+
res: options.res,
|
|
189
|
+
coreQueryFn: options.coreQueryFn,
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function findManyUnsupportedReason(plan: AutoIncludePlan): string | null {
|
|
194
|
+
for (const stage of plan.stages) {
|
|
195
|
+
const rel = stage.relationField
|
|
196
|
+
if (rel.parentLinkFields.length !== 1 || rel.childLinkFields.length !== 1) {
|
|
197
|
+
return 'auto-progressive fallback: composite link fields not supported for findMany batched auto-include'
|
|
198
|
+
}
|
|
199
|
+
if (stage.depth > 1) {
|
|
200
|
+
return 'auto-progressive fallback: nested relations not supported for findMany batched auto-include'
|
|
201
|
+
}
|
|
202
|
+
if (rel.isList) {
|
|
203
|
+
const sa = stage.stageArgs
|
|
204
|
+
if (
|
|
205
|
+
sa.take !== undefined ||
|
|
206
|
+
sa.skip !== undefined ||
|
|
207
|
+
sa.cursor !== undefined ||
|
|
208
|
+
sa.distinct !== undefined
|
|
209
|
+
) {
|
|
210
|
+
return 'auto-progressive fallback: per-parent take/skip/cursor/distinct on to-many relations not supported for findMany batched auto-include'
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return null
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function runOneStageSingle(options: {
|
|
126
218
|
extended: unknown
|
|
127
219
|
models: Record<string, ModelRelationMap>
|
|
128
220
|
stage: AutoIncludeStage
|
|
@@ -186,72 +278,15 @@ async function runOneStage(options: {
|
|
|
186
278
|
sendSSEField(res, stage.relationPath, publicResult)
|
|
187
279
|
}
|
|
188
280
|
|
|
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(
|
|
281
|
+
async function runAutoIncludeSingle(
|
|
222
282
|
options: RunAutoIncludeOptions,
|
|
283
|
+
plan: AutoIncludePlan,
|
|
223
284
|
): Promise<void> {
|
|
224
|
-
const {
|
|
285
|
+
const { res, ctx, baseOp, delegateKey, models, signal } = options
|
|
225
286
|
|
|
226
287
|
const isClientGone = () =>
|
|
227
288
|
signal?.aborted === true || res.writableEnded || res.destroyed
|
|
228
289
|
|
|
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
290
|
let keepalive: IntervalHandle | null = null
|
|
256
291
|
try {
|
|
257
292
|
initSSE(res)
|
|
@@ -265,7 +300,7 @@ export async function runAutoIncludeProgressive(
|
|
|
265
300
|
|
|
266
301
|
let rootResult: unknown
|
|
267
302
|
try {
|
|
268
|
-
rootResult = await rootDelegate[baseOp](plan.rootArgs)
|
|
303
|
+
rootResult = await rootDelegate[baseOp as Exclude<AutoIncludeBaseOp, 'findMany'>](plan.rootArgs)
|
|
269
304
|
} catch (err) {
|
|
270
305
|
if (isClientGone()) return
|
|
271
306
|
console.error('[auto-progressive] root query failed:', err)
|
|
@@ -309,7 +344,7 @@ export async function runAutoIncludeProgressive(
|
|
|
309
344
|
await runConcurrent(group, STAGE_CONCURRENCY, async (stage) => {
|
|
310
345
|
if (isAborted()) return
|
|
311
346
|
try {
|
|
312
|
-
await
|
|
347
|
+
await runOneStageSingle({
|
|
313
348
|
extended,
|
|
314
349
|
models,
|
|
315
350
|
stage,
|
|
@@ -351,4 +386,317 @@ export async function runAutoIncludeProgressive(
|
|
|
351
386
|
} finally {
|
|
352
387
|
endSSE(res, keepalive)
|
|
353
388
|
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function buildStageQueryArgs(
|
|
392
|
+
stage: AutoIncludeStage,
|
|
393
|
+
childKey: string,
|
|
394
|
+
inChunk: unknown[],
|
|
395
|
+
): { args: Record<string, unknown>; injectedChildPath: string | null } {
|
|
396
|
+
const baseSelect = isPlainObject(stage.stageArgs.select)
|
|
397
|
+
? (stage.stageArgs.select as Record<string, unknown>)
|
|
398
|
+
: null
|
|
399
|
+
const baseOmit = isPlainObject(stage.stageArgs.omit)
|
|
400
|
+
? (stage.stageArgs.omit as Record<string, unknown>)
|
|
401
|
+
: null
|
|
402
|
+
|
|
403
|
+
const finalArgs: Record<string, unknown> = { ...stage.stageArgs }
|
|
404
|
+
finalArgs.where = mergeWhere(stage.stageArgs.where, { [childKey]: { in: inChunk } })
|
|
405
|
+
|
|
406
|
+
let injectedChildPath: string | null = null
|
|
407
|
+
|
|
408
|
+
if (baseSelect) {
|
|
409
|
+
if (baseSelect[childKey] !== true) {
|
|
410
|
+
finalArgs.select = { ...baseSelect, [childKey]: true }
|
|
411
|
+
injectedChildPath = stage.relationPath + '.' + childKey
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (baseOmit && baseOmit[childKey] === true) {
|
|
416
|
+
const nextOmit: Record<string, unknown> = {}
|
|
417
|
+
for (const [k, v] of Object.entries(baseOmit)) {
|
|
418
|
+
if (k === childKey) continue
|
|
419
|
+
nextOmit[k] = v
|
|
420
|
+
}
|
|
421
|
+
if (Object.keys(nextOmit).length > 0) {
|
|
422
|
+
finalArgs.omit = nextOmit
|
|
423
|
+
} else {
|
|
424
|
+
delete finalArgs.omit
|
|
425
|
+
}
|
|
426
|
+
injectedChildPath = stage.relationPath + '.' + childKey
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return { args: finalArgs, injectedChildPath }
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function collectDistinctParentValues(
|
|
433
|
+
rows: Record<string, unknown>[],
|
|
434
|
+
parentKey: string,
|
|
435
|
+
): unknown[] {
|
|
436
|
+
const seen = new Set<string>()
|
|
437
|
+
const out: unknown[] = []
|
|
438
|
+
for (const row of rows) {
|
|
439
|
+
const v = row[parentKey]
|
|
440
|
+
if (v === undefined || v === null) continue
|
|
441
|
+
const k = normalizeKey(v)
|
|
442
|
+
if (seen.has(k)) continue
|
|
443
|
+
seen.add(k)
|
|
444
|
+
out.push(v)
|
|
445
|
+
}
|
|
446
|
+
return out
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function groupRelatedRows(
|
|
450
|
+
children: unknown[],
|
|
451
|
+
childKey: string,
|
|
452
|
+
): Map<string, unknown[]> {
|
|
453
|
+
const grouped = new Map<string, unknown[]>()
|
|
454
|
+
for (const child of children) {
|
|
455
|
+
if (!isPlainObject(child)) continue
|
|
456
|
+
const k = child[childKey]
|
|
457
|
+
if (k === undefined || k === null) continue
|
|
458
|
+
const key = normalizeKey(k)
|
|
459
|
+
let arr = grouped.get(key)
|
|
460
|
+
if (!arr) {
|
|
461
|
+
arr = []
|
|
462
|
+
grouped.set(key, arr)
|
|
463
|
+
}
|
|
464
|
+
arr.push(child)
|
|
465
|
+
}
|
|
466
|
+
return grouped
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
async function runOneStageMany(options: {
|
|
470
|
+
extended: unknown
|
|
471
|
+
models: Record<string, ModelRelationMap>
|
|
472
|
+
stage: AutoIncludeStage
|
|
473
|
+
internalRows: Record<string, unknown>[]
|
|
474
|
+
publicRows: Record<string, unknown>[]
|
|
475
|
+
internalFieldPaths: string[]
|
|
476
|
+
res: Response
|
|
477
|
+
isAborted: () => boolean
|
|
478
|
+
}): Promise<void> {
|
|
479
|
+
const {
|
|
480
|
+
extended, models, stage, internalRows, publicRows,
|
|
481
|
+
internalFieldPaths, res, isAborted,
|
|
482
|
+
} = options
|
|
483
|
+
|
|
484
|
+
if (isAborted()) return
|
|
485
|
+
|
|
486
|
+
const rel = stage.relationField
|
|
487
|
+
const parentKey = rel.parentLinkFields[0]
|
|
488
|
+
const childKey = rel.childLinkFields[0]
|
|
489
|
+
|
|
490
|
+
const targetModel = models[rel.type]
|
|
491
|
+
if (!targetModel) {
|
|
492
|
+
throw new Error('Target model not in relation metadata: ' + rel.type)
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const distinctValues = collectDistinctParentValues(internalRows, parentKey)
|
|
496
|
+
const delegate: PrismaDelegate = getDelegate(extended, targetModel.delegateKey)
|
|
497
|
+
|
|
498
|
+
const children: unknown[] = []
|
|
499
|
+
let injectedChildPath: string | null = null
|
|
500
|
+
|
|
501
|
+
for (let i = 0; i < distinctValues.length; i += MAX_IN_CHUNK) {
|
|
502
|
+
if (isAborted()) return
|
|
503
|
+
const chunk = distinctValues.slice(i, i + MAX_IN_CHUNK)
|
|
504
|
+
const { args, injectedChildPath: ip } = buildStageQueryArgs(stage, childKey, chunk)
|
|
505
|
+
if (ip) injectedChildPath = ip
|
|
506
|
+
const partial = await delegate.findMany(args)
|
|
507
|
+
if (isAborted()) return
|
|
508
|
+
if (Array.isArray(partial)) {
|
|
509
|
+
for (const c of partial) children.push(c)
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const effectivePaths = injectedChildPath
|
|
514
|
+
? [...internalFieldPaths, injectedChildPath]
|
|
515
|
+
: internalFieldPaths
|
|
516
|
+
|
|
517
|
+
const grouped = groupRelatedRows(children, childKey)
|
|
518
|
+
const publicValues: unknown[] = new Array(internalRows.length)
|
|
519
|
+
|
|
520
|
+
for (let i = 0; i < internalRows.length; i++) {
|
|
521
|
+
const row = internalRows[i]
|
|
522
|
+
const fkVal = row[parentKey]
|
|
523
|
+
let internalVal: unknown
|
|
524
|
+
|
|
525
|
+
if (fkVal === undefined || fkVal === null) {
|
|
526
|
+
internalVal = emptyResultFor(rel.isList)
|
|
527
|
+
} else {
|
|
528
|
+
const matches = grouped.get(normalizeKey(fkVal)) ?? []
|
|
529
|
+
if (rel.isList) {
|
|
530
|
+
internalVal = matches
|
|
531
|
+
} else {
|
|
532
|
+
internalVal = matches.length > 0 ? matches[0] : null
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const publicVal = buildPublicForStage(internalVal, effectivePaths, stage.relationPath)
|
|
537
|
+
|
|
538
|
+
internalRows[i][stage.relationName] = internalVal
|
|
539
|
+
publicRows[i][stage.relationName] = publicVal
|
|
540
|
+
publicValues[i] = publicVal
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (isAborted()) return
|
|
544
|
+
sendSSERelationBatch(res, stage.relationPath, publicValues)
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
async function runAutoIncludeMany(
|
|
548
|
+
options: RunAutoIncludeOptions,
|
|
549
|
+
plan: AutoIncludePlan,
|
|
550
|
+
): Promise<void> {
|
|
551
|
+
const { res, ctx, delegateKey, models, signal } = options
|
|
552
|
+
|
|
553
|
+
const isClientGone = () =>
|
|
554
|
+
signal?.aborted === true || res.writableEnded || res.destroyed
|
|
555
|
+
|
|
556
|
+
let keepalive: IntervalHandle | null = null
|
|
557
|
+
try {
|
|
558
|
+
initSSE(res)
|
|
559
|
+
keepalive = startSSEKeepalive(res)
|
|
560
|
+
if (isClientGone()) return
|
|
561
|
+
|
|
562
|
+
const extended = await getExtendedClient(ctx)
|
|
563
|
+
if (isClientGone()) return
|
|
564
|
+
|
|
565
|
+
const rootDelegate = getDelegate(extended, delegateKey)
|
|
566
|
+
const rootArgs = applyPaginationLimits(plan.rootArgs, ctx.paginationConfig)
|
|
567
|
+
|
|
568
|
+
let rootResult: unknown
|
|
569
|
+
try {
|
|
570
|
+
rootResult = await rootDelegate.findMany(rootArgs)
|
|
571
|
+
} catch (err) {
|
|
572
|
+
if (isClientGone()) return
|
|
573
|
+
console.error('[auto-progressive] root findMany failed:', err)
|
|
574
|
+
sendSSEError(res, mapError(err).message)
|
|
575
|
+
return
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (isClientGone()) return
|
|
579
|
+
|
|
580
|
+
if (!Array.isArray(rootResult)) {
|
|
581
|
+
sendSSEError(res, 'auto-progressive: unexpected non-array root result for findMany')
|
|
582
|
+
return
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const internalRows: Record<string, unknown>[] = new Array(rootResult.length)
|
|
586
|
+
const publicRows: Record<string, unknown>[] = new Array(rootResult.length)
|
|
587
|
+
|
|
588
|
+
for (let i = 0; i < rootResult.length; i++) {
|
|
589
|
+
const row = rootResult[i]
|
|
590
|
+
if (!isPlainObject(row)) {
|
|
591
|
+
internalRows[i] = {}
|
|
592
|
+
publicRows[i] = {}
|
|
593
|
+
continue
|
|
594
|
+
}
|
|
595
|
+
const internalCopy: Record<string, unknown> = { ...row }
|
|
596
|
+
const publicCopy: Record<string, unknown> = { ...row }
|
|
597
|
+
stripInternalAtScope(publicCopy, plan.internalFieldPaths, '')
|
|
598
|
+
internalRows[i] = internalCopy
|
|
599
|
+
publicRows[i] = publicCopy
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
sendSSERootArray(res, publicRows)
|
|
603
|
+
sendSSEProgress(res, 'root', 0, plan.stages.length)
|
|
604
|
+
|
|
605
|
+
const groups = groupStagesByDepth(plan.stages)
|
|
606
|
+
let completed = 0
|
|
607
|
+
let stageErrorMessage: string | null = null
|
|
608
|
+
const isAborted = () =>
|
|
609
|
+
stageErrorMessage !== null ||
|
|
610
|
+
signal?.aborted === true ||
|
|
611
|
+
res.writableEnded ||
|
|
612
|
+
res.destroyed
|
|
613
|
+
|
|
614
|
+
for (const group of groups) {
|
|
615
|
+
if (isClientGone()) return
|
|
616
|
+
if (stageErrorMessage) break
|
|
617
|
+
|
|
618
|
+
await runConcurrent(group, STAGE_CONCURRENCY, async (stage) => {
|
|
619
|
+
if (isAborted()) return
|
|
620
|
+
try {
|
|
621
|
+
await runOneStageMany({
|
|
622
|
+
extended,
|
|
623
|
+
models,
|
|
624
|
+
stage,
|
|
625
|
+
internalRows,
|
|
626
|
+
publicRows,
|
|
627
|
+
internalFieldPaths: plan.internalFieldPaths,
|
|
628
|
+
res,
|
|
629
|
+
isAborted,
|
|
630
|
+
})
|
|
631
|
+
} catch (err) {
|
|
632
|
+
if (isAborted()) return
|
|
633
|
+
console.error('[auto-progressive] stage failed:', stage.relationPath, err)
|
|
634
|
+
stageErrorMessage = mapError(err).message
|
|
635
|
+
return
|
|
636
|
+
}
|
|
637
|
+
if (isAborted()) return
|
|
638
|
+
completed++
|
|
639
|
+
sendSSEProgress(res, stage.relationPath, completed, plan.stages.length)
|
|
640
|
+
})
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (isClientGone()) return
|
|
644
|
+
|
|
645
|
+
if (stageErrorMessage) {
|
|
646
|
+
if (!res.writableEnded && !res.destroyed) {
|
|
647
|
+
sendSSEError(res, stageErrorMessage)
|
|
648
|
+
}
|
|
649
|
+
return
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if (res.writableEnded || res.destroyed) return
|
|
653
|
+
sendSSEResult(res, publicRows)
|
|
654
|
+
} catch (err) {
|
|
655
|
+
if (isClientGone()) return
|
|
656
|
+
console.error('[auto-progressive] many dispatch error:', err)
|
|
657
|
+
if (!res.writableEnded && !res.destroyed) {
|
|
658
|
+
sendSSEError(res, mapError(err).message)
|
|
659
|
+
}
|
|
660
|
+
} finally {
|
|
661
|
+
endSSE(res, keepalive)
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
export async function runAutoIncludeProgressive(
|
|
666
|
+
options: RunAutoIncludeOptions,
|
|
667
|
+
): Promise<void> {
|
|
668
|
+
if (options.ctx.guardShape) {
|
|
669
|
+
return handleAutoIncludeFallback(
|
|
670
|
+
options,
|
|
671
|
+
'auto-progressive fallback: guard shape disables auto-include',
|
|
672
|
+
)
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const plan = planAutoInclude({
|
|
676
|
+
rootModelName: options.modelName,
|
|
677
|
+
models: options.models,
|
|
678
|
+
args: options.args,
|
|
679
|
+
})
|
|
680
|
+
|
|
681
|
+
if (plan.unsupportedReason) {
|
|
682
|
+
return handleAutoIncludeFallback(options, plan.unsupportedReason)
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (options.baseOp === 'findMany') {
|
|
686
|
+
const reason = findManyUnsupportedReason(plan)
|
|
687
|
+
if (reason) return handleAutoIncludeFallback(options, reason)
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if (plan.stages.length === 0) {
|
|
691
|
+
return runSingleResultSSE({
|
|
692
|
+
req: options.req,
|
|
693
|
+
res: options.res,
|
|
694
|
+
coreQueryFn: options.coreQueryFn,
|
|
695
|
+
})
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (options.baseOp === 'findMany') {
|
|
699
|
+
return runAutoIncludeMany(options, plan)
|
|
700
|
+
}
|
|
701
|
+
return runAutoIncludeSingle(options, plan)
|
|
354
702
|
}
|
|
@@ -491,6 +491,18 @@ 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
|
+
|
|
494
506
|
export function sendSSEError(res: SseWritable, message: string): boolean {
|
|
495
507
|
if (res.writableEnded || res.destroyed) return false
|
|
496
508
|
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'
|
|
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',
|
|
282
283
|
modelName: '${modelName}',
|
|
283
284
|
delegateKey: '${delegateKey}',
|
|
284
285
|
models: relationModels,
|