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.
- package/CHANGELOG.md +48 -0
- package/LICENSE +21 -0
- package/README.md +667 -1
- 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 +37 -2
- package/dist/ai-promise-db.d.ts.map +1 -1
- package/dist/ai-promise-db.js +530 -92
- 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 +11 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -2
- package/dist/index.js.map +1 -1
- package/dist/memory-provider.d.ts +145 -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 +104 -0
- package/dist/schema/cascade.d.ts.map +1 -0
- package/dist/schema/cascade.js +547 -0
- package/dist/schema/cascade.js.map +1 -0
- package/dist/schema/dependency-graph.d.ts +133 -0
- package/dist/schema/dependency-graph.d.ts.map +1 -0
- package/dist/schema/dependency-graph.js +355 -0
- package/dist/schema/dependency-graph.js.map +1 -0
- package/dist/schema/generation-context.d.ts +202 -0
- package/dist/schema/generation-context.d.ts.map +1 -0
- package/dist/schema/generation-context.js +393 -0
- package/dist/schema/generation-context.js.map +1 -0
- package/dist/schema/index.d.ts +201 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +1221 -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 +740 -0
- package/dist/schema/parse.js.map +1 -0
- package/dist/schema/provider.d.ts +177 -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 +549 -0
- package/dist/schema/resolve.js.map +1 -0
- package/dist/schema/semantic.d.ts +54 -0
- package/dist/schema/semantic.d.ts.map +1 -0
- package/dist/schema/semantic.js +335 -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/union-fallback.d.ts +219 -0
- package/dist/schema/union-fallback.d.ts.map +1 -0
- package/dist/schema/union-fallback.js +325 -0
- package/dist/schema/union-fallback.js.map +1 -0
- package/dist/schema/verb-derivation.d.ts +167 -0
- package/dist/schema/verb-derivation.d.ts.map +1 -0
- package/dist/schema/verb-derivation.js +281 -0
- package/dist/schema/verb-derivation.js.map +1 -0
- package/dist/schema.d.ts +25 -867
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +45 -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/type-guards.d.ts +167 -0
- package/dist/type-guards.d.ts.map +1 -0
- package/dist/type-guards.js +247 -0
- package/dist/type-guards.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/dist/validation.d.ts +168 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +667 -0
- package/dist/validation.js.map +1 -0
- package/package.json +21 -12
- 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,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
|
-
|
|
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
|
-
|
|
128
|
-
|
|
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
|
|
178
|
+
// Phase 2: Analyze recordings and batch-load relations
|
|
132
179
|
const batchLoads = analyzeBatchLoads(recordings, items);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
416
|
+
Promise.resolve(typedCallback(item, index)),
|
|
357
417
|
timeoutPromise,
|
|
358
418
|
]);
|
|
359
419
|
}
|
|
360
420
|
else {
|
|
361
|
-
result = await
|
|
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
|
|
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
|
-
|
|
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 (
|
|
547
|
-
return 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('_') ||
|
|
551
|
-
return target
|
|
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:
|
|
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
|
-
|
|
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 $
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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
|
|
920
|
+
for (const [relationName] of recording.relations) {
|
|
662
921
|
relationCounts.set(relationName, (relationCounts.get(relationName) || 0) + 1);
|
|
663
922
|
}
|
|
664
923
|
}
|
|
665
|
-
//
|
|
924
|
+
// Batch-load any relation that was accessed at least once
|
|
666
925
|
for (const [relationName, count] of relationCounts) {
|
|
667
|
-
if (count
|
|
668
|
-
// At least
|
|
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
|
|
673
|
-
|
|
674
|
-
|
|
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
|
|
677
|
-
|
|
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
|
-
|
|
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
|
-
|
|
696
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
//
|
|
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
|
-
|
|
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,
|