Skip to content

Commit 328367d

Browse files
authored
Merge pull request #7 from maifeeulasad/indexing-field
Indexing support
2 parents e070d33 + 6f38ca1 commit 328367d

5 files changed

Lines changed: 262 additions & 7 deletions

File tree

README.md

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,23 @@ npm i idb-ts
4848
Use decorators to define your data models with automatic schema management.
4949

5050
```typescript
51+
import { Database, DataClass, KeyPath, Index } from "idb-ts";
52+
5153
@DataClass()
5254
class User {
5355
@KeyPath()
5456
name: string;
57+
58+
@Index()
59+
email: string;
60+
5561
age: number;
5662
cell?: string;
5763
address: string;
5864

59-
constructor(name: string, age: number, address: string, cell?: string) {
65+
constructor(name: string, email: string, age: number, address: string, cell?: string) {
6066
this.name = name;
67+
this.email = email;
6168
this.age = age;
6269
this.address = address;
6370
this.cell = cell;
@@ -68,7 +75,10 @@ class User {
6875
class Location {
6976
@KeyPath()
7077
id: string;
78+
79+
@Index()
7180
city: string;
81+
7282
country: string;
7383

7484
constructor(id: string, city: string, country: string) {
@@ -85,11 +95,15 @@ Perform database operations in an intuitive way:
8595
```typescript
8696
const db = await Database.build("idb-crud", [User, Location]);
8797

88-
const alice = new User("Alice", 25, "123 Main St");
98+
const alice = new User("Alice", "alice@example.com", 25, "123 Main St");
99+
const bob = new User("Bob", "bob@example.com", 30, "456 Oak Ave");
89100
const nyc = new Location("1", "New York", "USA");
101+
const sf = new Location("2", "San Francisco", "USA");
90102

91103
await db.create(User, alice);
104+
await db.create(User, bob);
92105
await db.create(Location, nyc);
106+
await db.create(Location, sf);
93107

94108
const readAlice = await db.read(User, "Alice");
95109
console.log("👤 Read user:", readAlice);
@@ -101,6 +115,12 @@ await db.update(User, alice);
101115
const users = await db.list(User);
102116
console.log("📋 All users:", users);
103117

118+
const userByEmail = await db.findOneByIndex(User, 'email', 'bob@example.com');
119+
console.log("🔎 User by email:", userByEmail);
120+
121+
const locationsInSF = await db.findByIndex(Location, 'city', 'San Francisco');
122+
console.log("🌆 Locations in San Francisco:", locationsInSF);
123+
104124
await db.delete(User, "Alice");
105125
console.log("❌ User Alice deleted.");
106126

@@ -111,6 +131,46 @@ const locations = await db.list(Location);
111131
console.log("🌍 All locations:", locations);
112132
```
113133

134+
### 🔍 Indexing Support
135+
Create indexes on fields for fast querying:
136+
137+
```typescript
138+
@DataClass()
139+
class Product {
140+
@KeyPath()
141+
id: string;
142+
143+
@Index()
144+
category: string;
145+
146+
@Index()
147+
price: number;
148+
149+
name: string;
150+
description: string;
151+
152+
constructor(id: string, category: string, price: number, name: string, description: string) {
153+
this.id = id;
154+
this.category = category;
155+
this.price = price;
156+
this.name = name;
157+
this.description = description;
158+
}
159+
}
160+
161+
const db = await Database.build("products-db", [Product]);
162+
163+
const electronics = await db.findByIndex(Product, 'category', 'Electronics');
164+
165+
const expensiveItems = await db.findByIndex(Product, 'price', 999.99);
166+
167+
const firstElectronic = await db.findOneByIndex(Product, 'category', 'Electronics');
168+
```
169+
170+
#### Index Methods:
171+
- `findByIndex<T>(cls, indexName, value): Promise<T[]>` - Find all records matching the index value
172+
- `findOneByIndex<T>(cls, indexName, value): Promise<T | undefined>` - Find the first record matching the index value
173+
114174
---
115175

116176
## 🔗 Useful Links

__tests__/annotation.test.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Database, KeyPath, DataClass } from '../index';
1+
import { Database, KeyPath, DataClass, Index } from '../index';
22

33
describe('IndexedDB annotation', () => {
44
it('should throw error if class is missing @KeyPath', async () => {
@@ -54,4 +54,20 @@ describe('IndexedDB annotation', () => {
5454
}).rejects.toThrow(/All classes should be decorated/);
5555
});
5656

57+
it('should work with @Index decorator', async () => {
58+
@DataClass()
59+
class IndexedUser {
60+
@KeyPath()
61+
id!: string;
62+
63+
@Index()
64+
email!: string;
65+
66+
name!: string;
67+
}
68+
69+
const db = await Database.build('IndexedDB', [IndexedUser]);
70+
expect(db).toBeDefined();
71+
});
72+
5773
});

__tests__/database.test.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
1-
import { Database, KeyPath, DataClass } from '../index';
1+
import { Database, KeyPath, DataClass, Index } from '../index';
22

33
@DataClass()
44
class User {
55
@KeyPath()
66
id!: string;
77

8+
@Index()
9+
email!: string;
10+
811
name!: string;
912
age!: number;
1013

11-
constructor(id: string, name: string, age: number) {
14+
constructor(id: string, name: string, age: number, email?: string) {
1215
this.id = id;
1316
this.name = name;
1417
this.age = age;
18+
this.email = email || `${name.toLowerCase()}@example.com`;
1519
}
1620
}
1721

@@ -61,4 +65,22 @@ describe('IndexedDB CRUD', () => {
6165
expect(deleted).toBeUndefined();
6266
});
6367

68+
it('should find users by email index', async () => {
69+
const user = new User('u5', 'Diana', 35, 'diana@example.com');
70+
await db.create(User, user);
71+
72+
const found = await db.findByIndex(User, 'email', 'diana@example.com');
73+
expect(found.length).toBe(1);
74+
expect(found[0].name).toBe('Diana');
75+
});
76+
77+
it('should find one user by email index', async () => {
78+
const user = new User('u6', 'Eve', 29, 'eve@example.com');
79+
await db.create(User, user);
80+
81+
const found = await db.findOneByIndex(User, 'email', 'eve@example.com');
82+
expect(found).toBeDefined();
83+
expect(found!.name).toBe('Eve');
84+
});
85+
6486
});

__tests__/indexing.test.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { Database, KeyPath, DataClass, Index } from '../index';
2+
3+
@DataClass()
4+
class User {
5+
@KeyPath()
6+
id!: string;
7+
8+
@Index()
9+
email!: string;
10+
11+
@Index()
12+
age!: number;
13+
14+
name!: string;
15+
16+
constructor(id: string, email: string, age: number, name: string) {
17+
this.id = id;
18+
this.email = email;
19+
this.age = age;
20+
this.name = name;
21+
}
22+
}
23+
24+
describe('IndexedDB Indexing', () => {
25+
let db: Database;
26+
27+
beforeAll(async () => {
28+
db = await Database.build('IndexingTestDB', [User]);
29+
});
30+
31+
beforeEach(async () => {
32+
// Clear existing data
33+
const users = await db.list(User);
34+
for (const user of users) {
35+
await db.delete(User, user.id);
36+
}
37+
38+
// Add test data
39+
await db.create(User, new User('u1', 'alice@example.com', 30, 'Alice'));
40+
await db.create(User, new User('u2', 'bob@example.com', 25, 'Bob'));
41+
await db.create(User, new User('u3', 'charlie@example.com', 30, 'Charlie'));
42+
await db.create(User, new User('u4', 'alice@work.com', 28, 'Alice Smith'));
43+
});
44+
45+
it('should find users by email index', async () => {
46+
const users = await db.findByIndex(User, 'email', 'alice@example.com');
47+
expect(users.length).toBe(1);
48+
expect(users[0].name).toBe('Alice');
49+
expect(users[0].email).toBe('alice@example.com');
50+
});
51+
52+
it('should find multiple users by age index', async () => {
53+
const users = await db.findByIndex(User, 'age', 30);
54+
expect(users.length).toBe(2);
55+
const names = users.map(u => u.name).sort();
56+
expect(names).toEqual(['Alice', 'Charlie']);
57+
});
58+
59+
it('should find single user by email index', async () => {
60+
const user = await db.findOneByIndex(User, 'email', 'bob@example.com');
61+
expect(user).toBeDefined();
62+
expect(user!.name).toBe('Bob');
63+
expect(user!.age).toBe(25);
64+
});
65+
66+
it('should return undefined when no user found by index', async () => {
67+
const user = await db.findOneByIndex(User, 'email', 'nonexistent@example.com');
68+
expect(user).toBeUndefined();
69+
});
70+
71+
it('should return empty array when no users found by index', async () => {
72+
const users = await db.findByIndex(User, 'age', 99);
73+
expect(users).toEqual([]);
74+
});
75+
76+
it('should throw error when querying non-existent index', async () => {
77+
await expect(db.findByIndex(User, 'nonexistent', 'value')).rejects.toThrow(
78+
"Index 'nonexistent' does not exist on User"
79+
);
80+
});
81+
82+
it('should throw error when querying non-existent index with findOneByIndex', async () => {
83+
await expect(db.findOneByIndex(User, 'nonexistent', 'value')).rejects.toThrow(
84+
"Index 'nonexistent' does not exist on User"
85+
);
86+
});
87+
88+
it('should work with non-indexed fields not being queryable by index', async () => {
89+
// name field is not indexed, so this should throw an error
90+
await expect(db.findByIndex(User, 'name', 'Alice')).rejects.toThrow(
91+
"Index 'name' does not exist on User"
92+
);
93+
});
94+
});

index.ts

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ function KeyPath(): PropertyDecorator {
88
};
99
}
1010

11+
function Index(): PropertyDecorator {
12+
return (target: Object, propertyKey: string | symbol) => {
13+
const constructor = target.constructor as Function;
14+
const existing = Reflect.getMetadata("indexes", constructor) || [];
15+
Reflect.defineMetadata("indexes", [...existing, propertyKey as string], constructor);
16+
};
17+
}
18+
1119
function DataClass(): ClassDecorator {
1220
return (target: Function) => {
1321
if (!Reflect.getMetadata("keypath", target)) {
@@ -49,11 +57,18 @@ class Database {
4957

5058
this.classes.forEach((cls) => {
5159
const keyPathFields = Reflect.getMetadata("keypath", cls) || [];
60+
const indexFields = Reflect.getMetadata("indexes", cls) || [];
5261

5362
const storeName = cls.name.toLowerCase() + "s";
5463

5564
if (!db.objectStoreNames.contains(storeName)) {
56-
db.createObjectStore(storeName, { keyPath: keyPathFields[0] });
65+
const store = db.createObjectStore(storeName, { keyPath: keyPathFields[0] });
66+
67+
indexFields.forEach((indexField: string) => {
68+
if (!store.indexNames.contains(indexField)) {
69+
store.createIndex(indexField, indexField, { unique: false });
70+
}
71+
});
5772
}
5873
});
5974
};
@@ -183,6 +198,54 @@ class Database {
183198
};
184199
});
185200
}
201+
202+
async findByIndex<T>(cls: { new(...args: any[]): T }, indexName: string, value: any): Promise<T[]> {
203+
return new Promise((resolve, reject) => {
204+
const store = this.getObjectStore(cls.name, "readonly");
205+
206+
if (!store.indexNames.contains(indexName)) {
207+
reject(new Error(`Index '${indexName}' does not exist on ${cls.name}`));
208+
return;
209+
}
210+
211+
const index = store.index(indexName);
212+
const request = index.getAll(value);
213+
214+
request.onsuccess = () => {
215+
console.debug(`Items found by index ${indexName} with value ${value}:`, request.result);
216+
resolve(request.result as T[]);
217+
};
218+
219+
request.onerror = () => {
220+
console.error(`Error finding items by index ${indexName}:`, request.error);
221+
reject(request.error);
222+
};
223+
});
224+
}
225+
226+
async findOneByIndex<T>(cls: { new(...args: any[]): T }, indexName: string, value: any): Promise<T | undefined> {
227+
return new Promise((resolve, reject) => {
228+
const store = this.getObjectStore(cls.name, "readonly");
229+
230+
if (!store.indexNames.contains(indexName)) {
231+
reject(new Error(`Index '${indexName}' does not exist on ${cls.name}`));
232+
return;
233+
}
234+
235+
const index = store.index(indexName);
236+
const request = index.get(value);
237+
238+
request.onsuccess = () => {
239+
console.debug(`Item found by index ${indexName} with value ${value}:`, request.result);
240+
resolve(request.result as T | undefined);
241+
};
242+
243+
request.onerror = () => {
244+
console.error(`Error finding item by index ${indexName}:`, request.error);
245+
reject(request.error);
246+
};
247+
});
248+
}
186249
}
187250

188-
export { Database, KeyPath, DataClass };
251+
export { Database, KeyPath, DataClass, Index };

0 commit comments

Comments
 (0)