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 +366 -319
- package/lib/index.d.ts +95 -6
- package/lib/index.esm.js +562 -186
- package/lib/index.js +562 -186
- package/package.json +11 -8
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
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
|
|
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
|
-
|
|
29
|
-
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
30
36
|
|
|
31
|
-
## π¦ Installation
|
|
32
|
-
Install via npm and start using IndexedDB like a pro! β‘
|
|
33
37
|
```sh
|
|
34
|
-
npm
|
|
35
|
-
pnpm add idb-ts
|
|
36
|
-
yarn add idb-ts
|
|
38
|
+
npm install idb-ts
|
|
39
|
+
pnpm add idb-ts
|
|
40
|
+
yarn add idb-ts
|
|
37
41
|
```
|
|
38
42
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
52
|
-
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Quick Start
|
|
53
74
|
|
|
54
75
|
```typescript
|
|
55
|
-
import
|
|
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
|
-
|
|
77
|
-
class Location {
|
|
78
|
-
@KeyPath()
|
|
79
|
-
id!: string;
|
|
91
|
+
const db = await Database.build<{ User: EntityRepository<User> }>('mydb', [User]);
|
|
80
92
|
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
97
|
+
---
|
|
85
98
|
|
|
86
|
-
|
|
87
|
-
this.id = id;
|
|
88
|
-
this.city = city;
|
|
89
|
-
this.country = country;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
```
|
|
99
|
+
## Defining Entities
|
|
93
100
|
|
|
94
|
-
|
|
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
|
-
|
|
104
|
+
import { Database, DataClass, KeyPath, Index, Validate } from 'idb-ts';
|
|
99
105
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
106
|
+
@DataClass({ version: 1 })
|
|
107
|
+
class User {
|
|
108
|
+
@KeyPath({ generator: 'uuid' })
|
|
109
|
+
id!: string;
|
|
104
110
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
111
|
-
|
|
115
|
+
@Validate((v) => typeof v === 'number' && v >= 0, 'age must be non-negative')
|
|
116
|
+
age!: number;
|
|
112
117
|
|
|
113
|
-
|
|
114
|
-
|
|
118
|
+
name!: string;
|
|
119
|
+
}
|
|
120
|
+
```
|
|
115
121
|
|
|
116
|
-
|
|
117
|
-
console.log("π All users:", users);
|
|
122
|
+
### Decorator reference
|
|
118
123
|
|
|
119
|
-
|
|
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
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
+
| Option | Type | Default | Description |
|
|
129
|
+
|---|---|---|---|
|
|
130
|
+
| `version` | `number` | `1` | Schema version. Increment when the entity's store or indexes change. |
|
|
128
131
|
|
|
129
|
-
|
|
130
|
-
console.log("π All locations:", locations);
|
|
131
|
-
```
|
|
132
|
+
#### `@KeyPath(options?)`
|
|
132
133
|
|
|
133
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
143
|
-
category!: string;
|
|
141
|
+
#### `@CompositeKeyPath(fields, options?)`
|
|
144
142
|
|
|
145
|
-
|
|
146
|
-
price!: number;
|
|
143
|
+
Class-level decorator for composite primary keys. Cannot be combined with `@KeyPath`.
|
|
147
144
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
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
|
-
|
|
157
|
+
Creates an IDB index on the decorated field, enabling efficient lookups via `findByIndex` and `findOneByIndex`.
|
|
172
158
|
|
|
173
|
-
|
|
159
|
+
| Option | Type | Description |
|
|
160
|
+
|---|---|---|
|
|
161
|
+
| `unique` | `boolean` | Enforce uniqueness on the indexed field. |
|
|
174
162
|
|
|
175
|
-
|
|
176
|
-
- `__idb_updatedAt`: numeric epoch milliseconds updated on each successful update.
|
|
163
|
+
#### `@Validate(predicate, message)`
|
|
177
164
|
|
|
178
|
-
|
|
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
|
-
|
|
167
|
+
#### `@RetentionPolicy(options)`
|
|
181
168
|
|
|
182
|
-
|
|
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
|
-
|
|
171
|
+
---
|
|
188
172
|
|
|
189
|
-
|
|
173
|
+
## Database Initialisation
|
|
190
174
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
175
|
+
```typescript
|
|
176
|
+
const db = await Database.build<{
|
|
177
|
+
User: EntityRepository<User>;
|
|
178
|
+
Order: EntityRepository<Order>;
|
|
179
|
+
}>('shop', [User, Order]);
|
|
180
|
+
```
|
|
194
181
|
|
|
195
|
-
|
|
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
|
-
|
|
184
|
+
The effective database version is the highest `version` value declared across all registered entities.
|
|
198
185
|
|
|
199
|
-
|
|
200
|
-
@RetentionPolicy({ seconds: 60 * 60 * 24 * 30 }) // 30 days
|
|
201
|
-
@DataClass()
|
|
202
|
-
class Session { /* ... */ }
|
|
186
|
+
### Inspecting database metadata
|
|
203
187
|
|
|
204
|
-
|
|
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
|
-
|
|
195
|
+
### Closing the connection
|
|
208
196
|
|
|
209
|
-
|
|
210
|
-
|
|
197
|
+
```typescript
|
|
198
|
+
db.close(); // Stops the retention cleanup timer and closes the IDB connection.
|
|
199
|
+
```
|
|
211
200
|
|
|
212
|
-
|
|
201
|
+
---
|
|
213
202
|
|
|
214
|
-
|
|
203
|
+
## CRUD Operations
|
|
215
204
|
|
|
216
|
-
|
|
205
|
+
Each entity is accessible as a named property on the database object. All methods return `Promise`.
|
|
217
206
|
|
|
218
|
-
|
|
207
|
+
```typescript
|
|
208
|
+
// Create
|
|
209
|
+
await db.User.create(user);
|
|
210
|
+
await db.User.createMany([alice, bob, charlie]);
|
|
219
211
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
227
|
-
|
|
217
|
+
// Update
|
|
218
|
+
await db.User.update(updatedUser);
|
|
219
|
+
await db.User.updateMany([user1, user2]);
|
|
228
220
|
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
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
|
-
|
|
232
|
+
### Index lookups
|
|
237
233
|
|
|
238
|
-
|
|
234
|
+
```typescript
|
|
235
|
+
const allAdmins = await db.User.findByIndex('role', 'admin');
|
|
236
|
+
const firstAdmin = await db.User.findOneByIndex('role', 'admin');
|
|
237
|
+
```
|
|
239
238
|
|
|
240
|
-
|
|
239
|
+
Querying a non-existent index throws immediately.
|
|
241
240
|
|
|
242
|
-
|
|
243
|
-
- `updateMany(items: T[])`: updates multiple items.
|
|
244
|
-
- `deleteMany(keys: Array<string | string[] | number>)`: deletes multiple keys.
|
|
241
|
+
---
|
|
245
242
|
|
|
246
|
-
|
|
243
|
+
## Automatic Timestamps
|
|
247
244
|
|
|
248
|
-
|
|
245
|
+
Every record written through a repository automatically receives two internal fields:
|
|
249
246
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
252
|
+
`__idb_createdAt` is preserved across updates; `__idb_updatedAt` is refreshed on every write.
|
|
256
253
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
##
|
|
261
|
+
## Query Builder
|
|
266
262
|
|
|
267
|
-
|
|
263
|
+
`EntityRepository.query()` returns a typed `QueryBuilder<T>` for constructing complex filter expressions, sorting, pagination, and aggregations.
|
|
268
264
|
|
|
269
|
-
###
|
|
270
|
-
Perfect for entities where you want the database to automatically generate sequential IDs:
|
|
265
|
+
### Filtering
|
|
271
266
|
|
|
272
267
|
```typescript
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
268
|
+
const results = await db.User.query()
|
|
269
|
+
.where('age').gte(18)
|
|
270
|
+
.and('status').equals('active')
|
|
271
|
+
.execute();
|
|
272
|
+
```
|
|
277
273
|
|
|
278
|
-
|
|
279
|
-
completed!: boolean;
|
|
274
|
+
#### Available operators
|
|
280
275
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
291
|
+
### Logical grouping
|
|
288
292
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
###
|
|
297
|
-
Generate keys automatically using built-in generators:
|
|
311
|
+
### Sorting and pagination
|
|
298
312
|
|
|
299
|
-
#### UUID Keys
|
|
300
313
|
```typescript
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
307
|
-
category!: string;
|
|
322
|
+
### Index and range acceleration
|
|
308
323
|
|
|
309
|
-
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
326
|
+
```typescript
|
|
327
|
+
await db.Product.query()
|
|
328
|
+
.useIndex('price')
|
|
329
|
+
.range(10, 100)
|
|
330
|
+
.execute();
|
|
331
|
+
```
|
|
318
332
|
|
|
319
|
-
|
|
333
|
+
### Aggregations
|
|
320
334
|
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
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
|
-
|
|
333
|
-
type!: string;
|
|
349
|
+
---
|
|
334
350
|
|
|
335
|
-
|
|
351
|
+
## Key Management
|
|
336
352
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
344
|
-
|
|
361
|
+
title!: string;
|
|
362
|
+
}
|
|
345
363
|
```
|
|
346
364
|
|
|
347
|
-
|
|
365
|
+
### Built-in generators
|
|
366
|
+
|
|
348
367
|
```typescript
|
|
349
368
|
@DataClass()
|
|
350
|
-
class
|
|
351
|
-
@KeyPath({ generator: '
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
userId!: string;
|
|
355
|
-
expiresAt!: Date;
|
|
369
|
+
class Document {
|
|
370
|
+
@KeyPath({ generator: 'uuid' }) // RFC 4122 v4
|
|
371
|
+
id!: string;
|
|
372
|
+
}
|
|
356
373
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
374
|
+
@DataClass()
|
|
375
|
+
class Event {
|
|
376
|
+
@KeyPath({ generator: 'timestamp' }) // Date.now()
|
|
377
|
+
id!: number;
|
|
361
378
|
}
|
|
362
379
|
|
|
363
|
-
|
|
364
|
-
|
|
380
|
+
@DataClass()
|
|
381
|
+
class Session {
|
|
382
|
+
@KeyPath({ generator: 'random' }) // Base-36 random string
|
|
383
|
+
id!: string;
|
|
384
|
+
}
|
|
365
385
|
```
|
|
366
386
|
|
|
367
|
-
### Custom
|
|
368
|
-
Create your own key generation logic:
|
|
387
|
+
### Custom generator
|
|
369
388
|
|
|
370
389
|
```typescript
|
|
371
390
|
@DataClass()
|
|
372
391
|
class Invoice {
|
|
373
|
-
@KeyPath({
|
|
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
|
-
###
|
|
392
|
-
Handle many-to-many relationships with composite keys using the `@CompositeKeyPath` decorator:
|
|
404
|
+
### Using generators directly
|
|
393
405
|
|
|
394
406
|
```typescript
|
|
395
|
-
import {
|
|
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
|
-
|
|
429
|
+
// Create
|
|
430
|
+
await db.UserProject.create(new UserProject('u1', 'p1', 'developer'));
|
|
417
431
|
|
|
418
|
-
//
|
|
419
|
-
await db.UserProject.
|
|
420
|
-
await db.UserProject.
|
|
421
|
-
|
|
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
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
447
|
-
const
|
|
448
|
-
|
|
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
|
-
|
|
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
|
-
|
|
500
|
+
---
|
|
456
501
|
|
|
457
|
-
|
|
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
|
-
@
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
@
|
|
464
|
-
|
|
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
|
-
|
|
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
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
-
|
|
484
|
-
|
|
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
|
-
|
|
544
|
+
---
|
|
491
545
|
|
|
492
|
-
|
|
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
|
-
|
|
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
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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
|
-
##
|
|
514
|
-
|
|
515
|
-
-
|
|
516
|
-
-
|
|
517
|
-
-
|
|
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).
|