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.
- package/.turbo/turbo-build.log +5 -0
- package/CHANGELOG.md +9 -0
- package/README.md +381 -68
- package/TESTING.md +410 -0
- package/TEST_SUMMARY.md +250 -0
- package/TODO.md +128 -0
- package/dist/ai-promise-db.d.ts +370 -0
- package/dist/ai-promise-db.d.ts.map +1 -0
- package/dist/ai-promise-db.js +839 -0
- package/dist/ai-promise-db.js.map +1 -0
- package/dist/authorization.d.ts +531 -0
- package/dist/authorization.d.ts.map +1 -0
- package/dist/authorization.js +632 -0
- package/dist/authorization.js.map +1 -0
- package/dist/durable-clickhouse.d.ts +193 -0
- package/dist/durable-clickhouse.d.ts.map +1 -0
- package/dist/durable-clickhouse.js +422 -0
- package/dist/durable-clickhouse.js.map +1 -0
- package/dist/durable-promise.d.ts +182 -0
- package/dist/durable-promise.d.ts.map +1 -0
- package/dist/durable-promise.js +409 -0
- package/dist/durable-promise.js.map +1 -0
- package/dist/execution-queue.d.ts +239 -0
- package/dist/execution-queue.d.ts.map +1 -0
- package/dist/execution-queue.js +400 -0
- package/dist/execution-queue.js.map +1 -0
- package/dist/index.d.ts +50 -191
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +79 -462
- package/dist/index.js.map +1 -0
- package/dist/linguistic.d.ts +115 -0
- package/dist/linguistic.d.ts.map +1 -0
- package/dist/linguistic.js +379 -0
- package/dist/linguistic.js.map +1 -0
- package/dist/memory-provider.d.ts +304 -0
- package/dist/memory-provider.d.ts.map +1 -0
- package/dist/memory-provider.js +785 -0
- package/dist/memory-provider.js.map +1 -0
- package/dist/schema.d.ts +899 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +1165 -0
- package/dist/schema.js.map +1 -0
- package/dist/tests.d.ts +107 -0
- package/dist/tests.d.ts.map +1 -0
- package/dist/tests.js +568 -0
- package/dist/tests.js.map +1 -0
- package/dist/types.d.ts +972 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +126 -0
- package/dist/types.js.map +1 -0
- package/package.json +37 -37
- package/src/ai-promise-db.ts +1243 -0
- package/src/authorization.ts +1102 -0
- package/src/durable-clickhouse.ts +596 -0
- package/src/durable-promise.ts +582 -0
- package/src/execution-queue.ts +608 -0
- package/src/index.test.ts +868 -0
- package/src/index.ts +337 -0
- package/src/linguistic.ts +404 -0
- package/src/memory-provider.test.ts +1036 -0
- package/src/memory-provider.ts +1119 -0
- package/src/schema.test.ts +1254 -0
- package/src/schema.ts +2296 -0
- package/src/tests.ts +725 -0
- package/src/types.ts +1177 -0
- package/test/README.md +153 -0
- package/test/edge-cases.test.ts +646 -0
- package/test/provider-resolution.test.ts +402 -0
- package/tsconfig.json +9 -0
- package/vitest.config.ts +19 -0
- package/dist/index.d.mts +0 -195
- 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
|