@objectql/driver-memory 4.0.2 → 4.0.4

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.
@@ -279,4 +279,275 @@ describe('MemoryDriver', () => {
279
279
  expect(result.deletedCount).toBe(2);
280
280
  });
281
281
  });
282
+
283
+ describe('Aggregate Method', () => {
284
+ beforeEach(async () => {
285
+ // Create test data for aggregation
286
+ await driver.create('orders', { id: '1', customer: 'Alice', product: 'Laptop', amount: 1200, quantity: 1, status: 'completed' });
287
+ await driver.create('orders', { id: '2', customer: 'Bob', product: 'Mouse', amount: 25, quantity: 2, status: 'completed' });
288
+ await driver.create('orders', { id: '3', customer: 'Alice', product: 'Keyboard', amount: 75, quantity: 1, status: 'pending' });
289
+ await driver.create('orders', { id: '4', customer: 'Charlie', product: 'Monitor', amount: 350, quantity: 1, status: 'completed' });
290
+ await driver.create('orders', { id: '5', customer: 'Bob', product: 'Laptop', amount: 1200, quantity: 1, status: 'cancelled' });
291
+ });
292
+
293
+ it('should aggregate with $match stage', async () => {
294
+ const results = await driver.aggregate('orders', [
295
+ { $match: { status: 'completed' } }
296
+ ]);
297
+
298
+ expect(results).toBeDefined();
299
+ expect(results.length).toBe(3);
300
+ expect(results.every((r: any) => r.status === 'completed')).toBe(true);
301
+ });
302
+
303
+ it('should aggregate with $group and $sum', async () => {
304
+ const results = await driver.aggregate('orders', [
305
+ { $match: { status: 'completed' } },
306
+ {
307
+ $group: {
308
+ _id: '$customer',
309
+ totalAmount: { $sum: '$amount' }
310
+ }
311
+ }
312
+ ]);
313
+
314
+ expect(results).toBeDefined();
315
+ expect(results.length).toBeGreaterThan(0);
316
+
317
+ const alice = results.find((r: any) => r._id === 'Alice');
318
+ expect(alice).toBeDefined();
319
+ expect(alice.totalAmount).toBe(1200);
320
+ });
321
+
322
+ it('should aggregate with $group and $avg', async () => {
323
+ const results = await driver.aggregate('orders', [
324
+ {
325
+ $group: {
326
+ _id: null,
327
+ avgAmount: { $avg: '$amount' }
328
+ }
329
+ }
330
+ ]);
331
+
332
+ expect(results).toBeDefined();
333
+ expect(results.length).toBe(1);
334
+ expect(results[0].avgAmount).toBeCloseTo(570, 0);
335
+ });
336
+
337
+ it('should aggregate with $project stage', async () => {
338
+ const results = await driver.aggregate('orders', [
339
+ { $match: { status: 'completed' } },
340
+ {
341
+ $project: {
342
+ customer: 1,
343
+ product: 1,
344
+ total: { $multiply: ['$amount', '$quantity'] }
345
+ }
346
+ }
347
+ ]);
348
+
349
+ expect(results).toBeDefined();
350
+ expect(results.length).toBe(3);
351
+ expect(results[0]).toHaveProperty('total');
352
+ });
353
+
354
+ it('should aggregate with $sort stage', async () => {
355
+ const results = await driver.aggregate('orders', [
356
+ { $match: { status: 'completed' } },
357
+ { $sort: { amount: -1 } }
358
+ ]);
359
+
360
+ expect(results).toBeDefined();
361
+ expect(results.length).toBe(3);
362
+ expect(results[0].amount).toBe(1200);
363
+ expect(results[2].amount).toBe(25);
364
+ });
365
+ });
366
+
367
+ describe('Transaction Support', () => {
368
+ it('should begin and commit a transaction', async () => {
369
+ const tx = await driver.beginTransaction();
370
+ expect(tx).toBeDefined();
371
+ expect(tx.id).toBeDefined();
372
+
373
+ await driver.create(TEST_OBJECT, { id: '1', name: 'Alice' });
374
+ await driver.commitTransaction(tx);
375
+
376
+ const result = await driver.findOne(TEST_OBJECT, '1');
377
+ expect(result).toBeDefined();
378
+ expect(result.name).toBe('Alice');
379
+ });
380
+
381
+ it('should rollback a transaction', async () => {
382
+ await driver.create(TEST_OBJECT, { id: '1', name: 'Alice' });
383
+
384
+ const tx = await driver.beginTransaction();
385
+
386
+ await driver.update(TEST_OBJECT, '1', { name: 'Bob' });
387
+ await driver.create(TEST_OBJECT, { id: '2', name: 'Charlie' });
388
+
389
+ await driver.rollbackTransaction(tx);
390
+
391
+ const result1 = await driver.findOne(TEST_OBJECT, '1');
392
+ expect(result1.name).toBe('Alice'); // Should be reverted
393
+
394
+ const result2 = await driver.findOne(TEST_OBJECT, '2');
395
+ expect(result2).toBeNull(); // Should not exist
396
+ });
397
+
398
+ it('should handle multiple transactions', async () => {
399
+ const tx1 = await driver.beginTransaction();
400
+ const tx2 = await driver.beginTransaction();
401
+
402
+ expect(tx1.id).not.toBe(tx2.id);
403
+
404
+ await driver.commitTransaction(tx1);
405
+ await driver.commitTransaction(tx2);
406
+ });
407
+
408
+ it('should throw error for invalid transaction', async () => {
409
+ await expect(
410
+ driver.commitTransaction({ id: 'invalid-tx-id' })
411
+ ).rejects.toThrow();
412
+ });
413
+ });
414
+
415
+ describe('Indexing', () => {
416
+ it('should build indexes on initialization', () => {
417
+ const indexedDriver = new MemoryDriver({
418
+ initialData: {
419
+ users: [
420
+ { id: '1', name: 'Alice', email: 'alice@example.com', role: 'admin' },
421
+ { id: '2', name: 'Bob', email: 'bob@example.com', role: 'user' },
422
+ { id: '3', name: 'Charlie', email: 'charlie@example.com', role: 'user' }
423
+ ]
424
+ },
425
+ indexes: {
426
+ users: ['email', 'role']
427
+ }
428
+ });
429
+
430
+ expect(indexedDriver).toBeDefined();
431
+ expect(indexedDriver.getSize()).toBe(3);
432
+ });
433
+
434
+ it('should update indexes when creating records', async () => {
435
+ const indexedDriver = new MemoryDriver({
436
+ indexes: {
437
+ users: ['email']
438
+ }
439
+ });
440
+
441
+ await indexedDriver.create('users', { id: '1', name: 'Alice', email: 'alice@example.com' });
442
+ await indexedDriver.create('users', { id: '2', name: 'Bob', email: 'bob@example.com' });
443
+
444
+ const results = await indexedDriver.find('users', {});
445
+ expect(results.length).toBe(2);
446
+ });
447
+
448
+ it('should update indexes when updating records', async () => {
449
+ const indexedDriver = new MemoryDriver({
450
+ indexes: {
451
+ users: ['email']
452
+ }
453
+ });
454
+
455
+ await indexedDriver.create('users', { id: '1', name: 'Alice', email: 'alice@example.com' });
456
+ await indexedDriver.update('users', '1', { email: 'alice.new@example.com' });
457
+
458
+ const result = await indexedDriver.findOne('users', '1');
459
+ expect(result.email).toBe('alice.new@example.com');
460
+ });
461
+
462
+ it('should remove from indexes when deleting records', async () => {
463
+ const indexedDriver = new MemoryDriver({
464
+ indexes: {
465
+ users: ['email']
466
+ }
467
+ });
468
+
469
+ await indexedDriver.create('users', { id: '1', name: 'Alice', email: 'alice@example.com' });
470
+ await indexedDriver.delete('users', '1');
471
+
472
+ const result = await indexedDriver.findOne('users', '1');
473
+ expect(result).toBeNull();
474
+ });
475
+ });
476
+
477
+ describe('Persistence', () => {
478
+ const testFilePath = '/tmp/objectql-memory-test.json';
479
+
480
+ afterEach(() => {
481
+ // Clean up test file
482
+ try {
483
+ const fs = require('fs');
484
+ if (fs.existsSync(testFilePath)) {
485
+ fs.unlinkSync(testFilePath);
486
+ }
487
+ } catch (e) {
488
+ // Ignore errors
489
+ }
490
+ });
491
+
492
+ it('should save data to disk on disconnect', async () => {
493
+ const persistentDriver = new MemoryDriver({
494
+ persistence: {
495
+ filePath: testFilePath,
496
+ autoSaveInterval: 10000 // Long interval, we'll manually trigger
497
+ }
498
+ });
499
+
500
+ await persistentDriver.create('users', { id: '1', name: 'Alice' });
501
+ await persistentDriver.disconnect();
502
+
503
+ const fs = require('fs');
504
+ expect(fs.existsSync(testFilePath)).toBe(true);
505
+
506
+ const fileData = JSON.parse(fs.readFileSync(testFilePath, 'utf8'));
507
+ expect(fileData.store).toBeDefined();
508
+ expect(fileData.store['users:1']).toBeDefined();
509
+ expect(fileData.store['users:1'].name).toBe('Alice');
510
+ });
511
+
512
+ it('should load data from disk on initialization', async () => {
513
+ // First, create a driver and save data
514
+ const driver1 = new MemoryDriver({
515
+ persistence: {
516
+ filePath: testFilePath,
517
+ autoSaveInterval: 10000
518
+ }
519
+ });
520
+
521
+ await driver1.create('users', { id: '1', name: 'Alice' });
522
+ await driver1.create('users', { id: '2', name: 'Bob' });
523
+ await driver1.disconnect();
524
+
525
+ // Now create a new driver that should load the data
526
+ const driver2 = new MemoryDriver({
527
+ persistence: {
528
+ filePath: testFilePath,
529
+ autoSaveInterval: 10000
530
+ }
531
+ });
532
+
533
+ const users = await driver2.find('users', {});
534
+ expect(users.length).toBe(2);
535
+ expect(users.some(u => u.name === 'Alice')).toBe(true);
536
+ expect(users.some(u => u.name === 'Bob')).toBe(true);
537
+
538
+ await driver2.disconnect();
539
+ });
540
+
541
+ it('should handle missing persistence file gracefully', () => {
542
+ const driver = new MemoryDriver({
543
+ persistence: {
544
+ filePath: '/tmp/nonexistent-file.json',
545
+ autoSaveInterval: 10000
546
+ }
547
+ });
548
+
549
+ expect(driver).toBeDefined();
550
+ expect(driver.getSize()).toBe(0);
551
+ });
552
+ });
282
553
  });
@@ -0,0 +1,38 @@
1
+ /**
2
+ * ObjectQL Memory Driver TCK Tests
3
+ * Copyright (c) 2026-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ /**
10
+ * Memory Driver TCK (Technology Compatibility Kit) Tests
11
+ *
12
+ * This test suite verifies that the Memory driver passes all TCK requirements.
13
+ * The Memory driver serves as the reference implementation for all ObjectQL drivers.
14
+ */
15
+
16
+ import { runDriverTCK } from '@objectql/driver-tck';
17
+ import { MemoryDriver } from '../src';
18
+
19
+ describe('MemoryDriver TCK Compliance', () => {
20
+ runDriverTCK(
21
+ () => new MemoryDriver(),
22
+ {
23
+ // Memory driver supports all features
24
+ skip: {
25
+ // No features to skip - Memory driver is the reference implementation
26
+ },
27
+ timeout: 30000,
28
+ hooks: {
29
+ beforeEach: async () => {
30
+ // No special setup needed
31
+ },
32
+ afterEach: async () => {
33
+ // Cleanup handled by driver.clear() in TCK
34
+ }
35
+ }
36
+ }
37
+ );
38
+ });