ai-database 2.0.1 → 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.
- package/CHANGELOG.md +43 -0
- package/dist/actions.d.ts +247 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +260 -0
- package/dist/actions.js.map +1 -0
- package/dist/ai-promise-db.d.ts +34 -2
- package/dist/ai-promise-db.d.ts.map +1 -1
- package/dist/ai-promise-db.js +511 -66
- package/dist/ai-promise-db.js.map +1 -1
- package/dist/constants.d.ts +16 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +16 -0
- package/dist/constants.js.map +1 -0
- package/dist/events.d.ts +153 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +154 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/dist/memory-provider.d.ts +144 -2
- package/dist/memory-provider.d.ts.map +1 -1
- package/dist/memory-provider.js +569 -13
- package/dist/memory-provider.js.map +1 -1
- package/dist/schema/cascade.d.ts +96 -0
- package/dist/schema/cascade.d.ts.map +1 -0
- package/dist/schema/cascade.js +528 -0
- package/dist/schema/cascade.js.map +1 -0
- package/dist/schema/index.d.ts +197 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +1211 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/parse.d.ts +225 -0
- package/dist/schema/parse.d.ts.map +1 -0
- package/dist/schema/parse.js +732 -0
- package/dist/schema/parse.js.map +1 -0
- package/dist/schema/provider.d.ts +176 -0
- package/dist/schema/provider.d.ts.map +1 -0
- package/dist/schema/provider.js +258 -0
- package/dist/schema/provider.js.map +1 -0
- package/dist/schema/resolve.d.ts +87 -0
- package/dist/schema/resolve.d.ts.map +1 -0
- package/dist/schema/resolve.js +474 -0
- package/dist/schema/resolve.js.map +1 -0
- package/dist/schema/semantic.d.ts +53 -0
- package/dist/schema/semantic.d.ts.map +1 -0
- package/dist/schema/semantic.js +247 -0
- package/dist/schema/semantic.js.map +1 -0
- package/dist/schema/types.d.ts +528 -0
- package/dist/schema/types.d.ts.map +1 -0
- package/dist/schema/types.js +9 -0
- package/dist/schema/types.js.map +1 -0
- package/dist/schema.d.ts +24 -867
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +41 -1124
- package/dist/schema.js.map +1 -1
- package/dist/semantic.d.ts +175 -0
- package/dist/semantic.d.ts.map +1 -0
- package/dist/semantic.js +338 -0
- package/dist/semantic.js.map +1 -0
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +13 -4
- package/.turbo/turbo-build.log +0 -5
- package/TESTING.md +0 -410
- package/TEST_SUMMARY.md +0 -250
- package/TODO.md +0 -128
- package/src/ai-promise-db.ts +0 -1243
- package/src/authorization.ts +0 -1102
- package/src/durable-clickhouse.ts +0 -596
- package/src/durable-promise.ts +0 -582
- package/src/execution-queue.ts +0 -608
- package/src/index.test.ts +0 -868
- package/src/index.ts +0 -337
- package/src/linguistic.ts +0 -404
- package/src/memory-provider.test.ts +0 -1036
- package/src/memory-provider.ts +0 -1119
- package/src/schema.test.ts +0 -1254
- package/src/schema.ts +0 -2296
- package/src/tests.ts +0 -725
- package/src/types.ts +0 -1177
- package/test/README.md +0 -153
- package/test/edge-cases.test.ts +0 -646
- package/test/provider-resolution.test.ts +0 -402
- package/tsconfig.json +0 -9
- package/vitest.config.ts +0 -19
package/dist/ai-promise-db.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
128
|
-
|
|
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
|
|
176
|
+
// Phase 2: Analyze recordings and batch-load relations
|
|
132
177
|
const batchLoads = analyzeBatchLoads(recordings, items);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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 $
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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
|
|
877
|
+
for (const [relationName] of recording.relations) {
|
|
662
878
|
relationCounts.set(relationName, (relationCounts.get(relationName) || 0) + 1);
|
|
663
879
|
}
|
|
664
880
|
}
|
|
665
|
-
//
|
|
881
|
+
// Batch-load any relation that was accessed at least once
|
|
666
882
|
for (const [relationName, count] of relationCounts) {
|
|
667
|
-
if (count
|
|
668
|
-
// At least
|
|
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
|
|
673
|
-
|
|
674
|
-
|
|
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 (
|
|
677
|
-
|
|
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
|
-
|
|
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
|
-
|
|
696
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
//
|
|
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,
|