ai-database 2.0.2 → 2.1.1

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 (88) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/actions.d.ts +247 -0
  3. package/dist/actions.d.ts.map +1 -0
  4. package/dist/actions.js +260 -0
  5. package/dist/actions.js.map +1 -0
  6. package/dist/ai-promise-db.d.ts +34 -2
  7. package/dist/ai-promise-db.d.ts.map +1 -1
  8. package/dist/ai-promise-db.js +511 -66
  9. package/dist/ai-promise-db.js.map +1 -1
  10. package/dist/constants.d.ts +16 -0
  11. package/dist/constants.d.ts.map +1 -0
  12. package/dist/constants.js +16 -0
  13. package/dist/constants.js.map +1 -0
  14. package/dist/events.d.ts +153 -0
  15. package/dist/events.d.ts.map +1 -0
  16. package/dist/events.js +154 -0
  17. package/dist/events.js.map +1 -0
  18. package/dist/index.d.ts +8 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +13 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/memory-provider.d.ts +144 -2
  23. package/dist/memory-provider.d.ts.map +1 -1
  24. package/dist/memory-provider.js +569 -13
  25. package/dist/memory-provider.js.map +1 -1
  26. package/dist/schema/cascade.d.ts +96 -0
  27. package/dist/schema/cascade.d.ts.map +1 -0
  28. package/dist/schema/cascade.js +528 -0
  29. package/dist/schema/cascade.js.map +1 -0
  30. package/dist/schema/index.d.ts +197 -0
  31. package/dist/schema/index.d.ts.map +1 -0
  32. package/dist/schema/index.js +1211 -0
  33. package/dist/schema/index.js.map +1 -0
  34. package/dist/schema/parse.d.ts +225 -0
  35. package/dist/schema/parse.d.ts.map +1 -0
  36. package/dist/schema/parse.js +732 -0
  37. package/dist/schema/parse.js.map +1 -0
  38. package/dist/schema/provider.d.ts +176 -0
  39. package/dist/schema/provider.d.ts.map +1 -0
  40. package/dist/schema/provider.js +258 -0
  41. package/dist/schema/provider.js.map +1 -0
  42. package/dist/schema/resolve.d.ts +87 -0
  43. package/dist/schema/resolve.d.ts.map +1 -0
  44. package/dist/schema/resolve.js +474 -0
  45. package/dist/schema/resolve.js.map +1 -0
  46. package/dist/schema/semantic.d.ts +53 -0
  47. package/dist/schema/semantic.d.ts.map +1 -0
  48. package/dist/schema/semantic.js +247 -0
  49. package/dist/schema/semantic.js.map +1 -0
  50. package/dist/schema/types.d.ts +528 -0
  51. package/dist/schema/types.d.ts.map +1 -0
  52. package/dist/schema/types.js +9 -0
  53. package/dist/schema/types.js.map +1 -0
  54. package/dist/schema.d.ts +24 -867
  55. package/dist/schema.d.ts.map +1 -1
  56. package/dist/schema.js +41 -1124
  57. package/dist/schema.js.map +1 -1
  58. package/dist/semantic.d.ts +175 -0
  59. package/dist/semantic.d.ts.map +1 -0
  60. package/dist/semantic.js +338 -0
  61. package/dist/semantic.js.map +1 -0
  62. package/dist/types.d.ts +14 -0
  63. package/dist/types.d.ts.map +1 -1
  64. package/dist/types.js.map +1 -1
  65. package/package.json +13 -4
  66. package/.turbo/turbo-build.log +0 -5
  67. package/TESTING.md +0 -410
  68. package/TEST_SUMMARY.md +0 -250
  69. package/TODO.md +0 -128
  70. package/src/ai-promise-db.ts +0 -1243
  71. package/src/authorization.ts +0 -1102
  72. package/src/durable-clickhouse.ts +0 -596
  73. package/src/durable-promise.ts +0 -582
  74. package/src/execution-queue.ts +0 -608
  75. package/src/index.test.ts +0 -868
  76. package/src/index.ts +0 -337
  77. package/src/linguistic.ts +0 -404
  78. package/src/memory-provider.test.ts +0 -1036
  79. package/src/memory-provider.ts +0 -1119
  80. package/src/schema.test.ts +0 -1254
  81. package/src/schema.ts +0 -2296
  82. package/src/tests.ts +0 -725
  83. package/src/types.ts +0 -1177
  84. package/test/README.md +0 -153
  85. package/test/edge-cases.test.ts +0 -646
  86. package/test/provider-resolution.test.ts +0 -402
  87. package/tsconfig.json +0 -9
  88. package/vitest.config.ts +0 -19
@@ -20,6 +20,42 @@
20
20
  *
21
21
  * @packageDocumentation
22
22
  */
23
+ import { Semaphore } from './memory-provider.js';
24
+ // Provider resolver - will be set by schema.ts
25
+ let providerResolver = null;
26
+ /**
27
+ * Set the provider resolver function (called from schema.ts)
28
+ */
29
+ export function setProviderResolver(resolver) {
30
+ providerResolver = resolver;
31
+ }
32
+ /**
33
+ * Get the provider for batch operations
34
+ */
35
+ async function getProvider() {
36
+ if (providerResolver) {
37
+ return providerResolver();
38
+ }
39
+ return null;
40
+ }
41
+ // Schema info for batch loading - stores relation field info for entity types
42
+ // Maps entityType -> fieldName -> relatedType
43
+ let schemaRelationInfo = null;
44
+ /**
45
+ * Set schema relation info for batch loading nested relations
46
+ * Called from schema.ts when DB() is initialized
47
+ */
48
+ export function setSchemaRelationInfo(info) {
49
+ schemaRelationInfo = info;
50
+ }
51
+ /**
52
+ * Get the related type for a field on an entity type
53
+ */
54
+ function getRelatedType(entityType, fieldName) {
55
+ if (!schemaRelationInfo)
56
+ return undefined;
57
+ return schemaRelationInfo.get(entityType)?.get(fieldName);
58
+ }
23
59
  // =============================================================================
24
60
  // Types
25
61
  // =============================================================================
@@ -84,7 +120,13 @@ export class DBPromise {
84
120
  }
85
121
  // Execute the query
86
122
  const result = await this._options.executor();
87
- this._resolvedValue = result;
123
+ // If result is an array, wrap it with batch-loading map
124
+ if (Array.isArray(result)) {
125
+ this._resolvedValue = createBatchLoadingArray(result);
126
+ }
127
+ else {
128
+ this._resolvedValue = result;
129
+ }
88
130
  this._isResolved = true;
89
131
  return this._resolvedValue;
90
132
  }
@@ -113,8 +155,7 @@ export class DBPromise {
113
155
  }
114
156
  // Create recording context
115
157
  const recordings = [];
116
- // Record what the callback accesses for each item
117
- const recordedResults = [];
158
+ // Phase 1: Record what the callback accesses for each item (using placeholder proxies)
118
159
  for (let i = 0; i < items.length; i++) {
119
160
  const item = items[i];
120
161
  const recording = {
@@ -123,17 +164,29 @@ export class DBPromise {
123
164
  };
124
165
  // Create a recording proxy for this item
125
166
  const recordingProxy = createRecordingProxy(item, recording);
126
- // Execute callback with recording proxy
127
- const result = callback(recordingProxy, i);
128
- recordedResults.push(result);
167
+ // Execute callback with recording proxy to discover accesses
168
+ try {
169
+ callback(recordingProxy, i);
170
+ }
171
+ catch {
172
+ // Ignore errors during recording phase - they'll surface in Phase 3
173
+ }
129
174
  recordings.push(recording);
130
175
  }
131
- // Analyze recordings to find batch-loadable relations
176
+ // Phase 2: Analyze recordings and batch-load relations
132
177
  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);
178
+ const loadedRelations = await executeBatchLoads(batchLoads, recordings);
179
+ // Phase 3: Re-run callback with enriched items that have loaded relations
180
+ const enrichedItems = [];
181
+ for (let i = 0; i < items.length; i++) {
182
+ enrichedItems.push(enrichItemWithLoadedRelations(items[i], loadedRelations));
183
+ }
184
+ // Execute callback again with enriched data
185
+ const results = [];
186
+ for (let i = 0; i < enrichedItems.length; i++) {
187
+ results.push(callback(enrichedItems[i], i));
188
+ }
189
+ return results;
137
190
  },
138
191
  });
139
192
  }
@@ -579,35 +632,6 @@ const DB_PROXY_HANDLERS = {
579
632
  function sleep(ms) {
580
633
  return new Promise((resolve) => setTimeout(resolve, ms));
581
634
  }
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
635
  /**
612
636
  * Get nested value from object
613
637
  */
@@ -620,6 +644,131 @@ function getNestedValue(obj, path) {
620
644
  }
621
645
  return current;
622
646
  }
647
+ /**
648
+ * Create an array that has a batch-loading map method
649
+ * When .map() is called on the resolved array, it performs batch loading of relations
650
+ */
651
+ function createBatchLoadingArray(items) {
652
+ // Create a new array with all the original items
653
+ const batchArray = [...items];
654
+ // Override the map method to do batch loading
655
+ Object.defineProperty(batchArray, 'map', {
656
+ value: async function (callback) {
657
+ const items = this;
658
+ // Phase 1: Record what the callback accesses using placeholder proxies
659
+ const recordings = [];
660
+ for (let i = 0; i < items.length; i++) {
661
+ const item = items[i];
662
+ const recording = {
663
+ paths: new Set(),
664
+ relations: new Map(),
665
+ };
666
+ // Create a recording proxy for this item
667
+ const recordingProxy = createRecordingProxy(item, recording);
668
+ // Execute callback with recording proxy to discover accesses
669
+ try {
670
+ callback(recordingProxy, i);
671
+ }
672
+ catch {
673
+ // Ignore errors during recording phase - they'll surface in Phase 3
674
+ }
675
+ recordings.push(recording);
676
+ }
677
+ // Phase 2: Analyze recordings and batch-load relations
678
+ const batchLoads = analyzeBatchLoads(recordings, items);
679
+ const loadedRelations = await executeBatchLoads(batchLoads, recordings);
680
+ // Phase 3: Re-run callback with enriched items that have loaded relations
681
+ const enrichedItems = [];
682
+ for (let i = 0; i < items.length; i++) {
683
+ enrichedItems.push(enrichItemWithLoadedRelations(items[i], loadedRelations));
684
+ }
685
+ // Execute callback again with enriched data
686
+ const results = [];
687
+ for (let i = 0; i < enrichedItems.length; i++) {
688
+ results.push(callback(enrichedItems[i], i));
689
+ }
690
+ return results;
691
+ },
692
+ writable: true,
693
+ configurable: true,
694
+ enumerable: false,
695
+ });
696
+ return batchArray;
697
+ }
698
+ /**
699
+ * Create a proxy that records nested property accesses for relations
700
+ * This returns placeholder values to allow the callback to complete
701
+ *
702
+ * When accessing customer.address.city:
703
+ * - At depth 0 (path=[]), accessing 'address' records it in nestedPaths
704
+ * - Accessing 'city' on 'address' creates a nestedRelation for 'address' and records 'city' in its nestedPaths
705
+ */
706
+ function createRelationRecordingProxy(relationRecording, path = [], currentNestedRecording) {
707
+ // Return a proxy that records all nested accesses
708
+ return new Proxy({}, {
709
+ get(target, prop) {
710
+ if (typeof prop === 'symbol') {
711
+ return undefined;
712
+ }
713
+ // For common array methods that don't need recording
714
+ if (prop === 'map' || prop === 'filter' || prop === 'forEach' || prop === 'length') {
715
+ if (relationRecording.isArray) {
716
+ // Return array-like behavior
717
+ if (prop === 'length')
718
+ return 0;
719
+ if (prop === 'map')
720
+ return (fn) => [];
721
+ if (prop === 'filter')
722
+ return (fn) => [];
723
+ if (prop === 'forEach')
724
+ return (fn) => { };
725
+ }
726
+ return undefined;
727
+ }
728
+ if (path.length === 0) {
729
+ // First level: recording access to properties of the relation itself (e.g., customer.address)
730
+ // This is a direct property access on the relation - record it
731
+ relationRecording.nestedPaths.add(prop);
732
+ // Create a nested recording for potential deeper access
733
+ // We don't know if 'prop' is a relation yet, but if there's further access we'll need it
734
+ let nestedRec = relationRecording.nestedRelations.get(prop);
735
+ if (!nestedRec) {
736
+ nestedRec = {
737
+ type: 'unknown', // Type will be inferred when loading
738
+ isArray: false,
739
+ nestedPaths: new Set(),
740
+ nestedRelations: new Map(),
741
+ };
742
+ relationRecording.nestedRelations.set(prop, nestedRec);
743
+ }
744
+ // Return a proxy that will record further accesses into the nested recording
745
+ return createRelationRecordingProxy(relationRecording, [prop], nestedRec);
746
+ }
747
+ else {
748
+ // Deeper level: recording access to properties of a nested relation (e.g., customer.address.city)
749
+ // Record this property in the current nested recording
750
+ if (currentNestedRecording) {
751
+ currentNestedRecording.nestedPaths.add(prop);
752
+ // Create another nested recording for even deeper access
753
+ let deeperRec = currentNestedRecording.nestedRelations.get(prop);
754
+ if (!deeperRec) {
755
+ deeperRec = {
756
+ type: 'unknown',
757
+ isArray: false,
758
+ nestedPaths: new Set(),
759
+ nestedRelations: new Map(),
760
+ };
761
+ currentNestedRecording.nestedRelations.set(prop, deeperRec);
762
+ }
763
+ return createRelationRecordingProxy(relationRecording, [...path, prop], deeperRec);
764
+ }
765
+ // Fallback - just record in nestedPaths of root
766
+ relationRecording.nestedPaths.add(prop);
767
+ return createRelationRecordingProxy(relationRecording, [...path, prop]);
768
+ }
769
+ },
770
+ });
771
+ }
623
772
  /**
624
773
  * Create a proxy that records property accesses
625
774
  */
@@ -634,15 +783,82 @@ function createRecordingProxy(item, recording) {
634
783
  }
635
784
  recording.paths.add(prop);
636
785
  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
- });
786
+ // If accessing a relation (identified by $type marker from hydration)
787
+ // Note: proxies may not expose $type in 'has' trap, so check via property access
788
+ const maybeType = value && typeof value === 'object' ? value.$type : undefined;
789
+ if (maybeType && typeof maybeType === 'string') {
790
+ const relationType = maybeType;
791
+ // Get or create the relation recording
792
+ let relationRecording = recording.relations.get(prop);
793
+ if (!relationRecording) {
794
+ relationRecording = {
795
+ type: relationType,
796
+ isArray: Array.isArray(value),
797
+ nestedPaths: new Set(),
798
+ nestedRelations: new Map(),
799
+ };
800
+ recording.relations.set(prop, relationRecording);
801
+ }
802
+ // Return a proxy that records nested accesses but uses placeholder values
803
+ return createRelationRecordingProxy(relationRecording);
804
+ }
805
+ // Handle arrays with potential relation elements (like members: ['->User'])
806
+ if (Array.isArray(value)) {
807
+ // Check if the array itself is a relation array (has $type marker from thenableArray)
808
+ const arrayType = value.$type;
809
+ const isArrayRelation = arrayType !== undefined || value.$isArrayRelation;
810
+ // Also check if array contains relation proxies (for backwards compatibility)
811
+ const hasRelationElements = !isArrayRelation && value.some(v => v && typeof v === 'object' && v.$type !== undefined);
812
+ if (isArrayRelation || hasRelationElements) {
813
+ // Get the type from the array $type or first element
814
+ let relationType = arrayType;
815
+ if (!relationType) {
816
+ const firstRelation = value.find(v => v && typeof v === 'object' && v.$type !== undefined);
817
+ relationType = firstRelation ? firstRelation.$type : 'unknown';
818
+ }
819
+ let relationRecording = recording.relations.get(prop);
820
+ if (!relationRecording) {
821
+ relationRecording = {
822
+ type: relationType,
823
+ isArray: true,
824
+ nestedPaths: new Set(),
825
+ nestedRelations: new Map(),
826
+ };
827
+ recording.relations.set(prop, relationRecording);
828
+ }
829
+ // Return a proxy array that records element accesses
830
+ return new Proxy(value, {
831
+ get(arrayTarget, arrayProp) {
832
+ if (arrayProp === 'map') {
833
+ return (fn) => {
834
+ // Record what the map callback accesses
835
+ const elementProxy = createRelationRecordingProxy(relationRecording);
836
+ // Execute callback to record accesses, but we can't return real results
837
+ try {
838
+ fn(elementProxy, 0);
839
+ }
840
+ catch { }
841
+ return [];
842
+ };
843
+ }
844
+ if (arrayProp === 'length')
845
+ return value.length;
846
+ if (arrayProp === 'filter')
847
+ return (fn) => [];
848
+ if (arrayProp === 'forEach')
849
+ return (fn) => { };
850
+ // Numeric index access
851
+ if (!isNaN(Number(arrayProp))) {
852
+ return createRelationRecordingProxy(relationRecording);
853
+ }
854
+ return Reflect.get(arrayTarget, arrayProp);
855
+ }
856
+ });
857
+ }
858
+ // Regular array - wrap for recording
859
+ return createRecordingProxy(value, recording);
644
860
  }
645
- // Return a nested recording proxy for objects
861
+ // Return a nested recording proxy for regular objects
646
862
  if (value && typeof value === 'object') {
647
863
  return createRecordingProxy(value, recording);
648
864
  }
@@ -658,27 +874,62 @@ function analyzeBatchLoads(recordings, items) {
658
874
  // Find common relations across all recordings
659
875
  const relationCounts = new Map();
660
876
  for (const recording of recordings) {
661
- for (const [relationName, relation] of recording.relations) {
877
+ for (const [relationName] of recording.relations) {
662
878
  relationCounts.set(relationName, (relationCounts.get(relationName) || 0) + 1);
663
879
  }
664
880
  }
665
- // Only batch-load relations accessed in all (or most) items
881
+ // Batch-load any relation that was accessed at least once
666
882
  for (const [relationName, count] of relationCounts) {
667
- if (count >= recordings.length * 0.5) {
668
- // At least 50% of items access this relation
883
+ if (count > 0) {
884
+ // At least one item accesses this relation
669
885
  const ids = [];
670
886
  for (let i = 0; i < items.length; i++) {
671
887
  const item = items[i];
672
- const relationId = item[relationName];
673
- if (typeof relationId === 'string') {
674
- ids.push(relationId);
888
+ const relationValue = item[relationName];
889
+ // Handle array relations (e.g., members: ['id1', 'id2'])
890
+ if (Array.isArray(relationValue)) {
891
+ for (const element of relationValue) {
892
+ if (typeof element === 'string') {
893
+ ids.push(element);
894
+ }
895
+ else if (element && typeof element === 'object') {
896
+ // Try valueOf() for thenable proxies
897
+ if (typeof element.valueOf === 'function') {
898
+ const val = element.valueOf();
899
+ if (typeof val === 'string') {
900
+ ids.push(val);
901
+ }
902
+ }
903
+ else if (element.$id) {
904
+ ids.push(element.$id);
905
+ }
906
+ }
907
+ }
908
+ }
909
+ else if (typeof relationValue === 'string') {
910
+ ids.push(relationValue);
675
911
  }
676
- else if (relationId && typeof relationId === 'object' && '$id' in relationId) {
677
- ids.push(relationId.$id);
912
+ else if (relationValue && typeof relationValue === 'object') {
913
+ // Try valueOf() for thenable proxies (single relation)
914
+ if (typeof relationValue.valueOf === 'function') {
915
+ const val = relationValue.valueOf();
916
+ if (typeof val === 'string') {
917
+ ids.push(val);
918
+ }
919
+ }
920
+ else if (relationValue.$id) {
921
+ ids.push(relationValue.$id);
922
+ }
678
923
  }
679
924
  }
680
925
  if (ids.length > 0) {
681
- const relation = recordings[0]?.relations.get(relationName);
926
+ // Find the relation info from any recording that has it
927
+ let relation;
928
+ for (const recording of recordings) {
929
+ relation = recording.relations.get(relationName);
930
+ if (relation)
931
+ break;
932
+ }
682
933
  if (relation) {
683
934
  batchLoads.set(relationName, { type: relation.type, ids });
684
935
  }
@@ -688,23 +939,202 @@ function analyzeBatchLoads(recordings, items) {
688
939
  return batchLoads;
689
940
  }
690
941
  /**
691
- * Execute batch loads for relations
942
+ * Execute batch loads for relations, including nested relations recursively
692
943
  */
693
- async function executeBatchLoads(batchLoads) {
944
+ async function executeBatchLoads(batchLoads, recordings) {
694
945
  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
946
+ const provider = await getProvider();
947
+ if (!provider) {
948
+ // No provider available, return empty results
949
+ for (const [relationName] of batchLoads) {
950
+ results.set(relationName, new Map());
951
+ }
952
+ return results;
953
+ }
954
+ // Collect nested relation info from recordings
955
+ const nestedRelationInfo = new Map();
956
+ if (recordings) {
957
+ for (const recording of recordings) {
958
+ for (const [relationName, relationRecording] of recording.relations) {
959
+ if (!nestedRelationInfo.has(relationName)) {
960
+ nestedRelationInfo.set(relationName, {
961
+ nestedPaths: new Set(relationRecording.nestedPaths),
962
+ nestedRelations: new Map(relationRecording.nestedRelations),
963
+ });
964
+ }
965
+ else {
966
+ // Merge nested paths
967
+ const existing = nestedRelationInfo.get(relationName);
968
+ for (const path of relationRecording.nestedPaths) {
969
+ existing.nestedPaths.add(path);
970
+ }
971
+ for (const [nestedName, nestedRec] of relationRecording.nestedRelations) {
972
+ if (!existing.nestedRelations.has(nestedName)) {
973
+ existing.nestedRelations.set(nestedName, nestedRec);
974
+ }
975
+ }
976
+ }
977
+ }
978
+ }
979
+ }
980
+ // Batch load each relation type
697
981
  for (const [relationName, { type, ids }] of batchLoads) {
698
- results.set(relationName, new Map());
982
+ const relationResults = new Map();
983
+ // Deduplicate IDs
984
+ const uniqueIds = [...new Set(ids)];
985
+ // Fetch all entities in parallel
986
+ const entities = await Promise.all(uniqueIds.map(id => provider.get(type, id)));
987
+ // Map results by ID
988
+ for (let i = 0; i < uniqueIds.length; i++) {
989
+ const entity = entities[i];
990
+ if (entity) {
991
+ const entityId = (entity.$id || entity.id);
992
+ relationResults.set(entityId, entity);
993
+ }
994
+ }
995
+ results.set(relationName, relationResults);
996
+ // Check for nested relations that need to be loaded
997
+ const nestedInfo = nestedRelationInfo.get(relationName);
998
+ if (nestedInfo && nestedInfo.nestedPaths.size > 0) {
999
+ // For each nested path, check if it's actually a relation (string ID) on loaded entities
1000
+ const nestedBatchLoads = new Map();
1001
+ for (const nestedPath of nestedInfo.nestedPaths) {
1002
+ // Collect IDs from all loaded entities for this nested path
1003
+ const nestedIds = [];
1004
+ let nestedType;
1005
+ for (const entity of relationResults.values()) {
1006
+ const entityObj = entity;
1007
+ const entityType = entityObj.$type;
1008
+ const nestedValue = entityObj[nestedPath];
1009
+ if (typeof nestedValue === 'string') {
1010
+ // It's a string - could be an ID
1011
+ nestedIds.push(nestedValue);
1012
+ // Try to determine the type from various sources
1013
+ if (!nestedType) {
1014
+ // First, check the nested relation recording
1015
+ const nestedRecording = nestedInfo.nestedRelations.get(nestedPath);
1016
+ if (nestedRecording && nestedRecording.type !== 'unknown') {
1017
+ nestedType = nestedRecording.type;
1018
+ }
1019
+ // Then, try to get from schema info using the entity's $type
1020
+ if (!nestedType && entityType) {
1021
+ nestedType = getRelatedType(entityType, nestedPath);
1022
+ }
1023
+ }
1024
+ }
1025
+ else if (nestedValue && typeof nestedValue === 'object') {
1026
+ // Check if it has a $type marker (for already-hydrated proxies)
1027
+ const valueType = nestedValue.$type;
1028
+ if (valueType && typeof valueType === 'string') {
1029
+ nestedType = valueType;
1030
+ // Try to get the ID
1031
+ if (typeof nestedValue.valueOf === 'function') {
1032
+ const val = nestedValue.valueOf();
1033
+ if (typeof val === 'string') {
1034
+ nestedIds.push(val);
1035
+ }
1036
+ }
1037
+ }
1038
+ }
1039
+ }
1040
+ if (nestedIds.length > 0 && nestedType) {
1041
+ nestedBatchLoads.set(nestedPath, { type: nestedType, ids: nestedIds });
1042
+ }
1043
+ }
1044
+ // Recursively load nested relations
1045
+ if (nestedBatchLoads.size > 0) {
1046
+ // Create nested recordings for the next level if available
1047
+ const nestedRecordings = [];
1048
+ for (const nestedRecording of nestedInfo.nestedRelations.values()) {
1049
+ nestedRecordings.push({
1050
+ paths: new Set(),
1051
+ relations: new Map([[nestedRecording.type, nestedRecording]]),
1052
+ });
1053
+ }
1054
+ const nestedResults = await executeBatchLoads(nestedBatchLoads, nestedRecordings.length > 0 ? nestedRecordings : undefined);
1055
+ // Enrich the already-loaded entities with their nested relations
1056
+ for (const [entityId, entity] of relationResults) {
1057
+ const enrichedEntity = enrichItemWithLoadedRelations(entity, nestedResults);
1058
+ relationResults.set(entityId, enrichedEntity);
1059
+ }
1060
+ }
1061
+ }
699
1062
  }
700
1063
  return results;
701
1064
  }
702
1065
  /**
703
- * Apply batch-loaded results to the mapped results
1066
+ * Enrich an item with loaded relations, replacing thenable proxies with actual data
1067
+ */
1068
+ function enrichItemWithLoadedRelations(item, loadedRelations) {
1069
+ const enriched = { ...item };
1070
+ for (const [relationName, relationData] of loadedRelations) {
1071
+ const relationValue = item[relationName];
1072
+ if (relationValue === undefined || relationValue === null) {
1073
+ continue;
1074
+ }
1075
+ // Handle array relations
1076
+ if (Array.isArray(relationValue)) {
1077
+ const loadedArray = [];
1078
+ for (const element of relationValue) {
1079
+ let idStr;
1080
+ if (typeof element === 'string') {
1081
+ idStr = element;
1082
+ }
1083
+ else if (element && typeof element === 'object') {
1084
+ // Try valueOf for thenable proxies
1085
+ if (typeof element.valueOf === 'function') {
1086
+ const val = element.valueOf();
1087
+ if (typeof val === 'string') {
1088
+ idStr = val;
1089
+ }
1090
+ }
1091
+ if (!idStr && element.$id) {
1092
+ idStr = element.$id;
1093
+ }
1094
+ }
1095
+ if (idStr) {
1096
+ const loaded = relationData.get(idStr);
1097
+ if (loaded) {
1098
+ loadedArray.push(loaded);
1099
+ }
1100
+ }
1101
+ }
1102
+ enriched[relationName] = loadedArray;
1103
+ }
1104
+ else {
1105
+ // Handle single relations - get the ID from the thenable proxy or direct value
1106
+ let relationId;
1107
+ if (typeof relationValue === 'string') {
1108
+ relationId = relationValue;
1109
+ }
1110
+ else if (relationValue && typeof relationValue === 'object') {
1111
+ // Try to get ID from proxy's valueOf or toString
1112
+ if ('valueOf' in relationValue && typeof relationValue.valueOf === 'function') {
1113
+ const val = relationValue.valueOf();
1114
+ if (typeof val === 'string') {
1115
+ relationId = val;
1116
+ }
1117
+ }
1118
+ // Also check for $id
1119
+ if (!relationId && '$id' in relationValue) {
1120
+ relationId = relationValue.$id;
1121
+ }
1122
+ }
1123
+ if (relationId) {
1124
+ const loaded = relationData.get(relationId);
1125
+ if (loaded) {
1126
+ enriched[relationName] = loaded;
1127
+ }
1128
+ }
1129
+ }
1130
+ }
1131
+ return enriched;
1132
+ }
1133
+ /**
1134
+ * Apply batch-loaded results to the mapped results (deprecated, kept for compatibility)
704
1135
  */
705
1136
  function applyBatchResults(results, loadedRelations, originalItems) {
706
- // For now, return results as-is
707
- // Actual implementation would inject loaded relations
1137
+ // No longer used - enrichment happens before callback re-run
708
1138
  return results;
709
1139
  }
710
1140
  // =============================================================================
@@ -829,6 +1259,21 @@ export function wrapEntityOperations(typeName, operations, actionsAPI) {
829
1259
  });
830
1260
  return listPromise.forEach(callback, options);
831
1261
  },
1262
+ // Semantic search methods
1263
+ semanticSearch(query, options) {
1264
+ if (operations.semanticSearch) {
1265
+ return operations.semanticSearch(query, options);
1266
+ }
1267
+ // Fallback: return empty array if not supported
1268
+ return Promise.resolve([]);
1269
+ },
1270
+ hybridSearch(query, options) {
1271
+ if (operations.hybridSearch) {
1272
+ return operations.hybridSearch(query, options);
1273
+ }
1274
+ // Fallback: return empty array if not supported
1275
+ return Promise.resolve([]);
1276
+ },
832
1277
  // Mutations don't need wrapping
833
1278
  create: operations.create,
834
1279
  update: operations.update,