ai-database 2.0.2 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/actions.d.ts +247 -0
  3. package/dist/actions.d.ts.map +1 -0
  4. package/dist/actions.js +260 -0
  5. package/dist/actions.js.map +1 -0
  6. package/dist/ai-promise-db.d.ts +34 -2
  7. package/dist/ai-promise-db.d.ts.map +1 -1
  8. package/dist/ai-promise-db.js +511 -66
  9. package/dist/ai-promise-db.js.map +1 -1
  10. package/dist/constants.d.ts +16 -0
  11. package/dist/constants.d.ts.map +1 -0
  12. package/dist/constants.js +16 -0
  13. package/dist/constants.js.map +1 -0
  14. package/dist/events.d.ts +153 -0
  15. package/dist/events.d.ts.map +1 -0
  16. package/dist/events.js +154 -0
  17. package/dist/events.js.map +1 -0
  18. package/dist/index.d.ts +8 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +13 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/memory-provider.d.ts +144 -2
  23. package/dist/memory-provider.d.ts.map +1 -1
  24. package/dist/memory-provider.js +569 -13
  25. package/dist/memory-provider.js.map +1 -1
  26. package/dist/schema/cascade.d.ts +96 -0
  27. package/dist/schema/cascade.d.ts.map +1 -0
  28. package/dist/schema/cascade.js +528 -0
  29. package/dist/schema/cascade.js.map +1 -0
  30. package/dist/schema/index.d.ts +197 -0
  31. package/dist/schema/index.d.ts.map +1 -0
  32. package/dist/schema/index.js +1211 -0
  33. package/dist/schema/index.js.map +1 -0
  34. package/dist/schema/parse.d.ts +225 -0
  35. package/dist/schema/parse.d.ts.map +1 -0
  36. package/dist/schema/parse.js +732 -0
  37. package/dist/schema/parse.js.map +1 -0
  38. package/dist/schema/provider.d.ts +176 -0
  39. package/dist/schema/provider.d.ts.map +1 -0
  40. package/dist/schema/provider.js +258 -0
  41. package/dist/schema/provider.js.map +1 -0
  42. package/dist/schema/resolve.d.ts +87 -0
  43. package/dist/schema/resolve.d.ts.map +1 -0
  44. package/dist/schema/resolve.js +474 -0
  45. package/dist/schema/resolve.js.map +1 -0
  46. package/dist/schema/semantic.d.ts +53 -0
  47. package/dist/schema/semantic.d.ts.map +1 -0
  48. package/dist/schema/semantic.js +247 -0
  49. package/dist/schema/semantic.js.map +1 -0
  50. package/dist/schema/types.d.ts +528 -0
  51. package/dist/schema/types.d.ts.map +1 -0
  52. package/dist/schema/types.js +9 -0
  53. package/dist/schema/types.js.map +1 -0
  54. package/dist/schema.d.ts +24 -867
  55. package/dist/schema.d.ts.map +1 -1
  56. package/dist/schema.js +41 -1124
  57. package/dist/schema.js.map +1 -1
  58. package/dist/semantic.d.ts +175 -0
  59. package/dist/semantic.d.ts.map +1 -0
  60. package/dist/semantic.js +338 -0
  61. package/dist/semantic.js.map +1 -0
  62. package/dist/types.d.ts +14 -0
  63. package/dist/types.d.ts.map +1 -1
  64. package/dist/types.js.map +1 -1
  65. package/package.json +13 -4
  66. package/.turbo/turbo-build.log +0 -5
  67. package/TESTING.md +0 -410
  68. package/TEST_SUMMARY.md +0 -250
  69. package/TODO.md +0 -128
  70. package/src/ai-promise-db.ts +0 -1243
  71. package/src/authorization.ts +0 -1102
  72. package/src/durable-clickhouse.ts +0 -596
  73. package/src/durable-promise.ts +0 -582
  74. package/src/execution-queue.ts +0 -608
  75. package/src/index.test.ts +0 -868
  76. package/src/index.ts +0 -337
  77. package/src/linguistic.ts +0 -404
  78. package/src/memory-provider.test.ts +0 -1036
  79. package/src/memory-provider.ts +0 -1119
  80. package/src/schema.test.ts +0 -1254
  81. package/src/schema.ts +0 -2296
  82. package/src/tests.ts +0 -725
  83. package/src/types.ts +0 -1177
  84. package/test/README.md +0 -153
  85. package/test/edge-cases.test.ts +0 -646
  86. package/test/provider-resolution.test.ts +0 -402
  87. package/tsconfig.json +0 -9
  88. package/vitest.config.ts +0 -19
@@ -1,1243 +0,0 @@
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
- }