idb-ts 3.3.0 β†’ 3.7.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
@@ -39,37 +39,47 @@ npm i idb-ts
39
39
  - ⚑ **Easy CRUD Operations** - Perform create, read, update, and delete seamlessly.
40
40
  - πŸš€ **Fully Typed API** - Benefit from TypeScript’s powerful type system.
41
41
  - 🏎️ **Performance Optimized** - Minimal overhead with IndexedDB's native capabilities.
42
+ - πŸ”„ **Schema Versioning** - Manage database schema evolution with automatic migration support.
43
+ - πŸ”‘ **Advanced Key Management** - Auto-increment, UUID, timestamp, custom generators, and composite keys.
42
44
 
43
45
  ---
44
46
 
45
47
  ## πŸ“– Example Usage
46
48
 
47
49
  ### πŸ—οΈ Declaring Entities
48
- Use decorators to define your data models with automatic schema management.
50
+ Use decorators to define your data models. Each class must have exactly one `@KeyPath()` and be decorated with `@DataClass()`.
49
51
 
50
52
  ```typescript
53
+ import { Database, DataClass, KeyPath, Index } from "idb-ts";
54
+
51
55
  @DataClass()
52
56
  class User {
53
57
  @KeyPath()
54
- name: string;
55
- age: number;
56
- cell?: string;
57
- address: string;
58
+ id!: string;
59
+
60
+ @Index()
61
+ email!: string;
58
62
 
59
- constructor(name: string, age: number, address: string, cell?: string) {
63
+ name!: string;
64
+ age!: number;
65
+
66
+ constructor(id: string, name: string, age: number, email?: string) {
67
+ this.id = id;
60
68
  this.name = name;
61
69
  this.age = age;
62
- this.address = address;
63
- this.cell = cell;
70
+ this.email = email || `${name.toLowerCase()}@example.com`;
64
71
  }
65
72
  }
66
73
 
67
74
  @DataClass()
68
75
  class Location {
69
76
  @KeyPath()
70
- id: string;
71
- city: string;
72
- country: string;
77
+ id!: string;
78
+
79
+ @Index()
80
+ city!: string;
81
+
82
+ country!: string;
73
83
 
74
84
  constructor(id: string, city: string, country: string) {
75
85
  this.id = id;
@@ -80,42 +90,345 @@ class Location {
80
90
  ```
81
91
 
82
92
  ### πŸ”„ CRUD Operations
83
- Perform database operations in an intuitive way:
93
+ Perform database operations using the repository API:
84
94
 
85
95
  ```typescript
86
96
  const db = await Database.build("idb-crud", [User, Location]);
87
97
 
88
- const alice = new User("Alice", 25, "123 Main St");
98
+ const alice = new User("u1", "Alice", 25);
99
+ const bob = new User("u2", "Bob", 30);
89
100
  const nyc = new Location("1", "New York", "USA");
101
+ const sf = new Location("2", "San Francisco", "USA");
90
102
 
91
- await db.create(User, alice);
92
- await db.create(Location, nyc);
103
+ await db.User.create(alice);
104
+ await db.User.create(bob);
105
+ await db.Location.create(nyc);
106
+ await db.Location.create(sf);
93
107
 
94
- const readAlice = await db.read(User, "Alice");
108
+ const readAlice = await db.User.read("u1");
95
109
  console.log("πŸ‘€ Read user:", readAlice);
96
110
 
97
111
  alice.age = 26;
98
- alice.address = "789 Maple St";
99
- await db.update(User, alice);
112
+ await db.User.update(alice);
100
113
 
101
- const users = await db.list(User);
114
+ const users = await db.User.list();
102
115
  console.log("πŸ“‹ All users:", users);
103
116
 
104
- await db.delete(User, "Alice");
117
+ // Pagination
118
+ const page1 = await db.User.listPaginated(1, 2); // page 1, 2 users per page
119
+ console.log("πŸ“„ Page 1:", page1);
120
+
121
+ await db.User.delete("u1");
105
122
  console.log("❌ User Alice deleted.");
106
123
 
107
- const remainingUsers = await db.list(User);
124
+ const remainingUsers = await db.User.list();
108
125
  console.log("πŸ” Remaining users:", remainingUsers);
109
126
 
110
- const locations = await db.list(Location);
127
+ const locations = await db.Location.list();
111
128
  console.log("🌍 All locations:", locations);
112
129
  ```
113
130
 
131
+ ### πŸ” Indexing Support
132
+ Create indexes on fields for fast querying. Query indexes using the repository API:
133
+
134
+ ```typescript
135
+ @DataClass()
136
+ class Product {
137
+ @KeyPath()
138
+ id!: string;
139
+
140
+ @Index()
141
+ category!: string;
142
+
143
+ @Index()
144
+ price!: number;
145
+
146
+ name!: string;
147
+ description!: string;
148
+
149
+ constructor(id: string, category: string, price: number, name: string, description: string) {
150
+ this.id = id;
151
+ this.category = category;
152
+ this.price = price;
153
+ this.name = name;
154
+ this.description = description;
155
+ }
156
+ }
157
+
158
+ const db = await Database.build("products-db", [Product]);
159
+
160
+ const electronics = await db.Product.findByIndex('category', 'Electronics');
161
+ const expensiveItems = await db.Product.findByIndex('price', 999.99);
162
+ const firstElectronic = await db.Product.findOneByIndex('category', 'Electronics');
163
+ ```
164
+
165
+ #### Index Methods:
166
+ - `findByIndex(indexName, value): Promise<T[]>` - Find all records matching the index value
167
+ - `findOneByIndex(indexName, value): Promise<T | undefined>` - Find the first record matching the index value
168
+
169
+ #### Error Handling
170
+ - If you query a non-existent index, an error is thrown:
171
+ ```typescript
172
+ await db.Product.findByIndex('nonexistent', 'value'); // throws
173
+ ```
174
+
175
+ ---
176
+
177
+ ## πŸ”‘ Multi-Field & Composite Key Support
178
+
179
+ idb-ts provides flexible key management options including auto-increment keys, key generators, and composite keys for complex data relationships.
180
+
181
+ ### Auto-Increment Keys
182
+ Perfect for entities where you want the database to automatically generate sequential IDs:
183
+
184
+ ```typescript
185
+ @DataClass()
186
+ class Task {
187
+ @KeyPath({ autoIncrement: true })
188
+ id!: number;
189
+
190
+ title!: string;
191
+ completed!: boolean;
192
+
193
+ constructor(title: string, completed = false) {
194
+ this.title = title;
195
+ this.completed = completed;
196
+ }
197
+ }
198
+
199
+ const db = await Database.build("tasks-db", [Task]);
200
+
201
+ // 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"));
204
+ console.log(task1.id); // 1
205
+ console.log(task2.id); // 2
206
+ ```
207
+
208
+ ### Key Generators
209
+ Generate keys automatically using built-in generators:
210
+
211
+ #### UUID Keys
212
+ ```typescript
213
+ @DataClass()
214
+ class Document {
215
+ @KeyPath({ generator: 'uuid' })
216
+ uuid!: string;
217
+
218
+ @Index()
219
+ category!: string;
220
+
221
+ title!: string;
222
+ content!: string;
223
+
224
+ constructor(category: string, title: string, content: string) {
225
+ this.category = category;
226
+ this.title = title;
227
+ this.content = content;
228
+ }
229
+ }
230
+
231
+ const db = await Database.build("docs-db", [Document]);
232
+
233
+ const doc = await db.Document.create(new Document("tutorial", "Getting Started", "Welcome..."));
234
+ console.log(doc.uuid); // e.g., "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
235
+ ```
236
+
237
+ #### Timestamp Keys
238
+ ```typescript
239
+ @DataClass()
240
+ class Event {
241
+ @KeyPath({ generator: 'timestamp' })
242
+ timestamp!: number;
243
+
244
+ @Index()
245
+ type!: string;
246
+
247
+ data!: any;
248
+
249
+ constructor(type: string, data: any) {
250
+ this.type = type;
251
+ this.data = data;
252
+ }
253
+ }
254
+
255
+ const event = await db.Event.create(new Event("user_login", { userId: "123" }));
256
+ console.log(event.timestamp); // e.g., 1696118400000
257
+ ```
258
+
259
+ #### Random Keys
260
+ ```typescript
261
+ @DataClass()
262
+ class Session {
263
+ @KeyPath({ generator: 'random' })
264
+ sessionId!: string;
265
+
266
+ userId!: string;
267
+ expiresAt!: Date;
268
+
269
+ constructor(userId: string, expiresAt: Date) {
270
+ this.userId = userId;
271
+ this.expiresAt = expiresAt;
272
+ }
273
+ }
274
+
275
+ const session = await db.Session.create(new Session("user123", new Date()));
276
+ console.log(session.sessionId); // e.g., "xyz789abc123"
277
+ ```
278
+
279
+ ### Custom Key Generators
280
+ Create your own key generation logic:
281
+
282
+ ```typescript
283
+ @DataClass()
284
+ class Invoice {
285
+ @KeyPath({ generator: (entity: any) => `INV-${entity.year}-${String(entity.number).padStart(4, '0')}` })
286
+ invoiceId!: string;
287
+
288
+ year!: number;
289
+ number!: number;
290
+ amount!: number;
291
+
292
+ constructor(year: number, number: number, amount: number) {
293
+ this.year = year;
294
+ this.number = number;
295
+ this.amount = amount;
296
+ }
297
+ }
298
+
299
+ const invoice = await db.Invoice.create(new Invoice(2024, 1, 1500.00));
300
+ console.log(invoice.invoiceId); // "INV-2024-0001"
301
+ ```
302
+
303
+ ### Composite Keys
304
+ Handle many-to-many relationships with composite keys using the `@CompositeKeyPath` decorator:
305
+
306
+ ```typescript
307
+ import { CompositeKeyPath } from "idb-ts";
308
+
309
+ @CompositeKeyPath(['userId', 'projectId'])
310
+ @DataClass()
311
+ class UserProject {
312
+ userId!: string;
313
+ projectId!: string;
314
+
315
+ @Index()
316
+ role!: string;
317
+
318
+ joinedAt!: Date;
319
+
320
+ constructor(userId: string, projectId: string, role: string) {
321
+ this.userId = userId;
322
+ this.projectId = projectId;
323
+ this.role = role;
324
+ this.joinedAt = new Date();
325
+ }
326
+ }
327
+
328
+ const db = await Database.build("collaboration-db", [UserProject]);
329
+
330
+ // 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"));
334
+
335
+ // Read with composite key
336
+ const relationship = await db.UserProject.read(['user123', 'project456']);
337
+ console.log(relationship?.role); // "developer"
338
+
339
+ // Update relationship
340
+ if (relationship) {
341
+ relationship.role = "maintainer";
342
+ await db.UserProject.update(relationship);
343
+ }
344
+
345
+ // Delete with composite key
346
+ await db.UserProject.delete(['user123', 'project789']);
347
+
348
+ // Query by role index
349
+ const developers = await db.UserProject.findByIndex('role', 'developer');
350
+ ```
351
+
352
+ ### Key Generation Utilities
353
+ Access key generators directly for your custom logic:
354
+
355
+ ```typescript
356
+ import { KeyGenerators } from "idb-ts";
357
+
358
+ const uuid = KeyGenerators.uuid(); // Generate UUID
359
+ const timestamp = KeyGenerators.timestamp(); // Current timestamp
360
+ const random = KeyGenerators.random(); // Random string
361
+ ```
362
+
363
+ ---
364
+
365
+ ## πŸ”„ Schema Versioning
366
+
367
+ idb-ts supports schema versioning to manage database evolution over time. Version your entities and let the library handle automatic migration!
368
+
369
+ ### Basic Usage
370
+
371
+ ```typescript
372
+ @DataClass({ version: 1 })
373
+ class User {
374
+ @KeyPath() id!: string;
375
+ @Index() email!: string;
376
+ name!: string;
377
+ }
378
+
379
+ @DataClass({ version: 2 })
380
+ class Post {
381
+ @KeyPath() id!: string;
382
+ @Index() authorId!: string;
383
+ title!: string;
384
+ content!: string;
385
+ }
386
+
387
+ @DataClass({ version: 3 })
388
+ class Comment {
389
+ @KeyPath() id!: string;
390
+ @Index() postId!: string;
391
+ @Index() authorId!: string;
392
+ text!: string;
393
+ }
394
+
395
+ // Database version will be 3 (highest entity version)
396
+ const db = await Database.build("blog", [User, Post, Comment]);
397
+
398
+ console.log(db.getDatabaseVersion()); // 3
399
+ console.log(db.getEntityVersions()); // Map with entity versions
400
+ ```
401
+
402
+ ### Key Features
403
+
404
+ - **Automatic Version Calculation**: Database version = highest entity version
405
+ - **Seamless Migration**: Only new/updated entities are processed during upgrades
406
+ - **Backward Compatibility**: Entities without version default to version 1
407
+ - **Index Evolution**: New indexes are automatically created during migration
408
+
409
+ ### Version Management
410
+
411
+ ```typescript
412
+ // Check versions
413
+ const dbVersion = db.getDatabaseVersion();
414
+ const entityVersions = db.getEntityVersions();
415
+ const userVersion = db.getEntityVersion('User');
416
+
417
+ // 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
421
+ ```
422
+
423
+ πŸ“– **[Complete Schema Versioning Guide](./SCHEMA_VERSIONING.md)** - Detailed documentation with examples and best practices.
424
+
114
425
  ---
115
426
 
116
427
  ## πŸ”— Useful Links
117
428
  - πŸ“‚ **GitHub**: [maifeeulasad/idb-ts](https://github.com/maifeeulasad/idb-ts)
118
429
  - πŸ“¦ **NPM**: [idb-ts](https://www.npmjs.com/package/idb-ts)
430
+ - Demo: https://maifeeulasad.github.io/idb-ts/
431
+ - Code Coverage report: https://maifeeulasad.github.io/idb-ts/coverage/lcov-report/
119
432
 
120
433
  πŸŽ‰ **Enjoy seamless IndexedDB integration with TypeScript! Happy coding!** πŸš€
121
434
 
package/lib/index.d.ts CHANGED
@@ -1,31 +1,84 @@
1
1
  import 'reflect-metadata';
2
- declare function KeyPath(): PropertyDecorator;
3
- declare function DataClass(): ClassDecorator;
2
+ type QueryDirection = 'asc' | 'desc';
3
+ declare class QueryBuilder<T> {
4
+ private db;
5
+ private storeName;
6
+ private conditions;
7
+ private orderField?;
8
+ private orderDirection;
9
+ private limitCount?;
10
+ private offsetCount?;
11
+ private indexName?;
12
+ private rangeStart?;
13
+ private rangeEnd?;
14
+ private currentField?;
15
+ constructor(db: IDBDatabase, storeName: string);
16
+ where(field: string): this;
17
+ and(field: string): this;
18
+ equals(value: any): this;
19
+ gt(value: any): this;
20
+ gte(value: any): this;
21
+ lt(value: any): this;
22
+ lte(value: any): this;
23
+ orderBy(field: string, direction?: QueryDirection): this;
24
+ limit(n: number): this;
25
+ offset(n: number): this;
26
+ useIndex(indexName: string): this;
27
+ range(start: any, end: any): this;
28
+ execute(): Promise<T[]>;
29
+ }
30
+ interface KeyPathOptions {
31
+ autoIncrement?: boolean;
32
+ generator?: 'uuid' | 'timestamp' | 'random' | ((item?: any) => string | number);
33
+ }
34
+ interface KeyPathMetadata {
35
+ fields: string | string[];
36
+ options?: KeyPathOptions;
37
+ }
38
+ declare class KeyGenerators {
39
+ static uuid(): string;
40
+ static timestamp(): number;
41
+ static random(): string;
42
+ }
43
+ declare function KeyPath(options?: KeyPathOptions): PropertyDecorator;
44
+ declare function CompositeKeyPath(fields: string[], options?: KeyPathOptions): ClassDecorator;
45
+ declare function Index(): PropertyDecorator;
46
+ interface DataClassOptions {
47
+ version?: number;
48
+ }
49
+ declare function DataClass(options?: DataClassOptions): ClassDecorator;
50
+ interface EntityRepository<T> {
51
+ create(item: T): Promise<void>;
52
+ read(key: string | string[] | number): Promise<T | undefined>;
53
+ update(item: T): Promise<void>;
54
+ delete(key: string | string[] | number): Promise<void>;
55
+ list(): Promise<T[]>;
56
+ listPaginated(page: number, pageSize: number): Promise<T[]>;
57
+ findByIndex(indexName: string, value: any): Promise<T[]>;
58
+ findOneByIndex(indexName: string, value: any): Promise<T | undefined>;
59
+ count(): Promise<number>;
60
+ exists(key: string): Promise<boolean>;
61
+ clear(): Promise<void>;
62
+ query(): QueryBuilder<T>;
63
+ }
64
+ type DatabaseWithRepositories<T extends Record<string, any>> = Database & T;
4
65
  declare class Database {
5
66
  private dbName;
6
67
  private classes;
7
68
  private db;
69
+ private entityRepositories;
70
+ private dbVersion;
8
71
  private constructor();
9
- static build(dbName: string, classes: Function[]): Promise<Database>;
72
+ private calculateDatabaseVersion;
73
+ static build<T extends Record<string, EntityRepository<any>>>(dbName: string, classes: Function[]): Promise<DatabaseWithRepositories<T>>;
10
74
  private initDB;
11
- private getObjectStore;
12
- create<T>(cls: {
13
- new (...args: any[]): T;
14
- }, item: T): Promise<void>;
15
- read<T>(cls: {
16
- new (...args: any[]): T;
17
- }, key: string): Promise<T | undefined>;
18
- update<T>(cls: {
19
- new (...args: any[]): T;
20
- }, item: T): Promise<void>;
21
- delete<T>(cls: {
22
- new (...args: any[]): T;
23
- }, key: string): Promise<void>;
24
- list<T>(cls: {
25
- new (...args: any[]): T;
26
- }): Promise<T[]>;
27
- listPaginated<T>(cls: {
28
- new (...args: any[]): T;
29
- }, page: number, pageSize: number): Promise<T[]>;
75
+ private generateEntityRepositories;
76
+ private createEntityRepository;
77
+ private performOperation;
78
+ getAvailableEntities(): string[];
79
+ getDatabaseVersion(): number;
80
+ getEntityVersions(): Map<string, number>;
81
+ getEntityVersion(entityName: string): number | undefined;
30
82
  }
31
- export { Database, KeyPath, DataClass };
83
+ export { Database, KeyPath, CompositeKeyPath, DataClass, Index, EntityRepository, KeyGenerators };
84
+ export type { DatabaseWithRepositories, DataClassOptions, KeyPathOptions, KeyPathMetadata };