@asaidimu/utils-database 1.0.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/index.d.mts ADDED
@@ -0,0 +1,527 @@
1
+ import { QueryFilter, PaginationOptions } from '@asaidimu/query';
2
+ import { SchemaDefinition, SchemaChange, DataTransform, PredicateMap } from '@asaidimu/anansi';
3
+
4
+ declare const DEFAULT_KEYPATH = "$id";
5
+ interface CursorPaginationOptions {
6
+ limit: number;
7
+ cursor?: any;
8
+ direction?: "forward" | "backward";
9
+ }
10
+ interface CursorCallbackResult<T> {
11
+ value: T | null;
12
+ done: boolean;
13
+ offset?: number;
14
+ }
15
+ /**
16
+ * Callback function for cursor iteration over store records.
17
+ *
18
+ * @template T - The type of records stored.
19
+ * @param value - The current record value (cloned, not a live reference).
20
+ * @param key - The key (ID) of the current record.
21
+ * @param cursor - The underlying cursor object (implementation‑specific; may be `null` in memory adapters).
22
+ * @returns A promise that resolves to an object indicating whether iteration should stop, and an optional offset to advance.
23
+ */
24
+ type CursorCallback<T> = (value: T, key: string | number, cursor: any) => Promise<CursorCallbackResult<T>>;
25
+ /**
26
+ * A generic representation of a key range, replacing the browser-specific IDBKeyRange.
27
+ */
28
+ interface StoreKeyRange {
29
+ lower?: any;
30
+ upper?: any;
31
+ lowerOpen?: boolean;
32
+ upperOpen?: boolean;
33
+ }
34
+ /**
35
+ * Storage adapter interface for a single object store (collection).
36
+ *
37
+ * This interface abstracts all low‑level persistence operations, allowing
38
+ * different backends (IndexedDB, memory, remote, etc.) to be used interchangeably.
39
+ * All methods return promises and operate on clones of data to prevent
40
+ * unintended mutations.
41
+ *
42
+ * @template T - The type of objects stored in this store. Must include the key path property.
43
+ */
44
+ interface Store<T = any> {
45
+ /**
46
+ * Adds one or more records to the store.
47
+ *
48
+ * If a record does not have a value for the store's key path, an automatic
49
+ * key (e.g., auto‑incremented number) may be assigned. The store's key path
50
+ * property is then updated on the added record(s).
51
+ *
52
+ * @param data - A single record or an array of records to add.
53
+ * @returns A promise that resolves to:
54
+ * - the key(s) of the added record(s) – a single key if `data` was a single record,
55
+ * or an array of keys if `data` was an array.
56
+ * @throws {Error} If any record lacks the key path property and auto‑keying is not supported,
57
+ * or if a record with the same key already exists.
58
+ */
59
+ add(data: T | T[]): Promise<string | number | (string | number)[]>;
60
+ /**
61
+ * Removes all records from the store.
62
+ *
63
+ * @returns A promise that resolves when the store is cleared.
64
+ * @throws {Error} If the operation fails (e.g., store is closed).
65
+ */
66
+ clear(): Promise<void>;
67
+ /**
68
+ * Returns the total number of records in the store.
69
+ *
70
+ * @returns A promise that resolves to the record count.
71
+ * @throws {Error} If the operation fails.
72
+ */
73
+ count(): Promise<number>;
74
+ /**
75
+ * Deletes one or more records by their keys.
76
+ *
77
+ * @param id - A single key or an array of keys to delete.
78
+ * @returns A promise that resolves when the records are deleted.
79
+ * @throws {Error} If any key is `undefined` or the operation fails.
80
+ */
81
+ delete(id: string | number | (string | number)[]): Promise<void>;
82
+ /**
83
+ * Retrieves a single record by its primary key.
84
+ *
85
+ * @param id - The key of the record to retrieve.
86
+ * @returns A promise that resolves to the record (cloned) if found, otherwise `undefined`.
87
+ * @throws {Error} If the key is `undefined` or the operation fails.
88
+ */
89
+ getById(id: string | number): Promise<T | undefined>;
90
+ /**
91
+ * Retrieves a single record by an index and index key.
92
+ *
93
+ * @param index - The name of the index to query.
94
+ * @param key - The exact key value to look up in the index.
95
+ * @returns A promise that resolves to the first matching record (cloned), or `undefined` if none.
96
+ * @throws {Error} If the index does not exist, the key is `undefined`, or the operation fails.
97
+ */
98
+ getByIndex(index: string, key: any): Promise<T | undefined>;
99
+ /**
100
+ * Retrieves multiple records from an index, optionally within a key range.
101
+ *
102
+ * @param index - The name of the index to query.
103
+ * @param keyRange - Optional `StoreKeyRange` to filter results. If omitted, all records are returned.
104
+ * @returns A promise that resolves to an array of matching records (each cloned).
105
+ * @throws {Error} If the index does not exist or the operation fails.
106
+ */
107
+ getByKeyRange(index: string, keyRange?: StoreKeyRange): Promise<T[]>;
108
+ /**
109
+ * Retrieves all records from the store.
110
+ *
111
+ * @returns A promise that resolves to an array of all records (each cloned).
112
+ * @throws {Error} If the operation fails.
113
+ */
114
+ getAll(): Promise<T[]>;
115
+ /**
116
+ * Inserts or replaces a record.
117
+ *
118
+ * If a record with the same key already exists, it is replaced.
119
+ * The record must contain the store's key path property.
120
+ *
121
+ * @param data - The record to store.
122
+ * @returns A promise that resolves to the key of the stored record.
123
+ * @throws {Error} If the record lacks the key path property or the operation fails.
124
+ */
125
+ put(data: T): Promise<string | number>;
126
+ /**
127
+ * Iterates over records using a cursor, allowing early termination and skipping.
128
+ *
129
+ * The callback is invoked for each record in iteration order. The callback
130
+ * can control the iteration by returning `{ done: true }` to stop, or
131
+ * `{ offset: n }` to skip ahead `n` records.
132
+ *
133
+ * @param callback - Function called for each record.
134
+ * @param direction - Iteration direction: `"forward"` (ascending keys) or `"backward"` (descending keys).
135
+ * @param keyRange - An optional StoreKeyRange to start from specific points.
136
+ * @returns A promise that resolves to the last record processed (or `null` if none).
137
+ * @throws {Error} If the callback throws or the operation fails.
138
+ */
139
+ cursor(callback: CursorCallback<T>, direction?: "forward" | "backward", keyRange?: StoreKeyRange): Promise<T | null>;
140
+ /**
141
+ * Executes a batch of write operations atomically.
142
+ *
143
+ * All operations in the batch succeed or fail together. This is useful for
144
+ * maintaining consistency when multiple writes are required.
145
+ *
146
+ * @param operations - An array of operations. Each operation can be:
147
+ * - `{ type: "add" | "put", data: T | T[] }`
148
+ * - `{ type: "delete", data: string | number | (string | number)[] }`
149
+ * @returns A promise that resolves when the batch is committed.
150
+ * @throws {Error} If any operation fails or the batch cannot be completed.
151
+ */
152
+ batch(operations: Array<{
153
+ type: "add" | "put";
154
+ data: T | T[];
155
+ } | {
156
+ type: "delete";
157
+ data: string | number | (string | number)[];
158
+ }>): Promise<void>;
159
+ open(): Promise<void>;
160
+ }
161
+ interface Collection<T> {
162
+ /**
163
+ * Finds a single document matching the query.
164
+ * @param query - The query to execute.
165
+ * @returns A promise resolving to the matching document or `null` if not found.
166
+ */
167
+ find: (query: QueryFilter<T>) => Promise<Document<T> | null>;
168
+ /**
169
+ * Lists documents based on the provided query.
170
+ * @param query - The query to list documents (supports pagination and sorting).
171
+ * @returns A promise resolving to an array of documents.
172
+ */
173
+ list: (query: PaginationOptions) => Promise<AsyncIterator<Document<T>[]>>;
174
+ /**
175
+ * Filters documents based on the provided query.
176
+ * @param query - The query to filter documents.
177
+ * @returns A promise resolving to an array of matching documents.
178
+ */
179
+ filter: (query: QueryFilter<T>) => Promise<Document<T>[]>;
180
+ /**
181
+ * Creates a new document in the schema.
182
+ * @param initial - The initial data for the document.
183
+ * @returns A promise resolving to the created document.
184
+ */
185
+ create: (initial: T) => Promise<Document<T>>;
186
+ /**
187
+ * Subscribes to schema-level events (e.g., "create", "update", "delete", "access").
188
+ * @param event - The event type to subscribe to.
189
+ * @param callback - The function to call when the event occurs.
190
+ * @returns A promise resolving to an unsubscribe function.
191
+ */
192
+ subscribe: (event: CollectionEventType | TelemetryEventType, callback: (event: CollectionEvent<T> | TelemetryEvent) => void) => Promise<() => void>;
193
+ /**
194
+ * Validate data
195
+ * @param data - The data to validate
196
+ * @returns An object containing validation results
197
+ */
198
+ validate(data: Record<string, any>): Promise<{
199
+ value?: any;
200
+ issues: Array<{
201
+ message: string;
202
+ path: Array<string>;
203
+ }>;
204
+ }>;
205
+ }
206
+ /**
207
+ * Event payload for DocumentCursor events.
208
+ */
209
+ type CollectionEventType = "document:create" | "collection:read" | "migration:start" | "migration:end";
210
+ type CollectionEvent<T> = {
211
+ type: CollectionEventType;
212
+ document?: T;
213
+ model?: string;
214
+ method?: keyof Collection<T>;
215
+ metadata?: Record<string, unknown>;
216
+ timestamp: number;
217
+ };
218
+ interface Database {
219
+ /**
220
+ * Accesses a schema model by name.
221
+ * @param schemaName - The name of the schema to access.
222
+ * @returns A promise resolving to the schema's DocumentCursor.
223
+ * @throws DatabaseError
224
+ */
225
+ collection: <T>(schemaName: string) => Promise<Collection<T>>;
226
+ /**
227
+ * Creates a new schema model.
228
+ * @param schema - The schema definition.
229
+ * @returns A promise resolving to the created schema's DocumentCursor.
230
+ * @throws DatabaseError
231
+ */
232
+ createCollection: <T>(schema: SchemaDefinition) => Promise<Collection<T>>;
233
+ /**
234
+ * Deletes a schema by name.
235
+ * @param schemaName - The name of the schema to delete.
236
+ * @returns A promise resolving to `true` if successful, or `false` if an error occurs.
237
+ * @throws DatabaseError
238
+ */
239
+ deleteCollection: (schemaName: string) => Promise<boolean>;
240
+ /**
241
+ * Updates an existing schema.
242
+ * @param schema - The updated schema definition.
243
+ * @returns A promise resolving to `true` if successful, or `false` if an error occurs.
244
+ * @throws DatabaseError
245
+ */
246
+ updateCollection: (schema: SchemaDefinition) => Promise<boolean>;
247
+ /**
248
+ * Migrates an existing collection's data and updates its schema definition metadata.
249
+ * This function processes data in a streaming fashion to prevent loading
250
+ * the entire collection into memory.
251
+ *
252
+ * It will:
253
+ * 1. Verify the target collection exists.
254
+ * 2. Retrieve the collection's current schema definition from a metadata store ($index).
255
+ * 3. Initialize a `MigrationEngine` with this schema.
256
+ * 4. Allow a callback to define specific data transformations using the `MigrationEngine`.
257
+ * 5. **Crucially, it uses `migrationEngine.dryRun()` to get the `newSchema` that results**
258
+ * **from the transformations defined in the callback.**
259
+ * 6. Execute these transformations by streaming data from the collection,
260
+ * through the `MigrationEngine`, and back into the same collection.
261
+ * 7. Finally, update the schema definition for the collection in the `$index` metadata store
262
+ * to reflect this `newSchema`.
263
+ * All these steps for data and metadata updates happen within a single atomic IndexedDB transaction.
264
+ *
265
+ * Note: This function focuses solely on *data transformation* and *metadata updates*.
266
+ * It does NOT handle structural IndexedDB changes like adding/removing physical indexes or object stores,
267
+ * which still require an `onupgradeneeded` event (i.e., a database version upgrade).
268
+ *
269
+ * @param name - The name of the collection (IndexedDB object store) to migrate.
270
+ * @param {Object} opts - Options for the new migration
271
+ * @param {SchemaChange<any>[]} opts.changes - Array of schema changes
272
+ * @param {string} opts.description - Description of the migration
273
+ * @param {SchemaChange<any>[]} [opts.rollback] - Optional rollback changes
274
+ * @param {DataTransform<any, any>} [opts.transform] - Optional data transform
275
+ * @returns A Promise resolving to `true` if the migration completes successfully,
276
+ * @throws {DatabaseError} If the collection does not exist, its schema metadata is missing,
277
+ * or any IndexedDB operation/streaming fails critically.
278
+ */
279
+ migrateCollection: (name: string, opts: CollectionMigrationOptions, batchSize?: number) => Promise<boolean>;
280
+ /**
281
+ * Subscribes to database-level events (e.g., "schemaAdded", "schemaDeleted", "schemaAccessed", "migrate").
282
+ * @param event - The event type to subscribe to.
283
+ * @param callback - The function to call when the event occurs.
284
+ * @returns A promise resolving to an unsubscribe function.
285
+ */
286
+ subscribe: (event: DatabaseEventType | "telemetry", callback: (event: DatabaseEvent | TelemetryEvent) => void) => Promise<() => void>;
287
+ /**
288
+ * Closes the connection to the database
289
+ */
290
+ close: () => void;
291
+ }
292
+ type CollectionMigrationOptions = {
293
+ changes: SchemaChange<any>[];
294
+ description: string;
295
+ rollback?: SchemaChange<any>[];
296
+ transform?: string | DataTransform<any, any>;
297
+ };
298
+ /**
299
+ * Event payload for Database events.
300
+ */
301
+ type DatabaseEventType = "collection:create" | "collection:delete" | "collection:update" | "collection:read" | "migrate";
302
+ type DatabaseEvent = {
303
+ type: DatabaseEventType;
304
+ schema?: SchemaDefinition | Partial<SchemaDefinition>;
305
+ timestamp: number;
306
+ };
307
+ type Document<T> = {
308
+ readonly [K in keyof T]: T[K];
309
+ } & {
310
+ /**
311
+ * A unique identifier for the document
312
+ * @returns A promise resolving to `true` if successful, or `false` if an error occurs.
313
+ */
314
+ $id?: string;
315
+ /**
316
+ * A timestamp indicating when the document was created
317
+ */
318
+ $created?: string | Date;
319
+ /**
320
+ * A timestamp indicating when the document was last updated
321
+ */
322
+ $updated?: string | Date;
323
+ /**
324
+ * A number representing how many times the document has changed
325
+ */
326
+ $version?: number;
327
+ /**
328
+ * Fetches the latest data from the database
329
+ * @returns A promise resolving to `true` if successful, or `false` if an error occurs.
330
+ */
331
+ read: () => Promise<boolean>;
332
+ /**
333
+ * Updates the document with the provided properties.
334
+ * @param props - Partial object containing the fields to update.
335
+ * @returns A promise resolving to `true` if successful, or `false` if an error occurs.
336
+ */
337
+ update: (props: Partial<T>) => Promise<boolean>;
338
+ /**
339
+ * Deletes the document from the database.
340
+ * @returns A promise resolving to `true` if successful, or `false` if an error occurs.
341
+ */
342
+ delete: () => Promise<boolean>;
343
+ /**
344
+ * Subscribes to document events (e.g., "update", "delete", "access").
345
+ * @param event - The event type to subscribe to.
346
+ * @param callback - The function to call when the event occurs.
347
+ * @returns A promise resolving to an unsubscribe function.
348
+ */
349
+ subscribe: (event: DocumentEventType | TelemetryEventType, callback: (event: DocumentEvent<T> | TelemetryEvent) => void) => () => void;
350
+ state(): T;
351
+ };
352
+ /**
353
+ * Event payload for DocumentModel events.
354
+ */
355
+ type DocumentEventType = "document:create" | "document:write" | "document:update" | "document:delete" | "document:read";
356
+ type DocumentEvent<T> = {
357
+ type: DocumentEventType;
358
+ data?: Partial<T>;
359
+ timestamp: number;
360
+ };
361
+ type TelemetryEventType = "telemetry";
362
+ type TelemetryEvent = {
363
+ type: TelemetryEventType;
364
+ method: string;
365
+ timestamp: number;
366
+ source: any;
367
+ metadata: {
368
+ args: any[];
369
+ performance: {
370
+ durationMs: number;
371
+ };
372
+ source: {
373
+ level: "database" | "collection" | "document";
374
+ collection?: string;
375
+ document?: string;
376
+ };
377
+ result?: {
378
+ type: 'array' | string;
379
+ size?: number;
380
+ };
381
+ error: {
382
+ message: string;
383
+ name: string;
384
+ stack?: string;
385
+ } | null;
386
+ };
387
+ };
388
+ interface DatabaseConfig {
389
+ name: string;
390
+ keyPath?: string;
391
+ schemasStoreName?: string;
392
+ enableTelemetry?: boolean;
393
+ predicates?: PredicateMap;
394
+ validate?: boolean;
395
+ }
396
+
397
+ declare class MemoryStore<T extends Record<string, any>> implements Store<T> {
398
+ private readonly keyPath;
399
+ private data;
400
+ private nextId;
401
+ constructor(keyPath?: string);
402
+ open(): Promise<void>;
403
+ private getKey;
404
+ private clone;
405
+ add(data: T | T[]): Promise<string | number | (string | number)[]>;
406
+ put(data: T): Promise<string | number>;
407
+ batch(operations: Array<{
408
+ type: "add" | "put";
409
+ data: T | T[];
410
+ } | {
411
+ type: "delete";
412
+ data: string | number | (string | number)[];
413
+ }>): Promise<void>;
414
+ getById(id: string | number): Promise<T | undefined>;
415
+ delete(id: string | number | (string | number)[]): Promise<void>;
416
+ clear(): Promise<void>;
417
+ count(): Promise<number>;
418
+ getByIndex(index: string, key: any): Promise<T | undefined>;
419
+ getByKeyRange(indexName: string, keyRange?: StoreKeyRange): Promise<T[]>;
420
+ private isInKeyRange;
421
+ getAll(): Promise<T[]>;
422
+ cursor(callback: CursorCallback<T>, direction?: "forward" | "backward", keyRange?: StoreKeyRange): Promise<T | null>;
423
+ }
424
+
425
+ declare function createEphemeralStore<T extends Record<string, any>>(config: DatabaseConfig): MemoryStore<T>;
426
+
427
+ /**
428
+ * Manages the lifecycle and state of an IndexedDB connection, ensuring
429
+ * schema stores and collections are created and upgraded safely.
430
+ */
431
+ declare class ConnectionManager {
432
+ private readonly config;
433
+ private connectionInitializer;
434
+ private readonly schemasStoreName;
435
+ /**
436
+ * Initializes a new ConnectionManager.
437
+ *
438
+ * @param config - The database configuration options.
439
+ */
440
+ constructor(config: DatabaseConfig);
441
+ /**
442
+ * Opens the database and ensures the base schemas store exists.
443
+ *
444
+ * @returns A promise that resolves to the opened IDBDatabase instance.
445
+ * @throws {Error} If the database fails to open.
446
+ */
447
+ private openDatabase;
448
+ /**
449
+ * Upgrades the database version and executes the provided upgrade logic.
450
+ *
451
+ * @param currentConnection - The existing active IndexedDB connection to be closed.
452
+ * @param upgradeCallback - A callback function containing the structural changes to apply during the upgrade.
453
+ * @returns A promise that resolves to the newly upgraded IDBDatabase instance.
454
+ * @throws {Error} If the database upgrade fails.
455
+ */
456
+ private upgradeDatabase;
457
+ /**
458
+ * Ensures a collection object store exists within the database.
459
+ * Triggers an upgrade if the specified store is missing.
460
+ *
461
+ * @param collectionName - The name of the collection store to ensure.
462
+ * @param keyPath - The primary key path for the collection (defaults to "$id").
463
+ * @returns A promise that resolves when the store is confirmed to exist.
464
+ */
465
+ ensureStore(collectionName: string, keyPath?: string): Promise<void>;
466
+ /**
467
+ * Retrieves or opens the active database connection.
468
+ * Handles connection loss and version changes automatically.
469
+ *
470
+ * @returns A promise resolving to the active IDBDatabase connection.
471
+ * @throws {DatabaseError} If the connection initialization fails.
472
+ */
473
+ getConnection: () => Promise<IDBDatabase>;
474
+ /**
475
+ * Performs a version upgrade using custom structural logic.
476
+ * Resets the internal initialization state so subsequent callers wait for the new version.
477
+ *
478
+ * @param upgradeLogic - A callback executed during the IndexedDB upgradeneeded event.
479
+ * @returns A promise resolving to the upgraded IDBDatabase connection.
480
+ * @throws {DatabaseError} If the internal upgrade procedure fails.
481
+ */
482
+ upgrade(upgradeLogic: (db: IDBDatabase) => void): Promise<IDBDatabase>;
483
+ /**
484
+ * Closes the active database connection and resets the internal initialization state.
485
+ */
486
+ close(): void;
487
+ }
488
+
489
+ declare class IndexedDBStore<T extends Record<string, any>> implements Store<T> {
490
+ private readonly getConnection;
491
+ private readonly storeName;
492
+ private readonly keyPath;
493
+ private readonly onOpen;
494
+ constructor(getConnection: () => Promise<IDBDatabase>, storeName: string, keyPath: string | undefined, onOpen: () => Promise<void>);
495
+ open(): Promise<void>;
496
+ /**
497
+ * Map native DOMExceptions to internal DatabaseErrors.
498
+ */
499
+ private mapError;
500
+ /**
501
+ * Core wrapper for transaction safety. It converts IDB request callbacks
502
+ * into a single Promise while preserving transaction activity.
503
+ */
504
+ private withTx;
505
+ private requestToPromise;
506
+ put(data: T): Promise<string | number>;
507
+ add(data: T | T[]): Promise<string | number | (string | number)[]>;
508
+ batch(operations: Array<{
509
+ type: "add" | "put";
510
+ data: T | T[];
511
+ } | {
512
+ type: "delete";
513
+ data: string | number | (string | number)[];
514
+ }>): Promise<void>;
515
+ getById(id: string | number): Promise<T | undefined>;
516
+ delete(id: string | number | (string | number)[]): Promise<void>;
517
+ clear(): Promise<void>;
518
+ count(): Promise<number>;
519
+ getByIndex(indexName: string, key: any): Promise<T | undefined>;
520
+ getByKeyRange(indexName: string, keyRange?: StoreKeyRange): Promise<T[]>;
521
+ getAll(): Promise<T[]>;
522
+ cursor(callback: CursorCallback<T>, direction?: "forward" | "backward", keyRange?: StoreKeyRange): Promise<T | null>;
523
+ }
524
+
525
+ declare const createIndexedDbStore: <T extends Record<string, any>>(config: DatabaseConfig) => Store<T>;
526
+
527
+ export { type Collection, type CollectionEvent, type CollectionEventType, type CollectionMigrationOptions, ConnectionManager, type CursorCallback, type CursorCallbackResult, type CursorPaginationOptions, DEFAULT_KEYPATH, type Database, type DatabaseConfig, type DatabaseEvent, type DatabaseEventType, type Document, type DocumentEvent, type DocumentEventType, IndexedDBStore, type Store, type StoreKeyRange, type TelemetryEvent, type TelemetryEventType, createEphemeralStore, createIndexedDbStore };