idb-ts 3.10.0 β†’ 3.12.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/README.md CHANGED
@@ -24,17 +24,22 @@
24
24
  </a>
25
25
  </p>
26
26
 
27
-
28
27
  ## πŸ“Œ Introduction
28
+
29
29
  **idb-ts** is a lightweight, declarative, and type-safe way to work with IndexedDB using TypeScript. Effortlessly perform CRUD operations on your database with clean, structured code! πŸ”₯
30
30
 
31
31
  ## πŸ“¦ Installation
32
+
32
33
  Install via npm and start using IndexedDB like a pro! ⚑
34
+
33
35
  ```sh
34
- npm i idb-ts
36
+ npm i idb-ts # for pure npm users
37
+ pnpm add idb-ts # for pnpm users
38
+ yarn add idb-ts # for yarn users
35
39
  ```
36
40
 
37
41
  ## ✨ Features
42
+
38
43
  - βœ… **Declarative & Type-Safe** - Define your data models with decorators.
39
44
  - ⚑ **Easy CRUD Operations** - Perform create, read, update, and delete seamlessly.
40
45
  - πŸš€ **Fully Typed API** - Benefit from TypeScript’s powerful type system.
@@ -47,10 +52,11 @@ npm i idb-ts
47
52
  ## πŸ“– Example Usage
48
53
 
49
54
  ### πŸ—οΈ Declaring Entities
55
+
50
56
  Use decorators to define your data models. Each class must have exactly one `@KeyPath()` and be decorated with `@DataClass()`.
51
57
 
52
58
  ```typescript
53
- import { Database, DataClass, KeyPath, Index } from "idb-ts";
59
+ import { Database, DataClass, KeyPath, Index } from 'idb-ts';
54
60
 
55
61
  @DataClass()
56
62
  class User {
@@ -90,45 +96,47 @@ class Location {
90
96
  ```
91
97
 
92
98
  ### πŸ”„ CRUD Operations
99
+
93
100
  Perform database operations using the repository API:
94
101
 
95
102
  ```typescript
96
- const db = await Database.build("idb-crud", [User, Location]);
103
+ const db = await Database.build('idb-crud', [User, Location]);
97
104
 
98
- const alice = new User("u1", "Alice", 25);
99
- const bob = new User("u2", "Bob", 30);
100
- const nyc = new Location("1", "New York", "USA");
101
- const sf = new Location("2", "San Francisco", "USA");
105
+ const alice = new User('u1', 'Alice', 25);
106
+ const bob = new User('u2', 'Bob', 30);
107
+ const nyc = new Location('1', 'New York', 'USA');
108
+ const sf = new Location('2', 'San Francisco', 'USA');
102
109
 
103
110
  await db.User.create(alice);
104
111
  await db.User.create(bob);
105
112
  await db.Location.create(nyc);
106
113
  await db.Location.create(sf);
107
114
 
108
- const readAlice = await db.User.read("u1");
109
- console.log("πŸ‘€ Read user:", readAlice);
115
+ const readAlice = await db.User.read('u1');
116
+ console.log('πŸ‘€ Read user:', readAlice);
110
117
 
111
118
  alice.age = 26;
112
119
  await db.User.update(alice);
113
120
 
114
121
  const users = await db.User.list();
115
- console.log("πŸ“‹ All users:", users);
122
+ console.log('πŸ“‹ All users:', users);
116
123
 
117
124
  // Pagination
118
125
  const page1 = await db.User.listPaginated(1, 2); // page 1, 2 users per page
119
- console.log("πŸ“„ Page 1:", page1);
126
+ console.log('πŸ“„ Page 1:', page1);
120
127
 
121
- await db.User.delete("u1");
122
- console.log("❌ User Alice deleted.");
128
+ await db.User.delete('u1');
129
+ console.log('❌ User Alice deleted.');
123
130
 
124
131
  const remainingUsers = await db.User.list();
125
- console.log("πŸ” Remaining users:", remainingUsers);
132
+ console.log('πŸ” Remaining users:', remainingUsers);
126
133
 
127
134
  const locations = await db.Location.list();
128
- console.log("🌍 All locations:", locations);
135
+ console.log('🌍 All locations:', locations);
129
136
  ```
130
137
 
131
138
  ### πŸ” Indexing Support
139
+
132
140
  Create indexes on fields for fast querying. Query indexes using the repository API:
133
141
 
134
142
  ```typescript
@@ -146,7 +154,13 @@ class Product {
146
154
  name!: string;
147
155
  description!: string;
148
156
 
149
- constructor(id: string, category: string, price: number, name: string, description: string) {
157
+ constructor(
158
+ id: string,
159
+ category: string,
160
+ price: number,
161
+ name: string,
162
+ description: string,
163
+ ) {
150
164
  this.id = id;
151
165
  this.category = category;
152
166
  this.price = price;
@@ -155,18 +169,114 @@ class Product {
155
169
  }
156
170
  }
157
171
 
158
- const db = await Database.build("products-db", [Product]);
172
+ const db = await Database.build('products-db', [Product]);
159
173
 
160
174
  const electronics = await db.Product.findByIndex('category', 'Electronics');
161
175
  const expensiveItems = await db.Product.findByIndex('price', 999.99);
162
- const firstElectronic = await db.Product.findOneByIndex('category', 'Electronics');
176
+ const firstElectronic = await db.Product.findOneByIndex(
177
+ 'category',
178
+ 'Electronics',
179
+ );
163
180
  ```
164
181
 
165
182
  #### Index Methods:
183
+
166
184
  - `findByIndex(indexName, value): Promise<T[]>` - Find all records matching the index value
167
185
  - `findOneByIndex(indexName, value): Promise<T | undefined>` - Find the first record matching the index value
168
186
 
187
+ ### Creation & Update Timestamps
188
+
189
+ Each entity managed by `idb-ts` automatically gets two internal timestamp fields:
190
+
191
+ - `__idb_createdAt`: numeric epoch milliseconds set when the record is first created.
192
+ - `__idb_updatedAt`: numeric epoch milliseconds updated on each successful update.
193
+
194
+ These fields are applied automatically during `create` and `update` operations and can be used for auditing, sorting, or retention policies. They are stored as numbers (milliseconds since Unix epoch).
195
+
196
+ Example usage (reading timestamps):
197
+
198
+ ```ts
199
+ const item = await db.MyEntity.read('key');
200
+ console.log(item.__idb_createdAt, item.__idb_updatedAt);
201
+ ```
202
+
203
+ ### Retention Policy & Cleanup Job
204
+
205
+ `idb-ts` supports per-entity data retention via the `@RetentionPolicy()` class decorator. It accepts the following options:
206
+
207
+ - `seconds` (required): number of seconds after which records are considered expired.
208
+ - `enabled` (optional, default `true`): whether cleanup is active for this entity.
209
+ - `field` (optional, default `__idb_createdAt`): the numeric field to use for age calculation (usually creation timestamp).
210
+
211
+ When one or more entities register retention policies, the library computes a single cleanup interval equal to the greatest common divisor (GCD) of all configured `seconds` values and runs a background cleanup job at that interval. On each tick the job scans the configured entity stores and deletes records whose `field` value is older than the configured retention window.
212
+
213
+ Example:
214
+
215
+ ```ts
216
+ @RetentionPolicy({ seconds: 60 * 60 * 24 * 30 }) // 30 days
217
+ @DataClass()
218
+ class Session {
219
+ /* ... */
220
+ }
221
+
222
+ // Database will run a periodic cleanup that removes sessions older than 30 days
223
+ ```
224
+
225
+ Notes:
226
+
227
+ - Cleanup runs with readwrite transactions and deletes records one-by-one via cursors. It runs at startup and then periodically. Logs are emitted for inspection when debug logging is enabled.
228
+ - To temporarily disable cleanup for an entity, set `enabled: false` on the decorator.
229
+
230
+ ### Field Validation
231
+
232
+ You can declare validation rules for individual properties using the `@Validate(predicate, message)` property decorator. Each rule must provide a predicate function that receives the property value and the full item and returns `true` when valid.
233
+
234
+ Validation is enforced on `create` and `update` operations. If any rule fails, the repository operation throws an error with a concise message describing the failing fields.
235
+
236
+ Example:
237
+
238
+ ```ts
239
+ @DataClass()
240
+ class User {
241
+ @KeyPath()
242
+ id!: string;
243
+
244
+ @Validate(
245
+ (v) => typeof v === 'string' && v.includes('@'),
246
+ 'must be a valid email',
247
+ )
248
+ email!: string;
249
+
250
+ @Validate((v) => typeof v === 'number' && v >= 0, 'age must be >= 0')
251
+ age!: number;
252
+ }
253
+
254
+ await db.User.create(new User('u1', 'alice@example.com', 30));
255
+ ```
256
+
257
+ The thrown error contains all failing rules in the format `field: message` joined by `; `.
258
+
259
+ ### Bulk Operations
260
+
261
+ Repositories include convenience bulk helpers for common batch operations:
262
+
263
+ - `createMany(items: T[])`: creates multiple items (runs validators and generators for each item).
264
+ - `updateMany(items: T[])`: updates multiple items.
265
+ - `deleteMany(keys: Array<string | string[] | number>)`: deletes multiple keys.
266
+
267
+ These helpers are implemented by iterating the corresponding single-item operations. They are convenient for simple bulk workloads but are not currently implemented as a single atomic transaction across all items. For high-throughput or atomic requirements, consider batching items into a single transaction or performing multiple operations inside a custom `performOperation` call.
268
+
269
+ Example:
270
+
271
+ ```ts
272
+ await db.User.createMany([alice, bob, charlie]);
273
+ await db.User.deleteMany(['u1', 'u2']);
274
+ ```
275
+
276
+ Performance note: `createMany` will trigger validation and key generation per item. If you need large batch inserts frequently, batching these into a single transaction or adding a dedicated bulk API may improve throughput.
277
+
169
278
  #### Error Handling
279
+
170
280
  - If you query a non-existent index, an error is thrown:
171
281
  ```typescript
172
282
  await db.Product.findByIndex('nonexistent', 'value'); // throws
@@ -179,6 +289,7 @@ const firstElectronic = await db.Product.findOneByIndex('category', 'Electronics
179
289
  idb-ts provides flexible key management options including auto-increment keys, key generators, and composite keys for complex data relationships.
180
290
 
181
291
  ### Auto-Increment Keys
292
+
182
293
  Perfect for entities where you want the database to automatically generate sequential IDs:
183
294
 
184
295
  ```typescript
@@ -196,19 +307,21 @@ class Task {
196
307
  }
197
308
  }
198
309
 
199
- const db = await Database.build("tasks-db", [Task]);
310
+ const db = await Database.build('tasks-db', [Task]);
200
311
 
201
312
  // IDs are automatically generated: 1, 2, 3, etc.
202
- const task1 = await db.Task.create(new Task("Learn TypeScript"));
203
- const task2 = await db.Task.create(new Task("Build amazing apps"));
313
+ const task1 = await db.Task.create(new Task('Learn TypeScript'));
314
+ const task2 = await db.Task.create(new Task('Build amazing apps'));
204
315
  console.log(task1.id); // 1
205
316
  console.log(task2.id); // 2
206
317
  ```
207
318
 
208
319
  ### Key Generators
320
+
209
321
  Generate keys automatically using built-in generators:
210
322
 
211
323
  #### UUID Keys
324
+
212
325
  ```typescript
213
326
  @DataClass()
214
327
  class Document {
@@ -228,13 +341,16 @@ class Document {
228
341
  }
229
342
  }
230
343
 
231
- const db = await Database.build("docs-db", [Document]);
344
+ const db = await Database.build('docs-db', [Document]);
232
345
 
233
- const doc = await db.Document.create(new Document("tutorial", "Getting Started", "Welcome..."));
346
+ const doc = await db.Document.create(
347
+ new Document('tutorial', 'Getting Started', 'Welcome...'),
348
+ );
234
349
  console.log(doc.uuid); // e.g., "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
235
350
  ```
236
351
 
237
352
  #### Timestamp Keys
353
+
238
354
  ```typescript
239
355
  @DataClass()
240
356
  class Event {
@@ -252,11 +368,12 @@ class Event {
252
368
  }
253
369
  }
254
370
 
255
- const event = await db.Event.create(new Event("user_login", { userId: "123" }));
371
+ const event = await db.Event.create(new Event('user_login', { userId: '123' }));
256
372
  console.log(event.timestamp); // e.g., 1696118400000
257
373
  ```
258
374
 
259
375
  #### Random Keys
376
+
260
377
  ```typescript
261
378
  @DataClass()
262
379
  class Session {
@@ -272,17 +389,21 @@ class Session {
272
389
  }
273
390
  }
274
391
 
275
- const session = await db.Session.create(new Session("user123", new Date()));
392
+ const session = await db.Session.create(new Session('user123', new Date()));
276
393
  console.log(session.sessionId); // e.g., "xyz789abc123"
277
394
  ```
278
395
 
279
396
  ### Custom Key Generators
397
+
280
398
  Create your own key generation logic:
281
399
 
282
400
  ```typescript
283
401
  @DataClass()
284
402
  class Invoice {
285
- @KeyPath({ generator: (entity: any) => `INV-${entity.year}-${String(entity.number).padStart(4, '0')}` })
403
+ @KeyPath({
404
+ generator: (entity: any) =>
405
+ `INV-${entity.year}-${String(entity.number).padStart(4, '0')}`,
406
+ })
286
407
  invoiceId!: string;
287
408
 
288
409
  year!: number;
@@ -296,15 +417,16 @@ class Invoice {
296
417
  }
297
418
  }
298
419
 
299
- const invoice = await db.Invoice.create(new Invoice(2024, 1, 1500.00));
420
+ const invoice = await db.Invoice.create(new Invoice(2024, 1, 1500.0));
300
421
  console.log(invoice.invoiceId); // "INV-2024-0001"
301
422
  ```
302
423
 
303
424
  ### Composite Keys
425
+
304
426
  Handle many-to-many relationships with composite keys using the `@CompositeKeyPath` decorator:
305
427
 
306
428
  ```typescript
307
- import { CompositeKeyPath } from "idb-ts";
429
+ import { CompositeKeyPath } from 'idb-ts';
308
430
 
309
431
  @CompositeKeyPath(['userId', 'projectId'])
310
432
  @DataClass()
@@ -325,12 +447,14 @@ class UserProject {
325
447
  }
326
448
  }
327
449
 
328
- const db = await Database.build("collaboration-db", [UserProject]);
450
+ const db = await Database.build('collaboration-db', [UserProject]);
329
451
 
330
452
  // Create relationships
331
- await db.UserProject.create(new UserProject("user123", "project456", "developer"));
332
- await db.UserProject.create(new UserProject("user123", "project789", "admin"));
333
- await db.UserProject.create(new UserProject("user456", "project456", "viewer"));
453
+ await db.UserProject.create(
454
+ new UserProject('user123', 'project456', 'developer'),
455
+ );
456
+ await db.UserProject.create(new UserProject('user123', 'project789', 'admin'));
457
+ await db.UserProject.create(new UserProject('user456', 'project456', 'viewer'));
334
458
 
335
459
  // Read with composite key
336
460
  const relationship = await db.UserProject.read(['user123', 'project456']);
@@ -338,7 +462,7 @@ console.log(relationship?.role); // "developer"
338
462
 
339
463
  // Update relationship
340
464
  if (relationship) {
341
- relationship.role = "maintainer";
465
+ relationship.role = 'maintainer';
342
466
  await db.UserProject.update(relationship);
343
467
  }
344
468
 
@@ -350,16 +474,113 @@ const developers = await db.UserProject.findByIndex('role', 'developer');
350
474
  ```
351
475
 
352
476
  ### Key Generation Utilities
477
+
353
478
  Access key generators directly for your custom logic:
354
479
 
355
480
  ```typescript
356
- import { KeyGenerators } from "idb-ts";
481
+ import { KeyGenerators } from 'idb-ts';
357
482
 
358
- const uuid = KeyGenerators.uuid(); // Generate UUID
483
+ const uuid = KeyGenerators.uuid(); // Generate UUID
359
484
  const timestamp = KeyGenerators.timestamp(); // Current timestamp
360
- const random = KeyGenerators.random(); // Random string
485
+ const random = KeyGenerators.random(); // Random string
486
+ ```
487
+
488
+ ### Transaction API
489
+
490
+ idb-ts provides a simple, atomic Transaction API that lets you group multiple repository operations into a single native IndexedDB transaction. Operations performed inside a transaction are committed together or discarded together on failure.
491
+
492
+ Usage β€” callback form (automatic commit/rollback):
493
+
494
+ ```ts
495
+ await db.transaction(async (tx) => {
496
+ await tx.User.create(user);
497
+ await tx.Order.create(order);
498
+ await tx.OrderItem.create(item);
499
+ // If the callback returns successfully, the transaction is committed.
500
+ // If an exception is thrown, the transaction is aborted and all writes are rolled back.
501
+ });
502
+ ```
503
+
504
+ Usage β€” explicit control:
505
+
506
+ ```ts
507
+ const tx = await db.beginTransaction(['User', 'Order'], 'readwrite');
508
+ try {
509
+ await tx.User.create(user);
510
+ await tx.Order.create(order);
511
+ await tx.commit(); // explicitly commit, but optional
512
+ } catch (e) {
513
+ await tx.rollback(); // abort and rollback
514
+ }
361
515
  ```
362
516
 
517
+ Key notes and behavior:
518
+
519
+ - Atomicity: all repository writes that use the transaction handle (`tx.Entity.*`) share the same native `IDBTransaction` and are atomic β€” either all succeed (commit) or none persist (abort/rollback).
520
+ - Scope: `beginTransaction` accepts an array of entity names (e.g., `['User','Order']`) and opens a native transaction over those object stores. The callback form uses a transaction covering all registered entities.
521
+ - Querying inside transactions: use `tx.Entity.query()` or other `tx.Entity.*` repository methods to ensure those reads/writes are performed on the same underlying transaction.
522
+ - Modes: transactions support standard IndexedDB modes (`'readonly'` or `'readwrite'`). The default for `beginTransaction` and the callback wrapper is `'readwrite'`.
523
+ - Commit/abort semantics: modern browsers may perform implicit commit when the transaction's event loop completes; `commit()` is called when available. `rollback()` triggers `transaction.abort()`.
524
+ - Error handling: if an error is thrown in the callback form the library will call `rollback()` and rethrow the error to the caller.
525
+ - Limitations: composite operations that span many stores still must list all involved entity names when using `beginTransaction`. Long-running synchronous work inside a transaction can increase the risk of versionchange or blocked events β€” keep transaction work asynchronous and short.
526
+
527
+ Example patterns:
528
+
529
+ - Batch inserts in one transaction for performance and atomic safety.
530
+ - Run a read-modify-write sequence in a single transaction to avoid lost updates.
531
+
532
+ The Transaction API is covered by the test suite in `__tests__/transaction.test.ts` which demonstrates both the callback and explicit modes.
533
+
534
+ ### Advanced Querying
535
+
536
+ `idb-ts` includes a typed query builder for field-level filtering, logical grouping, and basic aggregations. The available operators are constrained by the field type, so string-only and array-only operations are only exposed where they make sense.
537
+
538
+ ```ts
539
+ const users = await db.User.query()
540
+ .where('name').startsWith('John')
541
+ .and('email').endsWith('@gmail.com')
542
+ .and('description').contains('important')
543
+ .execute();
544
+
545
+ const activeOrTrial = await db.User.query()
546
+ .where('age').gte(18)
547
+ .or()
548
+ .where('hasParentalConsent').equals(true)
549
+ .execute();
550
+
551
+ const premiumUsers = await db.User.query()
552
+ .where((qb) =>
553
+ qb.where('type').equals('premium').and('status').equals('active'),
554
+ )
555
+ .or()
556
+ .where('isTrial').equals(true)
557
+ .execute();
558
+ ```
559
+
560
+ Supported operators include:
561
+
562
+ - String operations: `startsWith`, `endsWith`, `contains`, `matches`
563
+ - Range operations: `between`, `notBetween`
564
+ - Collection operations: `contains`, `containsAny`, `containsAll`, `in`, `notIn`
565
+ - Logical chaining: `and()`, `or()`, and grouped predicates via `where((qb) => ...)`
566
+
567
+ Aggregations are available directly on the query builder:
568
+
569
+ ```ts
570
+ await db.Order.query().sum('amount');
571
+ await db.Order.query().avg('price');
572
+ await db.Order.query().min('date');
573
+ await db.Order.query().max('date');
574
+ await db.Order.query().groupBy('status').count();
575
+ ```
576
+
577
+ Notes:
578
+
579
+ - `sum()` and `avg()` are numeric-only.
580
+ - `min()` and `max()` are available for comparable scalar fields.
581
+ - `groupBy(...).count()` returns grouped counts by the selected field.
582
+ - TypeScript will reject unsupported operator/field combinations at compile time.
583
+
363
584
  ---
364
585
 
365
586
  ## πŸ”„ Schema Versioning
@@ -393,7 +614,7 @@ class Comment {
393
614
  }
394
615
 
395
616
  // Database version will be 3 (highest entity version)
396
- const db = await Database.build("blog", [User, Post, Comment]);
617
+ const db = await Database.build('blog', [User, Post, Comment]);
397
618
 
398
619
  console.log(db.getDatabaseVersion()); // 3
399
620
  console.log(db.getEntityVersions()); // Map with entity versions
@@ -415,20 +636,18 @@ const entityVersions = db.getEntityVersions();
415
636
  const userVersion = db.getEntityVersion('User');
416
637
 
417
638
  // Version upgrade flow:
418
- // v1.0: User(v1) β†’ Database v1
419
- // v1.1: User(v1), Post(v2) β†’ Database v2
420
- // v1.2: User(v1), Post(v2), Comment(v3) β†’ Database v3
639
+ // v1.0: User(v1) -> Database v1
640
+ // v1.1: User(v1), Post(v2) -> Database v2
641
+ // v1.2: User(v1), Post(v2), Comment(v3) -> Database v3
421
642
  ```
422
643
 
423
- πŸ“– **[Complete Schema Versioning Guide](./SCHEMA_VERSIONING.md)** - Detailed documentation with examples and best practices.
424
-
425
644
  ---
426
645
 
427
646
  ## πŸ”— Useful Links
647
+
428
648
  - πŸ“‚ **GitHub**: [maifeeulasad/idb-ts](https://github.com/maifeeulasad/idb-ts)
429
649
  - πŸ“¦ **NPM**: [idb-ts](https://www.npmjs.com/package/idb-ts)
430
650
  - Demo: https://maifeeulasad.github.io/idb-ts/
431
651
  - Code Coverage report: https://maifeeulasad.github.io/idb-ts/coverage/lcov-report/
432
652
 
433
653
  πŸŽ‰ **Enjoy seamless IndexedDB integration with TypeScript! Happy coding!** πŸš€
434
-