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 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 single-record relation reads
15
+ - Express-only progressive read streaming over Server-Sent Events (SSE), using manual stages or auto-include splitting for supported relation reads, including deep `findMany` / `findManyPaginated` auto-include paths
16
16
  - Express-only standalone materialized view router for read-only access to registered PostgreSQL materialized views
17
17
  - OpenAPI 3.1 spec (JSON and YAML endpoints registered automatically per router)
18
18
  - Documentation helpers for contract view and Scalar UI (require manual mounting)
@@ -74,7 +74,7 @@ Some operations require newer versions:
74
74
 
75
75
  The Hono target v1 is tested on Node.js runtimes only. See [Cloudflare Workers and edge runtimes](#cloudflare-workers-and-edge-runtimes).
76
76
 
77
- Progressive Endpoint Composition over Server-Sent Events is currently supported by the Express target only. Express supports both manual staged streaming and auto-include streaming for single-record relation reads. Fastify and Hono continue to support normal JSON read and write routes.
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' }` | Single-record reads where the client already sends a Prisma `include` or relation `select` tree and you want relation fields streamed progressively |
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 single-record query first, streams root fields, then loads supported included relations as separate follow-up queries and streams each relation path as it resolves.
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 only supports single-record read operations:
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
- If auto-include is configured on another operation, the router either falls back to single-result SSE or sends an SSE error depending on `fallback`.
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
- The final `result.data` is the accumulated object built from all applied patches, unless a manual stage returns a stop result.
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
- Auto-include sends root scalar fields first, then sends relation field events as separate relation queries finish:
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 single-record reads.
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 to-one relation loading through to-one parents
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
- // patch local field state
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
- // replace with final result
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 isSingleRecordRead =
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 (!isSingleRecordRead) {
242
+ if (!isAutoIncludeReadable) {
242
243
  if (progressiveConfig.fallback === 'error') {
243
- emitTerminalSSEError(res, 'auto-progressive fallback: operation not single-record')
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":"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"}
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.51.0",
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: 'findUnique' | 'findUniqueOrThrow' | 'findFirst' | 'findFirstOrThrow'
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
- async function runOneStage(options: {
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 groupStagesByDepth(stages: AutoIncludeStage[]): AutoIncludeStage[][] {
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 { req, res, ctx, args, baseOp, modelName, delegateKey, models, variantConfig, coreQueryFn, signal } = options
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 runOneStage({
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 isSingleRecordRead =
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 (!isSingleRecordRead) {
258
+ if (!isAutoIncludeReadable) {
258
259
  if (progressiveConfig.fallback === 'error') {
259
- emitTerminalSSEError(res, 'auto-progressive fallback: operation not single-record')
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,