ai-database 0.1.0 → 2.0.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 (72) hide show
  1. package/.turbo/turbo-build.log +5 -0
  2. package/CHANGELOG.md +9 -0
  3. package/README.md +381 -68
  4. package/TESTING.md +410 -0
  5. package/TEST_SUMMARY.md +250 -0
  6. package/TODO.md +128 -0
  7. package/dist/ai-promise-db.d.ts +370 -0
  8. package/dist/ai-promise-db.d.ts.map +1 -0
  9. package/dist/ai-promise-db.js +839 -0
  10. package/dist/ai-promise-db.js.map +1 -0
  11. package/dist/authorization.d.ts +531 -0
  12. package/dist/authorization.d.ts.map +1 -0
  13. package/dist/authorization.js +632 -0
  14. package/dist/authorization.js.map +1 -0
  15. package/dist/durable-clickhouse.d.ts +193 -0
  16. package/dist/durable-clickhouse.d.ts.map +1 -0
  17. package/dist/durable-clickhouse.js +422 -0
  18. package/dist/durable-clickhouse.js.map +1 -0
  19. package/dist/durable-promise.d.ts +182 -0
  20. package/dist/durable-promise.d.ts.map +1 -0
  21. package/dist/durable-promise.js +409 -0
  22. package/dist/durable-promise.js.map +1 -0
  23. package/dist/execution-queue.d.ts +239 -0
  24. package/dist/execution-queue.d.ts.map +1 -0
  25. package/dist/execution-queue.js +400 -0
  26. package/dist/execution-queue.js.map +1 -0
  27. package/dist/index.d.ts +50 -191
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +79 -462
  30. package/dist/index.js.map +1 -0
  31. package/dist/linguistic.d.ts +115 -0
  32. package/dist/linguistic.d.ts.map +1 -0
  33. package/dist/linguistic.js +379 -0
  34. package/dist/linguistic.js.map +1 -0
  35. package/dist/memory-provider.d.ts +304 -0
  36. package/dist/memory-provider.d.ts.map +1 -0
  37. package/dist/memory-provider.js +785 -0
  38. package/dist/memory-provider.js.map +1 -0
  39. package/dist/schema.d.ts +899 -0
  40. package/dist/schema.d.ts.map +1 -0
  41. package/dist/schema.js +1165 -0
  42. package/dist/schema.js.map +1 -0
  43. package/dist/tests.d.ts +107 -0
  44. package/dist/tests.d.ts.map +1 -0
  45. package/dist/tests.js +568 -0
  46. package/dist/tests.js.map +1 -0
  47. package/dist/types.d.ts +972 -0
  48. package/dist/types.d.ts.map +1 -0
  49. package/dist/types.js +126 -0
  50. package/dist/types.js.map +1 -0
  51. package/package.json +37 -37
  52. package/src/ai-promise-db.ts +1243 -0
  53. package/src/authorization.ts +1102 -0
  54. package/src/durable-clickhouse.ts +596 -0
  55. package/src/durable-promise.ts +582 -0
  56. package/src/execution-queue.ts +608 -0
  57. package/src/index.test.ts +868 -0
  58. package/src/index.ts +337 -0
  59. package/src/linguistic.ts +404 -0
  60. package/src/memory-provider.test.ts +1036 -0
  61. package/src/memory-provider.ts +1119 -0
  62. package/src/schema.test.ts +1254 -0
  63. package/src/schema.ts +2296 -0
  64. package/src/tests.ts +725 -0
  65. package/src/types.ts +1177 -0
  66. package/test/README.md +153 -0
  67. package/test/edge-cases.test.ts +646 -0
  68. package/test/provider-resolution.test.ts +402 -0
  69. package/tsconfig.json +9 -0
  70. package/vitest.config.ts +19 -0
  71. package/dist/index.d.mts +0 -195
  72. package/dist/index.mjs +0 -430
@@ -0,0 +1,839 @@
1
+ /**
2
+ * AIPromise Database Layer
3
+ *
4
+ * Brings promise pipelining, destructuring schema inference, and batch
5
+ * processing to database operations—just like ai-functions.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * // Chain without await
10
+ * const leads = db.Lead.list()
11
+ * const enriched = await leads.map(lead => ({
12
+ * lead,
13
+ * customer: lead.customer, // Batch loaded
14
+ * orders: lead.customer.orders, // Batch loaded
15
+ * }))
16
+ *
17
+ * // Destructure for projections
18
+ * const { name, email } = await db.Lead.first()
19
+ * ```
20
+ *
21
+ * @packageDocumentation
22
+ */
23
+ // =============================================================================
24
+ // Types
25
+ // =============================================================================
26
+ /** Symbol to identify DBPromise instances */
27
+ export const DB_PROMISE_SYMBOL = Symbol.for('db-promise');
28
+ /** Symbol to get raw promise */
29
+ export const RAW_DB_PROMISE_SYMBOL = Symbol.for('db-promise-raw');
30
+ // =============================================================================
31
+ // DBPromise Implementation
32
+ // =============================================================================
33
+ /**
34
+ * DBPromise - Promise pipelining for database operations
35
+ *
36
+ * Like AIPromise but for database queries. Enables:
37
+ * - Property access tracking for projections
38
+ * - Batch relationship loading
39
+ * - .map() for processing arrays efficiently
40
+ */
41
+ export class DBPromise {
42
+ [DB_PROMISE_SYMBOL] = true;
43
+ _options;
44
+ _accessedProps = new Set();
45
+ _propertyPath;
46
+ _parent;
47
+ _resolver = null;
48
+ _resolvedValue;
49
+ _isResolved = false;
50
+ _pendingRelations = new Map();
51
+ constructor(options) {
52
+ this._options = options;
53
+ this._propertyPath = options.propertyPath || [];
54
+ this._parent = options.parent || null;
55
+ // Return proxy for property tracking
56
+ return new Proxy(this, DB_PROXY_HANDLERS);
57
+ }
58
+ /** Get accessed properties */
59
+ get accessedProps() {
60
+ return this._accessedProps;
61
+ }
62
+ /** Get property path */
63
+ get path() {
64
+ return this._propertyPath;
65
+ }
66
+ /** Check if resolved */
67
+ get isResolved() {
68
+ return this._isResolved;
69
+ }
70
+ /**
71
+ * Resolve this promise
72
+ */
73
+ async resolve() {
74
+ if (this._isResolved) {
75
+ return this._resolvedValue;
76
+ }
77
+ // If this is a property access on parent, resolve parent first
78
+ if (this._parent && this._propertyPath.length > 0) {
79
+ const parentValue = await this._parent.resolve();
80
+ const value = getNestedValue(parentValue, this._propertyPath);
81
+ this._resolvedValue = value;
82
+ this._isResolved = true;
83
+ return this._resolvedValue;
84
+ }
85
+ // Execute the query
86
+ const result = await this._options.executor();
87
+ this._resolvedValue = result;
88
+ this._isResolved = true;
89
+ return this._resolvedValue;
90
+ }
91
+ /**
92
+ * Map over array results with batch optimization
93
+ *
94
+ * @example
95
+ * ```ts
96
+ * const customers = db.Customer.list()
97
+ * const withOrders = await customers.map(customer => ({
98
+ * name: customer.name,
99
+ * orders: customer.orders, // Batch loaded!
100
+ * total: customer.orders.length,
101
+ * }))
102
+ * ```
103
+ */
104
+ map(callback) {
105
+ const parentPromise = this;
106
+ return new DBPromise({
107
+ type: this._options.type,
108
+ executor: async () => {
109
+ // Resolve the parent array
110
+ const items = await parentPromise.resolve();
111
+ if (!Array.isArray(items)) {
112
+ throw new Error('Cannot map over non-array result');
113
+ }
114
+ // Create recording context
115
+ const recordings = [];
116
+ // Record what the callback accesses for each item
117
+ const recordedResults = [];
118
+ for (let i = 0; i < items.length; i++) {
119
+ const item = items[i];
120
+ const recording = {
121
+ paths: new Set(),
122
+ relations: new Map(),
123
+ };
124
+ // Create a recording proxy for this item
125
+ const recordingProxy = createRecordingProxy(item, recording);
126
+ // Execute callback with recording proxy
127
+ const result = callback(recordingProxy, i);
128
+ recordedResults.push(result);
129
+ recordings.push(recording);
130
+ }
131
+ // Analyze recordings to find batch-loadable relations
132
+ 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);
137
+ },
138
+ });
139
+ }
140
+ /**
141
+ * Filter results
142
+ */
143
+ filter(predicate) {
144
+ const parentPromise = this;
145
+ return new DBPromise({
146
+ type: this._options.type,
147
+ executor: async () => {
148
+ const items = await parentPromise.resolve();
149
+ if (!Array.isArray(items)) {
150
+ return items;
151
+ }
152
+ return items.filter(predicate);
153
+ },
154
+ });
155
+ }
156
+ /**
157
+ * Sort results
158
+ */
159
+ sort(compareFn) {
160
+ const parentPromise = this;
161
+ return new DBPromise({
162
+ type: this._options.type,
163
+ executor: async () => {
164
+ const items = await parentPromise.resolve();
165
+ if (!Array.isArray(items)) {
166
+ return items;
167
+ }
168
+ return [...items].sort(compareFn);
169
+ },
170
+ });
171
+ }
172
+ /**
173
+ * Limit results
174
+ */
175
+ limit(n) {
176
+ const parentPromise = this;
177
+ return new DBPromise({
178
+ type: this._options.type,
179
+ executor: async () => {
180
+ const items = await parentPromise.resolve();
181
+ if (!Array.isArray(items)) {
182
+ return items;
183
+ }
184
+ return items.slice(0, n);
185
+ },
186
+ });
187
+ }
188
+ /**
189
+ * Get first item
190
+ */
191
+ first() {
192
+ const parentPromise = this;
193
+ return new DBPromise({
194
+ type: this._options.type,
195
+ executor: async () => {
196
+ const items = await parentPromise.resolve();
197
+ if (Array.isArray(items)) {
198
+ return items[0] ?? null;
199
+ }
200
+ return items;
201
+ },
202
+ });
203
+ }
204
+ /**
205
+ * Process each item with concurrency control, progress tracking, and error handling
206
+ *
207
+ * Designed for large-scale operations like AI generations or workflows.
208
+ *
209
+ * @example
210
+ * ```ts
211
+ * // Simple - process sequentially
212
+ * await db.Lead.list().forEach(async lead => {
213
+ * await processLead(lead)
214
+ * })
215
+ *
216
+ * // With concurrency and progress
217
+ * await db.Lead.list().forEach(async lead => {
218
+ * const analysis = await ai`analyze ${lead}`
219
+ * await db.Lead.update(lead.$id, { analysis })
220
+ * }, {
221
+ * concurrency: 10,
222
+ * onProgress: p => console.log(`${p.completed}/${p.total} (${p.rate}/s)`),
223
+ * })
224
+ *
225
+ * // With error handling and retries
226
+ * const result = await db.Order.list().forEach(async order => {
227
+ * await sendInvoice(order)
228
+ * }, {
229
+ * concurrency: 5,
230
+ * maxRetries: 3,
231
+ * retryDelay: attempt => 1000 * Math.pow(2, attempt),
232
+ * onError: (err, order) => err.code === 'RATE_LIMIT' ? 'retry' : 'continue',
233
+ * })
234
+ *
235
+ * console.log(`Sent ${result.completed}, failed ${result.failed}`)
236
+ * ```
237
+ */
238
+ async forEach(callback, options = {}) {
239
+ const { concurrency = 1, batchSize = 100, maxRetries = 0, retryDelay = 1000, onProgress, onError = 'continue', onComplete, signal, timeout, persist, resume, } = options;
240
+ const startTime = Date.now();
241
+ const errors = [];
242
+ let completed = 0;
243
+ let failed = 0;
244
+ let skipped = 0;
245
+ let cancelled = false;
246
+ let actionId;
247
+ // Persistence state
248
+ let processedIds = new Set();
249
+ let persistCounter = 0;
250
+ const getItemId = (item) => item?.$id ?? item?.id ?? String(item);
251
+ // Get actions API from options (injected by wrapEntityOperations)
252
+ const actionsAPI = this._options.actionsAPI;
253
+ // Initialize persistence if enabled
254
+ if (persist || resume) {
255
+ if (!actionsAPI) {
256
+ throw new Error('Persistence requires actions API - use db.Entity.forEach instead of db.Entity.list().forEach');
257
+ }
258
+ // Auto-generate action type from entity name
259
+ const actionType = typeof persist === 'string' ? persist : `${this._options.type ?? 'unknown'}.forEach`;
260
+ if (resume) {
261
+ // Resume from existing action
262
+ const existingAction = await actionsAPI.get(resume);
263
+ if (existingAction) {
264
+ actionId = existingAction.id;
265
+ processedIds = new Set(existingAction.data?.processedIds ?? []);
266
+ await actionsAPI.update(actionId, { status: 'active' });
267
+ }
268
+ else {
269
+ throw new Error(`Action ${resume} not found`);
270
+ }
271
+ }
272
+ else {
273
+ // Create new action
274
+ const action = await actionsAPI.create({
275
+ type: actionType,
276
+ data: { processedIds: [] },
277
+ });
278
+ actionId = action.id;
279
+ }
280
+ }
281
+ // Resolve the items
282
+ const items = await this.resolve();
283
+ if (!Array.isArray(items)) {
284
+ throw new Error('forEach can only be called on array results');
285
+ }
286
+ const total = items.length;
287
+ // Update action with total if persistence is enabled
288
+ if ((persist || resume) && actionId && actionsAPI) {
289
+ await actionsAPI.update(actionId, { total, status: 'active' });
290
+ }
291
+ // Helper to calculate progress
292
+ const getProgress = (index, current) => {
293
+ const elapsed = Date.now() - startTime;
294
+ const processed = completed + failed + skipped;
295
+ const rate = processed > 0 ? (processed / elapsed) * 1000 : 0;
296
+ const remaining = rate > 0 && total ? ((total - processed) / rate) * 1000 : undefined;
297
+ return {
298
+ index,
299
+ total,
300
+ completed,
301
+ failed,
302
+ skipped,
303
+ current,
304
+ elapsed,
305
+ remaining,
306
+ rate,
307
+ };
308
+ };
309
+ // Helper to persist progress
310
+ const persistProgress = async (itemId) => {
311
+ if ((!persist && !resume) || !actionId || !actionsAPI)
312
+ return;
313
+ processedIds.add(itemId);
314
+ persistCounter++;
315
+ // Persist every 10 items to reduce overhead
316
+ if (persistCounter % 10 === 0) {
317
+ await actionsAPI.update(actionId, {
318
+ progress: completed + failed + skipped,
319
+ data: { processedIds: Array.from(processedIds) },
320
+ });
321
+ }
322
+ };
323
+ // Helper to get retry delay
324
+ const getRetryDelay = (attempt) => {
325
+ return typeof retryDelay === 'function' ? retryDelay(attempt) : retryDelay;
326
+ };
327
+ // Helper to handle error
328
+ const handleError = async (error, item, attempt) => {
329
+ if (typeof onError === 'function') {
330
+ return onError(error, item, attempt);
331
+ }
332
+ return onError;
333
+ };
334
+ // Process a single item with retries
335
+ const processItem = async (item, index) => {
336
+ if (cancelled || signal?.aborted) {
337
+ cancelled = true;
338
+ return;
339
+ }
340
+ // Check if already processed (for resume)
341
+ const itemId = getItemId(item);
342
+ if (processedIds.has(itemId)) {
343
+ skipped++;
344
+ return;
345
+ }
346
+ let attempt = 0;
347
+ while (true) {
348
+ try {
349
+ // Create timeout wrapper if needed
350
+ let result;
351
+ if (timeout) {
352
+ const timeoutPromise = new Promise((_, reject) => {
353
+ setTimeout(() => reject(new Error(`Timeout after ${timeout}ms`)), timeout);
354
+ });
355
+ result = await Promise.race([
356
+ Promise.resolve(callback(item, index)),
357
+ timeoutPromise,
358
+ ]);
359
+ }
360
+ else {
361
+ result = await callback(item, index);
362
+ }
363
+ // Success
364
+ completed++;
365
+ await persistProgress(itemId);
366
+ await onComplete?.(item, result, index);
367
+ onProgress?.(getProgress(index, item));
368
+ return;
369
+ }
370
+ catch (error) {
371
+ attempt++;
372
+ const action = await handleError(error, item, attempt);
373
+ switch (action) {
374
+ case 'retry':
375
+ if (attempt <= maxRetries) {
376
+ await sleep(getRetryDelay(attempt));
377
+ continue; // Retry
378
+ }
379
+ // Fall through to continue if max retries exceeded
380
+ failed++;
381
+ await persistProgress(itemId); // Still mark as processed
382
+ errors.push({ item, error: error, index });
383
+ onProgress?.(getProgress(index, item));
384
+ return;
385
+ case 'skip':
386
+ skipped++;
387
+ onProgress?.(getProgress(index, item));
388
+ return;
389
+ case 'stop':
390
+ failed++;
391
+ await persistProgress(itemId);
392
+ errors.push({ item, error: error, index });
393
+ cancelled = true;
394
+ return;
395
+ case 'continue':
396
+ default:
397
+ failed++;
398
+ await persistProgress(itemId);
399
+ errors.push({ item, error: error, index });
400
+ onProgress?.(getProgress(index, item));
401
+ return;
402
+ }
403
+ }
404
+ }
405
+ };
406
+ // Process items with concurrency
407
+ try {
408
+ if (concurrency === 1) {
409
+ // Sequential processing
410
+ for (let i = 0; i < items.length; i++) {
411
+ if (cancelled || signal?.aborted) {
412
+ cancelled = true;
413
+ break;
414
+ }
415
+ await processItem(items[i], i);
416
+ }
417
+ }
418
+ else {
419
+ // Concurrent processing with semaphore
420
+ const semaphore = new Semaphore(concurrency);
421
+ const promises = [];
422
+ for (let i = 0; i < items.length; i++) {
423
+ if (cancelled || signal?.aborted) {
424
+ cancelled = true;
425
+ break;
426
+ }
427
+ const itemIndex = i;
428
+ const item = items[i];
429
+ promises.push(semaphore.acquire().then(async (release) => {
430
+ try {
431
+ await processItem(item, itemIndex);
432
+ }
433
+ finally {
434
+ release();
435
+ }
436
+ }));
437
+ }
438
+ await Promise.all(promises);
439
+ }
440
+ }
441
+ finally {
442
+ // Final persistence update
443
+ if ((persist || resume) && actionId && actionsAPI) {
444
+ const finalResult = {
445
+ total,
446
+ completed,
447
+ failed,
448
+ skipped,
449
+ elapsed: Date.now() - startTime,
450
+ errors,
451
+ cancelled,
452
+ actionId,
453
+ };
454
+ await actionsAPI.update(actionId, {
455
+ status: cancelled ? 'failed' : 'completed',
456
+ progress: completed + failed + skipped,
457
+ data: { processedIds: Array.from(processedIds) },
458
+ result: finalResult,
459
+ error: cancelled ? 'Cancelled' : errors.length > 0 ? `${errors.length} items failed` : undefined,
460
+ });
461
+ }
462
+ }
463
+ return {
464
+ total,
465
+ completed,
466
+ failed,
467
+ skipped,
468
+ elapsed: Date.now() - startTime,
469
+ errors,
470
+ cancelled,
471
+ actionId,
472
+ };
473
+ }
474
+ /**
475
+ * Async iteration
476
+ */
477
+ async *[Symbol.asyncIterator]() {
478
+ const items = await this.resolve();
479
+ if (Array.isArray(items)) {
480
+ for (const item of items) {
481
+ yield item;
482
+ }
483
+ }
484
+ else {
485
+ yield items;
486
+ }
487
+ }
488
+ /**
489
+ * Promise interface - then()
490
+ */
491
+ then(onfulfilled, onrejected) {
492
+ if (!this._resolver) {
493
+ this._resolver = new Promise((resolve, reject) => {
494
+ queueMicrotask(async () => {
495
+ try {
496
+ const value = await this.resolve();
497
+ resolve(value);
498
+ }
499
+ catch (error) {
500
+ reject(error);
501
+ }
502
+ });
503
+ });
504
+ }
505
+ return this._resolver.then(onfulfilled, onrejected);
506
+ }
507
+ /**
508
+ * Promise interface - catch()
509
+ */
510
+ catch(onrejected) {
511
+ return this.then(null, onrejected);
512
+ }
513
+ /**
514
+ * Promise interface - finally()
515
+ */
516
+ finally(onfinally) {
517
+ return this.then((value) => {
518
+ onfinally?.();
519
+ return value;
520
+ }, (reason) => {
521
+ onfinally?.();
522
+ throw reason;
523
+ });
524
+ }
525
+ }
526
+ // =============================================================================
527
+ // Proxy Handlers
528
+ // =============================================================================
529
+ const DB_PROXY_HANDLERS = {
530
+ get(target, prop, receiver) {
531
+ // Handle symbols
532
+ if (typeof prop === 'symbol') {
533
+ if (prop === DB_PROMISE_SYMBOL)
534
+ return true;
535
+ if (prop === RAW_DB_PROMISE_SYMBOL)
536
+ return target;
537
+ if (prop === Symbol.asyncIterator)
538
+ return target[Symbol.asyncIterator].bind(target);
539
+ return target[prop];
540
+ }
541
+ // Handle promise methods
542
+ if (prop === 'then' || prop === 'catch' || prop === 'finally') {
543
+ return target[prop].bind(target);
544
+ }
545
+ // Handle DBPromise methods
546
+ if (['map', 'filter', 'sort', 'limit', 'first', 'forEach', 'resolve'].includes(prop)) {
547
+ return target[prop].bind(target);
548
+ }
549
+ // Handle internal properties
550
+ if (prop.startsWith('_') || ['accessedProps', 'path', 'isResolved'].includes(prop)) {
551
+ return target[prop];
552
+ }
553
+ // Track property access
554
+ target.accessedProps.add(prop);
555
+ // Return a new DBPromise for the property path
556
+ return new DBPromise({
557
+ type: target['_options']?.type,
558
+ parent: target,
559
+ propertyPath: [...target.path, prop],
560
+ executor: async () => {
561
+ const parentValue = await target.resolve();
562
+ return getNestedValue(parentValue, [prop]);
563
+ },
564
+ });
565
+ },
566
+ set() {
567
+ throw new Error('DBPromise properties are read-only');
568
+ },
569
+ deleteProperty() {
570
+ throw new Error('DBPromise properties cannot be deleted');
571
+ },
572
+ };
573
+ // =============================================================================
574
+ // Helper Functions
575
+ // =============================================================================
576
+ /**
577
+ * Sleep helper
578
+ */
579
+ function sleep(ms) {
580
+ return new Promise((resolve) => setTimeout(resolve, ms));
581
+ }
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
+ /**
612
+ * Get nested value from object
613
+ */
614
+ function getNestedValue(obj, path) {
615
+ let current = obj;
616
+ for (const key of path) {
617
+ if (current === null || current === undefined)
618
+ return undefined;
619
+ current = current[key];
620
+ }
621
+ return current;
622
+ }
623
+ /**
624
+ * Create a proxy that records property accesses
625
+ */
626
+ function createRecordingProxy(item, recording) {
627
+ if (typeof item !== 'object' || item === null) {
628
+ return item;
629
+ }
630
+ return new Proxy(item, {
631
+ get(target, prop) {
632
+ if (typeof prop === 'symbol') {
633
+ return target[prop];
634
+ }
635
+ recording.paths.add(prop);
636
+ 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
+ });
644
+ }
645
+ // Return a nested recording proxy for objects
646
+ if (value && typeof value === 'object') {
647
+ return createRecordingProxy(value, recording);
648
+ }
649
+ return value;
650
+ },
651
+ });
652
+ }
653
+ /**
654
+ * Analyze recordings to find batch-loadable relations
655
+ */
656
+ function analyzeBatchLoads(recordings, items) {
657
+ const batchLoads = new Map();
658
+ // Find common relations across all recordings
659
+ const relationCounts = new Map();
660
+ for (const recording of recordings) {
661
+ for (const [relationName, relation] of recording.relations) {
662
+ relationCounts.set(relationName, (relationCounts.get(relationName) || 0) + 1);
663
+ }
664
+ }
665
+ // Only batch-load relations accessed in all (or most) items
666
+ for (const [relationName, count] of relationCounts) {
667
+ if (count >= recordings.length * 0.5) {
668
+ // At least 50% of items access this relation
669
+ const ids = [];
670
+ for (let i = 0; i < items.length; i++) {
671
+ const item = items[i];
672
+ const relationId = item[relationName];
673
+ if (typeof relationId === 'string') {
674
+ ids.push(relationId);
675
+ }
676
+ else if (relationId && typeof relationId === 'object' && '$id' in relationId) {
677
+ ids.push(relationId.$id);
678
+ }
679
+ }
680
+ if (ids.length > 0) {
681
+ const relation = recordings[0]?.relations.get(relationName);
682
+ if (relation) {
683
+ batchLoads.set(relationName, { type: relation.type, ids });
684
+ }
685
+ }
686
+ }
687
+ }
688
+ return batchLoads;
689
+ }
690
+ /**
691
+ * Execute batch loads for relations
692
+ */
693
+ async function executeBatchLoads(batchLoads) {
694
+ 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
697
+ for (const [relationName, { type, ids }] of batchLoads) {
698
+ results.set(relationName, new Map());
699
+ }
700
+ return results;
701
+ }
702
+ /**
703
+ * Apply batch-loaded results to the mapped results
704
+ */
705
+ function applyBatchResults(results, loadedRelations, originalItems) {
706
+ // For now, return results as-is
707
+ // Actual implementation would inject loaded relations
708
+ return results;
709
+ }
710
+ // =============================================================================
711
+ // Check Functions
712
+ // =============================================================================
713
+ /**
714
+ * Check if a value is a DBPromise
715
+ */
716
+ export function isDBPromise(value) {
717
+ return (value !== null &&
718
+ typeof value === 'object' &&
719
+ DB_PROMISE_SYMBOL in value &&
720
+ value[DB_PROMISE_SYMBOL] === true);
721
+ }
722
+ /**
723
+ * Get the raw DBPromise from a proxied value
724
+ */
725
+ export function getRawDBPromise(value) {
726
+ if (RAW_DB_PROMISE_SYMBOL in value) {
727
+ return value[RAW_DB_PROMISE_SYMBOL];
728
+ }
729
+ return value;
730
+ }
731
+ // =============================================================================
732
+ // Factory Functions
733
+ // =============================================================================
734
+ /**
735
+ * Create a DBPromise for a list query
736
+ */
737
+ export function createListPromise(type, executor) {
738
+ return new DBPromise({ type, executor });
739
+ }
740
+ /**
741
+ * Create a DBPromise for a single entity query
742
+ */
743
+ export function createEntityPromise(type, executor) {
744
+ return new DBPromise({ type, executor });
745
+ }
746
+ /**
747
+ * Create a DBPromise for a search query
748
+ */
749
+ export function createSearchPromise(type, executor) {
750
+ return new DBPromise({ type, executor });
751
+ }
752
+ // =============================================================================
753
+ // Entity Operations Wrapper
754
+ // =============================================================================
755
+ /**
756
+ * Wrap EntityOperations to return DBPromise
757
+ */
758
+ export function wrapEntityOperations(typeName, operations, actionsAPI) {
759
+ return {
760
+ get(id) {
761
+ return new DBPromise({
762
+ type: typeName,
763
+ executor: () => operations.get(id),
764
+ actionsAPI,
765
+ });
766
+ },
767
+ list(options) {
768
+ return new DBPromise({
769
+ type: typeName,
770
+ executor: () => operations.list(options),
771
+ actionsAPI,
772
+ });
773
+ },
774
+ find(where) {
775
+ return new DBPromise({
776
+ type: typeName,
777
+ executor: () => operations.find(where),
778
+ actionsAPI,
779
+ });
780
+ },
781
+ search(query, options) {
782
+ return new DBPromise({
783
+ type: typeName,
784
+ executor: () => operations.search(query, options),
785
+ actionsAPI,
786
+ });
787
+ },
788
+ first() {
789
+ return new DBPromise({
790
+ type: typeName,
791
+ executor: async () => {
792
+ const items = await operations.list({ limit: 1 });
793
+ return items[0] ?? null;
794
+ },
795
+ actionsAPI,
796
+ });
797
+ },
798
+ /**
799
+ * Process all entities with concurrency, progress, and optional persistence
800
+ *
801
+ * Supports two calling styles:
802
+ * - forEach(callback, options?) - callback first
803
+ * - forEach(options, callback) - options first (with where filter)
804
+ *
805
+ * @example
806
+ * ```ts
807
+ * await db.Lead.forEach(lead => console.log(lead.name))
808
+ * await db.Lead.forEach(processLead, { concurrency: 10 })
809
+ * await db.Lead.forEach({ where: { status: 'active' } }, processLead)
810
+ * await db.Lead.forEach(processLead, { persist: true })
811
+ * await db.Lead.forEach(processLead, { resume: 'action-123' })
812
+ * ```
813
+ */
814
+ async forEach(callbackOrOptions, callbackOrOpts) {
815
+ // Detect which calling style is being used
816
+ const isOptionsFirst = typeof callbackOrOptions === 'object' && callbackOrOptions !== null && !('call' in callbackOrOptions);
817
+ const callback = isOptionsFirst
818
+ ? callbackOrOpts
819
+ : callbackOrOptions;
820
+ const options = isOptionsFirst
821
+ ? callbackOrOptions
822
+ : (callbackOrOpts ?? {});
823
+ // Extract where filter and pass to list
824
+ const listOptions = options.where ? { where: options.where } : undefined;
825
+ const listPromise = new DBPromise({
826
+ type: typeName,
827
+ executor: () => operations.list(listOptions),
828
+ actionsAPI,
829
+ });
830
+ return listPromise.forEach(callback, options);
831
+ },
832
+ // Mutations don't need wrapping
833
+ create: operations.create,
834
+ update: operations.update,
835
+ upsert: operations.upsert,
836
+ delete: operations.delete,
837
+ };
838
+ }
839
+ //# sourceMappingURL=ai-promise-db.js.map