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 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 merged into the final result.
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]`. 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.
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. Nested relation stages emit progress and are included in the final result:
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
- { "type": "progress", "stage": "companies.users", "completed": 2, "total": 3 }
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
- { "type": "progress", "stage": "companies.users.profile", "completed": 3, "total": 3 }
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 can stream `relationBatch` events. Deeper stages are merged into the server-side result and visible in the terminal `result` event.
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
- [event.relationPath]: event.values[index],
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, consume the final `result` event as the authoritative nested payload. Only direct root relation stages emit `relationBatch` events.
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.53.0",
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 collectParentPairs(
243
+ function collectParentEntries(
239
244
  rootPairs: RowPair[],
240
245
  parentPath: string,
241
- ): RowPair[] {
242
- if (parentPath === '') return rootPairs
243
- let pairs = rootPairs
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: RowPair[] = []
246
- for (const pair of pairs) {
247
- const internalValue = pair.internal[segment]
248
- const publicValue = pair.public[segment]
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({ internal: internalItem, public: publicItem })
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({ internal: internalValue, public: publicValue })
276
+ next.push({
277
+ internal: internalValue,
278
+ public: publicValue,
279
+ locator: [...entry.locator, segment],
280
+ })
262
281
  }
263
282
  }
264
- pairs = next
283
+ entries = next
265
284
  }
266
- return pairs
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
- parentPairs: RowPair[]
544
+ parentEntries: ParentEntry[]
526
545
  internalFieldPaths: string[]
527
546
  res: Response
528
547
  isAborted: () => boolean
529
548
  }): Promise<void> {
530
- const { extended, models, stage, parentPairs, internalFieldPaths, res, isAborted } = options
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 (parentPairs.length === 0) {
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 = parentPairs.map((p) => p.internal)
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(parentPairs.length)
595
+ const publicValues: unknown[] = new Array(parentEntries.length)
575
596
 
576
- for (let i = 0; i < parentPairs.length; i++) {
577
- const pair = parentPairs[i]
578
- const fkVal = pair.internal[parentKey]
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
- pair.internal[stage.relationName] = internalVal
595
- pair.public[stage.relationName] = publicVal
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 parentPairs = collectParentPairs(rootPairs, stage.parentPath)
683
+ const parentEntries = collectParentEntries(rootPairs, stage.parentPath)
656
684
  try {
657
685
  await runOneStageMany({
658
686
  extended,
659
687
  models,
660
688
  stage,
661
- parentPairs,
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,