Skip to content

Commit df07477

Browse files
authored
Merge pull request #11 from maifeeulasad/multifiled-and-compositekey
multi field and composite key support
2 parents 9d4f217 + 2409562 commit df07477

3 files changed

Lines changed: 532 additions & 19 deletions

File tree

__tests__/composite-keys.test.ts

Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
import { Database, KeyPath, DataClass, Index, EntityRepository, KeyGenerators, CompositeKeyPath } from '../index';
2+
3+
// Test entities with different key configurations
4+
5+
// Auto-increment key
6+
@DataClass({ version: 1 })
7+
class AutoIncrementEntity {
8+
@KeyPath({ autoIncrement: true })
9+
id!: number;
10+
11+
name!: string;
12+
value!: number;
13+
14+
constructor(name: string, value: number) {
15+
this.name = name;
16+
this.value = value;
17+
}
18+
}
19+
20+
// UUID generated key
21+
@DataClass({ version: 1 })
22+
class UUIDEntity {
23+
@KeyPath({ generator: 'uuid' })
24+
uuid!: string;
25+
26+
@Index()
27+
category!: string;
28+
29+
title!: string;
30+
31+
constructor(category: string, title: string) {
32+
this.category = category;
33+
this.title = title;
34+
}
35+
}
36+
37+
// Timestamp generated key
38+
@DataClass({ version: 1 })
39+
class TimestampEntity {
40+
@KeyPath({ generator: 'timestamp' })
41+
timestamp!: number;
42+
43+
event!: string;
44+
data!: any;
45+
46+
constructor(event: string, data: any) {
47+
this.event = event;
48+
this.data = data;
49+
}
50+
}
51+
52+
// Random generated key
53+
@DataClass({ version: 1 })
54+
class RandomEntity {
55+
@KeyPath({ generator: 'random' })
56+
randomId!: string;
57+
58+
description!: string;
59+
60+
constructor(description: string) {
61+
this.description = description;
62+
}
63+
}
64+
65+
// Custom key generator
66+
@DataClass({ version: 1 })
67+
class CustomKeyEntity {
68+
@KeyPath({ generator: (item: any) => `custom_${item.type}_${Date.now()}` })
69+
customId!: string;
70+
71+
type!: string;
72+
content!: string;
73+
74+
constructor(type: string, content: string) {
75+
this.type = type;
76+
this.content = content;
77+
}
78+
}
79+
80+
// Composite key entity
81+
@DataClass({ version: 1 })
82+
@CompositeKeyPath(['userId', 'projectId'])
83+
class UserProject {
84+
userId!: string;
85+
projectId!: string;
86+
87+
@Index()
88+
role!: string;
89+
90+
@Index()
91+
joinedAt!: Date;
92+
93+
constructor(userId: string, projectId: string, role: string) {
94+
this.userId = userId;
95+
this.projectId = projectId;
96+
this.role = role;
97+
this.joinedAt = new Date();
98+
}
99+
}
100+
101+
// Traditional single key (for comparison)
102+
@DataClass({ version: 1 })
103+
class TraditionalEntity {
104+
@KeyPath()
105+
id!: string;
106+
107+
name!: string;
108+
109+
constructor(id: string, name: string) {
110+
this.id = id;
111+
this.name = name;
112+
}
113+
}
114+
115+
describe('Multi-Field & Composite Key Support', () => {
116+
let db: any;
117+
118+
beforeAll(async () => {
119+
// Clear any existing database
120+
const deleteRequest = indexedDB.deleteDatabase('CompositeKeyTestDB');
121+
await new Promise<void>((resolve) => {
122+
deleteRequest.onsuccess = () => resolve();
123+
deleteRequest.onerror = () => resolve(); // Continue even if deletion fails
124+
});
125+
126+
db = await Database.build('CompositeKeyTestDB', [
127+
AutoIncrementEntity,
128+
UUIDEntity,
129+
TimestampEntity,
130+
RandomEntity,
131+
CustomKeyEntity,
132+
UserProject,
133+
TraditionalEntity
134+
]);
135+
});
136+
137+
describe('Auto-increment keys', () => {
138+
it('should create entities with auto-increment keys', async () => {
139+
const entity1 = new AutoIncrementEntity('First', 100);
140+
const entity2 = new AutoIncrementEntity('Second', 200);
141+
142+
await db.AutoIncrementEntity.create(entity1);
143+
await db.AutoIncrementEntity.create(entity2);
144+
145+
const items = await db.AutoIncrementEntity.list();
146+
expect(items.length).toBe(2);
147+
expect(items[0].id).toBeDefined();
148+
expect(items[1].id).toBeDefined();
149+
expect(items[0].id).not.toBe(items[1].id);
150+
});
151+
});
152+
153+
describe('UUID generated keys', () => {
154+
it('should create entities with UUID keys', async () => {
155+
const entity = new UUIDEntity('test', 'Test Entity');
156+
157+
await db.UUIDEntity.create(entity);
158+
159+
expect(entity.uuid).toBeDefined();
160+
expect(entity.uuid).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i);
161+
162+
const retrieved = await db.UUIDEntity.read(entity.uuid);
163+
expect(retrieved).toEqual(entity);
164+
});
165+
166+
it('should generate different UUIDs for different entities', async () => {
167+
const entity1 = new UUIDEntity('test1', 'Test Entity 1');
168+
const entity2 = new UUIDEntity('test2', 'Test Entity 2');
169+
170+
await db.UUIDEntity.create(entity1);
171+
await db.UUIDEntity.create(entity2);
172+
173+
expect(entity1.uuid).not.toBe(entity2.uuid);
174+
});
175+
});
176+
177+
describe('Timestamp generated keys', () => {
178+
it('should create entities with timestamp keys', async () => {
179+
const beforeTime = Date.now();
180+
const entity = new TimestampEntity('user_login', { userId: 'user123' });
181+
182+
await db.TimestampEntity.create(entity);
183+
const afterTime = Date.now();
184+
185+
expect(entity.timestamp).toBeDefined();
186+
expect(entity.timestamp).toBeGreaterThanOrEqual(beforeTime);
187+
expect(entity.timestamp).toBeLessThanOrEqual(afterTime);
188+
189+
const retrieved = await db.TimestampEntity.read(entity.timestamp);
190+
expect(retrieved).toEqual(entity);
191+
});
192+
});
193+
194+
describe('Random generated keys', () => {
195+
it('should create entities with random keys', async () => {
196+
const entity = new RandomEntity('Random test entity');
197+
198+
await db.RandomEntity.create(entity);
199+
200+
expect(entity.randomId).toBeDefined();
201+
expect(typeof entity.randomId).toBe('string');
202+
expect(entity.randomId.length).toBeGreaterThan(0);
203+
204+
const retrieved = await db.RandomEntity.read(entity.randomId);
205+
expect(retrieved).toEqual(entity);
206+
});
207+
208+
it('should generate different random keys for different entities', async () => {
209+
const entity1 = new RandomEntity('Random entity 1');
210+
const entity2 = new RandomEntity('Random entity 2');
211+
212+
await db.RandomEntity.create(entity1);
213+
await db.RandomEntity.create(entity2);
214+
215+
expect(entity1.randomId).not.toBe(entity2.randomId);
216+
});
217+
});
218+
219+
describe('Custom key generators', () => {
220+
it('should create entities with custom generated keys', async () => {
221+
const entity = new CustomKeyEntity('blog_post', 'My first blog post');
222+
223+
await db.CustomKeyEntity.create(entity);
224+
225+
expect(entity.customId).toBeDefined();
226+
expect(entity.customId).toMatch(/^custom_blog_post_\d+$/);
227+
228+
const retrieved = await db.CustomKeyEntity.read(entity.customId);
229+
expect(retrieved).toEqual(entity);
230+
});
231+
});
232+
233+
describe('Composite keys', () => {
234+
it('should create entities with composite keys', async () => {
235+
const userProject = new UserProject('user123', 'project456', 'developer');
236+
237+
await db.UserProject.create(userProject);
238+
239+
// Read using composite key
240+
const retrieved = await db.UserProject.read(['user123', 'project456']);
241+
expect(retrieved).toBeDefined();
242+
expect(retrieved.userId).toBe('user123');
243+
expect(retrieved.projectId).toBe('project456');
244+
expect(retrieved.role).toBe('developer');
245+
});
246+
247+
it('should handle multiple entities with different composite keys', async () => {
248+
const userProject1 = new UserProject('user123', 'project789', 'admin');
249+
const userProject2 = new UserProject('user456', 'project456', 'viewer');
250+
251+
await db.UserProject.create(userProject1);
252+
await db.UserProject.create(userProject2);
253+
254+
const retrieved1 = await db.UserProject.read(['user123', 'project789']);
255+
const retrieved2 = await db.UserProject.read(['user456', 'project456']);
256+
257+
expect(retrieved1?.role).toBe('admin');
258+
expect(retrieved2?.role).toBe('viewer');
259+
});
260+
261+
it('should support updates with composite keys', async () => {
262+
const userProject = new UserProject('user789', 'project123', 'contributor');
263+
await db.UserProject.create(userProject);
264+
265+
// Update the entity
266+
userProject.role = 'maintainer';
267+
await db.UserProject.update(userProject);
268+
269+
const retrieved = await db.UserProject.read(['user789', 'project123']);
270+
expect(retrieved?.role).toBe('maintainer');
271+
});
272+
273+
it('should support deletion with composite keys', async () => {
274+
const userProject = new UserProject('user999', 'project999', 'temp');
275+
await db.UserProject.create(userProject);
276+
277+
// Verify it exists
278+
let retrieved = await db.UserProject.read(['user999', 'project999']);
279+
expect(retrieved).toBeDefined();
280+
281+
// Delete it
282+
await db.UserProject.delete(['user999', 'project999']);
283+
284+
// Verify it's gone
285+
retrieved = await db.UserProject.read(['user999', 'project999']);
286+
expect(retrieved).toBeUndefined();
287+
});
288+
289+
it('should support index queries on composite key entities', async () => {
290+
// Find by role index
291+
const developers = await db.UserProject.findByIndex('role', 'developer');
292+
expect(developers.length).toBeGreaterThanOrEqual(1);
293+
expect(developers.every((up: any) => up.role === 'developer')).toBe(true);
294+
});
295+
});
296+
297+
describe('Traditional single keys (backward compatibility)', () => {
298+
it('should still work with traditional single key syntax', async () => {
299+
const entity = new TraditionalEntity('trad_1', 'Traditional Entity');
300+
301+
await db.TraditionalEntity.create(entity);
302+
303+
const retrieved = await db.TraditionalEntity.read('trad_1');
304+
expect(retrieved).toEqual(entity);
305+
});
306+
});
307+
308+
describe('Key generation utilities', () => {
309+
it('should provide access to key generators', async () => {
310+
expect(KeyGenerators.uuid).toBeDefined();
311+
expect(KeyGenerators.timestamp).toBeDefined();
312+
expect(KeyGenerators.random).toBeDefined();
313+
314+
const uuid = KeyGenerators.uuid();
315+
const timestamp = KeyGenerators.timestamp();
316+
const random = KeyGenerators.random();
317+
318+
expect(typeof uuid).toBe('string');
319+
expect(typeof timestamp).toBe('number');
320+
expect(typeof random).toBe('string');
321+
});
322+
});
323+
324+
describe('Error handling', () => {
325+
it('should handle missing composite key parts gracefully', async () => {
326+
try {
327+
await db.UserProject.read(['user123']); // Missing projectId
328+
// Should either work or throw a clear error
329+
} catch (error) {
330+
expect(error).toBeDefined();
331+
}
332+
});
333+
});
334+
});

0 commit comments

Comments
 (0)