ai-database 2.0.2 → 2.1.3

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 (114) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/LICENSE +21 -0
  3. package/README.md +667 -1
  4. package/dist/actions.d.ts +247 -0
  5. package/dist/actions.d.ts.map +1 -0
  6. package/dist/actions.js +260 -0
  7. package/dist/actions.js.map +1 -0
  8. package/dist/ai-promise-db.d.ts +37 -2
  9. package/dist/ai-promise-db.d.ts.map +1 -1
  10. package/dist/ai-promise-db.js +530 -92
  11. package/dist/ai-promise-db.js.map +1 -1
  12. package/dist/constants.d.ts +16 -0
  13. package/dist/constants.d.ts.map +1 -0
  14. package/dist/constants.js +16 -0
  15. package/dist/constants.js.map +1 -0
  16. package/dist/events.d.ts +153 -0
  17. package/dist/events.d.ts.map +1 -0
  18. package/dist/events.js +154 -0
  19. package/dist/events.js.map +1 -0
  20. package/dist/index.d.ts +11 -2
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +20 -2
  23. package/dist/index.js.map +1 -1
  24. package/dist/memory-provider.d.ts +145 -2
  25. package/dist/memory-provider.d.ts.map +1 -1
  26. package/dist/memory-provider.js +569 -13
  27. package/dist/memory-provider.js.map +1 -1
  28. package/dist/schema/cascade.d.ts +104 -0
  29. package/dist/schema/cascade.d.ts.map +1 -0
  30. package/dist/schema/cascade.js +547 -0
  31. package/dist/schema/cascade.js.map +1 -0
  32. package/dist/schema/dependency-graph.d.ts +133 -0
  33. package/dist/schema/dependency-graph.d.ts.map +1 -0
  34. package/dist/schema/dependency-graph.js +355 -0
  35. package/dist/schema/dependency-graph.js.map +1 -0
  36. package/dist/schema/generation-context.d.ts +202 -0
  37. package/dist/schema/generation-context.d.ts.map +1 -0
  38. package/dist/schema/generation-context.js +393 -0
  39. package/dist/schema/generation-context.js.map +1 -0
  40. package/dist/schema/index.d.ts +201 -0
  41. package/dist/schema/index.d.ts.map +1 -0
  42. package/dist/schema/index.js +1221 -0
  43. package/dist/schema/index.js.map +1 -0
  44. package/dist/schema/parse.d.ts +225 -0
  45. package/dist/schema/parse.d.ts.map +1 -0
  46. package/dist/schema/parse.js +740 -0
  47. package/dist/schema/parse.js.map +1 -0
  48. package/dist/schema/provider.d.ts +177 -0
  49. package/dist/schema/provider.d.ts.map +1 -0
  50. package/dist/schema/provider.js +258 -0
  51. package/dist/schema/provider.js.map +1 -0
  52. package/dist/schema/resolve.d.ts +87 -0
  53. package/dist/schema/resolve.d.ts.map +1 -0
  54. package/dist/schema/resolve.js +549 -0
  55. package/dist/schema/resolve.js.map +1 -0
  56. package/dist/schema/semantic.d.ts +54 -0
  57. package/dist/schema/semantic.d.ts.map +1 -0
  58. package/dist/schema/semantic.js +335 -0
  59. package/dist/schema/semantic.js.map +1 -0
  60. package/dist/schema/types.d.ts +528 -0
  61. package/dist/schema/types.d.ts.map +1 -0
  62. package/dist/schema/types.js +9 -0
  63. package/dist/schema/types.js.map +1 -0
  64. package/dist/schema/union-fallback.d.ts +219 -0
  65. package/dist/schema/union-fallback.d.ts.map +1 -0
  66. package/dist/schema/union-fallback.js +325 -0
  67. package/dist/schema/union-fallback.js.map +1 -0
  68. package/dist/schema/verb-derivation.d.ts +167 -0
  69. package/dist/schema/verb-derivation.d.ts.map +1 -0
  70. package/dist/schema/verb-derivation.js +281 -0
  71. package/dist/schema/verb-derivation.js.map +1 -0
  72. package/dist/schema.d.ts +25 -867
  73. package/dist/schema.d.ts.map +1 -1
  74. package/dist/schema.js +45 -1124
  75. package/dist/schema.js.map +1 -1
  76. package/dist/semantic.d.ts +175 -0
  77. package/dist/semantic.d.ts.map +1 -0
  78. package/dist/semantic.js +338 -0
  79. package/dist/semantic.js.map +1 -0
  80. package/dist/type-guards.d.ts +167 -0
  81. package/dist/type-guards.d.ts.map +1 -0
  82. package/dist/type-guards.js +247 -0
  83. package/dist/type-guards.js.map +1 -0
  84. package/dist/types.d.ts +14 -0
  85. package/dist/types.d.ts.map +1 -1
  86. package/dist/types.js.map +1 -1
  87. package/dist/validation.d.ts +168 -0
  88. package/dist/validation.d.ts.map +1 -0
  89. package/dist/validation.js +667 -0
  90. package/dist/validation.js.map +1 -0
  91. package/package.json +21 -12
  92. package/.turbo/turbo-build.log +0 -5
  93. package/TESTING.md +0 -410
  94. package/TEST_SUMMARY.md +0 -250
  95. package/TODO.md +0 -128
  96. package/src/ai-promise-db.ts +0 -1243
  97. package/src/authorization.ts +0 -1102
  98. package/src/durable-clickhouse.ts +0 -596
  99. package/src/durable-promise.ts +0 -582
  100. package/src/execution-queue.ts +0 -608
  101. package/src/index.test.ts +0 -868
  102. package/src/index.ts +0 -337
  103. package/src/linguistic.ts +0 -404
  104. package/src/memory-provider.test.ts +0 -1036
  105. package/src/memory-provider.ts +0 -1119
  106. package/src/schema.test.ts +0 -1254
  107. package/src/schema.ts +0 -2296
  108. package/src/tests.ts +0 -725
  109. package/src/types.ts +0 -1177
  110. package/test/README.md +0 -153
  111. package/test/edge-cases.test.ts +0 -646
  112. package/test/provider-resolution.test.ts +0 -402
  113. package/tsconfig.json +0 -9
  114. package/vitest.config.ts +0 -19
@@ -20,6 +20,43 @@
20
20
  *
21
21
  * @packageDocumentation
22
22
  */
23
+ import { Semaphore } from './memory-provider.js';
24
+ import { isEntityArray, extractEntityId, extractMarkerType, isPlainObject, hasRelationElements, asCallback, asPredicate, asComparator, getSymbolProperty, asItem, } from './type-guards.js';
25
+ // Provider resolver - will be set by schema.ts
26
+ let providerResolver = null;
27
+ /**
28
+ * Set the provider resolver function (called from schema.ts)
29
+ */
30
+ export function setProviderResolver(resolver) {
31
+ providerResolver = resolver;
32
+ }
33
+ /**
34
+ * Get the provider for batch operations
35
+ */
36
+ async function getProvider() {
37
+ if (providerResolver) {
38
+ return providerResolver();
39
+ }
40
+ return null;
41
+ }
42
+ // Schema info for batch loading - stores relation field info for entity types
43
+ // Maps entityType -> fieldName -> relatedType
44
+ let schemaRelationInfo = null;
45
+ /**
46
+ * Set schema relation info for batch loading nested relations
47
+ * Called from schema.ts when DB() is initialized
48
+ */
49
+ export function setSchemaRelationInfo(info) {
50
+ schemaRelationInfo = info;
51
+ }
52
+ /**
53
+ * Get the related type for a field on an entity type
54
+ */
55
+ function getRelatedType(entityType, fieldName) {
56
+ if (!schemaRelationInfo)
57
+ return undefined;
58
+ return schemaRelationInfo.get(entityType)?.get(fieldName);
59
+ }
23
60
  // =============================================================================
24
61
  // Types
25
62
  // =============================================================================
@@ -84,7 +121,13 @@ export class DBPromise {
84
121
  }
85
122
  // Execute the query
86
123
  const result = await this._options.executor();
87
- this._resolvedValue = result;
124
+ // If result is an array, wrap it with batch-loading map
125
+ if (Array.isArray(result)) {
126
+ this._resolvedValue = createBatchLoadingArray(result);
127
+ }
128
+ else {
129
+ this._resolvedValue = result;
130
+ }
88
131
  this._isResolved = true;
89
132
  return this._resolvedValue;
90
133
  }
@@ -113,8 +156,7 @@ export class DBPromise {
113
156
  }
114
157
  // Create recording context
115
158
  const recordings = [];
116
- // Record what the callback accesses for each item
117
- const recordedResults = [];
159
+ // Phase 1: Record what the callback accesses for each item (using placeholder proxies)
118
160
  for (let i = 0; i < items.length; i++) {
119
161
  const item = items[i];
120
162
  const recording = {
@@ -123,17 +165,32 @@ export class DBPromise {
123
165
  };
124
166
  // Create a recording proxy for this item
125
167
  const recordingProxy = createRecordingProxy(item, recording);
126
- // Execute callback with recording proxy
127
- const result = callback(recordingProxy, i);
128
- recordedResults.push(result);
168
+ // Execute callback with recording proxy to discover accesses
169
+ // Use asCallback to convert the callback to accept unknown items
170
+ try {
171
+ asCallback(callback)(recordingProxy, i);
172
+ }
173
+ catch {
174
+ // Ignore errors during recording phase - they'll surface in Phase 3
175
+ }
129
176
  recordings.push(recording);
130
177
  }
131
- // Analyze recordings to find batch-loadable relations
178
+ // Phase 2: Analyze recordings and batch-load relations
132
179
  const batchLoads = analyzeBatchLoads(recordings, items);
133
- // Execute batch loads
134
- const loadedRelations = await executeBatchLoads(batchLoads);
135
- // Apply loaded relations to results
136
- return applyBatchResults(recordedResults, loadedRelations, items);
180
+ const loadedRelations = await executeBatchLoads(batchLoads, recordings);
181
+ // Phase 3: Re-run callback with enriched items that have loaded relations
182
+ const enrichedItems = [];
183
+ for (let i = 0; i < items.length; i++) {
184
+ enrichedItems.push(enrichItemWithLoadedRelations(items[i], loadedRelations));
185
+ }
186
+ // Execute callback again with enriched data
187
+ // Use asCallback to convert the callback to accept unknown items
188
+ const results = [];
189
+ const typedCallback = asCallback(callback);
190
+ for (let i = 0; i < enrichedItems.length; i++) {
191
+ results.push(typedCallback(enrichedItems[i], i));
192
+ }
193
+ return results;
137
194
  },
138
195
  });
139
196
  }
@@ -149,7 +206,8 @@ export class DBPromise {
149
206
  if (!Array.isArray(items)) {
150
207
  return items;
151
208
  }
152
- return items.filter(predicate);
209
+ // Use asPredicate to convert the predicate to accept unknown items
210
+ return items.filter(asPredicate(predicate));
153
211
  },
154
212
  });
155
213
  }
@@ -165,7 +223,8 @@ export class DBPromise {
165
223
  if (!Array.isArray(items)) {
166
224
  return items;
167
225
  }
168
- return [...items].sort(compareFn);
226
+ // Use asComparator to convert the compareFn to accept unknown items
227
+ return [...items].sort(asComparator(compareFn));
169
228
  },
170
229
  });
171
230
  }
@@ -247,7 +306,7 @@ export class DBPromise {
247
306
  // Persistence state
248
307
  let processedIds = new Set();
249
308
  let persistCounter = 0;
250
- const getItemId = (item) => item?.$id ?? item?.id ?? String(item);
309
+ const getItemId = (item) => extractEntityId(item) ?? String(item);
251
310
  // Get actions API from options (injected by wrapEntityOperations)
252
311
  const actionsAPI = this._options.actionsAPI;
253
312
  // Initialize persistence if enabled
@@ -324,10 +383,10 @@ export class DBPromise {
324
383
  const getRetryDelay = (attempt) => {
325
384
  return typeof retryDelay === 'function' ? retryDelay(attempt) : retryDelay;
326
385
  };
327
- // Helper to handle error
386
+ // Helper to handle error - use asItem for type conversion
328
387
  const handleError = async (error, item, attempt) => {
329
388
  if (typeof onError === 'function') {
330
- return onError(error, item, attempt);
389
+ return onError(error, asItem(item), attempt);
331
390
  }
332
391
  return onError;
333
392
  };
@@ -346,24 +405,25 @@ export class DBPromise {
346
405
  let attempt = 0;
347
406
  while (true) {
348
407
  try {
349
- // Create timeout wrapper if needed
408
+ // Create timeout wrapper if needed - use asCallback for type conversion
350
409
  let result;
410
+ const typedCallback = asCallback(callback);
351
411
  if (timeout) {
352
412
  const timeoutPromise = new Promise((_, reject) => {
353
413
  setTimeout(() => reject(new Error(`Timeout after ${timeout}ms`)), timeout);
354
414
  });
355
415
  result = await Promise.race([
356
- Promise.resolve(callback(item, index)),
416
+ Promise.resolve(typedCallback(item, index)),
357
417
  timeoutPromise,
358
418
  ]);
359
419
  }
360
420
  else {
361
- result = await callback(item, index);
421
+ result = await typedCallback(item, index);
362
422
  }
363
423
  // Success
364
424
  completed++;
365
425
  await persistProgress(itemId);
366
- await onComplete?.(item, result, index);
426
+ await onComplete?.(asItem(item), result, index);
367
427
  onProgress?.(getProgress(index, item));
368
428
  return;
369
429
  }
@@ -473,6 +533,9 @@ export class DBPromise {
473
533
  }
474
534
  /**
475
535
  * Async iteration
536
+ *
537
+ * The yield casts use a local type alias because TypeScript cannot narrow
538
+ * the conditional type `T extends (infer I)[] ? I : T` at runtime.
476
539
  */
477
540
  async *[Symbol.asyncIterator]() {
478
541
  const items = await this.resolve();
@@ -526,9 +589,42 @@ export class DBPromise {
526
589
  // =============================================================================
527
590
  // Proxy Handlers
528
591
  // =============================================================================
592
+ /**
593
+ * Known DBPromise methods that need proxy handling.
594
+ */
595
+ const DBPROMISE_METHODS = new Set([
596
+ 'map', 'filter', 'sort', 'limit', 'first', 'forEach', 'resolve',
597
+ 'then', 'catch', 'finally'
598
+ ]);
599
+ /**
600
+ * Known internal properties that need proxy handling.
601
+ */
602
+ const INTERNAL_PROPS = new Set(['accessedProps', 'path', 'isResolved']);
603
+ /**
604
+ * Access a property on DBPromise by name, with type safety.
605
+ */
606
+ function getDBPromiseProperty(target, prop) {
607
+ return target[prop];
608
+ }
609
+ /**
610
+ * Get and bind a method from DBPromise by name.
611
+ */
612
+ function bindDBPromiseMethod(target, prop) {
613
+ const method = getDBPromiseProperty(target, prop);
614
+ if (typeof method === 'function') {
615
+ return method.bind(target);
616
+ }
617
+ throw new Error(`${prop} is not a method on DBPromise`);
618
+ }
619
+ /**
620
+ * Proxy handlers for DBPromise property access tracking.
621
+ *
622
+ * Uses type-safe helper functions to access class methods and properties
623
+ * from within the ProxyHandler where the target type is DBPromise<unknown>.
624
+ */
529
625
  const DB_PROXY_HANDLERS = {
530
- get(target, prop, receiver) {
531
- // Handle symbols
626
+ get(target, prop) {
627
+ // Handle symbols - access internal symbol-keyed properties
532
628
  if (typeof prop === 'symbol') {
533
629
  if (prop === DB_PROMISE_SYMBOL)
534
630
  return true;
@@ -536,25 +632,23 @@ const DB_PROXY_HANDLERS = {
536
632
  return target;
537
633
  if (prop === Symbol.asyncIterator)
538
634
  return target[Symbol.asyncIterator].bind(target);
539
- return target[prop];
540
- }
541
- // Handle promise methods
542
- if (prop === 'then' || prop === 'catch' || prop === 'finally') {
543
- return target[prop].bind(target);
635
+ // Use getSymbolProperty for type-safe symbol access
636
+ return getSymbolProperty(target, prop);
544
637
  }
545
- // Handle DBPromise methods
546
- if (['map', 'filter', 'sort', 'limit', 'first', 'forEach', 'resolve'].includes(prop)) {
547
- return target[prop].bind(target);
638
+ // Handle promise and DBPromise methods
639
+ if (DBPROMISE_METHODS.has(prop)) {
640
+ return bindDBPromiseMethod(target, prop);
548
641
  }
549
- // Handle internal properties
550
- if (prop.startsWith('_') || ['accessedProps', 'path', 'isResolved'].includes(prop)) {
551
- return target[prop];
642
+ // Handle internal properties - private properties and getters
643
+ if (prop.startsWith('_') || INTERNAL_PROPS.has(prop)) {
644
+ return getDBPromiseProperty(target, prop);
552
645
  }
553
646
  // Track property access
554
647
  target.accessedProps.add(prop);
555
648
  // Return a new DBPromise for the property path
649
+ const internals = target;
556
650
  return new DBPromise({
557
- type: target['_options']?.type,
651
+ type: internals._options?.type,
558
652
  parent: target,
559
653
  propertyPath: [...target.path, prop],
560
654
  executor: async () => {
@@ -579,35 +673,6 @@ const DB_PROXY_HANDLERS = {
579
673
  function sleep(ms) {
580
674
  return new Promise((resolve) => setTimeout(resolve, ms));
581
675
  }
582
- /**
583
- * Simple semaphore for concurrency control
584
- */
585
- class Semaphore {
586
- permits;
587
- queue = [];
588
- constructor(permits) {
589
- this.permits = permits;
590
- }
591
- async acquire() {
592
- if (this.permits > 0) {
593
- this.permits--;
594
- return () => this.release();
595
- }
596
- return new Promise((resolve) => {
597
- this.queue.push(() => {
598
- this.permits--;
599
- resolve(() => this.release());
600
- });
601
- });
602
- }
603
- release() {
604
- this.permits++;
605
- const next = this.queue.shift();
606
- if (next) {
607
- next();
608
- }
609
- }
610
- }
611
676
  /**
612
677
  * Get nested value from object
613
678
  */
@@ -620,6 +685,131 @@ function getNestedValue(obj, path) {
620
685
  }
621
686
  return current;
622
687
  }
688
+ /**
689
+ * Create an array that has a batch-loading map method
690
+ * When .map() is called on the resolved array, it performs batch loading of relations
691
+ */
692
+ function createBatchLoadingArray(items) {
693
+ // Create a new array with all the original items
694
+ const batchArray = [...items];
695
+ // Override the map method to do batch loading
696
+ Object.defineProperty(batchArray, 'map', {
697
+ value: async function (callback) {
698
+ const items = this;
699
+ // Phase 1: Record what the callback accesses using placeholder proxies
700
+ const recordings = [];
701
+ for (let i = 0; i < items.length; i++) {
702
+ const item = items[i];
703
+ const recording = {
704
+ paths: new Set(),
705
+ relations: new Map(),
706
+ };
707
+ // Create a recording proxy for this item
708
+ const recordingProxy = createRecordingProxy(item, recording);
709
+ // Execute callback with recording proxy to discover accesses
710
+ try {
711
+ callback(recordingProxy, i);
712
+ }
713
+ catch {
714
+ // Ignore errors during recording phase - they'll surface in Phase 3
715
+ }
716
+ recordings.push(recording);
717
+ }
718
+ // Phase 2: Analyze recordings and batch-load relations
719
+ const batchLoads = analyzeBatchLoads(recordings, items);
720
+ const loadedRelations = await executeBatchLoads(batchLoads, recordings);
721
+ // Phase 3: Re-run callback with enriched items that have loaded relations
722
+ const enrichedItems = [];
723
+ for (let i = 0; i < items.length; i++) {
724
+ enrichedItems.push(enrichItemWithLoadedRelations(items[i], loadedRelations));
725
+ }
726
+ // Execute callback again with enriched data
727
+ const results = [];
728
+ for (let i = 0; i < enrichedItems.length; i++) {
729
+ results.push(callback(enrichedItems[i], i));
730
+ }
731
+ return results;
732
+ },
733
+ writable: true,
734
+ configurable: true,
735
+ enumerable: false,
736
+ });
737
+ return batchArray;
738
+ }
739
+ /**
740
+ * Create a proxy that records nested property accesses for relations
741
+ * This returns placeholder values to allow the callback to complete
742
+ *
743
+ * When accessing customer.address.city:
744
+ * - At depth 0 (path=[]), accessing 'address' records it in nestedPaths
745
+ * - Accessing 'city' on 'address' creates a nestedRelation for 'address' and records 'city' in its nestedPaths
746
+ */
747
+ function createRelationRecordingProxy(relationRecording, path = [], currentNestedRecording) {
748
+ // Return a proxy that records all nested accesses
749
+ return new Proxy({}, {
750
+ get(target, prop) {
751
+ if (typeof prop === 'symbol') {
752
+ return undefined;
753
+ }
754
+ // For common array methods that don't need recording
755
+ if (prop === 'map' || prop === 'filter' || prop === 'forEach' || prop === 'length') {
756
+ if (relationRecording.isArray) {
757
+ // Return array-like behavior
758
+ if (prop === 'length')
759
+ return 0;
760
+ if (prop === 'map')
761
+ return (fn) => [];
762
+ if (prop === 'filter')
763
+ return (fn) => [];
764
+ if (prop === 'forEach')
765
+ return (fn) => { };
766
+ }
767
+ return undefined;
768
+ }
769
+ if (path.length === 0) {
770
+ // First level: recording access to properties of the relation itself (e.g., customer.address)
771
+ // This is a direct property access on the relation - record it
772
+ relationRecording.nestedPaths.add(prop);
773
+ // Create a nested recording for potential deeper access
774
+ // We don't know if 'prop' is a relation yet, but if there's further access we'll need it
775
+ let nestedRec = relationRecording.nestedRelations.get(prop);
776
+ if (!nestedRec) {
777
+ nestedRec = {
778
+ type: 'unknown', // Type will be inferred when loading
779
+ isArray: false,
780
+ nestedPaths: new Set(),
781
+ nestedRelations: new Map(),
782
+ };
783
+ relationRecording.nestedRelations.set(prop, nestedRec);
784
+ }
785
+ // Return a proxy that will record further accesses into the nested recording
786
+ return createRelationRecordingProxy(relationRecording, [prop], nestedRec);
787
+ }
788
+ else {
789
+ // Deeper level: recording access to properties of a nested relation (e.g., customer.address.city)
790
+ // Record this property in the current nested recording
791
+ if (currentNestedRecording) {
792
+ currentNestedRecording.nestedPaths.add(prop);
793
+ // Create another nested recording for even deeper access
794
+ let deeperRec = currentNestedRecording.nestedRelations.get(prop);
795
+ if (!deeperRec) {
796
+ deeperRec = {
797
+ type: 'unknown',
798
+ isArray: false,
799
+ nestedPaths: new Set(),
800
+ nestedRelations: new Map(),
801
+ };
802
+ currentNestedRecording.nestedRelations.set(prop, deeperRec);
803
+ }
804
+ return createRelationRecordingProxy(relationRecording, [...path, prop], deeperRec);
805
+ }
806
+ // Fallback - just record in nestedPaths of root
807
+ relationRecording.nestedPaths.add(prop);
808
+ return createRelationRecordingProxy(relationRecording, [...path, prop]);
809
+ }
810
+ },
811
+ });
812
+ }
623
813
  /**
624
814
  * Create a proxy that records property accesses
625
815
  */
@@ -630,19 +820,88 @@ function createRecordingProxy(item, recording) {
630
820
  return new Proxy(item, {
631
821
  get(target, prop) {
632
822
  if (typeof prop === 'symbol') {
633
- return target[prop];
823
+ // Use getSymbolProperty for type-safe symbol access
824
+ return getSymbolProperty(target, prop);
634
825
  }
635
826
  recording.paths.add(prop);
636
827
  const value = target[prop];
637
- // If accessing a relation (identified by $id or Promise), record it
638
- if (value && typeof value === 'object' && '$type' in value) {
639
- recording.relations.set(prop, {
640
- type: value.$type,
641
- isArray: Array.isArray(value),
642
- nestedPaths: new Set(),
643
- });
828
+ // If accessing a relation (identified by $type marker from hydration)
829
+ // Note: proxies may not expose $type in 'has' trap, so check via property access
830
+ const maybeType = extractMarkerType(value);
831
+ if (maybeType) {
832
+ const relationType = maybeType;
833
+ // Get or create the relation recording
834
+ let relationRecording = recording.relations.get(prop);
835
+ if (!relationRecording) {
836
+ relationRecording = {
837
+ type: relationType,
838
+ isArray: Array.isArray(value),
839
+ nestedPaths: new Set(),
840
+ nestedRelations: new Map(),
841
+ };
842
+ recording.relations.set(prop, relationRecording);
843
+ }
844
+ // Return a proxy that records nested accesses but uses placeholder values
845
+ return createRelationRecordingProxy(relationRecording);
846
+ }
847
+ // Handle arrays with potential relation elements (like members: ['->User'])
848
+ if (Array.isArray(value)) {
849
+ // Check if the array itself is a relation array (has $type marker from thenableArray)
850
+ const arrayMarker = isEntityArray(value) ? value : null;
851
+ const arrayType = arrayMarker?.$type;
852
+ const isArrayRelationFlag = arrayMarker !== null;
853
+ // Also check if array contains relation proxies (for backwards compatibility)
854
+ const hasRelationElementsFlag = !isArrayRelationFlag && hasRelationElements(value);
855
+ if (isArrayRelationFlag || hasRelationElementsFlag) {
856
+ // Get the type from the array $type or first element
857
+ let relationType = arrayType;
858
+ if (!relationType) {
859
+ const firstRelation = value.find(v => extractMarkerType(v) !== undefined);
860
+ relationType = firstRelation ? extractMarkerType(firstRelation) ?? 'unknown' : 'unknown';
861
+ }
862
+ let relationRecording = recording.relations.get(prop);
863
+ if (!relationRecording) {
864
+ relationRecording = {
865
+ type: relationType,
866
+ isArray: true,
867
+ nestedPaths: new Set(),
868
+ nestedRelations: new Map(),
869
+ };
870
+ recording.relations.set(prop, relationRecording);
871
+ }
872
+ // Return a proxy array that records element accesses
873
+ return new Proxy(value, {
874
+ get(arrayTarget, arrayProp) {
875
+ if (arrayProp === 'map') {
876
+ return (fn) => {
877
+ // Record what the map callback accesses
878
+ const elementProxy = createRelationRecordingProxy(relationRecording);
879
+ // Execute callback to record accesses, but we can't return real results
880
+ try {
881
+ fn(elementProxy, 0);
882
+ }
883
+ catch { }
884
+ return [];
885
+ };
886
+ }
887
+ if (arrayProp === 'length')
888
+ return value.length;
889
+ if (arrayProp === 'filter')
890
+ return (fn) => [];
891
+ if (arrayProp === 'forEach')
892
+ return (fn) => { };
893
+ // Numeric index access
894
+ if (!isNaN(Number(arrayProp))) {
895
+ return createRelationRecordingProxy(relationRecording);
896
+ }
897
+ return Reflect.get(arrayTarget, arrayProp);
898
+ }
899
+ });
900
+ }
901
+ // Regular array - wrap for recording
902
+ return createRecordingProxy(value, recording);
644
903
  }
645
- // Return a nested recording proxy for objects
904
+ // Return a nested recording proxy for regular objects
646
905
  if (value && typeof value === 'object') {
647
906
  return createRecordingProxy(value, recording);
648
907
  }
@@ -658,27 +917,43 @@ function analyzeBatchLoads(recordings, items) {
658
917
  // Find common relations across all recordings
659
918
  const relationCounts = new Map();
660
919
  for (const recording of recordings) {
661
- for (const [relationName, relation] of recording.relations) {
920
+ for (const [relationName] of recording.relations) {
662
921
  relationCounts.set(relationName, (relationCounts.get(relationName) || 0) + 1);
663
922
  }
664
923
  }
665
- // Only batch-load relations accessed in all (or most) items
924
+ // Batch-load any relation that was accessed at least once
666
925
  for (const [relationName, count] of relationCounts) {
667
- if (count >= recordings.length * 0.5) {
668
- // At least 50% of items access this relation
926
+ if (count > 0) {
927
+ // At least one item accesses this relation
669
928
  const ids = [];
670
929
  for (let i = 0; i < items.length; i++) {
671
930
  const item = items[i];
672
- const relationId = item[relationName];
673
- if (typeof relationId === 'string') {
674
- ids.push(relationId);
931
+ const relationValue = item[relationName];
932
+ // Handle array relations (e.g., members: ['id1', 'id2'])
933
+ if (Array.isArray(relationValue)) {
934
+ for (const element of relationValue) {
935
+ const elementId = extractEntityId(element);
936
+ if (elementId) {
937
+ ids.push(elementId);
938
+ }
939
+ }
675
940
  }
676
- else if (relationId && typeof relationId === 'object' && '$id' in relationId) {
677
- ids.push(relationId.$id);
941
+ else {
942
+ // Handle single relations - string ID or proxy object
943
+ const relationId = extractEntityId(relationValue);
944
+ if (relationId) {
945
+ ids.push(relationId);
946
+ }
678
947
  }
679
948
  }
680
949
  if (ids.length > 0) {
681
- const relation = recordings[0]?.relations.get(relationName);
950
+ // Find the relation info from any recording that has it
951
+ let relation;
952
+ for (const recording of recordings) {
953
+ relation = recording.relations.get(relationName);
954
+ if (relation)
955
+ break;
956
+ }
682
957
  if (relation) {
683
958
  batchLoads.set(relationName, { type: relation.type, ids });
684
959
  }
@@ -688,23 +963,169 @@ function analyzeBatchLoads(recordings, items) {
688
963
  return batchLoads;
689
964
  }
690
965
  /**
691
- * Execute batch loads for relations
966
+ * Execute batch loads for relations, including nested relations recursively
692
967
  */
693
- async function executeBatchLoads(batchLoads) {
968
+ async function executeBatchLoads(batchLoads, recordings) {
694
969
  const results = new Map();
695
- // For now, return empty - actual implementation would batch query
696
- // This is a placeholder that will be filled in by the actual DB integration
970
+ const provider = await getProvider();
971
+ if (!provider) {
972
+ // No provider available, return empty results
973
+ for (const [relationName] of batchLoads) {
974
+ results.set(relationName, new Map());
975
+ }
976
+ return results;
977
+ }
978
+ // Collect nested relation info from recordings
979
+ const nestedRelationInfo = new Map();
980
+ if (recordings) {
981
+ for (const recording of recordings) {
982
+ for (const [relationName, relationRecording] of recording.relations) {
983
+ if (!nestedRelationInfo.has(relationName)) {
984
+ nestedRelationInfo.set(relationName, {
985
+ nestedPaths: new Set(relationRecording.nestedPaths),
986
+ nestedRelations: new Map(relationRecording.nestedRelations),
987
+ });
988
+ }
989
+ else {
990
+ // Merge nested paths
991
+ const existing = nestedRelationInfo.get(relationName);
992
+ for (const path of relationRecording.nestedPaths) {
993
+ existing.nestedPaths.add(path);
994
+ }
995
+ for (const [nestedName, nestedRec] of relationRecording.nestedRelations) {
996
+ if (!existing.nestedRelations.has(nestedName)) {
997
+ existing.nestedRelations.set(nestedName, nestedRec);
998
+ }
999
+ }
1000
+ }
1001
+ }
1002
+ }
1003
+ }
1004
+ // Batch load each relation type
697
1005
  for (const [relationName, { type, ids }] of batchLoads) {
698
- results.set(relationName, new Map());
1006
+ const relationResults = new Map();
1007
+ // Deduplicate IDs
1008
+ const uniqueIds = [...new Set(ids)];
1009
+ // Fetch all entities in parallel
1010
+ const entities = await Promise.all(uniqueIds.map(id => provider.get(type, id)));
1011
+ // Map results by ID
1012
+ for (let i = 0; i < uniqueIds.length; i++) {
1013
+ const entity = entities[i];
1014
+ if (entity) {
1015
+ const entityId = (entity.$id || entity.id);
1016
+ relationResults.set(entityId, entity);
1017
+ }
1018
+ }
1019
+ results.set(relationName, relationResults);
1020
+ // Check for nested relations that need to be loaded
1021
+ const nestedInfo = nestedRelationInfo.get(relationName);
1022
+ if (nestedInfo && nestedInfo.nestedPaths.size > 0) {
1023
+ // For each nested path, check if it's actually a relation (string ID) on loaded entities
1024
+ const nestedBatchLoads = new Map();
1025
+ for (const nestedPath of nestedInfo.nestedPaths) {
1026
+ // Collect IDs from all loaded entities for this nested path
1027
+ const nestedIds = [];
1028
+ let nestedType;
1029
+ for (const entity of relationResults.values()) {
1030
+ const entityObj = entity;
1031
+ const entityType = entityObj.$type;
1032
+ const nestedValue = entityObj[nestedPath];
1033
+ if (typeof nestedValue === 'string') {
1034
+ // It's a string - could be an ID
1035
+ nestedIds.push(nestedValue);
1036
+ // Try to determine the type from various sources
1037
+ if (!nestedType) {
1038
+ // First, check the nested relation recording
1039
+ const nestedRecording = nestedInfo.nestedRelations.get(nestedPath);
1040
+ if (nestedRecording && nestedRecording.type !== 'unknown') {
1041
+ nestedType = nestedRecording.type;
1042
+ }
1043
+ // Then, try to get from schema info using the entity's $type
1044
+ if (!nestedType && entityType) {
1045
+ nestedType = getRelatedType(entityType, nestedPath);
1046
+ }
1047
+ }
1048
+ }
1049
+ else if (isPlainObject(nestedValue)) {
1050
+ // Check if it has a $type marker (for already-hydrated proxies)
1051
+ const valueType = extractMarkerType(nestedValue);
1052
+ if (valueType) {
1053
+ nestedType = valueType;
1054
+ // Try to get the ID via valueOf or $id
1055
+ const nestedId = extractEntityId(nestedValue);
1056
+ if (nestedId) {
1057
+ nestedIds.push(nestedId);
1058
+ }
1059
+ }
1060
+ }
1061
+ }
1062
+ if (nestedIds.length > 0 && nestedType) {
1063
+ nestedBatchLoads.set(nestedPath, { type: nestedType, ids: nestedIds });
1064
+ }
1065
+ }
1066
+ // Recursively load nested relations
1067
+ if (nestedBatchLoads.size > 0) {
1068
+ // Create nested recordings for the next level if available
1069
+ const nestedRecordings = [];
1070
+ for (const nestedRecording of nestedInfo.nestedRelations.values()) {
1071
+ nestedRecordings.push({
1072
+ paths: new Set(),
1073
+ relations: new Map([[nestedRecording.type, nestedRecording]]),
1074
+ });
1075
+ }
1076
+ const nestedResults = await executeBatchLoads(nestedBatchLoads, nestedRecordings.length > 0 ? nestedRecordings : undefined);
1077
+ // Enrich the already-loaded entities with their nested relations
1078
+ for (const [entityId, entity] of relationResults) {
1079
+ const enrichedEntity = enrichItemWithLoadedRelations(entity, nestedResults);
1080
+ relationResults.set(entityId, enrichedEntity);
1081
+ }
1082
+ }
1083
+ }
699
1084
  }
700
1085
  return results;
701
1086
  }
702
1087
  /**
703
- * Apply batch-loaded results to the mapped results
1088
+ * Enrich an item with loaded relations, replacing thenable proxies with actual data
1089
+ */
1090
+ function enrichItemWithLoadedRelations(item, loadedRelations) {
1091
+ const enriched = { ...item };
1092
+ for (const [relationName, relationData] of loadedRelations) {
1093
+ const relationValue = item[relationName];
1094
+ if (relationValue === undefined || relationValue === null) {
1095
+ continue;
1096
+ }
1097
+ // Handle array relations
1098
+ if (Array.isArray(relationValue)) {
1099
+ const loadedArray = [];
1100
+ for (const element of relationValue) {
1101
+ const idStr = extractEntityId(element);
1102
+ if (idStr) {
1103
+ const loaded = relationData.get(idStr);
1104
+ if (loaded) {
1105
+ loadedArray.push(loaded);
1106
+ }
1107
+ }
1108
+ }
1109
+ enriched[relationName] = loadedArray;
1110
+ }
1111
+ else {
1112
+ // Handle single relations - get the ID from the thenable proxy or direct value
1113
+ const relationId = extractEntityId(relationValue);
1114
+ if (relationId) {
1115
+ const loaded = relationData.get(relationId);
1116
+ if (loaded) {
1117
+ enriched[relationName] = loaded;
1118
+ }
1119
+ }
1120
+ }
1121
+ }
1122
+ return enriched;
1123
+ }
1124
+ /**
1125
+ * Apply batch-loaded results to the mapped results (deprecated, kept for compatibility)
704
1126
  */
705
1127
  function applyBatchResults(results, loadedRelations, originalItems) {
706
- // For now, return results as-is
707
- // Actual implementation would inject loaded relations
1128
+ // No longer used - enrichment happens before callback re-run
708
1129
  return results;
709
1130
  }
710
1131
  // =============================================================================
@@ -724,7 +1145,9 @@ export function isDBPromise(value) {
724
1145
  */
725
1146
  export function getRawDBPromise(value) {
726
1147
  if (RAW_DB_PROMISE_SYMBOL in value) {
727
- return value[RAW_DB_PROMISE_SYMBOL];
1148
+ // We know the symbol exists from the check above, so this cast is safe
1149
+ const raw = value[RAW_DB_PROMISE_SYMBOL];
1150
+ return raw;
728
1151
  }
729
1152
  return value;
730
1153
  }
@@ -827,7 +1250,22 @@ export function wrapEntityOperations(typeName, operations, actionsAPI) {
827
1250
  executor: () => operations.list(listOptions),
828
1251
  actionsAPI,
829
1252
  });
830
- return listPromise.forEach(callback, options);
1253
+ return listPromise.forEach(asCallback(callback), options);
1254
+ },
1255
+ // Semantic search methods
1256
+ semanticSearch(query, options) {
1257
+ if (operations.semanticSearch) {
1258
+ return operations.semanticSearch(query, options);
1259
+ }
1260
+ // Fallback: return empty array if not supported
1261
+ return Promise.resolve([]);
1262
+ },
1263
+ hybridSearch(query, options) {
1264
+ if (operations.hybridSearch) {
1265
+ return operations.hybridSearch(query, options);
1266
+ }
1267
+ // Fallback: return empty array if not supported
1268
+ return Promise.resolve([]);
831
1269
  },
832
1270
  // Mutations don't need wrapping
833
1271
  create: operations.create,