ai-database 0.1.0 → 0.2.0
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/.turbo/turbo-test.log +102 -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,1243 @@
|
|
|
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
|
+
// =============================================================================
|
|
25
|
+
// Types
|
|
26
|
+
// =============================================================================
|
|
27
|
+
|
|
28
|
+
/** Symbol to identify DBPromise instances */
|
|
29
|
+
export const DB_PROMISE_SYMBOL = Symbol.for('db-promise')
|
|
30
|
+
|
|
31
|
+
/** Symbol to get raw promise */
|
|
32
|
+
export const RAW_DB_PROMISE_SYMBOL = Symbol.for('db-promise-raw')
|
|
33
|
+
|
|
34
|
+
/** Dependency for batch loading */
|
|
35
|
+
interface DBDependency {
|
|
36
|
+
type: string
|
|
37
|
+
ids: string[]
|
|
38
|
+
relation?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Options for DBPromise creation */
|
|
42
|
+
export interface DBPromiseOptions<T> {
|
|
43
|
+
/** The entity type */
|
|
44
|
+
type?: string
|
|
45
|
+
/** Parent promise (for relationship chains) */
|
|
46
|
+
parent?: DBPromise<unknown>
|
|
47
|
+
/** Property path from parent */
|
|
48
|
+
propertyPath?: string[]
|
|
49
|
+
/** Executor function */
|
|
50
|
+
executor: () => Promise<T>
|
|
51
|
+
/** Batch context for .map() */
|
|
52
|
+
batchContext?: BatchContext
|
|
53
|
+
/** Actions API for persistence (injected by wrapEntityOperations) */
|
|
54
|
+
actionsAPI?: ForEachActionsAPI
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Batch context for recording map operations */
|
|
58
|
+
interface BatchContext {
|
|
59
|
+
items: unknown[]
|
|
60
|
+
recordings: Map<string, PropertyRecording>
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Recording of property accesses */
|
|
64
|
+
interface PropertyRecording {
|
|
65
|
+
paths: Set<string>
|
|
66
|
+
relations: Map<string, RelationRecording>
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Recording of relation accesses */
|
|
70
|
+
interface RelationRecording {
|
|
71
|
+
type: string
|
|
72
|
+
isArray: boolean
|
|
73
|
+
nestedPaths: Set<string>
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// =============================================================================
|
|
77
|
+
// ForEach Types - For large-scale, slow operations
|
|
78
|
+
// =============================================================================
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Progress info for forEach operations
|
|
82
|
+
*/
|
|
83
|
+
export interface ForEachProgress {
|
|
84
|
+
/** Current item index (0-based) */
|
|
85
|
+
index: number
|
|
86
|
+
/** Total items (if known) */
|
|
87
|
+
total?: number
|
|
88
|
+
/** Number of items completed */
|
|
89
|
+
completed: number
|
|
90
|
+
/** Number of items failed */
|
|
91
|
+
failed: number
|
|
92
|
+
/** Number of items skipped */
|
|
93
|
+
skipped: number
|
|
94
|
+
/** Current item being processed */
|
|
95
|
+
current?: unknown
|
|
96
|
+
/** Elapsed time in ms */
|
|
97
|
+
elapsed: number
|
|
98
|
+
/** Estimated time remaining in ms (if total known) */
|
|
99
|
+
remaining?: number
|
|
100
|
+
/** Items per second */
|
|
101
|
+
rate: number
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Error handling result
|
|
106
|
+
*/
|
|
107
|
+
export type ForEachErrorAction = 'continue' | 'retry' | 'skip' | 'stop'
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Actions API interface for persistence (internal)
|
|
111
|
+
*/
|
|
112
|
+
export interface ForEachActionsAPI {
|
|
113
|
+
create(data: { type: string; data: unknown; total?: number }): Promise<{ id: string }>
|
|
114
|
+
get(id: string): Promise<ForEachActionState | null>
|
|
115
|
+
update(id: string, updates: Partial<ForEachActionState>): Promise<unknown>
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Action state for forEach persistence
|
|
120
|
+
*/
|
|
121
|
+
export interface ForEachActionState {
|
|
122
|
+
id: string
|
|
123
|
+
type: string
|
|
124
|
+
status: 'pending' | 'active' | 'completed' | 'failed'
|
|
125
|
+
progress?: number
|
|
126
|
+
total?: number
|
|
127
|
+
data: {
|
|
128
|
+
/** IDs of items that have been processed */
|
|
129
|
+
processedIds?: string[]
|
|
130
|
+
[key: string]: unknown
|
|
131
|
+
}
|
|
132
|
+
result?: ForEachResult
|
|
133
|
+
error?: string
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Options for forEach operations
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* ```ts
|
|
141
|
+
* // Simple
|
|
142
|
+
* await db.Lead.forEach(lead => console.log(lead.name))
|
|
143
|
+
*
|
|
144
|
+
* // With concurrency
|
|
145
|
+
* await db.Lead.forEach(async lead => {
|
|
146
|
+
* await processLead(lead)
|
|
147
|
+
* }, { concurrency: 10 })
|
|
148
|
+
*
|
|
149
|
+
* // Persist progress (survives crashes)
|
|
150
|
+
* await db.Lead.forEach(processLead, { persist: true })
|
|
151
|
+
*
|
|
152
|
+
* // Resume from where we left off
|
|
153
|
+
* await db.Lead.forEach(processLead, { resume: 'action-123' })
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
export interface ForEachOptions<T = unknown> {
|
|
157
|
+
/**
|
|
158
|
+
* Maximum concurrent operations (default: 1)
|
|
159
|
+
*/
|
|
160
|
+
concurrency?: number
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Batch size for fetching items (default: 100)
|
|
164
|
+
*/
|
|
165
|
+
batchSize?: number
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Maximum retries per item (default: 0)
|
|
169
|
+
*/
|
|
170
|
+
maxRetries?: number
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Delay between retries in ms, or function for backoff (default: 1000)
|
|
174
|
+
*/
|
|
175
|
+
retryDelay?: number | ((attempt: number) => number)
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Progress callback
|
|
179
|
+
*/
|
|
180
|
+
onProgress?: (progress: ForEachProgress) => void
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Error handling: 'continue' | 'retry' | 'skip' | 'stop' (default: 'continue')
|
|
184
|
+
*/
|
|
185
|
+
onError?: ForEachErrorAction | ((error: Error, item: T, attempt: number) => ForEachErrorAction | Promise<ForEachErrorAction>)
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Called when an item completes
|
|
189
|
+
*/
|
|
190
|
+
onComplete?: (item: T, result: unknown, index: number) => void | Promise<void>
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* AbortController signal
|
|
194
|
+
*/
|
|
195
|
+
signal?: AbortSignal
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Timeout per item in ms
|
|
199
|
+
*/
|
|
200
|
+
timeout?: number
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Persist progress to actions (survives crashes)
|
|
204
|
+
* - `true`: Auto-name action as "{Entity}.forEach"
|
|
205
|
+
* - `string`: Custom action name
|
|
206
|
+
*/
|
|
207
|
+
persist?: boolean | string
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Resume from existing action ID (skips already-processed items)
|
|
211
|
+
*/
|
|
212
|
+
resume?: string
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Filter entities before processing
|
|
216
|
+
*/
|
|
217
|
+
where?: Record<string, unknown>
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Result of forEach operation
|
|
222
|
+
*/
|
|
223
|
+
export interface ForEachResult {
|
|
224
|
+
/** Total items processed */
|
|
225
|
+
total: number
|
|
226
|
+
/** Items completed successfully */
|
|
227
|
+
completed: number
|
|
228
|
+
/** Items that failed */
|
|
229
|
+
failed: number
|
|
230
|
+
/** Items skipped */
|
|
231
|
+
skipped: number
|
|
232
|
+
/** Total elapsed time in ms */
|
|
233
|
+
elapsed: number
|
|
234
|
+
/** Errors encountered (if any) */
|
|
235
|
+
errors: Array<{ item: unknown; error: Error; index: number }>
|
|
236
|
+
/** Was the operation cancelled? */
|
|
237
|
+
cancelled: boolean
|
|
238
|
+
/** Action ID if persistence was enabled */
|
|
239
|
+
actionId?: string
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// =============================================================================
|
|
243
|
+
// DBPromise Implementation
|
|
244
|
+
// =============================================================================
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* DBPromise - Promise pipelining for database operations
|
|
248
|
+
*
|
|
249
|
+
* Like AIPromise but for database queries. Enables:
|
|
250
|
+
* - Property access tracking for projections
|
|
251
|
+
* - Batch relationship loading
|
|
252
|
+
* - .map() for processing arrays efficiently
|
|
253
|
+
*/
|
|
254
|
+
export class DBPromise<T> implements PromiseLike<T> {
|
|
255
|
+
readonly [DB_PROMISE_SYMBOL] = true
|
|
256
|
+
|
|
257
|
+
private _options: DBPromiseOptions<T>
|
|
258
|
+
private _accessedProps = new Set<string>()
|
|
259
|
+
private _propertyPath: string[]
|
|
260
|
+
private _parent: DBPromise<unknown> | null
|
|
261
|
+
private _resolver: Promise<T> | null = null
|
|
262
|
+
private _resolvedValue: T | undefined
|
|
263
|
+
private _isResolved = false
|
|
264
|
+
private _pendingRelations = new Map<string, DBDependency>()
|
|
265
|
+
|
|
266
|
+
constructor(options: DBPromiseOptions<T>) {
|
|
267
|
+
this._options = options
|
|
268
|
+
this._propertyPath = options.propertyPath || []
|
|
269
|
+
this._parent = options.parent || null
|
|
270
|
+
|
|
271
|
+
// Return proxy for property tracking
|
|
272
|
+
return new Proxy(this, DB_PROXY_HANDLERS) as DBPromise<T>
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Get accessed properties */
|
|
276
|
+
get accessedProps(): Set<string> {
|
|
277
|
+
return this._accessedProps
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Get property path */
|
|
281
|
+
get path(): string[] {
|
|
282
|
+
return this._propertyPath
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Check if resolved */
|
|
286
|
+
get isResolved(): boolean {
|
|
287
|
+
return this._isResolved
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Resolve this promise
|
|
292
|
+
*/
|
|
293
|
+
async resolve(): Promise<T> {
|
|
294
|
+
if (this._isResolved) {
|
|
295
|
+
return this._resolvedValue as T
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// If this is a property access on parent, resolve parent first
|
|
299
|
+
if (this._parent && this._propertyPath.length > 0) {
|
|
300
|
+
const parentValue = await this._parent.resolve()
|
|
301
|
+
const value = getNestedValue(parentValue, this._propertyPath)
|
|
302
|
+
this._resolvedValue = value as T
|
|
303
|
+
this._isResolved = true
|
|
304
|
+
return this._resolvedValue
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Execute the query
|
|
308
|
+
const result = await this._options.executor()
|
|
309
|
+
this._resolvedValue = result
|
|
310
|
+
this._isResolved = true
|
|
311
|
+
|
|
312
|
+
return this._resolvedValue
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Map over array results with batch optimization
|
|
317
|
+
*
|
|
318
|
+
* @example
|
|
319
|
+
* ```ts
|
|
320
|
+
* const customers = db.Customer.list()
|
|
321
|
+
* const withOrders = await customers.map(customer => ({
|
|
322
|
+
* name: customer.name,
|
|
323
|
+
* orders: customer.orders, // Batch loaded!
|
|
324
|
+
* total: customer.orders.length,
|
|
325
|
+
* }))
|
|
326
|
+
* ```
|
|
327
|
+
*/
|
|
328
|
+
map<U>(
|
|
329
|
+
callback: (item: DBPromise<T extends (infer I)[] ? I : T>, index: number) => U
|
|
330
|
+
): DBPromise<U[]> {
|
|
331
|
+
const parentPromise = this
|
|
332
|
+
|
|
333
|
+
return new DBPromise<U[]>({
|
|
334
|
+
type: this._options.type,
|
|
335
|
+
executor: async () => {
|
|
336
|
+
// Resolve the parent array
|
|
337
|
+
const items = await parentPromise.resolve()
|
|
338
|
+
if (!Array.isArray(items)) {
|
|
339
|
+
throw new Error('Cannot map over non-array result')
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Create recording context
|
|
343
|
+
const recordings: PropertyRecording[] = []
|
|
344
|
+
|
|
345
|
+
// Record what the callback accesses for each item
|
|
346
|
+
const recordedResults: U[] = []
|
|
347
|
+
|
|
348
|
+
for (let i = 0; i < items.length; i++) {
|
|
349
|
+
const item = items[i]
|
|
350
|
+
const recording: PropertyRecording = {
|
|
351
|
+
paths: new Set(),
|
|
352
|
+
relations: new Map(),
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Create a recording proxy for this item
|
|
356
|
+
const recordingProxy = createRecordingProxy(item, recording)
|
|
357
|
+
|
|
358
|
+
// Execute callback with recording proxy
|
|
359
|
+
const result = callback(recordingProxy as any, i)
|
|
360
|
+
recordedResults.push(result)
|
|
361
|
+
recordings.push(recording)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Analyze recordings to find batch-loadable relations
|
|
365
|
+
const batchLoads = analyzeBatchLoads(recordings, items)
|
|
366
|
+
|
|
367
|
+
// Execute batch loads
|
|
368
|
+
const loadedRelations = await executeBatchLoads(batchLoads)
|
|
369
|
+
|
|
370
|
+
// Apply loaded relations to results
|
|
371
|
+
return applyBatchResults(recordedResults, loadedRelations, items)
|
|
372
|
+
},
|
|
373
|
+
})
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Filter results
|
|
378
|
+
*/
|
|
379
|
+
filter(
|
|
380
|
+
predicate: (item: T extends (infer I)[] ? I : T, index: number) => boolean
|
|
381
|
+
): DBPromise<T> {
|
|
382
|
+
const parentPromise = this
|
|
383
|
+
|
|
384
|
+
return new DBPromise<T>({
|
|
385
|
+
type: this._options.type,
|
|
386
|
+
executor: async () => {
|
|
387
|
+
const items = await parentPromise.resolve()
|
|
388
|
+
if (!Array.isArray(items)) {
|
|
389
|
+
return items
|
|
390
|
+
}
|
|
391
|
+
return items.filter(predicate as any) as T
|
|
392
|
+
},
|
|
393
|
+
})
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Sort results
|
|
398
|
+
*/
|
|
399
|
+
sort(
|
|
400
|
+
compareFn?: (a: T extends (infer I)[] ? I : T, b: T extends (infer I)[] ? I : T) => number
|
|
401
|
+
): DBPromise<T> {
|
|
402
|
+
const parentPromise = this
|
|
403
|
+
|
|
404
|
+
return new DBPromise<T>({
|
|
405
|
+
type: this._options.type,
|
|
406
|
+
executor: async () => {
|
|
407
|
+
const items = await parentPromise.resolve()
|
|
408
|
+
if (!Array.isArray(items)) {
|
|
409
|
+
return items
|
|
410
|
+
}
|
|
411
|
+
return [...items].sort(compareFn as any) as T
|
|
412
|
+
},
|
|
413
|
+
})
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Limit results
|
|
418
|
+
*/
|
|
419
|
+
limit(n: number): DBPromise<T> {
|
|
420
|
+
const parentPromise = this
|
|
421
|
+
|
|
422
|
+
return new DBPromise<T>({
|
|
423
|
+
type: this._options.type,
|
|
424
|
+
executor: async () => {
|
|
425
|
+
const items = await parentPromise.resolve()
|
|
426
|
+
if (!Array.isArray(items)) {
|
|
427
|
+
return items
|
|
428
|
+
}
|
|
429
|
+
return items.slice(0, n) as T
|
|
430
|
+
},
|
|
431
|
+
})
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Get first item
|
|
436
|
+
*/
|
|
437
|
+
first(): DBPromise<T extends (infer I)[] ? I | null : T> {
|
|
438
|
+
const parentPromise = this
|
|
439
|
+
|
|
440
|
+
return new DBPromise({
|
|
441
|
+
type: this._options.type,
|
|
442
|
+
executor: async () => {
|
|
443
|
+
const items = await parentPromise.resolve()
|
|
444
|
+
if (Array.isArray(items)) {
|
|
445
|
+
return items[0] ?? null
|
|
446
|
+
}
|
|
447
|
+
return items
|
|
448
|
+
},
|
|
449
|
+
}) as DBPromise<T extends (infer I)[] ? I | null : T>
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Process each item with concurrency control, progress tracking, and error handling
|
|
454
|
+
*
|
|
455
|
+
* Designed for large-scale operations like AI generations or workflows.
|
|
456
|
+
*
|
|
457
|
+
* @example
|
|
458
|
+
* ```ts
|
|
459
|
+
* // Simple - process sequentially
|
|
460
|
+
* await db.Lead.list().forEach(async lead => {
|
|
461
|
+
* await processLead(lead)
|
|
462
|
+
* })
|
|
463
|
+
*
|
|
464
|
+
* // With concurrency and progress
|
|
465
|
+
* await db.Lead.list().forEach(async lead => {
|
|
466
|
+
* const analysis = await ai`analyze ${lead}`
|
|
467
|
+
* await db.Lead.update(lead.$id, { analysis })
|
|
468
|
+
* }, {
|
|
469
|
+
* concurrency: 10,
|
|
470
|
+
* onProgress: p => console.log(`${p.completed}/${p.total} (${p.rate}/s)`),
|
|
471
|
+
* })
|
|
472
|
+
*
|
|
473
|
+
* // With error handling and retries
|
|
474
|
+
* const result = await db.Order.list().forEach(async order => {
|
|
475
|
+
* await sendInvoice(order)
|
|
476
|
+
* }, {
|
|
477
|
+
* concurrency: 5,
|
|
478
|
+
* maxRetries: 3,
|
|
479
|
+
* retryDelay: attempt => 1000 * Math.pow(2, attempt),
|
|
480
|
+
* onError: (err, order) => err.code === 'RATE_LIMIT' ? 'retry' : 'continue',
|
|
481
|
+
* })
|
|
482
|
+
*
|
|
483
|
+
* console.log(`Sent ${result.completed}, failed ${result.failed}`)
|
|
484
|
+
* ```
|
|
485
|
+
*/
|
|
486
|
+
async forEach<U>(
|
|
487
|
+
callback: (item: T extends (infer I)[] ? I : T, index: number) => U | Promise<U>,
|
|
488
|
+
options: ForEachOptions<T extends (infer I)[] ? I : T> = {}
|
|
489
|
+
): Promise<ForEachResult> {
|
|
490
|
+
const {
|
|
491
|
+
concurrency = 1,
|
|
492
|
+
batchSize = 100,
|
|
493
|
+
maxRetries = 0,
|
|
494
|
+
retryDelay = 1000,
|
|
495
|
+
onProgress,
|
|
496
|
+
onError = 'continue',
|
|
497
|
+
onComplete,
|
|
498
|
+
signal,
|
|
499
|
+
timeout,
|
|
500
|
+
persist,
|
|
501
|
+
resume,
|
|
502
|
+
} = options
|
|
503
|
+
|
|
504
|
+
const startTime = Date.now()
|
|
505
|
+
const errors: ForEachResult['errors'] = []
|
|
506
|
+
let completed = 0
|
|
507
|
+
let failed = 0
|
|
508
|
+
let skipped = 0
|
|
509
|
+
let cancelled = false
|
|
510
|
+
let actionId: string | undefined
|
|
511
|
+
|
|
512
|
+
// Persistence state
|
|
513
|
+
let processedIds = new Set<string>()
|
|
514
|
+
let persistCounter = 0
|
|
515
|
+
const getItemId = (item: any) => item?.$id ?? item?.id ?? String(item)
|
|
516
|
+
|
|
517
|
+
// Get actions API from options (injected by wrapEntityOperations)
|
|
518
|
+
const actionsAPI = this._options.actionsAPI
|
|
519
|
+
|
|
520
|
+
// Initialize persistence if enabled
|
|
521
|
+
if (persist || resume) {
|
|
522
|
+
if (!actionsAPI) {
|
|
523
|
+
throw new Error('Persistence requires actions API - use db.Entity.forEach instead of db.Entity.list().forEach')
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Auto-generate action type from entity name
|
|
527
|
+
const actionType = typeof persist === 'string' ? persist : `${this._options.type ?? 'unknown'}.forEach`
|
|
528
|
+
|
|
529
|
+
if (resume) {
|
|
530
|
+
// Resume from existing action
|
|
531
|
+
const existingAction = await actionsAPI.get(resume)
|
|
532
|
+
if (existingAction) {
|
|
533
|
+
actionId = existingAction.id
|
|
534
|
+
processedIds = new Set(existingAction.data?.processedIds ?? [])
|
|
535
|
+
await actionsAPI.update(actionId, { status: 'active' })
|
|
536
|
+
} else {
|
|
537
|
+
throw new Error(`Action ${resume} not found`)
|
|
538
|
+
}
|
|
539
|
+
} else {
|
|
540
|
+
// Create new action
|
|
541
|
+
const action = await actionsAPI.create({
|
|
542
|
+
type: actionType,
|
|
543
|
+
data: { processedIds: [] },
|
|
544
|
+
})
|
|
545
|
+
actionId = action.id
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Resolve the items
|
|
550
|
+
const items = await this.resolve()
|
|
551
|
+
if (!Array.isArray(items)) {
|
|
552
|
+
throw new Error('forEach can only be called on array results')
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const total = items.length
|
|
556
|
+
|
|
557
|
+
// Update action with total if persistence is enabled
|
|
558
|
+
if ((persist || resume) && actionId && actionsAPI) {
|
|
559
|
+
await actionsAPI.update(actionId, { total, status: 'active' })
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Helper to calculate progress
|
|
563
|
+
const getProgress = (index: number, current?: unknown): ForEachProgress => {
|
|
564
|
+
const elapsed = Date.now() - startTime
|
|
565
|
+
const processed = completed + failed + skipped
|
|
566
|
+
const rate = processed > 0 ? (processed / elapsed) * 1000 : 0
|
|
567
|
+
const remaining = rate > 0 && total ? ((total - processed) / rate) * 1000 : undefined
|
|
568
|
+
|
|
569
|
+
return {
|
|
570
|
+
index,
|
|
571
|
+
total,
|
|
572
|
+
completed,
|
|
573
|
+
failed,
|
|
574
|
+
skipped,
|
|
575
|
+
current,
|
|
576
|
+
elapsed,
|
|
577
|
+
remaining,
|
|
578
|
+
rate,
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Helper to persist progress
|
|
583
|
+
const persistProgress = async (itemId: string): Promise<void> => {
|
|
584
|
+
if ((!persist && !resume) || !actionId || !actionsAPI) return
|
|
585
|
+
|
|
586
|
+
processedIds.add(itemId)
|
|
587
|
+
persistCounter++
|
|
588
|
+
|
|
589
|
+
// Persist every 10 items to reduce overhead
|
|
590
|
+
if (persistCounter % 10 === 0) {
|
|
591
|
+
await actionsAPI.update(actionId, {
|
|
592
|
+
progress: completed + failed + skipped,
|
|
593
|
+
data: { processedIds: Array.from(processedIds) },
|
|
594
|
+
})
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Helper to get retry delay
|
|
599
|
+
const getRetryDelay = (attempt: number): number => {
|
|
600
|
+
return typeof retryDelay === 'function' ? retryDelay(attempt) : retryDelay
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Helper to handle error
|
|
604
|
+
const handleError = async (
|
|
605
|
+
error: Error,
|
|
606
|
+
item: unknown,
|
|
607
|
+
attempt: number
|
|
608
|
+
): Promise<ForEachErrorAction> => {
|
|
609
|
+
if (typeof onError === 'function') {
|
|
610
|
+
return onError(error, item as any, attempt)
|
|
611
|
+
}
|
|
612
|
+
return onError
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Process a single item with retries
|
|
616
|
+
const processItem = async (item: unknown, index: number): Promise<void> => {
|
|
617
|
+
if (cancelled || signal?.aborted) {
|
|
618
|
+
cancelled = true
|
|
619
|
+
return
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Check if already processed (for resume)
|
|
623
|
+
const itemId = getItemId(item as any)
|
|
624
|
+
if (processedIds.has(itemId)) {
|
|
625
|
+
skipped++
|
|
626
|
+
return
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
let attempt = 0
|
|
630
|
+
while (true) {
|
|
631
|
+
try {
|
|
632
|
+
// Create timeout wrapper if needed
|
|
633
|
+
let result: U
|
|
634
|
+
if (timeout) {
|
|
635
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
636
|
+
setTimeout(() => reject(new Error(`Timeout after ${timeout}ms`)), timeout)
|
|
637
|
+
})
|
|
638
|
+
result = await Promise.race([
|
|
639
|
+
Promise.resolve(callback(item as any, index)),
|
|
640
|
+
timeoutPromise,
|
|
641
|
+
])
|
|
642
|
+
} else {
|
|
643
|
+
result = await callback(item as any, index)
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Success
|
|
647
|
+
completed++
|
|
648
|
+
await persistProgress(itemId)
|
|
649
|
+
await onComplete?.(item as any, result, index)
|
|
650
|
+
onProgress?.(getProgress(index, item))
|
|
651
|
+
return
|
|
652
|
+
|
|
653
|
+
} catch (error) {
|
|
654
|
+
attempt++
|
|
655
|
+
const action = await handleError(error as Error, item, attempt)
|
|
656
|
+
|
|
657
|
+
switch (action) {
|
|
658
|
+
case 'retry':
|
|
659
|
+
if (attempt <= maxRetries) {
|
|
660
|
+
await sleep(getRetryDelay(attempt))
|
|
661
|
+
continue // Retry
|
|
662
|
+
}
|
|
663
|
+
// Fall through to continue if max retries exceeded
|
|
664
|
+
failed++
|
|
665
|
+
await persistProgress(itemId) // Still mark as processed
|
|
666
|
+
errors.push({ item, error: error as Error, index })
|
|
667
|
+
onProgress?.(getProgress(index, item))
|
|
668
|
+
return
|
|
669
|
+
|
|
670
|
+
case 'skip':
|
|
671
|
+
skipped++
|
|
672
|
+
onProgress?.(getProgress(index, item))
|
|
673
|
+
return
|
|
674
|
+
|
|
675
|
+
case 'stop':
|
|
676
|
+
failed++
|
|
677
|
+
await persistProgress(itemId)
|
|
678
|
+
errors.push({ item, error: error as Error, index })
|
|
679
|
+
cancelled = true
|
|
680
|
+
return
|
|
681
|
+
|
|
682
|
+
case 'continue':
|
|
683
|
+
default:
|
|
684
|
+
failed++
|
|
685
|
+
await persistProgress(itemId)
|
|
686
|
+
errors.push({ item, error: error as Error, index })
|
|
687
|
+
onProgress?.(getProgress(index, item))
|
|
688
|
+
return
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// Process items with concurrency
|
|
695
|
+
try {
|
|
696
|
+
if (concurrency === 1) {
|
|
697
|
+
// Sequential processing
|
|
698
|
+
for (let i = 0; i < items.length; i++) {
|
|
699
|
+
if (cancelled || signal?.aborted) {
|
|
700
|
+
cancelled = true
|
|
701
|
+
break
|
|
702
|
+
}
|
|
703
|
+
await processItem(items[i], i)
|
|
704
|
+
}
|
|
705
|
+
} else {
|
|
706
|
+
// Concurrent processing with semaphore
|
|
707
|
+
const semaphore = new Semaphore(concurrency)
|
|
708
|
+
const promises: Promise<void>[] = []
|
|
709
|
+
|
|
710
|
+
for (let i = 0; i < items.length; i++) {
|
|
711
|
+
if (cancelled || signal?.aborted) {
|
|
712
|
+
cancelled = true
|
|
713
|
+
break
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const itemIndex = i
|
|
717
|
+
const item = items[i]
|
|
718
|
+
|
|
719
|
+
promises.push(
|
|
720
|
+
semaphore.acquire().then(async (release) => {
|
|
721
|
+
try {
|
|
722
|
+
await processItem(item, itemIndex)
|
|
723
|
+
} finally {
|
|
724
|
+
release()
|
|
725
|
+
}
|
|
726
|
+
})
|
|
727
|
+
)
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
await Promise.all(promises)
|
|
731
|
+
}
|
|
732
|
+
} finally {
|
|
733
|
+
// Final persistence update
|
|
734
|
+
if ((persist || resume) && actionId && actionsAPI) {
|
|
735
|
+
const finalResult: ForEachResult = {
|
|
736
|
+
total,
|
|
737
|
+
completed,
|
|
738
|
+
failed,
|
|
739
|
+
skipped,
|
|
740
|
+
elapsed: Date.now() - startTime,
|
|
741
|
+
errors,
|
|
742
|
+
cancelled,
|
|
743
|
+
actionId,
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
await actionsAPI.update(actionId, {
|
|
747
|
+
status: cancelled ? 'failed' : 'completed',
|
|
748
|
+
progress: completed + failed + skipped,
|
|
749
|
+
data: { processedIds: Array.from(processedIds) },
|
|
750
|
+
result: finalResult,
|
|
751
|
+
error: cancelled ? 'Cancelled' : errors.length > 0 ? `${errors.length} items failed` : undefined,
|
|
752
|
+
})
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
return {
|
|
757
|
+
total,
|
|
758
|
+
completed,
|
|
759
|
+
failed,
|
|
760
|
+
skipped,
|
|
761
|
+
elapsed: Date.now() - startTime,
|
|
762
|
+
errors,
|
|
763
|
+
cancelled,
|
|
764
|
+
actionId,
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Async iteration
|
|
770
|
+
*/
|
|
771
|
+
async *[Symbol.asyncIterator](): AsyncIterator<T extends (infer I)[] ? I : T> {
|
|
772
|
+
const items = await this.resolve()
|
|
773
|
+
if (Array.isArray(items)) {
|
|
774
|
+
for (const item of items) {
|
|
775
|
+
yield item as any
|
|
776
|
+
}
|
|
777
|
+
} else {
|
|
778
|
+
yield items as any
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Promise interface - then()
|
|
784
|
+
*/
|
|
785
|
+
then<TResult1 = T, TResult2 = never>(
|
|
786
|
+
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
|
|
787
|
+
onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null
|
|
788
|
+
): Promise<TResult1 | TResult2> {
|
|
789
|
+
if (!this._resolver) {
|
|
790
|
+
this._resolver = new Promise<T>((resolve, reject) => {
|
|
791
|
+
queueMicrotask(async () => {
|
|
792
|
+
try {
|
|
793
|
+
const value = await this.resolve()
|
|
794
|
+
resolve(value)
|
|
795
|
+
} catch (error) {
|
|
796
|
+
reject(error)
|
|
797
|
+
}
|
|
798
|
+
})
|
|
799
|
+
})
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
return this._resolver.then(onfulfilled, onrejected)
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Promise interface - catch()
|
|
807
|
+
*/
|
|
808
|
+
catch<TResult = never>(
|
|
809
|
+
onrejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null
|
|
810
|
+
): Promise<T | TResult> {
|
|
811
|
+
return this.then(null, onrejected)
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Promise interface - finally()
|
|
816
|
+
*/
|
|
817
|
+
finally(onfinally?: (() => void) | null): Promise<T> {
|
|
818
|
+
return this.then(
|
|
819
|
+
(value) => {
|
|
820
|
+
onfinally?.()
|
|
821
|
+
return value
|
|
822
|
+
},
|
|
823
|
+
(reason) => {
|
|
824
|
+
onfinally?.()
|
|
825
|
+
throw reason
|
|
826
|
+
}
|
|
827
|
+
)
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// =============================================================================
|
|
832
|
+
// Proxy Handlers
|
|
833
|
+
// =============================================================================
|
|
834
|
+
|
|
835
|
+
const DB_PROXY_HANDLERS: ProxyHandler<DBPromise<unknown>> = {
|
|
836
|
+
get(target, prop: string | symbol, receiver) {
|
|
837
|
+
// Handle symbols
|
|
838
|
+
if (typeof prop === 'symbol') {
|
|
839
|
+
if (prop === DB_PROMISE_SYMBOL) return true
|
|
840
|
+
if (prop === RAW_DB_PROMISE_SYMBOL) return target
|
|
841
|
+
if (prop === Symbol.asyncIterator) return target[Symbol.asyncIterator].bind(target)
|
|
842
|
+
return (target as any)[prop]
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// Handle promise methods
|
|
846
|
+
if (prop === 'then' || prop === 'catch' || prop === 'finally') {
|
|
847
|
+
return (target as any)[prop].bind(target)
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Handle DBPromise methods
|
|
851
|
+
if (['map', 'filter', 'sort', 'limit', 'first', 'forEach', 'resolve'].includes(prop)) {
|
|
852
|
+
return (target as any)[prop].bind(target)
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// Handle internal properties
|
|
856
|
+
if (prop.startsWith('_') || ['accessedProps', 'path', 'isResolved'].includes(prop)) {
|
|
857
|
+
return (target as any)[prop]
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Track property access
|
|
861
|
+
target.accessedProps.add(prop)
|
|
862
|
+
|
|
863
|
+
// Return a new DBPromise for the property path
|
|
864
|
+
return new DBPromise<unknown>({
|
|
865
|
+
type: target['_options']?.type,
|
|
866
|
+
parent: target,
|
|
867
|
+
propertyPath: [...target.path, prop],
|
|
868
|
+
executor: async () => {
|
|
869
|
+
const parentValue = await target.resolve()
|
|
870
|
+
return getNestedValue(parentValue, [prop])
|
|
871
|
+
},
|
|
872
|
+
})
|
|
873
|
+
},
|
|
874
|
+
|
|
875
|
+
set() {
|
|
876
|
+
throw new Error('DBPromise properties are read-only')
|
|
877
|
+
},
|
|
878
|
+
|
|
879
|
+
deleteProperty() {
|
|
880
|
+
throw new Error('DBPromise properties cannot be deleted')
|
|
881
|
+
},
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// =============================================================================
|
|
885
|
+
// Helper Functions
|
|
886
|
+
// =============================================================================
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* Sleep helper
|
|
890
|
+
*/
|
|
891
|
+
function sleep(ms: number): Promise<void> {
|
|
892
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Simple semaphore for concurrency control
|
|
897
|
+
*/
|
|
898
|
+
class Semaphore {
|
|
899
|
+
private permits: number
|
|
900
|
+
private queue: Array<() => void> = []
|
|
901
|
+
|
|
902
|
+
constructor(permits: number) {
|
|
903
|
+
this.permits = permits
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
async acquire(): Promise<() => void> {
|
|
907
|
+
if (this.permits > 0) {
|
|
908
|
+
this.permits--
|
|
909
|
+
return () => this.release()
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
return new Promise((resolve) => {
|
|
913
|
+
this.queue.push(() => {
|
|
914
|
+
this.permits--
|
|
915
|
+
resolve(() => this.release())
|
|
916
|
+
})
|
|
917
|
+
})
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
private release(): void {
|
|
921
|
+
this.permits++
|
|
922
|
+
const next = this.queue.shift()
|
|
923
|
+
if (next) {
|
|
924
|
+
next()
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* Get nested value from object
|
|
931
|
+
*/
|
|
932
|
+
function getNestedValue(obj: unknown, path: string[]): unknown {
|
|
933
|
+
let current = obj
|
|
934
|
+
for (const key of path) {
|
|
935
|
+
if (current === null || current === undefined) return undefined
|
|
936
|
+
current = (current as Record<string, unknown>)[key]
|
|
937
|
+
}
|
|
938
|
+
return current
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* Create a proxy that records property accesses
|
|
943
|
+
*/
|
|
944
|
+
function createRecordingProxy(
|
|
945
|
+
item: unknown,
|
|
946
|
+
recording: PropertyRecording
|
|
947
|
+
): unknown {
|
|
948
|
+
if (typeof item !== 'object' || item === null) {
|
|
949
|
+
return item
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
return new Proxy(item as Record<string, unknown>, {
|
|
953
|
+
get(target, prop: string | symbol) {
|
|
954
|
+
if (typeof prop === 'symbol') {
|
|
955
|
+
return target[prop as any]
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
recording.paths.add(prop)
|
|
959
|
+
|
|
960
|
+
const value = target[prop]
|
|
961
|
+
|
|
962
|
+
// If accessing a relation (identified by $id or Promise), record it
|
|
963
|
+
if (value && typeof value === 'object' && '$type' in (value as any)) {
|
|
964
|
+
recording.relations.set(prop, {
|
|
965
|
+
type: (value as any).$type,
|
|
966
|
+
isArray: Array.isArray(value),
|
|
967
|
+
nestedPaths: new Set(),
|
|
968
|
+
})
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// Return a nested recording proxy for objects
|
|
972
|
+
if (value && typeof value === 'object') {
|
|
973
|
+
return createRecordingProxy(value, recording)
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
return value
|
|
977
|
+
},
|
|
978
|
+
})
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* Analyze recordings to find batch-loadable relations
|
|
983
|
+
*/
|
|
984
|
+
function analyzeBatchLoads(
|
|
985
|
+
recordings: PropertyRecording[],
|
|
986
|
+
items: unknown[]
|
|
987
|
+
): Map<string, { type: string; ids: string[] }> {
|
|
988
|
+
const batchLoads = new Map<string, { type: string; ids: string[] }>()
|
|
989
|
+
|
|
990
|
+
// Find common relations across all recordings
|
|
991
|
+
const relationCounts = new Map<string, number>()
|
|
992
|
+
|
|
993
|
+
for (const recording of recordings) {
|
|
994
|
+
for (const [relationName, relation] of recording.relations) {
|
|
995
|
+
relationCounts.set(relationName, (relationCounts.get(relationName) || 0) + 1)
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// Only batch-load relations accessed in all (or most) items
|
|
1000
|
+
for (const [relationName, count] of relationCounts) {
|
|
1001
|
+
if (count >= recordings.length * 0.5) {
|
|
1002
|
+
// At least 50% of items access this relation
|
|
1003
|
+
const ids: string[] = []
|
|
1004
|
+
|
|
1005
|
+
for (let i = 0; i < items.length; i++) {
|
|
1006
|
+
const item = items[i] as Record<string, unknown>
|
|
1007
|
+
const relationId = item[relationName]
|
|
1008
|
+
if (typeof relationId === 'string') {
|
|
1009
|
+
ids.push(relationId)
|
|
1010
|
+
} else if (relationId && typeof relationId === 'object' && '$id' in relationId) {
|
|
1011
|
+
ids.push((relationId as any).$id)
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
if (ids.length > 0) {
|
|
1016
|
+
const relation = recordings[0]?.relations.get(relationName)
|
|
1017
|
+
if (relation) {
|
|
1018
|
+
batchLoads.set(relationName, { type: relation.type, ids })
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
return batchLoads
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/**
|
|
1028
|
+
* Execute batch loads for relations
|
|
1029
|
+
*/
|
|
1030
|
+
async function executeBatchLoads(
|
|
1031
|
+
batchLoads: Map<string, { type: string; ids: string[] }>
|
|
1032
|
+
): Promise<Map<string, Map<string, unknown>>> {
|
|
1033
|
+
const results = new Map<string, Map<string, unknown>>()
|
|
1034
|
+
|
|
1035
|
+
// For now, return empty - actual implementation would batch query
|
|
1036
|
+
// This is a placeholder that will be filled in by the actual DB integration
|
|
1037
|
+
|
|
1038
|
+
for (const [relationName, { type, ids }] of batchLoads) {
|
|
1039
|
+
results.set(relationName, new Map())
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
return results
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* Apply batch-loaded results to the mapped results
|
|
1047
|
+
*/
|
|
1048
|
+
function applyBatchResults<U>(
|
|
1049
|
+
results: U[],
|
|
1050
|
+
loadedRelations: Map<string, Map<string, unknown>>,
|
|
1051
|
+
originalItems: unknown[]
|
|
1052
|
+
): U[] {
|
|
1053
|
+
// For now, return results as-is
|
|
1054
|
+
// Actual implementation would inject loaded relations
|
|
1055
|
+
return results
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// =============================================================================
|
|
1059
|
+
// Check Functions
|
|
1060
|
+
// =============================================================================
|
|
1061
|
+
|
|
1062
|
+
/**
|
|
1063
|
+
* Check if a value is a DBPromise
|
|
1064
|
+
*/
|
|
1065
|
+
export function isDBPromise(value: unknown): value is DBPromise<unknown> {
|
|
1066
|
+
return (
|
|
1067
|
+
value !== null &&
|
|
1068
|
+
typeof value === 'object' &&
|
|
1069
|
+
DB_PROMISE_SYMBOL in value &&
|
|
1070
|
+
(value as any)[DB_PROMISE_SYMBOL] === true
|
|
1071
|
+
)
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
/**
|
|
1075
|
+
* Get the raw DBPromise from a proxied value
|
|
1076
|
+
*/
|
|
1077
|
+
export function getRawDBPromise<T>(value: DBPromise<T>): DBPromise<T> {
|
|
1078
|
+
if (RAW_DB_PROMISE_SYMBOL in value) {
|
|
1079
|
+
return (value as any)[RAW_DB_PROMISE_SYMBOL]
|
|
1080
|
+
}
|
|
1081
|
+
return value
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// =============================================================================
|
|
1085
|
+
// Factory Functions
|
|
1086
|
+
// =============================================================================
|
|
1087
|
+
|
|
1088
|
+
/**
|
|
1089
|
+
* Create a DBPromise for a list query
|
|
1090
|
+
*/
|
|
1091
|
+
export function createListPromise<T>(
|
|
1092
|
+
type: string,
|
|
1093
|
+
executor: () => Promise<T[]>
|
|
1094
|
+
): DBPromise<T[]> {
|
|
1095
|
+
return new DBPromise<T[]>({ type, executor })
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
/**
|
|
1099
|
+
* Create a DBPromise for a single entity query
|
|
1100
|
+
*/
|
|
1101
|
+
export function createEntityPromise<T>(
|
|
1102
|
+
type: string,
|
|
1103
|
+
executor: () => Promise<T | null>
|
|
1104
|
+
): DBPromise<T | null> {
|
|
1105
|
+
return new DBPromise<T | null>({ type, executor })
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
/**
|
|
1109
|
+
* Create a DBPromise for a search query
|
|
1110
|
+
*/
|
|
1111
|
+
export function createSearchPromise<T>(
|
|
1112
|
+
type: string,
|
|
1113
|
+
executor: () => Promise<T[]>
|
|
1114
|
+
): DBPromise<T[]> {
|
|
1115
|
+
return new DBPromise<T[]>({ type, executor })
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// =============================================================================
|
|
1119
|
+
// Entity Operations Wrapper
|
|
1120
|
+
// =============================================================================
|
|
1121
|
+
|
|
1122
|
+
/**
|
|
1123
|
+
* Wrap EntityOperations to return DBPromise
|
|
1124
|
+
*/
|
|
1125
|
+
export function wrapEntityOperations<T>(
|
|
1126
|
+
typeName: string,
|
|
1127
|
+
operations: {
|
|
1128
|
+
get: (id: string) => Promise<T | null>
|
|
1129
|
+
list: (options?: any) => Promise<T[]>
|
|
1130
|
+
find: (where: any) => Promise<T[]>
|
|
1131
|
+
search: (query: string, options?: any) => Promise<T[]>
|
|
1132
|
+
create: (...args: any[]) => Promise<T>
|
|
1133
|
+
update: (id: string, data: any) => Promise<T>
|
|
1134
|
+
upsert: (id: string, data: any) => Promise<T>
|
|
1135
|
+
delete: (id: string) => Promise<boolean>
|
|
1136
|
+
forEach: (...args: any[]) => Promise<void>
|
|
1137
|
+
},
|
|
1138
|
+
actionsAPI?: ForEachActionsAPI
|
|
1139
|
+
): {
|
|
1140
|
+
get: (id: string) => DBPromise<T | null>
|
|
1141
|
+
list: (options?: any) => DBPromise<T[]>
|
|
1142
|
+
find: (where: any) => DBPromise<T[]>
|
|
1143
|
+
search: (query: string, options?: any) => DBPromise<T[]>
|
|
1144
|
+
create: (...args: any[]) => Promise<T>
|
|
1145
|
+
update: (id: string, data: any) => Promise<T>
|
|
1146
|
+
upsert: (id: string, data: any) => Promise<T>
|
|
1147
|
+
delete: (id: string) => Promise<boolean>
|
|
1148
|
+
forEach: <U>(callback: (item: T, index: number) => U | Promise<U>, options?: ForEachOptions<T>) => Promise<ForEachResult>
|
|
1149
|
+
first: () => DBPromise<T | null>
|
|
1150
|
+
} {
|
|
1151
|
+
return {
|
|
1152
|
+
get(id: string): DBPromise<T | null> {
|
|
1153
|
+
return new DBPromise({
|
|
1154
|
+
type: typeName,
|
|
1155
|
+
executor: () => operations.get(id),
|
|
1156
|
+
actionsAPI,
|
|
1157
|
+
})
|
|
1158
|
+
},
|
|
1159
|
+
|
|
1160
|
+
list(options?: any): DBPromise<T[]> {
|
|
1161
|
+
return new DBPromise({
|
|
1162
|
+
type: typeName,
|
|
1163
|
+
executor: () => operations.list(options),
|
|
1164
|
+
actionsAPI,
|
|
1165
|
+
})
|
|
1166
|
+
},
|
|
1167
|
+
|
|
1168
|
+
find(where: any): DBPromise<T[]> {
|
|
1169
|
+
return new DBPromise({
|
|
1170
|
+
type: typeName,
|
|
1171
|
+
executor: () => operations.find(where),
|
|
1172
|
+
actionsAPI,
|
|
1173
|
+
})
|
|
1174
|
+
},
|
|
1175
|
+
|
|
1176
|
+
search(query: string, options?: any): DBPromise<T[]> {
|
|
1177
|
+
return new DBPromise({
|
|
1178
|
+
type: typeName,
|
|
1179
|
+
executor: () => operations.search(query, options),
|
|
1180
|
+
actionsAPI,
|
|
1181
|
+
})
|
|
1182
|
+
},
|
|
1183
|
+
|
|
1184
|
+
first(): DBPromise<T | null> {
|
|
1185
|
+
return new DBPromise({
|
|
1186
|
+
type: typeName,
|
|
1187
|
+
executor: async () => {
|
|
1188
|
+
const items = await operations.list({ limit: 1 })
|
|
1189
|
+
return items[0] ?? null
|
|
1190
|
+
},
|
|
1191
|
+
actionsAPI,
|
|
1192
|
+
})
|
|
1193
|
+
},
|
|
1194
|
+
|
|
1195
|
+
/**
|
|
1196
|
+
* Process all entities with concurrency, progress, and optional persistence
|
|
1197
|
+
*
|
|
1198
|
+
* Supports two calling styles:
|
|
1199
|
+
* - forEach(callback, options?) - callback first
|
|
1200
|
+
* - forEach(options, callback) - options first (with where filter)
|
|
1201
|
+
*
|
|
1202
|
+
* @example
|
|
1203
|
+
* ```ts
|
|
1204
|
+
* await db.Lead.forEach(lead => console.log(lead.name))
|
|
1205
|
+
* await db.Lead.forEach(processLead, { concurrency: 10 })
|
|
1206
|
+
* await db.Lead.forEach({ where: { status: 'active' } }, processLead)
|
|
1207
|
+
* await db.Lead.forEach(processLead, { persist: true })
|
|
1208
|
+
* await db.Lead.forEach(processLead, { resume: 'action-123' })
|
|
1209
|
+
* ```
|
|
1210
|
+
*/
|
|
1211
|
+
async forEach<U>(
|
|
1212
|
+
callbackOrOptions: ((item: T, index: number) => U | Promise<U>) | ForEachOptions<T>,
|
|
1213
|
+
callbackOrOpts?: ((item: T, index: number) => U | Promise<U>) | ForEachOptions<T>
|
|
1214
|
+
): Promise<ForEachResult> {
|
|
1215
|
+
// Detect which calling style is being used
|
|
1216
|
+
const isOptionsFirst = typeof callbackOrOptions === 'object' && callbackOrOptions !== null && !('call' in callbackOrOptions)
|
|
1217
|
+
|
|
1218
|
+
const callback = isOptionsFirst
|
|
1219
|
+
? (callbackOrOpts as (item: T, index: number) => U | Promise<U>)
|
|
1220
|
+
: (callbackOrOptions as (item: T, index: number) => U | Promise<U>)
|
|
1221
|
+
|
|
1222
|
+
const options = isOptionsFirst
|
|
1223
|
+
? (callbackOrOptions as ForEachOptions<T>)
|
|
1224
|
+
: ((callbackOrOpts ?? {}) as ForEachOptions<T>)
|
|
1225
|
+
|
|
1226
|
+
// Extract where filter and pass to list
|
|
1227
|
+
const listOptions = options.where ? { where: options.where } : undefined
|
|
1228
|
+
|
|
1229
|
+
const listPromise = new DBPromise<T[]>({
|
|
1230
|
+
type: typeName,
|
|
1231
|
+
executor: () => operations.list(listOptions),
|
|
1232
|
+
actionsAPI,
|
|
1233
|
+
})
|
|
1234
|
+
return listPromise.forEach(callback as any, options as any)
|
|
1235
|
+
},
|
|
1236
|
+
|
|
1237
|
+
// Mutations don't need wrapping
|
|
1238
|
+
create: operations.create,
|
|
1239
|
+
update: operations.update,
|
|
1240
|
+
upsert: operations.upsert,
|
|
1241
|
+
delete: operations.delete,
|
|
1242
|
+
}
|
|
1243
|
+
}
|