prisma-generator-express 1.59.0 → 1.61.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.
Files changed (67) hide show
  1. package/README.md +150 -14
  2. package/dist/client/encodeQueryParams.js +19 -23
  3. package/dist/client/encodeQueryParams.js.map +1 -1
  4. package/dist/copy/operationDefinitions.d.ts +37 -0
  5. package/dist/copy/operationDefinitions.js +412 -0
  6. package/dist/copy/operationDefinitions.js.map +1 -0
  7. package/dist/generators/generateFastifyHandler.js +10 -41
  8. package/dist/generators/generateFastifyHandler.js.map +1 -1
  9. package/dist/generators/generateHonoHandler.js +10 -41
  10. package/dist/generators/generateHonoHandler.js.map +1 -1
  11. package/dist/generators/generateModelMetadata.d.ts +8 -0
  12. package/dist/generators/generateModelMetadata.js +77 -0
  13. package/dist/generators/generateModelMetadata.js.map +1 -0
  14. package/dist/generators/generateOperationCore.js +20 -30
  15. package/dist/generators/generateOperationCore.js.map +1 -1
  16. package/dist/generators/generateQueryBuilderHelper.js +1 -1
  17. package/dist/generators/generateQueryBuilderHelper.js.map +1 -1
  18. package/dist/generators/generateRouteConfigType.js +11 -57
  19. package/dist/generators/generateRouteConfigType.js.map +1 -1
  20. package/dist/generators/generateRouter.js +64 -177
  21. package/dist/generators/generateRouter.js.map +1 -1
  22. package/dist/generators/generateRouterFastify.js +59 -169
  23. package/dist/generators/generateRouterFastify.js.map +1 -1
  24. package/dist/generators/generateRouterHono.js +52 -152
  25. package/dist/generators/generateRouterHono.js.map +1 -1
  26. package/dist/generators/generateUnifiedHandler.js +7 -30
  27. package/dist/generators/generateUnifiedHandler.js.map +1 -1
  28. package/dist/generators/generateUnifiedScalarUI.js +9 -74
  29. package/dist/generators/generateUnifiedScalarUI.js.map +1 -1
  30. package/dist/index.js +11 -0
  31. package/dist/index.js.map +1 -1
  32. package/dist/utils/copyFiles.js +7 -0
  33. package/dist/utils/copyFiles.js.map +1 -1
  34. package/dist/utils/writeFileSafely.js +5 -12
  35. package/dist/utils/writeFileSafely.js.map +1 -1
  36. package/package.json +12 -2
  37. package/src/client/encodeQueryParams.ts +23 -36
  38. package/src/copy/autoIncludePlanner.ts +14 -23
  39. package/src/copy/autoIncludePlannerGuarded.ts +477 -0
  40. package/src/copy/autoIncludeRuntime.ts +269 -261
  41. package/src/copy/autoIncludeRuntimeGuarded.ts +379 -0
  42. package/src/copy/buildModelOpenApi.ts +248 -628
  43. package/src/copy/concurrency.ts +20 -0
  44. package/src/copy/docsRenderer.ts +63 -333
  45. package/src/copy/errorMapper.ts +126 -0
  46. package/src/copy/guardHelpers.ts +56 -0
  47. package/src/copy/materializedCount.ts +68 -0
  48. package/src/copy/materializedRouter.ts +33 -29
  49. package/src/copy/operationDefinitions.ts +359 -35
  50. package/src/copy/operationRuntime.ts +11 -605
  51. package/src/copy/pagination.ts +151 -0
  52. package/src/copy/scalarTypes.ts +2 -0
  53. package/src/copy/sse.ts +296 -0
  54. package/src/generators/generateFastifyHandler.ts +13 -47
  55. package/src/generators/generateHonoHandler.ts +13 -47
  56. package/src/generators/generateModelMetadata.ts +92 -0
  57. package/src/generators/generateOperationCore.ts +19 -32
  58. package/src/generators/generateQueryBuilderHelper.ts +1 -1
  59. package/src/generators/generateRouteConfigType.ts +9 -60
  60. package/src/generators/generateRouter.ts +88 -180
  61. package/src/generators/generateRouterFastify.ts +65 -172
  62. package/src/generators/generateRouterHono.ts +58 -155
  63. package/src/generators/generateUnifiedHandler.ts +8 -33
  64. package/src/generators/generateUnifiedScalarUI.ts +9 -91
  65. package/src/index.ts +13 -1
  66. package/src/utils/copyFiles.ts +7 -0
  67. package/src/utils/writeFileSafely.ts +5 -11
@@ -1,8 +1,6 @@
1
1
  import type { Request, Response } from 'express'
2
+ import { HttpError, LOG_PREFIX, mapError } from './errorMapper'
2
3
  import {
3
- initSSE,
4
- endSSE,
5
- startSSEKeepalive,
6
4
  sendSSEField,
7
5
  sendSSEResult,
8
6
  sendSSEError,
@@ -13,18 +11,20 @@ import {
13
11
  sendSSEPageMeta,
14
12
  runSingleResultSSE,
15
13
  emitTerminalSSEError,
14
+ safeSendError,
16
15
  setByPath,
16
+ withSSE,
17
+ } from './sse'
18
+ import { applyPaginationLimits, countForPagination } from './pagination'
19
+ import {
17
20
  getDelegate,
18
21
  getExtendedClient,
19
- applyPaginationLimits,
20
- countForPagination,
21
- mapError,
22
- HttpError,
23
22
  type OperationContext,
24
23
  type PrismaDelegate,
25
24
  type FindManyPaginatedMode,
26
25
  } from './operationRuntime'
27
26
  import { isPlainObject } from './misc'
27
+ import { mapLimited } from './concurrency'
28
28
  import {
29
29
  planAutoInclude,
30
30
  type AutoIncludePlan,
@@ -32,12 +32,14 @@ import {
32
32
  type AutoIncludeStage,
33
33
  } from './autoIncludePlanner'
34
34
  import type { AutoIncludeProgressiveVariantConfig } from './routeConfig'
35
+ import {
36
+ isGuardedAutoIncludeBaseOp,
37
+ runGuardedAutoIncludeProgressive,
38
+ } from './autoIncludeRuntimeGuarded'
35
39
 
36
40
  const STAGE_CONCURRENCY = 4
37
41
  const MAX_IN_CHUNK = 1000
38
42
 
39
- type IntervalHandle = ReturnType<typeof setInterval>
40
-
41
43
  export type AutoIncludeBaseOp =
42
44
  | 'findUnique'
43
45
  | 'findUniqueOrThrow'
@@ -69,6 +71,13 @@ type ParentEntry = RowPair & {
69
71
  locator: Array<number | string>
70
72
  }
71
73
 
74
+ function createClientGoneChecker(
75
+ res: Response,
76
+ signal?: AbortSignal,
77
+ ): () => boolean {
78
+ return () => signal?.aborted === true || res.writableEnded || res.destroyed
79
+ }
80
+
72
81
  function readPath(source: Record<string, unknown>, path: string): unknown {
73
82
  if (path === '') return source
74
83
  const parts = path.split('.')
@@ -85,16 +94,11 @@ function stripInternalAtScope(
85
94
  internalPaths: string[],
86
95
  scopePath: string,
87
96
  ): void {
97
+ const prefix = scopePath === '' ? '' : scopePath + '.'
88
98
  for (const fullPath of internalPaths) {
89
- if (scopePath === '') {
90
- if (!fullPath.includes('.')) {
91
- delete target[fullPath]
92
- }
93
- continue
94
- }
95
- if (!fullPath.startsWith(scopePath + '.')) continue
96
- const relative = fullPath.slice(scopePath.length + 1)
97
- if (relative.includes('.')) continue
99
+ if (!fullPath.startsWith(prefix)) continue
100
+ const relative = fullPath.slice(prefix.length)
101
+ if (relative === '' || relative.includes('.')) continue
98
102
  delete target[relative]
99
103
  }
100
104
  }
@@ -103,7 +107,8 @@ function mergeWhere(
103
107
  userWhere: unknown,
104
108
  linkFilter: Record<string, unknown>,
105
109
  ): Record<string, unknown> {
106
- if (!isPlainObject(userWhere) || Object.keys(userWhere).length === 0) return linkFilter
110
+ if (!isPlainObject(userWhere) || Object.keys(userWhere).length === 0)
111
+ return linkFilter
107
112
  return { AND: [userWhere, linkFilter] }
108
113
  }
109
114
 
@@ -132,22 +137,14 @@ function buildPublicForStage(
132
137
  internalFieldPaths: string[],
133
138
  scopePath: string,
134
139
  ): unknown {
135
- if (Array.isArray(result)) {
136
- return result.map((item) => {
137
- if (isPlainObject(item)) {
138
- const copy: Record<string, unknown> = { ...item }
139
- stripInternalAtScope(copy, internalFieldPaths, scopePath)
140
- return copy
141
- }
142
- return item
143
- })
144
- }
145
- if (isPlainObject(result)) {
146
- const copy: Record<string, unknown> = { ...result }
140
+ const process = (item: unknown): unknown => {
141
+ if (!isPlainObject(item)) return item
142
+ const copy: Record<string, unknown> = { ...item }
147
143
  stripInternalAtScope(copy, internalFieldPaths, scopePath)
148
144
  return copy
149
145
  }
150
- return result
146
+ if (Array.isArray(result)) return result.map(process)
147
+ return process(result)
151
148
  }
152
149
 
153
150
  function normalizeKey(v: unknown): string {
@@ -170,26 +167,6 @@ function groupStagesByDepth(stages: AutoIncludeStage[]): AutoIncludeStage[][] {
170
167
  .map((d) => byDepth.get(d) as AutoIncludeStage[])
171
168
  }
172
169
 
173
- async function runConcurrent<T>(
174
- items: T[],
175
- limit: number,
176
- fn: (item: T) => Promise<void>,
177
- ): Promise<void> {
178
- let index = 0
179
- const workers: Promise<void>[] = []
180
- const workerCount = Math.min(limit, items.length)
181
- for (let w = 0; w < workerCount; w++) {
182
- workers.push((async () => {
183
- for (;;) {
184
- const i = index++
185
- if (i >= items.length) return
186
- await fn(items[i])
187
- }
188
- })())
189
- }
190
- await Promise.all(workers)
191
- }
192
-
193
170
  function handleAutoIncludeFallback(
194
171
  options: RunAutoIncludeOptions,
195
172
  message: string,
@@ -297,7 +274,16 @@ async function runOneStageSingle(options: {
297
274
  res: Response
298
275
  isAborted: () => boolean
299
276
  }): Promise<void> {
300
- const { extended, models, stage, internal, publicState, internalFieldPaths, res, isAborted } = options
277
+ const {
278
+ extended,
279
+ models,
280
+ stage,
281
+ internal,
282
+ publicState,
283
+ internalFieldPaths,
284
+ res,
285
+ isAborted,
286
+ } = options
301
287
  if (isAborted()) return
302
288
 
303
289
  const parentRaw = readPath(internal, stage.parentPath)
@@ -322,28 +308,46 @@ async function runOneStageSingle(options: {
322
308
 
323
309
  const targetModel = models[stage.relationField.type]
324
310
  if (!targetModel) {
325
- throw new Error('Target model not in relation metadata: ' + stage.relationField.type)
311
+ throw new HttpError(
312
+ 500,
313
+ 'Target model not in relation metadata: ' + stage.relationField.type,
314
+ )
326
315
  }
327
316
 
328
317
  const finalArgs: Record<string, unknown> = { ...stage.stageArgs }
329
318
  finalArgs.where = mergeWhere(stage.stageArgs.where, linkFilter)
330
319
 
331
- const delegate: PrismaDelegate = getDelegate(extended, targetModel.delegateKey)
332
- const method: 'findMany' | 'findFirst' = stage.relationField.isList ? 'findMany' : 'findFirst'
320
+ const delegate: PrismaDelegate = getDelegate(
321
+ extended,
322
+ targetModel.delegateKey,
323
+ )
324
+ const method: 'findMany' | 'findFirst' = stage.relationField.isList
325
+ ? 'findMany'
326
+ : 'findFirst'
333
327
  const result = await delegate[method](finalArgs)
334
328
 
335
329
  if (isAborted()) return
336
330
 
337
331
  const appliedInternal = setByPath(internal, stage.relationPath, result)
338
332
  if (!appliedInternal) {
339
- throw new Error('Failed to apply internal patch for ' + stage.relationPath)
333
+ throw new HttpError(
334
+ 500,
335
+ 'Failed to apply internal patch for ' + stage.relationPath,
336
+ )
340
337
  }
341
338
 
342
- const publicResult = buildPublicForStage(result, internalFieldPaths, stage.relationPath)
339
+ const publicResult = buildPublicForStage(
340
+ result,
341
+ internalFieldPaths,
342
+ stage.relationPath,
343
+ )
343
344
 
344
345
  const appliedPublic = setByPath(publicState, stage.relationPath, publicResult)
345
346
  if (!appliedPublic) {
346
- throw new Error('Failed to apply public patch for ' + stage.relationPath)
347
+ throw new HttpError(
348
+ 500,
349
+ 'Failed to apply public patch for ' + stage.relationPath,
350
+ )
347
351
  }
348
352
 
349
353
  sendSSEField(res, stage.relationPath, publicResult)
@@ -354,16 +358,9 @@ async function runAutoIncludeSingle(
354
358
  plan: AutoIncludePlan,
355
359
  ): Promise<void> {
356
360
  const { res, ctx, baseOp, delegateKey, models, signal } = options
361
+ const isClientGone = createClientGoneChecker(res, signal)
357
362
 
358
- const isClientGone = () =>
359
- signal?.aborted === true || res.writableEnded || res.destroyed
360
-
361
- let keepalive: IntervalHandle | null = null
362
- try {
363
- initSSE(res)
364
- keepalive = startSSEKeepalive(res)
365
- if (isClientGone()) return
366
-
363
+ await withSSE({ res, signal, label: 'single' }, async () => {
367
364
  const extended = await getExtendedClient(ctx)
368
365
  if (isClientGone()) return
369
366
 
@@ -371,10 +368,12 @@ async function runAutoIncludeSingle(
371
368
 
372
369
  let rootResult: unknown
373
370
  try {
374
- rootResult = await rootDelegate[baseOp as Exclude<AutoIncludeBaseOp, 'findMany' | 'findManyPaginated'>](plan.rootArgs)
371
+ rootResult = await rootDelegate[
372
+ baseOp as Exclude<AutoIncludeBaseOp, 'findMany' | 'findManyPaginated'>
373
+ ](plan.rootArgs)
375
374
  } catch (err) {
376
375
  if (isClientGone()) return
377
- console.error('[auto-progressive] root query failed:', err)
376
+ console.error(LOG_PREFIX, 'root query failed:', err)
378
377
  sendSSEError(res, mapError(err).message)
379
378
  return
380
379
  }
@@ -414,7 +413,7 @@ async function runAutoIncludeSingle(
414
413
  if (isClientGone()) return
415
414
  if (stageErrorMessage) break
416
415
 
417
- await runConcurrent(group, STAGE_CONCURRENCY, async (stage) => {
416
+ await mapLimited(group, STAGE_CONCURRENCY, async (stage) => {
418
417
  if (isAborted()) return
419
418
  try {
420
419
  await runOneStageSingle({
@@ -429,13 +428,18 @@ async function runAutoIncludeSingle(
429
428
  })
430
429
  } catch (err) {
431
430
  if (isAborted()) return
432
- console.error('[auto-progressive] stage failed:', stage.relationPath, err)
431
+ console.error(LOG_PREFIX, 'stage failed:', stage.relationPath, err)
433
432
  stageErrorMessage = mapError(err).message
434
433
  return
435
434
  }
436
435
  if (isAborted()) return
437
436
  completed++
438
- const ok = sendSSEProgress(res, stage.relationPath, completed, plan.stages.length)
437
+ const ok = sendSSEProgress(
438
+ res,
439
+ stage.relationPath,
440
+ completed,
441
+ plan.stages.length,
442
+ )
439
443
  if (!ok) return
440
444
  })
441
445
  }
@@ -443,23 +447,13 @@ async function runAutoIncludeSingle(
443
447
  if (isClientGone()) return
444
448
 
445
449
  if (stageErrorMessage) {
446
- if (!res.writableEnded && !res.destroyed) {
447
- sendSSEError(res, stageErrorMessage)
448
- }
450
+ safeSendError(res, stageErrorMessage)
449
451
  return
450
452
  }
451
453
 
452
454
  if (res.writableEnded || res.destroyed) return
453
455
  sendSSEResult(res, publicState)
454
- } catch (err) {
455
- if (isClientGone()) return
456
- console.error('[auto-progressive] dispatch error:', err)
457
- if (!res.writableEnded && !res.destroyed) {
458
- sendSSEError(res, mapError(err).message)
459
- }
460
- } finally {
461
- endSSE(res, keepalive)
462
- }
456
+ })
463
457
  }
464
458
 
465
459
  function buildStageQueryArgs(
@@ -475,7 +469,9 @@ function buildStageQueryArgs(
475
469
  : null
476
470
 
477
471
  const finalArgs: Record<string, unknown> = { ...stage.stageArgs }
478
- finalArgs.where = mergeWhere(stage.stageArgs.where, { [childKey]: { in: inChunk } })
472
+ finalArgs.where = mergeWhere(stage.stageArgs.where, {
473
+ [childKey]: { in: inChunk },
474
+ })
479
475
 
480
476
  let injectedChildPath: string | null = null
481
477
 
@@ -487,11 +483,7 @@ function buildStageQueryArgs(
487
483
  }
488
484
 
489
485
  if (baseOmit && baseOmit[childKey] === true) {
490
- const nextOmit: Record<string, unknown> = {}
491
- for (const [k, v] of Object.entries(baseOmit)) {
492
- if (k === childKey) continue
493
- nextOmit[k] = v
494
- }
486
+ const { [childKey]: _drop, ...nextOmit } = baseOmit
495
487
  if (Object.keys(nextOmit).length > 0) {
496
488
  finalArgs.omit = nextOmit
497
489
  } else {
@@ -549,7 +541,15 @@ async function runOneStageMany(options: {
549
541
  res: Response
550
542
  isAborted: () => boolean
551
543
  }): Promise<void> {
552
- const { extended, models, stage, parentEntries, internalFieldPaths, res, isAborted } = options
544
+ const {
545
+ extended,
546
+ models,
547
+ stage,
548
+ parentEntries,
549
+ internalFieldPaths,
550
+ res,
551
+ isAborted,
552
+ } = options
553
553
 
554
554
  if (isAborted()) return
555
555
 
@@ -559,7 +559,10 @@ async function runOneStageMany(options: {
559
559
 
560
560
  const targetModel = models[rel.type]
561
561
  if (!targetModel) {
562
- throw new Error('Target model not in relation metadata: ' + rel.type)
562
+ throw new HttpError(
563
+ 500,
564
+ 'Target model not in relation metadata: ' + rel.type,
565
+ )
563
566
  }
564
567
 
565
568
  if (parentEntries.length === 0) {
@@ -573,7 +576,10 @@ async function runOneStageMany(options: {
573
576
 
574
577
  const internalParents = parentEntries.map((p) => p.internal)
575
578
  const distinctValues = collectDistinctParentValues(internalParents, parentKey)
576
- const delegate: PrismaDelegate = getDelegate(extended, targetModel.delegateKey)
579
+ const delegate: PrismaDelegate = getDelegate(
580
+ extended,
581
+ targetModel.delegateKey,
582
+ )
577
583
 
578
584
  const children: unknown[] = []
579
585
  let injectedChildPath: string | null = null
@@ -581,7 +587,11 @@ async function runOneStageMany(options: {
581
587
  for (let i = 0; i < distinctValues.length; i += MAX_IN_CHUNK) {
582
588
  if (isAborted()) return
583
589
  const chunk = distinctValues.slice(i, i + MAX_IN_CHUNK)
584
- const { args, injectedChildPath: ip } = buildStageQueryArgs(stage, childKey, chunk)
590
+ const { args, injectedChildPath: ip } = buildStageQueryArgs(
591
+ stage,
592
+ childKey,
593
+ chunk,
594
+ )
585
595
  if (ip) injectedChildPath = ip
586
596
  const partial = await delegate.findMany(args)
587
597
  if (isAborted()) return
@@ -613,7 +623,11 @@ async function runOneStageMany(options: {
613
623
  }
614
624
  }
615
625
 
616
- const publicVal = buildPublicForStage(internalVal, effectivePaths, stage.relationPath)
626
+ const publicVal = buildPublicForStage(
627
+ internalVal,
628
+ effectivePaths,
629
+ stage.relationPath,
630
+ )
617
631
 
618
632
  entry.internal[stage.relationName] = internalVal
619
633
  entry.public[stage.relationName] = publicVal
@@ -678,10 +692,11 @@ async function processFindManyStages(args: {
678
692
  res.destroyed
679
693
 
680
694
  for (const group of groups) {
681
- if (signal?.aborted === true || res.writableEnded || res.destroyed) return stageErrorMessage
695
+ if (signal?.aborted === true || res.writableEnded || res.destroyed)
696
+ return stageErrorMessage
682
697
  if (stageErrorMessage) break
683
698
 
684
- await runConcurrent(group, STAGE_CONCURRENCY, async (stage) => {
699
+ await mapLimited(group, STAGE_CONCURRENCY, async (stage) => {
685
700
  if (isAborted()) return
686
701
  const parentEntries = collectParentEntries(rootPairs, stage.parentPath)
687
702
  try {
@@ -696,19 +711,50 @@ async function processFindManyStages(args: {
696
711
  })
697
712
  } catch (err) {
698
713
  if (isAborted()) return
699
- console.error('[auto-progressive] stage failed:', stage.relationPath, err)
714
+ console.error(LOG_PREFIX, 'stage failed:', stage.relationPath, err)
700
715
  stageErrorMessage = mapError(err).message
701
716
  return
702
717
  }
703
718
  if (isAborted()) return
704
719
  completed++
705
- const ok = sendSSEProgress(res, stage.relationPath, completed, plan.stages.length)
720
+ const ok = sendSSEProgress(
721
+ res,
722
+ stage.relationPath,
723
+ completed,
724
+ plan.stages.length,
725
+ )
706
726
  if (!ok) return
707
727
  })
708
728
  }
709
729
  return stageErrorMessage
710
730
  }
711
731
 
732
+ async function fetchRootAndCount(args: {
733
+ delegate: PrismaDelegate
734
+ rawClient: unknown
735
+ rootArgs: Record<string, unknown>
736
+ distinctCountLimit: number | undefined
737
+ countSource:
738
+ | NonNullable<OperationContext['paginationConfig']>['countSource']
739
+ | undefined
740
+ }): Promise<{ data: unknown[]; count: number }> {
741
+ const { delegate, rawClient, rootArgs, distinctCountLimit, countSource } =
742
+ args
743
+ const [data, count] = await Promise.all([
744
+ delegate.findMany(rootArgs),
745
+ countForPagination(
746
+ delegate,
747
+ rootArgs,
748
+ undefined,
749
+ undefined,
750
+ distinctCountLimit,
751
+ countSource,
752
+ rawClient,
753
+ ),
754
+ ])
755
+ return { data: data as unknown[], count }
756
+ }
757
+
712
758
  async function runPaginatedRoot(args: {
713
759
  extended: unknown
714
760
  delegateKey: string
@@ -730,196 +776,158 @@ async function runPaginatedRoot(args: {
730
776
  'findManyPaginatedMode="transaction" requires transaction support on the Prisma client',
731
777
  )
732
778
  }
733
- const result = await txClient.$transaction(async (tx: unknown) => {
734
- const txDelegate = getDelegate(tx, delegateKey)
735
- const [data, count] = await Promise.all([
736
- txDelegate.findMany(rootArgs),
737
- countForPagination(
738
- txDelegate,
739
- rootArgs,
740
- undefined,
741
- undefined,
742
- distinctCountLimit,
743
- countSource,
744
- tx,
745
- ),
746
- ])
747
- return { data, count }
748
- })
749
- return { data: result.data as unknown[], count: result.count }
779
+ return txClient.$transaction((tx: unknown) =>
780
+ fetchRootAndCount({
781
+ delegate: getDelegate(tx, delegateKey),
782
+ rawClient: tx,
783
+ rootArgs,
784
+ distinctCountLimit,
785
+ countSource,
786
+ }),
787
+ )
750
788
  }
751
789
 
752
- const rootDelegate = getDelegate(extended, delegateKey)
753
- const [data, count] = await Promise.all([
754
- rootDelegate.findMany(rootArgs),
755
- countForPagination(
756
- rootDelegate,
757
- rootArgs,
758
- undefined,
759
- undefined,
760
- distinctCountLimit,
761
- countSource,
762
- extended,
763
- ),
764
- ])
765
- return { data: data as unknown[], count }
790
+ return fetchRootAndCount({
791
+ delegate: getDelegate(extended, delegateKey),
792
+ rawClient: extended,
793
+ rootArgs,
794
+ distinctCountLimit,
795
+ countSource,
796
+ })
766
797
  }
767
798
 
768
- async function runAutoIncludeMany(
799
+ async function runAutoIncludeManyOrPaginated(
769
800
  options: RunAutoIncludeOptions,
770
801
  plan: AutoIncludePlan,
802
+ isPaginated: boolean,
771
803
  ): Promise<void> {
772
804
  const { res, ctx, delegateKey, models, signal } = options
805
+ const isClientGone = createClientGoneChecker(res, signal)
773
806
 
774
- const isClientGone = () =>
775
- signal?.aborted === true || res.writableEnded || res.destroyed
776
-
777
- let keepalive: IntervalHandle | null = null
778
- try {
779
- initSSE(res)
780
- keepalive = startSSEKeepalive(res)
781
- if (isClientGone()) return
782
-
783
- const extended = await getExtendedClient(ctx)
784
- if (isClientGone()) return
785
-
786
- const rootDelegate = getDelegate(extended, delegateKey)
787
- const rootArgs = applyPaginationLimits(plan.rootArgs, ctx.paginationConfig, !!ctx.guardShape)
788
-
789
- let rootResult: unknown
790
- try {
791
- rootResult = await rootDelegate.findMany(rootArgs)
792
- } catch (err) {
807
+ await withSSE(
808
+ { res, signal, label: isPaginated ? 'paginated' : 'many' },
809
+ async () => {
810
+ const extended = await getExtendedClient(ctx)
793
811
  if (isClientGone()) return
794
- console.error('[auto-progressive] root findMany failed:', err)
795
- sendSSEError(res, mapError(err).message)
796
- return
797
- }
798
-
799
- if (isClientGone()) return
800
-
801
- if (!Array.isArray(rootResult)) {
802
- sendSSEError(res, 'auto-progressive: unexpected non-array root result for findMany')
803
- return
804
- }
805
812
 
806
- const { publicRows, rootPairs } = buildRootPairs(rootResult, plan.internalFieldPaths)
813
+ const rootArgs = applyPaginationLimits(
814
+ plan.rootArgs,
815
+ ctx.paginationConfig,
816
+ !!ctx.guardShape,
817
+ )
807
818
 
808
- sendSSERootArray(res, publicRows)
809
- sendSSEProgress(res, 'root', 0, plan.stages.length)
819
+ let rootRows: unknown[]
820
+ let total = 0
821
+ let hasMore = false
810
822
 
811
- const stageError = await processFindManyStages({
812
- extended, models, plan, rootPairs, res, signal,
813
- })
814
-
815
- if (isClientGone()) return
816
-
817
- if (stageError) {
818
- if (!res.writableEnded && !res.destroyed) {
819
- sendSSEError(res, stageError)
823
+ try {
824
+ if (isPaginated) {
825
+ const mode: FindManyPaginatedMode =
826
+ ctx.findManyPaginatedMode ?? 'promiseAll'
827
+ const r = await runPaginatedRoot({
828
+ extended,
829
+ delegateKey,
830
+ rootArgs,
831
+ ctx,
832
+ mode,
833
+ })
834
+ rootRows = r.data
835
+ total = r.count
836
+ const skip = typeof rootArgs.skip === 'number' ? rootArgs.skip : 0
837
+ const takeRaw =
838
+ typeof rootArgs.take === 'number' ? rootArgs.take : rootRows.length
839
+ const absTake = Math.abs(takeRaw)
840
+ hasMore =
841
+ absTake > 0 &&
842
+ rootRows.length >= absTake &&
843
+ skip + rootRows.length < total
844
+ } else {
845
+ const rootDelegate = getDelegate(extended, delegateKey)
846
+ const result = await rootDelegate.findMany(rootArgs)
847
+ if (!Array.isArray(result)) {
848
+ safeSendError(
849
+ res,
850
+ 'auto-progressive: unexpected non-array root result for findMany',
851
+ )
852
+ return
853
+ }
854
+ rootRows = result
855
+ }
856
+ } catch (err) {
857
+ if (isClientGone()) return
858
+ console.error(
859
+ LOG_PREFIX,
860
+ isPaginated
861
+ ? 'root findManyPaginated failed:'
862
+ : 'root findMany failed:',
863
+ err,
864
+ )
865
+ sendSSEError(res, mapError(err).message)
866
+ return
820
867
  }
821
- return
822
- }
823
-
824
- if (res.writableEnded || res.destroyed) return
825
- sendSSEResult(res, publicRows)
826
- } catch (err) {
827
- if (isClientGone()) return
828
- console.error('[auto-progressive] many dispatch error:', err)
829
- if (!res.writableEnded && !res.destroyed) {
830
- sendSSEError(res, mapError(err).message)
831
- }
832
- } finally {
833
- endSSE(res, keepalive)
834
- }
835
- }
836
-
837
- async function runAutoIncludePaginated(
838
- options: RunAutoIncludeOptions,
839
- plan: AutoIncludePlan,
840
- ): Promise<void> {
841
- const { res, ctx, delegateKey, models, signal } = options
842
868
 
843
- const isClientGone = () =>
844
- signal?.aborted === true || res.writableEnded || res.destroyed
845
-
846
- let keepalive: IntervalHandle | null = null
847
- try {
848
- initSSE(res)
849
- keepalive = startSSEKeepalive(res)
850
- if (isClientGone()) return
869
+ if (isClientGone()) return
851
870
 
852
- const extended = await getExtendedClient(ctx)
853
- if (isClientGone()) return
871
+ const { publicRows, rootPairs } = buildRootPairs(
872
+ rootRows,
873
+ plan.internalFieldPaths,
874
+ )
854
875
 
855
- const rootArgs = applyPaginationLimits(plan.rootArgs, ctx.paginationConfig, !!ctx.guardShape)
856
- const mode: FindManyPaginatedMode = ctx.findManyPaginatedMode ?? 'promiseAll'
876
+ if (isPaginated) {
877
+ sendSSEPageMeta(res, total, hasMore)
878
+ }
879
+ sendSSERootArray(res, publicRows)
880
+ sendSSEProgress(res, 'root', 0, plan.stages.length)
857
881
 
858
- let rootRows: unknown[]
859
- let total: number
860
- try {
861
- const r = await runPaginatedRoot({
882
+ const stageError = await processFindManyStages({
862
883
  extended,
863
- delegateKey,
864
- rootArgs,
865
- ctx,
866
- mode,
884
+ models,
885
+ plan,
886
+ rootPairs,
887
+ res,
888
+ signal,
867
889
  })
868
- rootRows = r.data
869
- total = r.count
870
- } catch (err) {
871
- if (isClientGone()) return
872
- console.error('[auto-progressive] root findManyPaginated failed:', err)
873
- sendSSEError(res, mapError(err).message)
874
- return
875
- }
876
-
877
- if (isClientGone()) return
878
-
879
- const skip = typeof rootArgs.skip === 'number' ? rootArgs.skip : 0
880
- const takeRaw = typeof rootArgs.take === 'number' ? rootArgs.take : rootRows.length
881
- const absTake = Math.abs(takeRaw)
882
- const hasMore = absTake > 0 && rootRows.length >= absTake && skip + rootRows.length < total
883
-
884
- const { publicRows, rootPairs } = buildRootPairs(rootRows, plan.internalFieldPaths)
885
890
 
886
- sendSSEPageMeta(res, total, hasMore)
887
- sendSSERootArray(res, publicRows)
888
- sendSSEProgress(res, 'root', 0, plan.stages.length)
889
-
890
- const stageError = await processFindManyStages({
891
- extended, models, plan, rootPairs, res, signal,
892
- })
893
-
894
- if (isClientGone()) return
891
+ if (isClientGone()) return
895
892
 
896
- if (stageError) {
897
- if (!res.writableEnded && !res.destroyed) {
898
- sendSSEError(res, stageError)
893
+ if (stageError) {
894
+ safeSendError(res, stageError)
895
+ return
899
896
  }
900
- return
901
- }
902
897
 
903
- if (res.writableEnded || res.destroyed) return
904
- sendSSEResult(res, { data: publicRows, total, hasMore })
905
- } catch (err) {
906
- if (isClientGone()) return
907
- console.error('[auto-progressive] paginated dispatch error:', err)
908
- if (!res.writableEnded && !res.destroyed) {
909
- sendSSEError(res, mapError(err).message)
910
- }
911
- } finally {
912
- endSSE(res, keepalive)
913
- }
898
+ if (res.writableEnded || res.destroyed) return
899
+ if (isPaginated) {
900
+ sendSSEResult(res, { data: publicRows, total, hasMore })
901
+ } else {
902
+ sendSSEResult(res, publicRows)
903
+ }
904
+ },
905
+ )
914
906
  }
915
907
 
916
908
  export async function runAutoIncludeProgressive(
917
909
  options: RunAutoIncludeOptions,
918
910
  ): Promise<void> {
919
911
  if (options.ctx.guardShape) {
912
+ if (isGuardedAutoIncludeBaseOp(options.baseOp)) {
913
+ return runGuardedAutoIncludeProgressive({
914
+ req: options.req,
915
+ res: options.res,
916
+ ctx: options.ctx,
917
+ args: options.args,
918
+ baseOp: options.baseOp,
919
+ modelName: options.modelName,
920
+ delegateKey: options.delegateKey,
921
+ models: options.models,
922
+ variantConfig: options.variantConfig,
923
+ coreQueryFn: options.coreQueryFn,
924
+ signal: options.signal,
925
+ })
926
+ }
920
927
  return handleAutoIncludeFallback(
921
928
  options,
922
- 'auto-progressive fallback: guard shape disables auto-include',
929
+ 'auto-progressive fallback: guard shape disables auto-include for baseOp=' +
930
+ options.baseOp,
923
931
  )
924
932
  }
925
933
 
@@ -950,10 +958,10 @@ export async function runAutoIncludeProgressive(
950
958
  }
951
959
 
952
960
  if (options.baseOp === 'findMany') {
953
- return runAutoIncludeMany(options, plan)
961
+ return runAutoIncludeManyOrPaginated(options, plan, false)
954
962
  }
955
963
  if (options.baseOp === 'findManyPaginated') {
956
- return runAutoIncludePaginated(options, plan)
964
+ return runAutoIncludeManyOrPaginated(options, plan, true)
957
965
  }
958
966
  return runAutoIncludeSingle(options, plan)
959
- }
967
+ }