prisma-generator-express 1.53.0 → 1.54.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 +64 -9
- package/package.json +1 -1
- package/src/copy/autoIncludeRuntime.ts +52 -24
- package/src/copy/operationRuntime.ts +9 -0
package/README.md
CHANGED
|
@@ -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` and `findManyPaginated`, root rows are streamed first, direct root relation stages are streamed as index-aligned relation batches, and deeper nested relation stages are
|
|
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 streamed as locator-based nested relation batches. The terminal `result` event still contains the fully assembled payload.
|
|
1566
1566
|
|
|
1567
1567
|
### Request format
|
|
1568
1568
|
|
|
@@ -1643,6 +1643,21 @@ Relation batch event for a direct root relation in `findMany` / `findManyPaginat
|
|
|
1643
1643
|
{ "type": "relationBatch", "relationPath": "profile", "values": [{ "id": "profile-1" }, null] }
|
|
1644
1644
|
```
|
|
1645
1645
|
|
|
1646
|
+
Nested relation batch event for a depth-2-or-deeper relation in `findMany` / `findManyPaginated` auto-include:
|
|
1647
|
+
|
|
1648
|
+
```json
|
|
1649
|
+
{
|
|
1650
|
+
"type": "nestedRelationBatch",
|
|
1651
|
+
"relationPath": "companies.users",
|
|
1652
|
+
"depth": 2,
|
|
1653
|
+
"attachments": [
|
|
1654
|
+
{ "locator": [0, "companies", 0], "value": [{ "id": "user-1" }] }
|
|
1655
|
+
]
|
|
1656
|
+
}
|
|
1657
|
+
```
|
|
1658
|
+
|
|
1659
|
+
Each `attachments[].locator` is walked from `rootArray.data` to the parent object. The leaf field to assign is the last segment of `relationPath`. For example, `relationPath: "companies.users"` and `locator: [0, "companies", 0]` means `rootArray.data[0].companies[0].users = value`.
|
|
1660
|
+
|
|
1646
1661
|
Final result event:
|
|
1647
1662
|
|
|
1648
1663
|
```json
|
|
@@ -1657,7 +1672,7 @@ Error event:
|
|
|
1657
1672
|
|
|
1658
1673
|
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
1674
|
|
|
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]`.
|
|
1675
|
+
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]`. Each depth-2-or-deeper `nestedRelationBatch.attachments` array carries locator/value pairs that can be applied to the accumulated root rows immediately. The terminal `result.data` is the fully merged array and can be used as a final reconcile.
|
|
1661
1676
|
|
|
1662
1677
|
For `findManyPaginated` auto-include responses, `pageMeta` is sent before `rootArray`. The terminal `result.data` has the normal paginated shape: `{ data, total, hasMore }`.
|
|
1663
1678
|
|
|
@@ -1926,7 +1941,7 @@ On single-record reads, auto-include sends root scalar fields first, then sends
|
|
|
1926
1941
|
|
|
1927
1942
|
The final `result` event contains the assembled object.
|
|
1928
1943
|
|
|
1929
|
-
For `findMany`, auto-include sends the root rows first, then sends one relation batch event for each supported direct root relation stage.
|
|
1944
|
+
For `findMany`, auto-include sends the root rows first, then sends one relation batch event for each supported direct root relation stage. Depth-2-or-deeper stages send `nestedRelationBatch` events with locators pointing to the parent object inside the accumulated root rows:
|
|
1930
1945
|
|
|
1931
1946
|
```ts
|
|
1932
1947
|
const listConfig = {
|
|
@@ -1980,7 +1995,7 @@ Example shallow `findMany` auto-include event sequence:
|
|
|
1980
1995
|
{ "type": "result", "data": [{ "id": "user-1", "profile": { "id": "profile-1", "displayName": "Alice" } }, { "id": "user-2", "profile": null }] }
|
|
1981
1996
|
```
|
|
1982
1997
|
|
|
1983
|
-
Deep `findMany` and `findManyPaginated` requests use the same planner. Nested stages are loaded by flattening the parent rows at each path:
|
|
1998
|
+
Deep `findMany` and `findManyPaginated` requests use the same planner. Nested stages are loaded by flattening the parent rows at each path, then emitted as locator-based attachment batches:
|
|
1984
1999
|
|
|
1985
2000
|
```ts
|
|
1986
2001
|
const params = encodeQueryParams({
|
|
@@ -2026,11 +2041,25 @@ Example deep event sequence:
|
|
|
2026
2041
|
```
|
|
2027
2042
|
|
|
2028
2043
|
```json
|
|
2029
|
-
{
|
|
2044
|
+
{
|
|
2045
|
+
"type": "nestedRelationBatch",
|
|
2046
|
+
"relationPath": "companies.users",
|
|
2047
|
+
"depth": 2,
|
|
2048
|
+
"attachments": [
|
|
2049
|
+
{ "locator": [0, "companies", 0], "value": [{ "id": "user-1" }] }
|
|
2050
|
+
]
|
|
2051
|
+
}
|
|
2030
2052
|
```
|
|
2031
2053
|
|
|
2032
2054
|
```json
|
|
2033
|
-
{
|
|
2055
|
+
{
|
|
2056
|
+
"type": "nestedRelationBatch",
|
|
2057
|
+
"relationPath": "companies.users.profile",
|
|
2058
|
+
"depth": 3,
|
|
2059
|
+
"attachments": [
|
|
2060
|
+
{ "locator": [0, "companies", 0, "users", 0], "value": { "id": "profile-1", "displayName": "Alice" } }
|
|
2061
|
+
]
|
|
2062
|
+
}
|
|
2034
2063
|
```
|
|
2035
2064
|
|
|
2036
2065
|
```json
|
|
@@ -2068,7 +2097,7 @@ Supported `findMany` and `findManyPaginated` relation shapes:
|
|
|
2068
2097
|
- single-column link fields only
|
|
2069
2098
|
- nested depth up to the configured planner limit
|
|
2070
2099
|
|
|
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
|
|
2100
|
+
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 stream `relationBatch` events. Depth-2-or-deeper stages stream `nestedRelationBatch` events with locator/value attachments, then also appear in the terminal `result` event.
|
|
2072
2101
|
|
|
2073
2102
|
`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.
|
|
2074
2103
|
|
|
@@ -2137,6 +2166,20 @@ let rows: Array<Record<string, unknown>> = []
|
|
|
2137
2166
|
let fields: Record<string, unknown> = {}
|
|
2138
2167
|
let data: unknown = undefined
|
|
2139
2168
|
|
|
2169
|
+
const lastSegment = (path: string) => {
|
|
2170
|
+
const parts = path.split('.')
|
|
2171
|
+
return parts[parts.length - 1] ?? path
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
const walk = (source: Array<Record<string, unknown>>, locator: Array<number | string>) => {
|
|
2175
|
+
let cursor: unknown = source[locator[0] as number]
|
|
2176
|
+
for (let i = 1; i < locator.length; i++) {
|
|
2177
|
+
if (cursor == null) return null
|
|
2178
|
+
cursor = (cursor as Record<string | number, unknown>)[locator[i]]
|
|
2179
|
+
}
|
|
2180
|
+
return cursor
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2140
2183
|
while (true) {
|
|
2141
2184
|
const { value, done } = await reader.read()
|
|
2142
2185
|
if (done) break
|
|
@@ -2163,12 +2206,24 @@ while (true) {
|
|
|
2163
2206
|
}
|
|
2164
2207
|
|
|
2165
2208
|
if (event.type === 'relationBatch') {
|
|
2209
|
+
const field = lastSegment(event.relationPath)
|
|
2166
2210
|
rows = rows.map((row, index) => ({
|
|
2167
2211
|
...row,
|
|
2168
|
-
[
|
|
2212
|
+
[field]: event.values[index],
|
|
2169
2213
|
}))
|
|
2170
2214
|
}
|
|
2171
2215
|
|
|
2216
|
+
if (event.type === 'nestedRelationBatch') {
|
|
2217
|
+
const field = lastSegment(event.relationPath)
|
|
2218
|
+
for (const attachment of event.attachments) {
|
|
2219
|
+
const parent = walk(rows, attachment.locator)
|
|
2220
|
+
if (parent && typeof parent === 'object' && !Array.isArray(parent)) {
|
|
2221
|
+
const record = parent as Record<string, unknown>
|
|
2222
|
+
record[field] = attachment.value
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2172
2227
|
if (event.type === 'result') {
|
|
2173
2228
|
data = event.data
|
|
2174
2229
|
}
|
|
@@ -2184,7 +2239,7 @@ For React Query, include the variant and mode in the query key:
|
|
|
2184
2239
|
|
|
2185
2240
|
Do not reuse the same query key as the JSON endpoint because the same URL can return different shapes depending on `x-api-variant`.
|
|
2186
2241
|
|
|
2187
|
-
For deep `findMany` / `findManyPaginated` auto-include,
|
|
2242
|
+
For deep `findMany` / `findManyPaginated` auto-include, apply `nestedRelationBatch` events for progressive rendering and still treat the final `result` event as the authoritative nested payload for reconciliation. Direct root relation stages emit `relationBatch`; depth-2-or-deeper relation stages emit `nestedRelationBatch`.
|
|
2188
2243
|
|
|
2189
2244
|
### Runtime notes
|
|
2190
2245
|
|
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.54.0",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"license": "MIT",
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
sendSSEProgress,
|
|
10
10
|
sendSSERootArray,
|
|
11
11
|
sendSSERelationBatch,
|
|
12
|
+
sendSSENestedRelationBatch,
|
|
12
13
|
sendSSEPageMeta,
|
|
13
14
|
runSingleResultSSE,
|
|
14
15
|
emitTerminalSSEError,
|
|
@@ -62,6 +63,10 @@ type RowPair = {
|
|
|
62
63
|
public: Record<string, unknown>
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
type ParentEntry = RowPair & {
|
|
67
|
+
locator: Array<number | string>
|
|
68
|
+
}
|
|
69
|
+
|
|
65
70
|
function readPath(source: Record<string, unknown>, path: string): unknown {
|
|
66
71
|
if (path === '') return source
|
|
67
72
|
const parts = path.split('.')
|
|
@@ -235,35 +240,49 @@ function singleUnsupportedReason(plan: AutoIncludePlan): string | null {
|
|
|
235
240
|
return null
|
|
236
241
|
}
|
|
237
242
|
|
|
238
|
-
function
|
|
243
|
+
function collectParentEntries(
|
|
239
244
|
rootPairs: RowPair[],
|
|
240
245
|
parentPath: string,
|
|
241
|
-
):
|
|
242
|
-
|
|
243
|
-
|
|
246
|
+
): ParentEntry[] {
|
|
247
|
+
const initial: ParentEntry[] = rootPairs.map((p, i) => ({
|
|
248
|
+
internal: p.internal,
|
|
249
|
+
public: p.public,
|
|
250
|
+
locator: [i],
|
|
251
|
+
}))
|
|
252
|
+
if (parentPath === '') return initial
|
|
253
|
+
|
|
254
|
+
let entries = initial
|
|
244
255
|
for (const segment of parentPath.split('.')) {
|
|
245
|
-
const next:
|
|
246
|
-
for (const
|
|
247
|
-
const internalValue =
|
|
248
|
-
const publicValue =
|
|
256
|
+
const next: ParentEntry[] = []
|
|
257
|
+
for (const entry of entries) {
|
|
258
|
+
const internalValue = entry.internal[segment]
|
|
259
|
+
const publicValue = entry.public[segment]
|
|
249
260
|
if (Array.isArray(internalValue) && Array.isArray(publicValue)) {
|
|
250
261
|
const length = Math.min(internalValue.length, publicValue.length)
|
|
251
262
|
for (let i = 0; i < length; i++) {
|
|
252
263
|
const internalItem = internalValue[i]
|
|
253
264
|
const publicItem = publicValue[i]
|
|
254
265
|
if (isPlainObject(internalItem) && isPlainObject(publicItem)) {
|
|
255
|
-
next.push({
|
|
266
|
+
next.push({
|
|
267
|
+
internal: internalItem,
|
|
268
|
+
public: publicItem,
|
|
269
|
+
locator: [...entry.locator, segment, i],
|
|
270
|
+
})
|
|
256
271
|
}
|
|
257
272
|
}
|
|
258
273
|
continue
|
|
259
274
|
}
|
|
260
275
|
if (isPlainObject(internalValue) && isPlainObject(publicValue)) {
|
|
261
|
-
next.push({
|
|
276
|
+
next.push({
|
|
277
|
+
internal: internalValue,
|
|
278
|
+
public: publicValue,
|
|
279
|
+
locator: [...entry.locator, segment],
|
|
280
|
+
})
|
|
262
281
|
}
|
|
263
282
|
}
|
|
264
|
-
|
|
283
|
+
entries = next
|
|
265
284
|
}
|
|
266
|
-
return
|
|
285
|
+
return entries
|
|
267
286
|
}
|
|
268
287
|
|
|
269
288
|
async function runOneStageSingle(options: {
|
|
@@ -522,12 +541,12 @@ async function runOneStageMany(options: {
|
|
|
522
541
|
extended: unknown
|
|
523
542
|
models: Record<string, ModelRelationMap>
|
|
524
543
|
stage: AutoIncludeStage
|
|
525
|
-
|
|
544
|
+
parentEntries: ParentEntry[]
|
|
526
545
|
internalFieldPaths: string[]
|
|
527
546
|
res: Response
|
|
528
547
|
isAborted: () => boolean
|
|
529
548
|
}): Promise<void> {
|
|
530
|
-
const { extended, models, stage,
|
|
549
|
+
const { extended, models, stage, parentEntries, internalFieldPaths, res, isAborted } = options
|
|
531
550
|
|
|
532
551
|
if (isAborted()) return
|
|
533
552
|
|
|
@@ -540,14 +559,16 @@ async function runOneStageMany(options: {
|
|
|
540
559
|
throw new Error('Target model not in relation metadata: ' + rel.type)
|
|
541
560
|
}
|
|
542
561
|
|
|
543
|
-
if (
|
|
562
|
+
if (parentEntries.length === 0) {
|
|
544
563
|
if (stage.depth === 1) {
|
|
545
564
|
sendSSERelationBatch(res, stage.relationPath, [])
|
|
565
|
+
} else {
|
|
566
|
+
sendSSENestedRelationBatch(res, stage.relationPath, stage.depth, [])
|
|
546
567
|
}
|
|
547
568
|
return
|
|
548
569
|
}
|
|
549
570
|
|
|
550
|
-
const internalParents =
|
|
571
|
+
const internalParents = parentEntries.map((p) => p.internal)
|
|
551
572
|
const distinctValues = collectDistinctParentValues(internalParents, parentKey)
|
|
552
573
|
const delegate: PrismaDelegate = getDelegate(extended, targetModel.delegateKey)
|
|
553
574
|
|
|
@@ -571,11 +592,11 @@ async function runOneStageMany(options: {
|
|
|
571
592
|
: internalFieldPaths
|
|
572
593
|
|
|
573
594
|
const grouped = groupRelatedRows(children, childKey)
|
|
574
|
-
const publicValues: unknown[] = new Array(
|
|
595
|
+
const publicValues: unknown[] = new Array(parentEntries.length)
|
|
575
596
|
|
|
576
|
-
for (let i = 0; i <
|
|
577
|
-
const
|
|
578
|
-
const fkVal =
|
|
597
|
+
for (let i = 0; i < parentEntries.length; i++) {
|
|
598
|
+
const entry = parentEntries[i]
|
|
599
|
+
const fkVal = entry.internal[parentKey]
|
|
579
600
|
let internalVal: unknown
|
|
580
601
|
|
|
581
602
|
if (fkVal === undefined || fkVal === null) {
|
|
@@ -591,8 +612,8 @@ async function runOneStageMany(options: {
|
|
|
591
612
|
|
|
592
613
|
const publicVal = buildPublicForStage(internalVal, effectivePaths, stage.relationPath)
|
|
593
614
|
|
|
594
|
-
|
|
595
|
-
|
|
615
|
+
entry.internal[stage.relationName] = internalVal
|
|
616
|
+
entry.public[stage.relationName] = publicVal
|
|
596
617
|
publicValues[i] = publicVal
|
|
597
618
|
}
|
|
598
619
|
|
|
@@ -600,7 +621,14 @@ async function runOneStageMany(options: {
|
|
|
600
621
|
|
|
601
622
|
if (stage.depth === 1) {
|
|
602
623
|
sendSSERelationBatch(res, stage.relationPath, publicValues)
|
|
624
|
+
return
|
|
603
625
|
}
|
|
626
|
+
|
|
627
|
+
const attachments = parentEntries.map((entry, i) => ({
|
|
628
|
+
locator: entry.locator,
|
|
629
|
+
value: publicValues[i],
|
|
630
|
+
}))
|
|
631
|
+
sendSSENestedRelationBatch(res, stage.relationPath, stage.depth, attachments)
|
|
604
632
|
}
|
|
605
633
|
|
|
606
634
|
function buildRootPairs(
|
|
@@ -652,13 +680,13 @@ async function processFindManyStages(args: {
|
|
|
652
680
|
|
|
653
681
|
await runConcurrent(group, STAGE_CONCURRENCY, async (stage) => {
|
|
654
682
|
if (isAborted()) return
|
|
655
|
-
const
|
|
683
|
+
const parentEntries = collectParentEntries(rootPairs, stage.parentPath)
|
|
656
684
|
try {
|
|
657
685
|
await runOneStageMany({
|
|
658
686
|
extended,
|
|
659
687
|
models,
|
|
660
688
|
stage,
|
|
661
|
-
|
|
689
|
+
parentEntries,
|
|
662
690
|
internalFieldPaths: plan.internalFieldPaths,
|
|
663
691
|
res,
|
|
664
692
|
isAborted,
|
|
@@ -503,6 +503,15 @@ export function sendSSERelationBatch(
|
|
|
503
503
|
return sendSSE(res, { type: 'relationBatch', relationPath, values })
|
|
504
504
|
}
|
|
505
505
|
|
|
506
|
+
export function sendSSENestedRelationBatch(
|
|
507
|
+
res: SseWritable,
|
|
508
|
+
relationPath: string,
|
|
509
|
+
depth: number,
|
|
510
|
+
attachments: Array<{ locator: Array<number | string>; value: unknown }>,
|
|
511
|
+
): boolean {
|
|
512
|
+
return sendSSE(res, { type: 'nestedRelationBatch', relationPath, depth, attachments })
|
|
513
|
+
}
|
|
514
|
+
|
|
506
515
|
export function sendSSEPageMeta(
|
|
507
516
|
res: SseWritable,
|
|
508
517
|
total: number,
|