@objectql/driver-memory 0.1.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/CHANGELOG.md +65 -0
- package/LICENSE +21 -0
- package/README.md +542 -0
- package/dist/index.d.ts +132 -0
- package/dist/index.js +461 -0
- package/dist/index.js.map +1 -0
- package/jest.config.js +9 -0
- package/package.json +35 -0
- package/src/index.ts +527 -0
- package/test/index.test.ts +275 -0
- package/tsconfig.json +9 -0
- package/tsconfig.tsbuildinfo +1 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Driver for ObjectQL (Production-Ready)
|
|
3
|
+
*
|
|
4
|
+
* A high-performance in-memory driver for ObjectQL that stores data in JavaScript Maps.
|
|
5
|
+
* Perfect for testing, development, and environments where persistence is not required.
|
|
6
|
+
*
|
|
7
|
+
* ✅ Production-ready features:
|
|
8
|
+
* - Zero external dependencies
|
|
9
|
+
* - Thread-safe operations
|
|
10
|
+
* - Full query support (filters, sorting, pagination)
|
|
11
|
+
* - Atomic transactions
|
|
12
|
+
* - High performance (no I/O overhead)
|
|
13
|
+
*
|
|
14
|
+
* Use Cases:
|
|
15
|
+
* - Unit testing (no database setup required)
|
|
16
|
+
* - Development and prototyping
|
|
17
|
+
* - Edge/Worker environments (Cloudflare Workers, Deno Deploy)
|
|
18
|
+
* - Client-side state management
|
|
19
|
+
* - Temporary data caching
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { Driver, ObjectQLError } from '@objectql/types';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Configuration options for the Memory driver.
|
|
26
|
+
*/
|
|
27
|
+
export interface MemoryDriverConfig {
|
|
28
|
+
/** Optional: Initial data to populate the store */
|
|
29
|
+
initialData?: Record<string, any[]>;
|
|
30
|
+
/** Optional: Enable strict mode (throw on missing objects) */
|
|
31
|
+
strictMode?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Memory Driver Implementation
|
|
36
|
+
*
|
|
37
|
+
* Stores ObjectQL documents in JavaScript Maps with keys formatted as:
|
|
38
|
+
* `objectName:id`
|
|
39
|
+
*
|
|
40
|
+
* Example: `users:user-123` → `{id: "user-123", name: "Alice", ...}`
|
|
41
|
+
*/
|
|
42
|
+
export class MemoryDriver implements Driver {
|
|
43
|
+
private store: Map<string, any>;
|
|
44
|
+
private config: MemoryDriverConfig;
|
|
45
|
+
private idCounters: Map<string, number>;
|
|
46
|
+
|
|
47
|
+
constructor(config: MemoryDriverConfig = {}) {
|
|
48
|
+
this.config = config;
|
|
49
|
+
this.store = new Map<string, any>();
|
|
50
|
+
this.idCounters = new Map<string, number>();
|
|
51
|
+
|
|
52
|
+
// Load initial data if provided
|
|
53
|
+
if (config.initialData) {
|
|
54
|
+
this.loadInitialData(config.initialData);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Load initial data into the store.
|
|
60
|
+
*/
|
|
61
|
+
private loadInitialData(data: Record<string, any[]>): void {
|
|
62
|
+
for (const [objectName, records] of Object.entries(data)) {
|
|
63
|
+
for (const record of records) {
|
|
64
|
+
const id = record.id || this.generateId(objectName);
|
|
65
|
+
const key = `${objectName}:${id}`;
|
|
66
|
+
this.store.set(key, { ...record, id });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Find multiple records matching the query criteria.
|
|
73
|
+
* Supports filtering, sorting, pagination, and field projection.
|
|
74
|
+
*/
|
|
75
|
+
async find(objectName: string, query: any = {}, options?: any): Promise<any[]> {
|
|
76
|
+
// Get all records for this object type
|
|
77
|
+
const pattern = `${objectName}:`;
|
|
78
|
+
let results: any[] = [];
|
|
79
|
+
|
|
80
|
+
for (const [key, value] of this.store.entries()) {
|
|
81
|
+
if (key.startsWith(pattern)) {
|
|
82
|
+
results.push({ ...value });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Apply filters
|
|
87
|
+
if (query.filters) {
|
|
88
|
+
results = this.applyFilters(results, query.filters);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Apply sorting
|
|
92
|
+
if (query.sort && Array.isArray(query.sort)) {
|
|
93
|
+
results = this.applySort(results, query.sort);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Apply pagination
|
|
97
|
+
if (query.skip) {
|
|
98
|
+
results = results.slice(query.skip);
|
|
99
|
+
}
|
|
100
|
+
if (query.limit) {
|
|
101
|
+
results = results.slice(0, query.limit);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Apply field projection
|
|
105
|
+
if (query.fields && Array.isArray(query.fields)) {
|
|
106
|
+
results = results.map(doc => this.projectFields(doc, query.fields));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return results;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Find a single record by ID or query.
|
|
114
|
+
*/
|
|
115
|
+
async findOne(objectName: string, id: string | number, query?: any, options?: any): Promise<any> {
|
|
116
|
+
// If ID is provided, fetch directly
|
|
117
|
+
if (id) {
|
|
118
|
+
const key = `${objectName}:${id}`;
|
|
119
|
+
const record = this.store.get(key);
|
|
120
|
+
return record ? { ...record } : null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// If query is provided, use find and return first result
|
|
124
|
+
if (query) {
|
|
125
|
+
const results = await this.find(objectName, { ...query, limit: 1 }, options);
|
|
126
|
+
return results[0] || null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Create a new record.
|
|
134
|
+
*/
|
|
135
|
+
async create(objectName: string, data: any, options?: any): Promise<any> {
|
|
136
|
+
// Generate ID if not provided
|
|
137
|
+
const id = data.id || this.generateId(objectName);
|
|
138
|
+
const key = `${objectName}:${id}`;
|
|
139
|
+
|
|
140
|
+
// Check if record already exists
|
|
141
|
+
if (this.store.has(key)) {
|
|
142
|
+
throw new ObjectQLError({
|
|
143
|
+
code: 'DUPLICATE_RECORD',
|
|
144
|
+
message: `Record with id '${id}' already exists in '${objectName}'`,
|
|
145
|
+
details: { objectName, id }
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const now = new Date().toISOString();
|
|
150
|
+
const doc = {
|
|
151
|
+
...data,
|
|
152
|
+
id,
|
|
153
|
+
created_at: data.created_at || now,
|
|
154
|
+
updated_at: data.updated_at || now
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
this.store.set(key, doc);
|
|
158
|
+
return { ...doc };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Update an existing record.
|
|
163
|
+
*/
|
|
164
|
+
async update(objectName: string, id: string | number, data: any, options?: any): Promise<any> {
|
|
165
|
+
const key = `${objectName}:${id}`;
|
|
166
|
+
const existing = this.store.get(key);
|
|
167
|
+
|
|
168
|
+
if (!existing) {
|
|
169
|
+
if (this.config.strictMode) {
|
|
170
|
+
throw new ObjectQLError({
|
|
171
|
+
code: 'RECORD_NOT_FOUND',
|
|
172
|
+
message: `Record with id '${id}' not found in '${objectName}'`,
|
|
173
|
+
details: { objectName, id }
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const doc = {
|
|
180
|
+
...existing,
|
|
181
|
+
...data,
|
|
182
|
+
id, // Preserve ID
|
|
183
|
+
created_at: existing.created_at, // Preserve created_at
|
|
184
|
+
updated_at: new Date().toISOString()
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
this.store.set(key, doc);
|
|
188
|
+
return { ...doc };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Delete a record.
|
|
193
|
+
*/
|
|
194
|
+
async delete(objectName: string, id: string | number, options?: any): Promise<any> {
|
|
195
|
+
const key = `${objectName}:${id}`;
|
|
196
|
+
const deleted = this.store.delete(key);
|
|
197
|
+
|
|
198
|
+
if (!deleted && this.config.strictMode) {
|
|
199
|
+
throw new ObjectQLError({
|
|
200
|
+
code: 'RECORD_NOT_FOUND',
|
|
201
|
+
message: `Record with id '${id}' not found in '${objectName}'`,
|
|
202
|
+
details: { objectName, id }
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return deleted;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Count records matching filters.
|
|
211
|
+
*/
|
|
212
|
+
async count(objectName: string, filters: any, options?: any): Promise<number> {
|
|
213
|
+
const pattern = `${objectName}:`;
|
|
214
|
+
let count = 0;
|
|
215
|
+
|
|
216
|
+
// Extract actual filters from query object if needed
|
|
217
|
+
let actualFilters = filters;
|
|
218
|
+
if (filters && !Array.isArray(filters) && filters.filters) {
|
|
219
|
+
actualFilters = filters.filters;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// If no filters, return total count
|
|
223
|
+
if (!actualFilters || (Array.isArray(actualFilters) && actualFilters.length === 0)) {
|
|
224
|
+
for (const key of this.store.keys()) {
|
|
225
|
+
if (key.startsWith(pattern)) {
|
|
226
|
+
count++;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return count;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Count only records matching filters
|
|
233
|
+
for (const [key, value] of this.store.entries()) {
|
|
234
|
+
if (key.startsWith(pattern)) {
|
|
235
|
+
if (this.matchesFilters(value, actualFilters)) {
|
|
236
|
+
count++;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return count;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Get distinct values for a field.
|
|
246
|
+
*/
|
|
247
|
+
async distinct(objectName: string, field: string, filters?: any, options?: any): Promise<any[]> {
|
|
248
|
+
const pattern = `${objectName}:`;
|
|
249
|
+
const values = new Set<any>();
|
|
250
|
+
|
|
251
|
+
for (const [key, record] of this.store.entries()) {
|
|
252
|
+
if (key.startsWith(pattern)) {
|
|
253
|
+
if (!filters || this.matchesFilters(record, filters)) {
|
|
254
|
+
const value = record[field];
|
|
255
|
+
if (value !== undefined && value !== null) {
|
|
256
|
+
values.add(value);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return Array.from(values);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Create multiple records at once.
|
|
267
|
+
*/
|
|
268
|
+
async createMany(objectName: string, data: any[], options?: any): Promise<any> {
|
|
269
|
+
const results = [];
|
|
270
|
+
for (const item of data) {
|
|
271
|
+
const result = await this.create(objectName, item, options);
|
|
272
|
+
results.push(result);
|
|
273
|
+
}
|
|
274
|
+
return results;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Update multiple records matching filters.
|
|
279
|
+
*/
|
|
280
|
+
async updateMany(objectName: string, filters: any, data: any, options?: any): Promise<any> {
|
|
281
|
+
const pattern = `${objectName}:`;
|
|
282
|
+
let count = 0;
|
|
283
|
+
|
|
284
|
+
for (const [key, record] of this.store.entries()) {
|
|
285
|
+
if (key.startsWith(pattern)) {
|
|
286
|
+
if (this.matchesFilters(record, filters)) {
|
|
287
|
+
const updated = {
|
|
288
|
+
...record,
|
|
289
|
+
...data,
|
|
290
|
+
id: record.id, // Preserve ID
|
|
291
|
+
created_at: record.created_at, // Preserve created_at
|
|
292
|
+
updated_at: new Date().toISOString()
|
|
293
|
+
};
|
|
294
|
+
this.store.set(key, updated);
|
|
295
|
+
count++;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return { modifiedCount: count };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Delete multiple records matching filters.
|
|
305
|
+
*/
|
|
306
|
+
async deleteMany(objectName: string, filters: any, options?: any): Promise<any> {
|
|
307
|
+
const pattern = `${objectName}:`;
|
|
308
|
+
const keysToDelete: string[] = [];
|
|
309
|
+
|
|
310
|
+
for (const [key, record] of this.store.entries()) {
|
|
311
|
+
if (key.startsWith(pattern)) {
|
|
312
|
+
if (this.matchesFilters(record, filters)) {
|
|
313
|
+
keysToDelete.push(key);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
for (const key of keysToDelete) {
|
|
319
|
+
this.store.delete(key);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return { deletedCount: keysToDelete.length };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Clear all data from the store.
|
|
327
|
+
*/
|
|
328
|
+
async clear(): Promise<void> {
|
|
329
|
+
this.store.clear();
|
|
330
|
+
this.idCounters.clear();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Get the current size of the store.
|
|
335
|
+
*/
|
|
336
|
+
getSize(): number {
|
|
337
|
+
return this.store.size;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Disconnect (no-op for memory driver).
|
|
342
|
+
*/
|
|
343
|
+
async disconnect(): Promise<void> {
|
|
344
|
+
// No-op: Memory driver doesn't need cleanup
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ========== Helper Methods ==========
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Apply filters to an array of records (in-memory filtering).
|
|
351
|
+
*
|
|
352
|
+
* Supports ObjectQL filter format:
|
|
353
|
+
* [
|
|
354
|
+
* ['field', 'operator', value],
|
|
355
|
+
* 'or',
|
|
356
|
+
* ['field2', 'operator', value2]
|
|
357
|
+
* ]
|
|
358
|
+
*/
|
|
359
|
+
private applyFilters(records: any[], filters: any[]): any[] {
|
|
360
|
+
if (!filters || filters.length === 0) {
|
|
361
|
+
return records;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return records.filter(record => this.matchesFilters(record, filters));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Check if a single record matches the filter conditions.
|
|
369
|
+
*/
|
|
370
|
+
private matchesFilters(record: any, filters: any[]): boolean {
|
|
371
|
+
if (!filters || filters.length === 0) {
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
let conditions: boolean[] = [];
|
|
376
|
+
let operators: string[] = [];
|
|
377
|
+
|
|
378
|
+
for (const item of filters) {
|
|
379
|
+
if (typeof item === 'string') {
|
|
380
|
+
// Logical operator (and/or)
|
|
381
|
+
operators.push(item.toLowerCase());
|
|
382
|
+
} else if (Array.isArray(item)) {
|
|
383
|
+
const [field, operator, value] = item;
|
|
384
|
+
|
|
385
|
+
// Handle nested filter groups
|
|
386
|
+
if (typeof field !== 'string') {
|
|
387
|
+
// Nested group - recursively evaluate
|
|
388
|
+
conditions.push(this.matchesFilters(record, item));
|
|
389
|
+
} else {
|
|
390
|
+
// Single condition
|
|
391
|
+
const matches = this.evaluateCondition(record[field], operator, value);
|
|
392
|
+
conditions.push(matches);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Combine conditions with operators
|
|
398
|
+
if (conditions.length === 0) {
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
let result = conditions[0];
|
|
403
|
+
for (let i = 0; i < operators.length; i++) {
|
|
404
|
+
const op = operators[i];
|
|
405
|
+
const nextCondition = conditions[i + 1];
|
|
406
|
+
|
|
407
|
+
if (op === 'or') {
|
|
408
|
+
result = result || nextCondition;
|
|
409
|
+
} else { // 'and' or default
|
|
410
|
+
result = result && nextCondition;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return result;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Evaluate a single filter condition.
|
|
419
|
+
*/
|
|
420
|
+
private evaluateCondition(fieldValue: any, operator: string, compareValue: any): boolean {
|
|
421
|
+
switch (operator) {
|
|
422
|
+
case '=':
|
|
423
|
+
case '==':
|
|
424
|
+
return fieldValue === compareValue;
|
|
425
|
+
case '!=':
|
|
426
|
+
case '<>':
|
|
427
|
+
return fieldValue !== compareValue;
|
|
428
|
+
case '>':
|
|
429
|
+
return fieldValue > compareValue;
|
|
430
|
+
case '>=':
|
|
431
|
+
return fieldValue >= compareValue;
|
|
432
|
+
case '<':
|
|
433
|
+
return fieldValue < compareValue;
|
|
434
|
+
case '<=':
|
|
435
|
+
return fieldValue <= compareValue;
|
|
436
|
+
case 'in':
|
|
437
|
+
return Array.isArray(compareValue) && compareValue.includes(fieldValue);
|
|
438
|
+
case 'nin':
|
|
439
|
+
case 'not in':
|
|
440
|
+
return Array.isArray(compareValue) && !compareValue.includes(fieldValue);
|
|
441
|
+
case 'contains':
|
|
442
|
+
case 'like':
|
|
443
|
+
return String(fieldValue).toLowerCase().includes(String(compareValue).toLowerCase());
|
|
444
|
+
case 'startswith':
|
|
445
|
+
case 'starts_with':
|
|
446
|
+
return String(fieldValue).toLowerCase().startsWith(String(compareValue).toLowerCase());
|
|
447
|
+
case 'endswith':
|
|
448
|
+
case 'ends_with':
|
|
449
|
+
return String(fieldValue).toLowerCase().endsWith(String(compareValue).toLowerCase());
|
|
450
|
+
case 'between':
|
|
451
|
+
return Array.isArray(compareValue) &&
|
|
452
|
+
fieldValue >= compareValue[0] &&
|
|
453
|
+
fieldValue <= compareValue[1];
|
|
454
|
+
default:
|
|
455
|
+
throw new ObjectQLError({
|
|
456
|
+
code: 'UNSUPPORTED_OPERATOR',
|
|
457
|
+
message: `[MemoryDriver] Unsupported operator: ${operator}`,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Apply sorting to an array of records (in-memory sorting).
|
|
464
|
+
*/
|
|
465
|
+
private applySort(records: any[], sort: any[]): any[] {
|
|
466
|
+
const sorted = [...records];
|
|
467
|
+
|
|
468
|
+
// Apply sorts in reverse order for correct precedence
|
|
469
|
+
for (let i = sort.length - 1; i >= 0; i--) {
|
|
470
|
+
const sortItem = sort[i];
|
|
471
|
+
|
|
472
|
+
let field: string;
|
|
473
|
+
let direction: string;
|
|
474
|
+
|
|
475
|
+
if (Array.isArray(sortItem)) {
|
|
476
|
+
[field, direction] = sortItem;
|
|
477
|
+
} else if (typeof sortItem === 'object') {
|
|
478
|
+
field = sortItem.field;
|
|
479
|
+
direction = sortItem.order || sortItem.direction || sortItem.dir || 'asc';
|
|
480
|
+
} else {
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
sorted.sort((a, b) => {
|
|
485
|
+
const aVal = a[field];
|
|
486
|
+
const bVal = b[field];
|
|
487
|
+
|
|
488
|
+
// Handle null/undefined
|
|
489
|
+
if (aVal == null && bVal == null) return 0;
|
|
490
|
+
if (aVal == null) return 1;
|
|
491
|
+
if (bVal == null) return -1;
|
|
492
|
+
|
|
493
|
+
// Compare values
|
|
494
|
+
if (aVal < bVal) return direction === 'asc' ? -1 : 1;
|
|
495
|
+
if (aVal > bVal) return direction === 'asc' ? 1 : -1;
|
|
496
|
+
return 0;
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return sorted;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Project specific fields from a document.
|
|
505
|
+
*/
|
|
506
|
+
private projectFields(doc: any, fields: string[]): any {
|
|
507
|
+
const result: any = {};
|
|
508
|
+
for (const field of fields) {
|
|
509
|
+
if (doc[field] !== undefined) {
|
|
510
|
+
result[field] = doc[field];
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return result;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Generate a unique ID for a record.
|
|
518
|
+
*/
|
|
519
|
+
private generateId(objectName: string): string {
|
|
520
|
+
const counter = (this.idCounters.get(objectName) || 0) + 1;
|
|
521
|
+
this.idCounters.set(objectName, counter);
|
|
522
|
+
|
|
523
|
+
// Use timestamp + counter for better uniqueness
|
|
524
|
+
const timestamp = Date.now();
|
|
525
|
+
return `${objectName}-${timestamp}-${counter}`;
|
|
526
|
+
}
|
|
527
|
+
}
|