@objectql/driver-excel 0.2.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.
@@ -0,0 +1,566 @@
1
+ /**
2
+ * Excel Driver Tests
3
+ *
4
+ * Comprehensive test suite for the Excel ObjectQL driver.
5
+ */
6
+
7
+ import { ExcelDriver } from '../src';
8
+ import * as fs from 'fs';
9
+ import * as path from 'path';
10
+
11
+ describe('ExcelDriver', () => {
12
+ const TEST_DIR = path.join(__dirname, 'test-files');
13
+ const TEST_FILE = path.join(TEST_DIR, 'test-data.xlsx');
14
+ const TEST_OBJECT = 'test_users';
15
+ let driver: ExcelDriver;
16
+
17
+ beforeAll(() => {
18
+ // Create test directory
19
+ if (!fs.existsSync(TEST_DIR)) {
20
+ fs.mkdirSync(TEST_DIR, { recursive: true });
21
+ }
22
+ });
23
+
24
+ beforeEach(async () => {
25
+ // Clean up test file
26
+ if (fs.existsSync(TEST_FILE)) {
27
+ fs.unlinkSync(TEST_FILE);
28
+ }
29
+
30
+ // Create new driver using factory method
31
+ driver = await ExcelDriver.create({
32
+ filePath: TEST_FILE,
33
+ createIfMissing: true,
34
+ autoSave: true
35
+ });
36
+ });
37
+
38
+ afterEach(async () => {
39
+ await driver.disconnect();
40
+ });
41
+
42
+ afterAll(() => {
43
+ // Clean up test directory
44
+ if (fs.existsSync(TEST_DIR)) {
45
+ fs.rmSync(TEST_DIR, { recursive: true, force: true });
46
+ }
47
+ });
48
+
49
+ describe('Initialization', () => {
50
+ it('should create a new Excel file if it does not exist', () => {
51
+ expect(fs.existsSync(TEST_FILE)).toBe(true);
52
+ });
53
+
54
+ it('should load an existing Excel file', async () => {
55
+ // Create some data
56
+ await driver.create(TEST_OBJECT, {
57
+ name: 'Alice',
58
+ email: 'alice@example.com'
59
+ });
60
+
61
+ // Create new driver instance with same file
62
+ const driver2 = await ExcelDriver.create({
63
+ filePath: TEST_FILE,
64
+ createIfMissing: false
65
+ });
66
+
67
+ const results = await driver2.find(TEST_OBJECT);
68
+ expect(results.length).toBe(1);
69
+ expect(results[0].name).toBe('Alice');
70
+
71
+ await driver2.disconnect();
72
+ });
73
+
74
+ it('should throw error if file does not exist and createIfMissing is false', async () => {
75
+ const nonExistentFile = path.join(TEST_DIR, 'non-existent.xlsx');
76
+
77
+ await expect(
78
+ ExcelDriver.create({
79
+ filePath: nonExistentFile,
80
+ createIfMissing: false
81
+ })
82
+ ).rejects.toThrow('Excel file not found');
83
+ });
84
+
85
+ it('should support strict mode', async () => {
86
+ const strictDriver = await ExcelDriver.create({
87
+ filePath: path.join(TEST_DIR, 'strict-test.xlsx'),
88
+ strictMode: true,
89
+ createIfMissing: true
90
+ });
91
+
92
+ await expect(
93
+ strictDriver.update(TEST_OBJECT, 'non-existent', { name: 'Test' })
94
+ ).rejects.toThrow('Record with id \'non-existent\' not found');
95
+
96
+ await strictDriver.disconnect();
97
+ });
98
+ });
99
+
100
+ describe('CRUD Operations', () => {
101
+ it('should create a record', async () => {
102
+ const result = await driver.create(TEST_OBJECT, {
103
+ name: 'Alice',
104
+ email: 'alice@example.com',
105
+ role: 'admin'
106
+ });
107
+
108
+ expect(result).toHaveProperty('id');
109
+ expect(result.name).toBe('Alice');
110
+ expect(result.email).toBe('alice@example.com');
111
+ expect(result).toHaveProperty('created_at');
112
+ expect(result).toHaveProperty('updated_at');
113
+ });
114
+
115
+ it('should create a record with custom ID', async () => {
116
+ const result = await driver.create(TEST_OBJECT, {
117
+ id: 'custom-123',
118
+ name: 'Bob',
119
+ email: 'bob@example.com'
120
+ });
121
+
122
+ expect(result.id).toBe('custom-123');
123
+ expect(result.name).toBe('Bob');
124
+ });
125
+
126
+ it('should throw error on duplicate ID', async () => {
127
+ await driver.create(TEST_OBJECT, {
128
+ id: 'test-1',
129
+ name: 'Alice'
130
+ });
131
+
132
+ await expect(
133
+ driver.create(TEST_OBJECT, {
134
+ id: 'test-1',
135
+ name: 'Bob'
136
+ })
137
+ ).rejects.toThrow('Record with id \'test-1\' already exists');
138
+ });
139
+
140
+ it('should find a record by ID', async () => {
141
+ const created = await driver.create(TEST_OBJECT, {
142
+ name: 'Alice',
143
+ email: 'alice@example.com'
144
+ });
145
+
146
+ const found = await driver.findOne(TEST_OBJECT, created.id);
147
+ expect(found).toBeDefined();
148
+ expect(found.name).toBe('Alice');
149
+ expect(found.email).toBe('alice@example.com');
150
+ });
151
+
152
+ it('should return null for non-existent ID', async () => {
153
+ const result = await driver.findOne(TEST_OBJECT, 'non-existent-id');
154
+ expect(result).toBeNull();
155
+ });
156
+
157
+ it('should update a record', async () => {
158
+ const created = await driver.create(TEST_OBJECT, {
159
+ name: 'Alice',
160
+ email: 'alice@example.com'
161
+ });
162
+
163
+ // Small delay to ensure updated_at timestamp differs from created_at
164
+ // Note: Using real setTimeout here is intentional to test actual timestamp behavior
165
+ await new Promise(resolve => setTimeout(resolve, 10));
166
+
167
+ const updated = await driver.update(TEST_OBJECT, created.id, {
168
+ email: 'alice.new@example.com'
169
+ });
170
+
171
+ expect(updated.email).toBe('alice.new@example.com');
172
+ expect(updated.name).toBe('Alice'); // Unchanged
173
+ expect(updated.created_at).toBe(created.created_at); // Preserved
174
+ expect(updated.updated_at).not.toBe(created.updated_at); // Changed
175
+ });
176
+
177
+ it('should delete a record', async () => {
178
+ const created = await driver.create(TEST_OBJECT, {
179
+ name: 'Alice'
180
+ });
181
+
182
+ const deleted = await driver.delete(TEST_OBJECT, created.id);
183
+ expect(deleted).toBe(true);
184
+
185
+ const found = await driver.findOne(TEST_OBJECT, created.id);
186
+ expect(found).toBeNull();
187
+ });
188
+
189
+ it('should return false when deleting non-existent record', async () => {
190
+ const deleted = await driver.delete(TEST_OBJECT, 'non-existent');
191
+ expect(deleted).toBe(false);
192
+ });
193
+ });
194
+
195
+ describe('Query Operations', () => {
196
+ beforeEach(async () => {
197
+ // Create test data
198
+ await driver.create(TEST_OBJECT, {
199
+ id: '1',
200
+ name: 'Alice',
201
+ email: 'alice@example.com',
202
+ role: 'admin',
203
+ age: 30
204
+ });
205
+ await driver.create(TEST_OBJECT, {
206
+ id: '2',
207
+ name: 'Bob',
208
+ email: 'bob@example.com',
209
+ role: 'user',
210
+ age: 25
211
+ });
212
+ await driver.create(TEST_OBJECT, {
213
+ id: '3',
214
+ name: 'Charlie',
215
+ email: 'charlie@example.com',
216
+ role: 'user',
217
+ age: 35
218
+ });
219
+ });
220
+
221
+ it('should find all records', async () => {
222
+ const results = await driver.find(TEST_OBJECT);
223
+ expect(results.length).toBe(3);
224
+ });
225
+
226
+ it('should filter records with equality operator', async () => {
227
+ const results = await driver.find(TEST_OBJECT, {
228
+ filters: [['role', '=', 'user']]
229
+ });
230
+ expect(results.length).toBe(2);
231
+ expect(results.every(r => r.role === 'user')).toBe(true);
232
+ });
233
+
234
+ it('should filter records with greater than operator', async () => {
235
+ const results = await driver.find(TEST_OBJECT, {
236
+ filters: [['age', '>', 25]]
237
+ });
238
+ expect(results.length).toBe(2);
239
+ expect(results.every(r => r.age > 25)).toBe(true);
240
+ });
241
+
242
+ it('should filter records with contains operator', async () => {
243
+ const results = await driver.find(TEST_OBJECT, {
244
+ filters: [['name', 'contains', 'li']]
245
+ });
246
+ expect(results.length).toBe(2); // Alice and Charlie
247
+ });
248
+
249
+ it('should support OR filters', async () => {
250
+ const results = await driver.find(TEST_OBJECT, {
251
+ filters: [
252
+ ['name', '=', 'Alice'],
253
+ 'or',
254
+ ['name', '=', 'Bob']
255
+ ]
256
+ });
257
+ expect(results.length).toBe(2);
258
+ });
259
+
260
+ it('should sort records ascending', async () => {
261
+ const results = await driver.find(TEST_OBJECT, {
262
+ sort: [['age', 'asc']]
263
+ });
264
+ expect(results[0].age).toBe(25);
265
+ expect(results[1].age).toBe(30);
266
+ expect(results[2].age).toBe(35);
267
+ });
268
+
269
+ it('should sort records descending', async () => {
270
+ const results = await driver.find(TEST_OBJECT, {
271
+ sort: [['age', 'desc']]
272
+ });
273
+ expect(results[0].age).toBe(35);
274
+ expect(results[1].age).toBe(30);
275
+ expect(results[2].age).toBe(25);
276
+ });
277
+
278
+ it('should support pagination with limit', async () => {
279
+ const results = await driver.find(TEST_OBJECT, {
280
+ limit: 2
281
+ });
282
+ expect(results.length).toBe(2);
283
+ });
284
+
285
+ it('should support pagination with skip', async () => {
286
+ const results = await driver.find(TEST_OBJECT, {
287
+ skip: 1,
288
+ limit: 2
289
+ });
290
+ expect(results.length).toBe(2);
291
+ });
292
+
293
+ it('should project specific fields', async () => {
294
+ const results = await driver.find(TEST_OBJECT, {
295
+ fields: ['name', 'email']
296
+ });
297
+ expect(results[0]).toHaveProperty('name');
298
+ expect(results[0]).toHaveProperty('email');
299
+ expect(results[0]).not.toHaveProperty('role');
300
+ expect(results[0]).not.toHaveProperty('age');
301
+ });
302
+
303
+ it('should count all records', async () => {
304
+ const count = await driver.count(TEST_OBJECT, {});
305
+ expect(count).toBe(3);
306
+ });
307
+
308
+ it('should count filtered records', async () => {
309
+ const count = await driver.count(TEST_OBJECT, {
310
+ filters: [['role', '=', 'user']]
311
+ });
312
+ expect(count).toBe(2);
313
+ });
314
+
315
+ it('should get distinct values', async () => {
316
+ const roles = await driver.distinct(TEST_OBJECT, 'role');
317
+ expect(roles).toHaveLength(2);
318
+ expect(roles).toContain('admin');
319
+ expect(roles).toContain('user');
320
+ });
321
+ });
322
+
323
+ describe('Bulk Operations', () => {
324
+ it('should create multiple records', async () => {
325
+ const results = await driver.createMany(TEST_OBJECT, [
326
+ { name: 'Alice', email: 'alice@example.com' },
327
+ { name: 'Bob', email: 'bob@example.com' }
328
+ ]);
329
+
330
+ expect(results).toHaveLength(2);
331
+ expect(results[0].name).toBe('Alice');
332
+ expect(results[1].name).toBe('Bob');
333
+ });
334
+
335
+ it('should update multiple records', async () => {
336
+ await driver.create(TEST_OBJECT, { id: '1', name: 'Alice', role: 'user' });
337
+ await driver.create(TEST_OBJECT, { id: '2', name: 'Bob', role: 'user' });
338
+ await driver.create(TEST_OBJECT, { id: '3', name: 'Charlie', role: 'admin' });
339
+
340
+ const result = await driver.updateMany(
341
+ TEST_OBJECT,
342
+ [['role', '=', 'user']],
343
+ { role: 'member' }
344
+ );
345
+
346
+ expect(result.modifiedCount).toBe(2);
347
+
348
+ const users = await driver.find(TEST_OBJECT, {
349
+ filters: [['role', '=', 'member']]
350
+ });
351
+ expect(users).toHaveLength(2);
352
+ });
353
+
354
+ it('should delete multiple records', async () => {
355
+ await driver.create(TEST_OBJECT, { id: '1', name: 'Alice', role: 'user' });
356
+ await driver.create(TEST_OBJECT, { id: '2', name: 'Bob', role: 'user' });
357
+ await driver.create(TEST_OBJECT, { id: '3', name: 'Charlie', role: 'admin' });
358
+
359
+ const result = await driver.deleteMany(
360
+ TEST_OBJECT,
361
+ [['role', '=', 'user']]
362
+ );
363
+
364
+ expect(result.deletedCount).toBe(2);
365
+
366
+ const remaining = await driver.find(TEST_OBJECT);
367
+ expect(remaining).toHaveLength(1);
368
+ expect(remaining[0].role).toBe('admin');
369
+ });
370
+ });
371
+
372
+ describe('File Persistence', () => {
373
+ it('should persist data to file', async () => {
374
+ await driver.create(TEST_OBJECT, {
375
+ name: 'Alice',
376
+ email: 'alice@example.com'
377
+ });
378
+
379
+ // Disconnect to ensure flush
380
+ await driver.disconnect();
381
+
382
+ // Verify file exists and has content
383
+ expect(fs.existsSync(TEST_FILE)).toBe(true);
384
+ const stats = fs.statSync(TEST_FILE);
385
+ expect(stats.size).toBeGreaterThan(0);
386
+ });
387
+
388
+ it('should load persisted data from file', async () => {
389
+ // Create and save data
390
+ await driver.create(TEST_OBJECT, {
391
+ id: 'persist-1',
392
+ name: 'Alice',
393
+ email: 'alice@example.com'
394
+ });
395
+ await driver.save();
396
+
397
+ // Create new driver instance
398
+ const driver2 = await ExcelDriver.create({
399
+ filePath: TEST_FILE,
400
+ createIfMissing: false
401
+ });
402
+
403
+ const found = await driver2.findOne(TEST_OBJECT, 'persist-1');
404
+ expect(found).toBeDefined();
405
+ expect(found.name).toBe('Alice');
406
+ expect(found.email).toBe('alice@example.com');
407
+
408
+ await driver2.disconnect();
409
+ });
410
+
411
+ it('should support multiple worksheets (object types)', async () => {
412
+ await driver.create('users', { name: 'Alice' });
413
+ await driver.create('products', { name: 'Product A' });
414
+
415
+ const users = await driver.find('users');
416
+ const products = await driver.find('products');
417
+
418
+ expect(users).toHaveLength(1);
419
+ expect(products).toHaveLength(1);
420
+ expect(users[0].name).toBe('Alice');
421
+ expect(products[0].name).toBe('Product A');
422
+ });
423
+
424
+ it('should handle autoSave disabled', async () => {
425
+ const manualDriver = await ExcelDriver.create({
426
+ filePath: path.join(TEST_DIR, 'manual-save.xlsx'),
427
+ autoSave: false,
428
+ createIfMissing: true
429
+ });
430
+
431
+ await manualDriver.create(TEST_OBJECT, {
432
+ name: 'Alice'
433
+ });
434
+
435
+ // Manually save
436
+ await manualDriver.save();
437
+
438
+ // Verify persistence
439
+ const driver2 = await ExcelDriver.create({
440
+ filePath: path.join(TEST_DIR, 'manual-save.xlsx'),
441
+ createIfMissing: false
442
+ });
443
+
444
+ const results = await driver2.find(TEST_OBJECT);
445
+ expect(results).toHaveLength(1);
446
+
447
+ await manualDriver.disconnect();
448
+ await driver2.disconnect();
449
+ });
450
+ });
451
+
452
+ describe('Edge Cases', () => {
453
+ it('should handle empty result sets', async () => {
454
+ const results = await driver.find('non_existent_object');
455
+ expect(results).toEqual([]);
456
+ });
457
+
458
+ it('should handle filters on empty data', async () => {
459
+ const results = await driver.find(TEST_OBJECT, {
460
+ filters: [['name', '=', 'Alice']]
461
+ });
462
+ expect(results).toEqual([]);
463
+ });
464
+
465
+ it('should handle null and undefined values', async () => {
466
+ const result = await driver.create(TEST_OBJECT, {
467
+ name: 'Alice',
468
+ optional_field: null
469
+ });
470
+
471
+ expect(result.optional_field).toBeNull();
472
+ });
473
+
474
+ it('should handle special characters in data', async () => {
475
+ const result = await driver.create(TEST_OBJECT, {
476
+ name: 'O\'Brien',
477
+ description: 'Test "quotes" & special <chars>'
478
+ });
479
+
480
+ const found = await driver.findOne(TEST_OBJECT, result.id);
481
+ expect(found.name).toBe('O\'Brien');
482
+ expect(found.description).toBe('Test "quotes" & special <chars>');
483
+ });
484
+ });
485
+
486
+ describe('File Per Object Mode', () => {
487
+ const FILE_PER_OBJECT_DIR = path.join(TEST_DIR, 'file-per-object');
488
+ let filePerObjectDriver: ExcelDriver;
489
+
490
+ beforeEach(async () => {
491
+ // Clean up directory
492
+ if (fs.existsSync(FILE_PER_OBJECT_DIR)) {
493
+ fs.rmSync(FILE_PER_OBJECT_DIR, { recursive: true, force: true });
494
+ }
495
+
496
+ // Create driver in file-per-object mode
497
+ filePerObjectDriver = await ExcelDriver.create({
498
+ filePath: FILE_PER_OBJECT_DIR,
499
+ fileStorageMode: 'file-per-object',
500
+ createIfMissing: true,
501
+ autoSave: true
502
+ });
503
+ });
504
+
505
+ afterEach(async () => {
506
+ if (filePerObjectDriver) {
507
+ await filePerObjectDriver.disconnect();
508
+ }
509
+ });
510
+
511
+ it('should create separate files for each object type', async () => {
512
+ await filePerObjectDriver.create('users', { name: 'Alice' });
513
+ await filePerObjectDriver.create('products', { name: 'Product A' });
514
+
515
+ // Check that separate files exist
516
+ expect(fs.existsSync(path.join(FILE_PER_OBJECT_DIR, 'users.xlsx'))).toBe(true);
517
+ expect(fs.existsSync(path.join(FILE_PER_OBJECT_DIR, 'products.xlsx'))).toBe(true);
518
+ });
519
+
520
+ it('should load data from separate files', async () => {
521
+ await filePerObjectDriver.create('users', { id: 'user-1', name: 'Alice' });
522
+ await filePerObjectDriver.create('products', { id: 'prod-1', name: 'Product A' });
523
+ await filePerObjectDriver.save();
524
+
525
+ // Create new driver instance and load from files
526
+ const driver2 = await ExcelDriver.create({
527
+ filePath: FILE_PER_OBJECT_DIR,
528
+ fileStorageMode: 'file-per-object',
529
+ createIfMissing: false
530
+ });
531
+
532
+ const users = await driver2.find('users');
533
+ const products = await driver2.find('products');
534
+
535
+ expect(users).toHaveLength(1);
536
+ expect(products).toHaveLength(1);
537
+ expect(users[0].name).toBe('Alice');
538
+ expect(products[0].name).toBe('Product A');
539
+
540
+ await driver2.disconnect();
541
+ });
542
+
543
+ it('should support all CRUD operations in file-per-object mode', async () => {
544
+ // Create
545
+ const user = await filePerObjectDriver.create('users', {
546
+ name: 'Bob',
547
+ email: 'bob@example.com'
548
+ });
549
+ expect(user.name).toBe('Bob');
550
+
551
+ // Read
552
+ const found = await filePerObjectDriver.findOne('users', user.id);
553
+ expect(found.name).toBe('Bob');
554
+
555
+ // Update
556
+ await filePerObjectDriver.update('users', user.id, { email: 'bob.new@example.com' });
557
+ const updated = await filePerObjectDriver.findOne('users', user.id);
558
+ expect(updated.email).toBe('bob.new@example.com');
559
+
560
+ // Delete
561
+ await filePerObjectDriver.delete('users', user.id);
562
+ const deleted = await filePerObjectDriver.findOne('users', user.id);
563
+ expect(deleted).toBeNull();
564
+ });
565
+ });
566
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "../../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src",
6
+ "composite": true
7
+ },
8
+ "include": ["src/**/*"],
9
+ "references": [
10
+ { "path": "../../foundation/types" }
11
+ ]
12
+ }