idb-ts 3.11.1 β†’ 3.13.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
@@ -1,4 +1,4 @@
1
- # πŸš€ idb-ts
1
+ # idb-ts
2
2
 
3
3
  <p align="center">
4
4
  <a href="https://www.npmjs.com/package/idb-ts">
@@ -20,380 +20,400 @@
20
20
  <img src="https://img.shields.io/github/watchers/maifeeulasad/idb-ts" alt="GitHub watchers">
21
21
  </a>
22
22
  <a href="https://img.shields.io/github/commits-since/maifeeulasad/idb-ts/latest/main?include_prereleases">
23
- <img src="https://img.shields.io/github/commits-since/maifeeulasad/idb-ts/latest/main?include_prereleases" alt="Commits after release">
23
+ <img src="https://img.shields.io/github/commits-since/maifeeulasad/idb-ts/latest/main?include_prereleases" alt="Commits since release">
24
24
  </a>
25
25
  </p>
26
26
 
27
+ ---
28
+
29
+ ## Introduction
30
+
31
+ **idb-ts** is a declarative, type-safe ORM layer for [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API). Define your data models with TypeScript decorators, and the library handles schema creation, key generation, validation, querying, transactions, and data retention automatically - with no external runtime dependencies.
27
32
 
28
- ## πŸ“Œ Introduction
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! πŸ”₯
33
+ ---
34
+
35
+ ## Installation
30
36
 
31
- ## πŸ“¦ Installation
32
- Install via npm and start using IndexedDB like a pro! ⚑
33
37
  ```sh
34
- npm i idb-ts # for pure npm users
35
- pnpm add idb-ts # for pnpm users
36
- yarn add idb-ts # for yarn users
38
+ npm install idb-ts
39
+ pnpm add idb-ts
40
+ yarn add idb-ts
37
41
  ```
38
42
 
39
- ## ✨ Features
40
- - βœ… **Declarative & Type-Safe** - Define your data models with decorators.
41
- - ⚑ **Easy CRUD Operations** - Perform create, read, update, and delete seamlessly.
42
- - πŸš€ **Fully Typed API** - Benefit from TypeScript’s powerful type system.
43
- - 🏎️ **Performance Optimized** - Minimal overhead with IndexedDB's native capabilities.
44
- - πŸ”„ **Schema Versioning** - Manage database schema evolution with automatic migration support.
45
- - πŸ”‘ **Advanced Key Management** - Auto-increment, UUID, timestamp, custom generators, and composite keys.
43
+ > **Requirement:** `reflect-metadata` must be imported once at your application entry point, and `experimentalDecorators` and `emitDecoratorMetadata` must be enabled in your `tsconfig.json`.
44
+
45
+ ```json
46
+ {
47
+ "compilerOptions": {
48
+ "experimentalDecorators": true,
49
+ "emitDecoratorMetadata": true
50
+ }
51
+ }
52
+ ```
46
53
 
47
54
  ---
48
55
 
49
- ## πŸ“– Example Usage
56
+ ## Feature Overview
57
+
58
+ | Feature | Description |
59
+ |---|---|
60
+ | **Declarative entity definition** | Define stores, keys, and indexes with class decorators |
61
+ | **Full CRUD API** | Create, read, update, delete, list, paginate, and count |
62
+ | **Typed query builder** | Chainable, type-checked filter, sort, and aggregation DSL |
63
+ | **Key generation** | Auto-increment, UUID v4, timestamp, random, or custom function |
64
+ | **Composite keys** | Multi-field primary keys for relational associations |
65
+ | **Field validation** | Per-property predicate rules enforced on write |
66
+ | **Schema versioning** | Automatic `onupgradeneeded` migration based on entity versions |
67
+ | **Transaction API** | Callback-based and explicit commit/rollback patterns |
68
+ | **Data retention** | Periodic background cleanup of expired records |
69
+ | **Automatic timestamps** | `__idb_createdAt` / `__idb_updatedAt` injected on every write |
50
70
 
51
- ### πŸ—οΈ Declaring Entities
52
- Use decorators to define your data models. Each class must have exactly one `@KeyPath()` and be decorated with `@DataClass()`.
71
+ ---
72
+
73
+ ## Quick Start
53
74
 
54
75
  ```typescript
55
- import { Database, DataClass, KeyPath, Index } from "idb-ts";
76
+ import 'reflect-metadata';
77
+ import { Database, DataClass, KeyPath, Index } from 'idb-ts';
56
78
 
57
79
  @DataClass()
58
80
  class User {
59
- @KeyPath()
81
+ @KeyPath({ generator: 'uuid' })
60
82
  id!: string;
61
83
 
62
- @Index()
84
+ @Index({ unique: true })
63
85
  email!: string;
64
86
 
65
87
  name!: string;
66
88
  age!: number;
67
-
68
- constructor(id: string, name: string, age: number, email?: string) {
69
- this.id = id;
70
- this.name = name;
71
- this.age = age;
72
- this.email = email || `${name.toLowerCase()}@example.com`;
73
- }
74
89
  }
75
90
 
76
- @DataClass()
77
- class Location {
78
- @KeyPath()
79
- id!: string;
91
+ const db = await Database.build<{ User: EntityRepository<User> }>('mydb', [User]);
80
92
 
81
- @Index()
82
- city!: string;
93
+ await db.User.create({ id: '', name: 'Alice', age: 30, email: 'alice@example.com' });
94
+ const alice = await db.User.findOneByIndex('email', 'alice@example.com');
95
+ ```
83
96
 
84
- country!: string;
97
+ ---
85
98
 
86
- constructor(id: string, city: string, country: string) {
87
- this.id = id;
88
- this.city = city;
89
- this.country = country;
90
- }
91
- }
92
- ```
99
+ ## Defining Entities
93
100
 
94
- ### πŸ”„ CRUD Operations
95
- Perform database operations using the repository API:
101
+ Every entity class must declare exactly one primary key field and be annotated with `@DataClass()`. Apply decorators in the order shown - TypeScript executes decorators bottom-up, so `@DataClass` must appear last (i.e., closest to the `class` keyword).
96
102
 
97
103
  ```typescript
98
- const db = await Database.build("idb-crud", [User, Location]);
104
+ import { Database, DataClass, KeyPath, Index, Validate } from 'idb-ts';
99
105
 
100
- const alice = new User("u1", "Alice", 25);
101
- const bob = new User("u2", "Bob", 30);
102
- const nyc = new Location("1", "New York", "USA");
103
- const sf = new Location("2", "San Francisco", "USA");
106
+ @DataClass({ version: 1 })
107
+ class User {
108
+ @KeyPath({ generator: 'uuid' })
109
+ id!: string;
104
110
 
105
- await db.User.create(alice);
106
- await db.User.create(bob);
107
- await db.Location.create(nyc);
108
- await db.Location.create(sf);
111
+ @Index({ unique: true })
112
+ @Validate((v) => typeof v === 'string' && v.includes('@'), 'must be a valid email')
113
+ email!: string;
109
114
 
110
- const readAlice = await db.User.read("u1");
111
- console.log("πŸ‘€ Read user:", readAlice);
115
+ @Validate((v) => typeof v === 'number' && v >= 0, 'age must be non-negative')
116
+ age!: number;
112
117
 
113
- alice.age = 26;
114
- await db.User.update(alice);
118
+ name!: string;
119
+ }
120
+ ```
115
121
 
116
- const users = await db.User.list();
117
- console.log("πŸ“‹ All users:", users);
122
+ ### Decorator reference
118
123
 
119
- // Pagination
120
- const page1 = await db.User.listPaginated(1, 2); // page 1, 2 users per page
121
- console.log("πŸ“„ Page 1:", page1);
124
+ #### `@DataClass(options?)`
122
125
 
123
- await db.User.delete("u1");
124
- console.log("❌ User Alice deleted.");
126
+ Marks a class as a managed entity. Must be applied exactly once per class, after all other idb-ts decorators.
125
127
 
126
- const remainingUsers = await db.User.list();
127
- console.log("πŸ” Remaining users:", remainingUsers);
128
+ | Option | Type | Default | Description |
129
+ |---|---|---|---|
130
+ | `version` | `number` | `1` | Schema version. Increment when the entity's store or indexes change. |
128
131
 
129
- const locations = await db.Location.list();
130
- console.log("🌍 All locations:", locations);
131
- ```
132
+ #### `@KeyPath(options?)`
132
133
 
133
- ### πŸ” Indexing Support
134
- Create indexes on fields for fast querying. Query indexes using the repository API:
134
+ Designates the decorated property as the primary key of the object store. Exactly one property per class may carry this decorator. For multi-field keys, use `@CompositeKeyPath` at the class level instead.
135
135
 
136
- ```typescript
137
- @DataClass()
138
- class Product {
139
- @KeyPath()
140
- id!: string;
136
+ | Option | Type | Default | Description |
137
+ |---|---|---|---|
138
+ | `autoIncrement` | `boolean` | `false` | Delegate key assignment to IndexedDB's auto-increment mechanism. |
139
+ | `generator` | `'uuid'` \| `'timestamp'` \| `'random'` \| `(item) => string \| number` | - | Automatic key generator invoked when the key field is absent or empty on `create`. |
141
140
 
142
- @Index()
143
- category!: string;
141
+ #### `@CompositeKeyPath(fields, options?)`
144
142
 
145
- @Index()
146
- price!: number;
143
+ Class-level decorator for composite primary keys. Cannot be combined with `@KeyPath`.
147
144
 
148
- name!: string;
149
- description!: string;
150
-
151
- constructor(id: string, category: string, price: number, name: string, description: string) {
152
- this.id = id;
153
- this.category = category;
154
- this.price = price;
155
- this.name = name;
156
- this.description = description;
157
- }
145
+ ```typescript
146
+ @CompositeKeyPath(['userId', 'projectId'])
147
+ @DataClass()
148
+ class UserProject {
149
+ userId!: string;
150
+ projectId!: string;
151
+ role!: string;
158
152
  }
159
-
160
- const db = await Database.build("products-db", [Product]);
161
-
162
- const electronics = await db.Product.findByIndex('category', 'Electronics');
163
- const expensiveItems = await db.Product.findByIndex('price', 999.99);
164
- const firstElectronic = await db.Product.findOneByIndex('category', 'Electronics');
165
153
  ```
166
154
 
167
- #### Index Methods:
168
- - `findByIndex(indexName, value): Promise<T[]>` - Find all records matching the index value
169
- - `findOneByIndex(indexName, value): Promise<T | undefined>` - Find the first record matching the index value
155
+ #### `@Index(options?)`
170
156
 
171
- ### Creation & Update Timestamps
157
+ Creates an IDB index on the decorated field, enabling efficient lookups via `findByIndex` and `findOneByIndex`.
172
158
 
173
- Each entity managed by `idb-ts` automatically gets two internal timestamp fields:
159
+ | Option | Type | Description |
160
+ |---|---|---|
161
+ | `unique` | `boolean` | Enforce uniqueness on the indexed field. |
174
162
 
175
- - `__idb_createdAt`: numeric epoch milliseconds set when the record is first created.
176
- - `__idb_updatedAt`: numeric epoch milliseconds updated on each successful update.
163
+ #### `@Validate(predicate, message)`
177
164
 
178
- 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).
165
+ Attaches a validation rule to the decorated property. Rules are enforced on every `create` and `update` call. If any rule fails, the operation throws with a message listing all failing fields.
179
166
 
180
- Example usage (reading timestamps):
167
+ #### `@RetentionPolicy(options)`
181
168
 
182
- ```ts
183
- const item = await db.MyEntity.read('key');
184
- console.log(item.__idb_createdAt, item.__idb_updatedAt);
185
- ```
169
+ Class-level decorator that configures automatic expiry and deletion of records. See [Data Retention](#data-retention) for full details.
186
170
 
187
- ### Retention Policy & Cleanup Job
171
+ ---
188
172
 
189
- `idb-ts` supports per-entity data retention via the `@RetentionPolicy()` class decorator. It accepts the following options:
173
+ ## Database Initialisation
190
174
 
191
- - `seconds` (required): number of seconds after which records are considered expired.
192
- - `enabled` (optional, default `true`): whether cleanup is active for this entity.
193
- - `field` (optional, default `__idb_createdAt`): the numeric field to use for age calculation (usually creation timestamp).
175
+ ```typescript
176
+ const db = await Database.build<{
177
+ User: EntityRepository<User>;
178
+ Order: EntityRepository<Order>;
179
+ }>('shop', [User, Order]);
180
+ ```
194
181
 
195
- 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.
182
+ `Database.build` opens (or upgrades) the IDB database, creates object stores and indexes for any entity whose version exceeds the stored database version, starts background retention jobs if applicable, and attaches typed repository properties to the returned object.
196
183
 
197
- Example:
184
+ The effective database version is the highest `version` value declared across all registered entities.
198
185
 
199
- ```ts
200
- @RetentionPolicy({ seconds: 60 * 60 * 24 * 30 }) // 30 days
201
- @DataClass()
202
- class Session { /* ... */ }
186
+ ### Inspecting database metadata
203
187
 
204
- // Database will run a periodic cleanup that removes sessions older than 30 days
188
+ ```typescript
189
+ db.getDatabaseVersion(); // number - current IDB version
190
+ db.getEntityVersions(); // Map<string, number>
191
+ db.getEntityVersion('User'); // number | undefined
192
+ db.getAvailableEntities(); // string[]
205
193
  ```
206
194
 
207
- Notes:
195
+ ### Closing the connection
208
196
 
209
- - 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.
210
- - To temporarily disable cleanup for an entity, set `enabled: false` on the decorator.
197
+ ```typescript
198
+ db.close(); // Stops the retention cleanup timer and closes the IDB connection.
199
+ ```
211
200
 
212
- ### Field Validation
201
+ ---
213
202
 
214
- 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.
203
+ ## CRUD Operations
215
204
 
216
- 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.
205
+ Each entity is accessible as a named property on the database object. All methods return `Promise`.
217
206
 
218
- Example:
207
+ ```typescript
208
+ // Create
209
+ await db.User.create(user);
210
+ await db.User.createMany([alice, bob, charlie]);
219
211
 
220
- ```ts
221
- @DataClass()
222
- class User {
223
- @KeyPath()
224
- id!: string;
212
+ // Read
213
+ const user = await db.User.read('u1'); // by primary key
214
+ const page = await db.User.listPaginated(1, 20); // 1-based pagination
215
+ const all = await db.User.list();
225
216
 
226
- @Validate((v) => typeof v === 'string' && v.includes('@'), 'must be a valid email')
227
- email!: string;
217
+ // Update
218
+ await db.User.update(updatedUser);
219
+ await db.User.updateMany([user1, user2]);
228
220
 
229
- @Validate((v) => typeof v === 'number' && v >= 0, 'age must be >= 0')
230
- age!: number;
231
- }
221
+ // Delete
222
+ await db.User.delete('u1');
223
+ await db.User.deleteMany(['u1', 'u2']);
224
+ await db.User.deleteWhere((q) => q.where('age').lt(18));
232
225
 
233
- await db.User.create(new User('u1', 'alice@example.com', 30));
226
+ // Utilities
227
+ const count = await db.User.count();
228
+ const exists = await db.User.exists('u1');
229
+ await db.User.clear();
234
230
  ```
235
231
 
236
- The thrown error contains all failing rules in the format `field: message` joined by `; `.
232
+ ### Index lookups
237
233
 
238
- ### Bulk Operations
234
+ ```typescript
235
+ const allAdmins = await db.User.findByIndex('role', 'admin');
236
+ const firstAdmin = await db.User.findOneByIndex('role', 'admin');
237
+ ```
239
238
 
240
- Repositories include convenience bulk helpers for common batch operations:
239
+ Querying a non-existent index throws immediately.
241
240
 
242
- - `createMany(items: T[])`: creates multiple items (runs validators and generators for each item).
243
- - `updateMany(items: T[])`: updates multiple items.
244
- - `deleteMany(keys: Array<string | string[] | number>)`: deletes multiple keys.
241
+ ---
245
242
 
246
- 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.
243
+ ## Automatic Timestamps
247
244
 
248
- Example:
245
+ Every record written through a repository automatically receives two internal fields:
249
246
 
250
- ```ts
251
- await db.User.createMany([alice, bob, charlie]);
252
- await db.User.deleteMany(['u1', 'u2']);
253
- ```
247
+ | Field | Type | Set on |
248
+ |---|---|---|
249
+ | `__idb_createdAt` | `number` (ms since epoch) | `create` only |
250
+ | `__idb_updatedAt` | `number` (ms since epoch) | `create` and `update` |
254
251
 
255
- 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.
252
+ `__idb_createdAt` is preserved across updates; `__idb_updatedAt` is refreshed on every write.
256
253
 
257
- #### Error Handling
258
- - If you query a non-existent index, an error is thrown:
259
- ```typescript
260
- await db.Product.findByIndex('nonexistent', 'value'); // throws
261
- ```
254
+ ```typescript
255
+ const item = await db.Session.read(key);
256
+ console.log(item.__idb_createdAt, item.__idb_updatedAt);
257
+ ```
262
258
 
263
259
  ---
264
260
 
265
- ## πŸ”‘ Multi-Field & Composite Key Support
261
+ ## Query Builder
266
262
 
267
- idb-ts provides flexible key management options including auto-increment keys, key generators, and composite keys for complex data relationships.
263
+ `EntityRepository.query()` returns a typed `QueryBuilder<T>` for constructing complex filter expressions, sorting, pagination, and aggregations.
268
264
 
269
- ### Auto-Increment Keys
270
- Perfect for entities where you want the database to automatically generate sequential IDs:
265
+ ### Filtering
271
266
 
272
267
  ```typescript
273
- @DataClass()
274
- class Task {
275
- @KeyPath({ autoIncrement: true })
276
- id!: number;
268
+ const results = await db.User.query()
269
+ .where('age').gte(18)
270
+ .and('status').equals('active')
271
+ .execute();
272
+ ```
277
273
 
278
- title!: string;
279
- completed!: boolean;
274
+ #### Available operators
280
275
 
281
- constructor(title: string, completed = false) {
282
- this.title = title;
283
- this.completed = completed;
284
- }
285
- }
276
+ | Operator | Field types | Description |
277
+ |---|---|---|
278
+ | `equals` | any | Strict equality (`===`) |
279
+ | `gt` / `gte` / `lt` / `lte` | `ComparableValue` | Comparison |
280
+ | `between(start, end)` | `ComparableValue` | Inclusive range |
281
+ | `notBetween(start, end)` | `ComparableValue` | Outside range |
282
+ | `startsWith` / `endsWith` | `string` | Prefix / suffix match |
283
+ | `contains` | `string` \| array | Substring or element membership |
284
+ | `matches` | `string` | Regular expression test |
285
+ | `in(values)` / `notIn(values)` | any | Membership test |
286
+ | `containsAny(values)` | array | At least one element matches |
287
+ | `containsAll(values)` | array | All elements present |
288
+
289
+ TypeScript enforces operator/type compatibility at compile time - string-only operators are not exposed on numeric fields, and so on.
286
290
 
287
- const db = await Database.build("tasks-db", [Task]);
291
+ ### Logical grouping
288
292
 
289
- // IDs are automatically generated: 1, 2, 3, etc.
290
- const task1 = await db.Task.create(new Task("Learn TypeScript"));
291
- const task2 = await db.Task.create(new Task("Build amazing apps"));
292
- console.log(task1.id); // 1
293
- console.log(task2.id); // 2
293
+ ```typescript
294
+ // OR connector
295
+ const results = await db.User.query()
296
+ .where('age').gte(18)
297
+ .or()
298
+ .where('hasParentalConsent').equals(true)
299
+ .execute();
300
+
301
+ // Grouped sub-expression
302
+ const premiumOrTrial = await db.User.query()
303
+ .where((qb) =>
304
+ qb.where('type').equals('premium').and('status').equals('active'),
305
+ )
306
+ .or()
307
+ .where('isTrial').equals(true)
308
+ .execute();
294
309
  ```
295
310
 
296
- ### Key Generators
297
- Generate keys automatically using built-in generators:
311
+ ### Sorting and pagination
298
312
 
299
- #### UUID Keys
300
313
  ```typescript
301
- @DataClass()
302
- class Document {
303
- @KeyPath({ generator: 'uuid' })
304
- uuid!: string;
314
+ await db.User.query()
315
+ .where('status').equals('active')
316
+ .orderBy('createdAt', 'desc')
317
+ .offset(20)
318
+ .limit(10)
319
+ .execute();
320
+ ```
305
321
 
306
- @Index()
307
- category!: string;
322
+ ### Index and range acceleration
308
323
 
309
- title!: string;
310
- content!: string;
324
+ When a field is indexed, you can constrain the initial IDB candidate set at the storage layer before in-memory filtering begins:
311
325
 
312
- constructor(category: string, title: string, content: string) {
313
- this.category = category;
314
- this.title = title;
315
- this.content = content;
316
- }
317
- }
326
+ ```typescript
327
+ await db.Product.query()
328
+ .useIndex('price')
329
+ .range(10, 100)
330
+ .execute();
331
+ ```
318
332
 
319
- const db = await Database.build("docs-db", [Document]);
333
+ ### Aggregations
320
334
 
321
- const doc = await db.Document.create(new Document("tutorial", "Getting Started", "Welcome..."));
322
- console.log(doc.uuid); // e.g., "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
335
+ ```typescript
336
+ await db.Order.query().where('status').equals('paid').count();
337
+ await db.Order.query().sum('amount');
338
+ await db.Order.query().avg('price');
339
+ await db.Order.query().min('createdAt');
340
+ await db.Order.query().max('createdAt');
341
+
342
+ // Grouped count
343
+ const byStatus = await db.Order.query().groupBy('status').count();
344
+ // [{ status: 'paid', count: 42 }, { status: 'pending', count: 7 }]
323
345
  ```
324
346
 
325
- #### Timestamp Keys
326
- ```typescript
327
- @DataClass()
328
- class Event {
329
- @KeyPath({ generator: 'timestamp' })
330
- timestamp!: number;
347
+ `sum` and `avg` are restricted to numeric fields. `min` and `max` accept any comparable field. `groupBy(...).count()` returns results sorted by group key.
331
348
 
332
- @Index()
333
- type!: string;
349
+ ---
334
350
 
335
- data!: any;
351
+ ## Key Management
336
352
 
337
- constructor(type: string, data: any) {
338
- this.type = type;
339
- this.data = data;
340
- }
341
- }
353
+ ### Auto-increment
354
+
355
+ ```typescript
356
+ @DataClass()
357
+ class Task {
358
+ @KeyPath({ autoIncrement: true })
359
+ id!: number; // Assigned by IndexedDB: 1, 2, 3, …
342
360
 
343
- const event = await db.Event.create(new Event("user_login", { userId: "123" }));
344
- console.log(event.timestamp); // e.g., 1696118400000
361
+ title!: string;
362
+ }
345
363
  ```
346
364
 
347
- #### Random Keys
365
+ ### Built-in generators
366
+
348
367
  ```typescript
349
368
  @DataClass()
350
- class Session {
351
- @KeyPath({ generator: 'random' })
352
- sessionId!: string;
353
-
354
- userId!: string;
355
- expiresAt!: Date;
369
+ class Document {
370
+ @KeyPath({ generator: 'uuid' }) // RFC 4122 v4
371
+ id!: string;
372
+ }
356
373
 
357
- constructor(userId: string, expiresAt: Date) {
358
- this.userId = userId;
359
- this.expiresAt = expiresAt;
360
- }
374
+ @DataClass()
375
+ class Event {
376
+ @KeyPath({ generator: 'timestamp' }) // Date.now()
377
+ id!: number;
361
378
  }
362
379
 
363
- const session = await db.Session.create(new Session("user123", new Date()));
364
- console.log(session.sessionId); // e.g., "xyz789abc123"
380
+ @DataClass()
381
+ class Session {
382
+ @KeyPath({ generator: 'random' }) // Base-36 random string
383
+ id!: string;
384
+ }
365
385
  ```
366
386
 
367
- ### Custom Key Generators
368
- Create your own key generation logic:
387
+ ### Custom generator
369
388
 
370
389
  ```typescript
371
390
  @DataClass()
372
391
  class Invoice {
373
- @KeyPath({ generator: (entity: any) => `INV-${entity.year}-${String(entity.number).padStart(4, '0')}` })
392
+ @KeyPath({
393
+ generator: (entity) =>
394
+ `INV-${entity.year}-${String(entity.number).padStart(4, '0')}`,
395
+ })
374
396
  invoiceId!: string;
375
397
 
376
398
  year!: number;
377
399
  number!: number;
378
- amount!: number;
379
-
380
- constructor(year: number, number: number, amount: number) {
381
- this.year = year;
382
- this.number = number;
383
- this.amount = amount;
384
- }
385
400
  }
386
-
387
- const invoice = await db.Invoice.create(new Invoice(2024, 1, 1500.00));
388
- console.log(invoice.invoiceId); // "INV-2024-0001"
401
+ // invoiceId β†’ "INV-2024-0001"
389
402
  ```
390
403
 
391
- ### Composite Keys
392
- Handle many-to-many relationships with composite keys using the `@CompositeKeyPath` decorator:
404
+ ### Using generators directly
393
405
 
394
406
  ```typescript
395
- import { CompositeKeyPath } from "idb-ts";
407
+ import { KeyGenerators } from 'idb-ts';
408
+
409
+ KeyGenerators.uuid(); // "a1b2c3d4-..."
410
+ KeyGenerators.timestamp(); // 1696118400000
411
+ KeyGenerators.random(); // "xyz789abc"
412
+ ```
413
+
414
+ ### Composite keys
396
415
 
416
+ ```typescript
397
417
  @CompositeKeyPath(['userId', 'projectId'])
398
418
  @DataClass()
399
419
  class UserProject {
@@ -404,117 +424,144 @@ class UserProject {
404
424
  role!: string;
405
425
 
406
426
  joinedAt!: Date;
407
-
408
- constructor(userId: string, projectId: string, role: string) {
409
- this.userId = userId;
410
- this.projectId = projectId;
411
- this.role = role;
412
- this.joinedAt = new Date();
413
- }
414
427
  }
415
428
 
416
- const db = await Database.build("collaboration-db", [UserProject]);
429
+ // Create
430
+ await db.UserProject.create(new UserProject('u1', 'p1', 'developer'));
417
431
 
418
- // Create relationships
419
- await db.UserProject.create(new UserProject("user123", "project456", "developer"));
420
- await db.UserProject.create(new UserProject("user123", "project789", "admin"));
421
- await db.UserProject.create(new UserProject("user456", "project456", "viewer"));
432
+ // Read / update / delete with composite key tuple
433
+ const rel = await db.UserProject.read(['u1', 'p1']);
434
+ await db.UserProject.delete(['u1', 'p1']);
435
+ ```
422
436
 
423
- // Read with composite key
424
- const relationship = await db.UserProject.read(['user123', 'project456']);
425
- console.log(relationship?.role); // "developer"
437
+ ---
438
+
439
+ ## Field Validation
440
+
441
+ Validation rules are declared per-property with `@Validate`. All rules for an entity are evaluated before any write; a single thrown error enumerates every failing rule.
442
+
443
+ ```typescript
444
+ @DataClass()
445
+ class User {
446
+ @KeyPath()
447
+ id!: string;
426
448
 
427
- // Update relationship
428
- if (relationship) {
429
- relationship.role = "maintainer";
430
- await db.UserProject.update(relationship);
449
+ @Validate(
450
+ (v) => typeof v === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
451
+ 'must be a valid email address',
452
+ )
453
+ email!: string;
454
+
455
+ @Validate((v) => Number.isInteger(v) && v >= 0, 'must be a non-negative integer')
456
+ age!: number;
431
457
  }
458
+ ```
432
459
 
433
- // Delete with composite key
434
- await db.UserProject.delete(['user123', 'project789']);
460
+ Error format on failure:
435
461
 
436
- // Query by role index
437
- const developers = await db.UserProject.findByIndex('role', 'developer');
438
462
  ```
463
+ Validation failed for User: email: must be a valid email address; age: must be a non-negative integer
464
+ ```
465
+
466
+ ---
467
+
468
+ ## Transactions
469
+
470
+ ### Callback form (recommended)
439
471
 
440
- ### Key Generation Utilities
441
- Access key generators directly for your custom logic:
472
+ The callback receives a `TransactionalDatabase` handle. On successful return the transaction is committed automatically. Any thrown error triggers an automatic rollback before rethrowing.
442
473
 
443
474
  ```typescript
444
- import { KeyGenerators } from "idb-ts";
475
+ await db.transaction(async (tx) => {
476
+ await tx.User.create(user);
477
+ await tx.Order.create(order);
478
+ await tx.OrderItem.create(item);
479
+ });
480
+ ```
481
+
482
+ ### Explicit form
445
483
 
446
- const uuid = KeyGenerators.uuid(); // Generate UUID
447
- const timestamp = KeyGenerators.timestamp(); // Current timestamp
448
- const random = KeyGenerators.random(); // Random string
484
+ ```typescript
485
+ const tx = await db.beginTransaction(['User', 'Order'], 'readwrite');
486
+ try {
487
+ await tx.User.create(user);
488
+ await tx.Order.create(order);
489
+ await tx.commit();
490
+ } catch (error) {
491
+ await tx.rollback();
492
+ throw error;
493
+ }
449
494
  ```
450
495
 
451
- ---
496
+ ### Transaction semantics
452
497
 
453
- ## πŸ”„ Schema Versioning
498
+ All repository operations performed through the `tx` handle share the same native `IDBTransaction`, ensuring atomicity. `beginTransaction` accepts an array of entity names that determines the transaction scope; the callback form spans all registered entities. The default mode is `'readwrite'`; pass `'readonly'` for read-only workloads. Use `tx.Entity.query()` to run queries within the same transaction boundary.
454
499
 
455
- idb-ts supports schema versioning to manage database evolution over time. Version your entities and let the library handle automatic migration!
500
+ ---
456
501
 
457
- ### Basic Usage
502
+ ## Data Retention
503
+
504
+ `@RetentionPolicy` triggers a background cleanup job that deletes records whose age exceeds the configured threshold.
458
505
 
459
506
  ```typescript
460
- @DataClass({ version: 1 })
461
- class User {
462
- @KeyPath() id!: string;
463
- @Index() email!: string;
464
- name!: string;
465
- }
507
+ @RetentionPolicy({ seconds: 60 * 60 * 24 * 30 }) // 30-day retention
508
+ @DataClass()
509
+ class Session {
510
+ @KeyPath({ generator: 'uuid' })
511
+ id!: string;
466
512
 
467
- @DataClass({ version: 2 })
468
- class Post {
469
- @KeyPath() id!: string;
470
- @Index() authorId!: string;
471
- title!: string;
472
- content!: string;
513
+ userId!: string;
473
514
  }
515
+ ```
474
516
 
475
- @DataClass({ version: 3 })
476
- class Comment {
477
- @KeyPath() id!: string;
478
- @Index() postId!: string;
479
- @Index() authorId!: string;
480
- text!: string;
481
- }
517
+ | Option | Type | Default | Description |
518
+ |---|---|---|---|
519
+ | `seconds` | `number` | - | **(Required)** Retention window in seconds. Must be a positive integer. |
520
+ | `enabled` | `boolean` | `true` | Set to `false` to suspend cleanup without removing the policy. |
521
+ | `field` | `string` | `'__idb_createdAt'` | Numeric timestamp field used to compute record age. |
522
+
523
+ When multiple entities define retention policies, the cleanup interval is set to the GCD of all configured `seconds` values in milliseconds, so a single timer satisfies every policy efficiently. The job runs immediately on database open and then on each interval tick, using cursor-based `readwrite` transactions.
524
+
525
+ ---
526
+
527
+ ## Schema Versioning
482
528
 
483
- // Database version will be 3 (highest entity version)
484
- const db = await Database.build("blog", [User, Post, Comment]);
529
+ Increment an entity's `version` to trigger `onupgradeneeded` and update its object store on the user's next visit. The effective database version is the maximum across all registered entities, so adding a new high-version entity is sufficient to initiate a migration.
530
+
531
+ ```typescript
532
+ @DataClass({ version: 1 }) class User { /* ... */ }
533
+ @DataClass({ version: 2 }) class Post { /* ... */ }
534
+ @DataClass({ version: 3 }) class Comment { /* ... */ }
535
+
536
+ // Database opens at version 3.
537
+ // If a user was on version 1, only Post (v2) and Comment (v3) stores are
538
+ // created or updated during onupgradeneeded.
539
+ const db = await Database.build('blog', [User, Post, Comment]);
485
540
 
486
541
  console.log(db.getDatabaseVersion()); // 3
487
- console.log(db.getEntityVersions()); // Map with entity versions
488
542
  ```
489
543
 
490
- ### Key Features
544
+ ---
491
545
 
492
- - **Automatic Version Calculation**: Database version = highest entity version
493
- - **Seamless Migration**: Only new/updated entities are processed during upgrades
494
- - **Backward Compatibility**: Entities without version default to version 1
495
- - **Index Evolution**: New indexes are automatically created during migration
546
+ ## Bulk Operations
496
547
 
497
- ### Version Management
548
+ All repository bulk helpers iterate the corresponding single-item operation and therefore enforce validation and key generation per item. They are not issued as a single atomic transaction. For atomic batch writes, use the [Transaction API](#transactions).
498
549
 
499
550
  ```typescript
500
- // Check versions
501
- const dbVersion = db.getDatabaseVersion();
502
- const entityVersions = db.getEntityVersions();
503
- const userVersion = db.getEntityVersion('User');
504
-
505
- // Version upgrade flow:
506
- // v1.0: User(v1) -> Database v1
507
- // v1.1: User(v1), Post(v2) -> Database v2
508
- // v1.2: User(v1), Post(v2), Comment(v3) -> Database v3
551
+ await db.User.createMany([alice, bob, charlie]);
552
+ await db.User.updateMany([alice, bob]);
553
+ await db.User.deleteMany(['u1', 'u2', 'u3']);
509
554
  ```
510
555
 
511
556
  ---
512
557
 
513
- ## πŸ”— Useful Links
514
- - πŸ“‚ **GitHub**: [maifeeulasad/idb-ts](https://github.com/maifeeulasad/idb-ts)
515
- - πŸ“¦ **NPM**: [idb-ts](https://www.npmjs.com/package/idb-ts)
516
- - Demo: https://maifeeulasad.github.io/idb-ts/
517
- - Code Coverage report: https://maifeeulasad.github.io/idb-ts/coverage/lcov-report/
558
+ ## Useful Links
559
+
560
+ - **GitHub**: [maifeeulasad/idb-ts](https://github.com/maifeeulasad/idb-ts)
561
+ - **NPM**: [idb-ts](https://www.npmjs.com/package/idb-ts)
562
+ - **Demo**: https://maifeeulasad.github.io/idb-ts/
563
+ - **Code Coverage report**: https://maifeeulasad.github.io/idb-ts/coverage/lcov-report/
518
564
 
519
565
  πŸŽ‰ **Enjoy seamless IndexedDB integration with TypeScript! Happy coding!** πŸš€
520
566
 
567
+ Made by [Maifee Ulasad](https://github.com/maifeeulasad) with :love: and :tea:. Licensed under [MIT](./LICENSE).