deepbase
Version:
⚡ DeepBase - Fastest and simplest way to add persistence to your projects.
725 lines (574 loc) • 23.9 kB
JavaScript
import assert from 'assert';
import { DeepBase, DeepBaseDriver } from '../src/index.js';
// Mock driver for testing
class MockDriver extends DeepBaseDriver {
constructor({name, ...opts} = {}) {
super(opts);
this.data = {};
this.connected = false;
this.name = name || 'mock';
}
async connect() {
this.connected = true;
await super.connect(); // Set _connected flag
}
async disconnect() {
this.connected = false;
this._connected = false;
}
async get(...args) {
if (!this.connected) throw new Error('Not connected');
if (args.length === 0) return { ...this.data };
let current = this.data;
for (const key of args) {
if (!current || typeof current !== 'object') return null;
current = current[key];
}
return current === undefined ? null : current;
}
async set(...args) {
if (!this.connected) throw new Error('Not connected');
if (args.length < 2) {
this.data = args[0] || {};
return [];
}
const keys = args.slice(0, -1);
const value = args[args.length - 1];
let current = this.data;
for (let i = 0; i < keys.length - 1; i++) {
if (!current[keys[i]]) current[keys[i]] = {};
current = current[keys[i]];
}
current[keys[keys.length - 1]] = value;
return keys;
}
async del(...keys) {
if (!this.connected) throw new Error('Not connected');
if (keys.length === 0) {
this.data = {};
return;
}
const key = keys.pop();
let current = this.data;
for (const k of keys) {
if (!current[k]) return;
current = current[k];
}
delete current[key];
}
async inc(...args) {
const i = args.pop();
const current = await this.get(...args) || 0;
return this.set(...args, current + i);
}
async dec(...args) {
const i = args.pop();
return this.inc(...args, -i);
}
async add(...keys) {
const value = keys.pop();
const id = Math.random().toString(36).substr(2, 10);
await this.set(...keys, id, value);
return [...keys, id];
}
async upd(...args) {
const func = args.pop();
return this.set(...args, func(await this.get(...args)));
}
}
describe('DeepBase Core', function() {
describe('Constructor', function() {
it('should accept single driver', function() {
const driver = new MockDriver();
const db = new DeepBase(driver);
assert.strictEqual(db.drivers.length, 1);
assert.strictEqual(db.drivers[0], driver);
});
it('should accept array of drivers', function() {
const driver1 = new MockDriver({ name: 'd1' });
const driver2 = new MockDriver({ name: 'd2' });
const db = new DeepBase([driver1, driver2]);
assert.strictEqual(db.drivers.length, 2);
assert.strictEqual(db.drivers[0], driver1);
assert.strictEqual(db.drivers[1], driver2);
});
it('should create JsonDriver for plain object (backward compatibility)', async function() {
// When a plain object is passed, it should create a JsonDriver with those options
const db = new DeepBase({ name: 'test-db' });
// Drivers are initialized lazily, so we need to initialize them first
await db._initializeDrivers();
assert.strictEqual(db.drivers.length, 1);
assert.ok(db.drivers[0] instanceof DeepBaseDriver);
});
it('should set default options', function() {
const db = new DeepBase(new MockDriver());
assert.strictEqual(db.opts.writeAll, true);
assert.strictEqual(db.opts.readFirst, true);
assert.strictEqual(db.opts.failOnPrimaryError, true);
});
it('should override default options', function() {
const db = new DeepBase(new MockDriver(), {
writeAll: false,
readFirst: false,
failOnPrimaryError: false
});
assert.strictEqual(db.opts.writeAll, false);
assert.strictEqual(db.opts.readFirst, false);
assert.strictEqual(db.opts.failOnPrimaryError, false);
});
});
describe('Connect/Disconnect', function() {
it('should connect all drivers', async function() {
const driver1 = new MockDriver({ name: 'd1' });
const driver2 = new MockDriver({ name: 'd2' });
const db = new DeepBase([driver1, driver2]);
const result = await db.connect();
assert.strictEqual(result.connected, 2);
assert.strictEqual(result.total, 2);
assert.ok(driver1.connected);
assert.ok(driver2.connected);
});
it('should disconnect all drivers', async function() {
const driver1 = new MockDriver({ name: 'd1' });
const driver2 = new MockDriver({ name: 'd2' });
const db = new DeepBase([driver1, driver2]);
await db.connect();
await db.disconnect();
assert.ok(!driver1.connected);
assert.ok(!driver2.connected);
});
});
describe('Single Driver Operations', function() {
let db;
beforeEach(async function() {
db = new DeepBase(new MockDriver());
await db.connect();
});
it('should set and get values', async function() {
await db.set('key', 'value');
const result = await db.get('key');
assert.strictEqual(result, 'value');
});
it('should delete values', async function() {
await db.set('key', 'value');
await db.del('key');
const result = await db.get('key');
assert.strictEqual(result, null);
});
it('should increment values', async function() {
await db.set('counter', 10);
await db.inc('counter', 5);
const result = await db.get('counter');
assert.strictEqual(result, 15);
});
it('should add items', async function() {
const path = await db.add('items', { value: 'test' });
assert.strictEqual(path.length, 2);
assert.strictEqual(path[0], 'items');
});
});
describe('getSync', function() {
it('throws when driver does not implement getSync', async function() {
const db = new DeepBase(new MockDriver());
await db.connect();
await db.set('key', 'value');
assert.throws(() => db.getSync('key'), /getSync\(\) is not implemented/);
});
it('getSync lazy-connects when using JsonDriver (no connect needed)', async function() {
const { JsonDriver } = await import('deepbase-json');
const driver = new JsonDriver({ name: 'getSync-lazy-db' });
const db = new DeepBase(driver);
assert.strictEqual(db.getSync('key'), null);
assert.ok(driver._connected);
});
it('returns value when using JsonDriver (sync-capable)', async function() {
const { JsonDriver } = await import('deepbase-json');
const driver = new JsonDriver({ name: 'getSync-json-test' });
const db = new DeepBase(driver);
await db.connect();
await db.set('key', 'value');
assert.strictEqual(db.getSync('key'), 'value');
await db.set('nested', 'a', 1);
assert.strictEqual(db.getSync('nested', 'a'), 1);
});
});
describe('Keys with Dots (Base Driver Methods)', function() {
it('should properly escape and unescape dots in keys', function() {
const driver = new MockDriver();
// Test _escapeDots
assert.strictEqual(driver._escapeDots('test.key'), 'test\\.key');
assert.strictEqual(driver._escapeDots('user@example.com'), 'user@example\\.com');
assert.strictEqual(driver._escapeDots('normal'), 'normal');
// Test _unescapeDots
assert.strictEqual(driver._unescapeDots('test\\.key'), 'test.key');
assert.strictEqual(driver._unescapeDots('user@example\\.com'), 'user@example.com');
assert.strictEqual(driver._unescapeDots('normal'), 'normal');
});
it('should convert path to key with proper escaping', function() {
const driver = new MockDriver();
// Test _pathToKey
assert.strictEqual(driver._pathToKey(['user', 'name']), 'user.name');
assert.strictEqual(driver._pathToKey(['user.name@domain.com', 'email']), 'user\\.name@domain\\.com.email');
assert.strictEqual(driver._pathToKey(['config.prod']), 'config\\.prod');
});
it('should convert key to path with proper unescaping', function() {
const driver = new MockDriver();
// Test _keyToPath
assert.deepStrictEqual(driver._keyToPath('user.name'), ['user', 'name']);
assert.deepStrictEqual(driver._keyToPath('user\\.name@domain\\.com.email'), ['user.name@domain.com', 'email']);
assert.deepStrictEqual(driver._keyToPath('config\\.prod'), ['config.prod']);
});
it('should handle backslashes in keys', function() {
const driver = new MockDriver();
// Test escaping backslashes
assert.strictEqual(driver._escapeDots('path\\to\\file'), 'path\\\\to\\\\file');
assert.strictEqual(driver._unescapeDots('path\\\\to\\\\file'), 'path\\to\\file');
// Test path conversion with backslashes
assert.strictEqual(driver._pathToKey(['path\\to\\file']), 'path\\\\to\\\\file');
assert.deepStrictEqual(driver._keyToPath('path\\\\to\\\\file'), ['path\\to\\file']);
});
});
describe('Multi-Driver Operations', function() {
let driver1, driver2, db;
beforeEach(async function() {
driver1 = new MockDriver({ name: 'd1' });
driver2 = new MockDriver({ name: 'd2' });
db = new DeepBase([driver1, driver2]);
await db.connect();
});
it('should write to all drivers when writeAll is true', async function() {
await db.set('key', 'value');
const result1 = await driver1.get('key');
const result2 = await driver2.get('key');
assert.strictEqual(result1, 'value');
assert.strictEqual(result2, 'value');
});
it('should write to primary only when writeAll is false', async function() {
const db2 = new DeepBase([driver1, driver2], { writeAll: false });
await db2.connect();
await db2.set('key', 'value');
const result1 = await driver1.get('key');
const result2 = await driver2.get('key');
assert.strictEqual(result1, 'value');
assert.strictEqual(result2, null);
});
it('should read from first available driver', async function() {
await driver1.set('key', 'from-driver1');
const result = await db.get('key');
assert.strictEqual(result, 'from-driver1');
});
it('should fallback to second driver if first fails', async function() {
driver1.connected = false; // Simulate runtime failure (driver reports as disconnected but _connected is still true)
await driver2.set('key', 'from-driver2');
const result = await db.get('key');
assert.strictEqual(result, 'from-driver2');
});
});
describe('Migration', function() {
let driver1, driver2, db;
beforeEach(async function() {
driver1 = new MockDriver({ name: 'source' });
driver2 = new MockDriver({ name: 'target' });
db = new DeepBase([driver1, driver2]);
await db.connect();
});
it('should migrate data from one driver to another', async function() {
// Setup source data
await driver1.set('users', 'alice', { age: 30 });
await driver1.set('users', 'bob', { age: 25 });
await driver1.set('config', 'version', '1.0');
// Migrate
const result = await db.migrate(0, 1);
assert.ok(result.migrated > 0);
assert.strictEqual(result.errors, 0);
// Verify target has data
const targetData = await driver2.get();
assert.ok(targetData.users);
assert.ok(targetData.config);
});
it('should clear target before migration when clear is true', async function() {
await driver2.set('existing', 'data');
await driver1.set('new', 'data');
await db.migrate(0, 1, { clear: true });
const existing = await driver2.get('existing');
assert.strictEqual(existing, null);
});
it('should not clear target when clear is false', async function() {
await driver2.set('existing', 'data');
await driver1.set('new', 'data');
await db.migrate(0, 1, { clear: false });
const existing = await driver2.get('existing');
assert.strictEqual(existing, 'data');
});
});
describe('Driver Management', function() {
it('should get driver by index', function() {
const driver1 = new MockDriver({ name: 'd1' });
const driver2 = new MockDriver({ name: 'd2' });
const db = new DeepBase([driver1, driver2]);
assert.strictEqual(db.getDriver(0), driver1);
assert.strictEqual(db.getDriver(1), driver2);
});
it('should get all drivers', function() {
const driver1 = new MockDriver({ name: 'd1' });
const driver2 = new MockDriver({ name: 'd2' });
const db = new DeepBase([driver1, driver2]);
const drivers = db.getDrivers();
assert.strictEqual(drivers.length, 2);
assert.strictEqual(drivers[0], driver1);
assert.strictEqual(drivers[1], driver2);
});
});
describe('Lazy Connect', function() {
it('should auto-connect when lazyConnect is true (default)', async function() {
const driver = new MockDriver({ name: 'lazy' });
const db = new DeepBase([driver]); // lazyConnect defaults to true
// Driver should not be connected yet
assert.strictEqual(driver._connected, false);
// First operation should auto-connect
await db.set('key', 'value');
assert.strictEqual(driver._connected, true);
assert.strictEqual(await db.get('key'), 'value');
});
it('should NOT auto-connect when lazyConnect is false', async function() {
const driver = new MockDriver({ name: 'manual' });
const db = new DeepBase([driver], { lazyConnect: false });
// Driver should not be connected
assert.strictEqual(driver._connected, false);
// Operation should fail without manual connect
try {
await db.set('key', 'value');
assert.fail('Should have thrown an error');
} catch (error) {
assert.strictEqual(error.message, 'Not connected');
}
// Manual connect should work
await db.connect();
assert.strictEqual(driver._connected, true);
await db.set('key', 'value');
assert.strictEqual(await db.get('key'), 'value');
});
it('should respect lazyConnect setting per instance', async function() {
const driver1 = new MockDriver({ name: 'auto' });
const driver2 = new MockDriver({ name: 'manual' });
const dbAuto = new DeepBase([driver1], { lazyConnect: true });
const dbManual = new DeepBase([driver2], { lazyConnect: false });
// Auto instance should connect automatically
await dbAuto.set('key', 'auto');
assert.strictEqual(driver1._connected, true);
// Manual instance should not connect automatically
assert.strictEqual(driver2._connected, false);
try {
await dbManual.set('key', 'manual');
assert.fail('Should have thrown an error');
} catch (error) {
assert.strictEqual(error.message, 'Not connected');
}
});
});
describe('Len', function() {
it('should return the number of keys in an object', async function() {
const driver = new MockDriver();
const db = new DeepBase([driver]);
await db.set('users', 'alice', { age: 30 });
await db.set('users', 'bob', { age: 25 });
await db.set('users', 'charlie', { age: 35 });
const count = await db.len('users');
assert.strictEqual(count, 3);
});
it('should return 0 for empty or non-existent paths', async function() {
const driver = new MockDriver();
const db = new DeepBase([driver]);
const count = await db.len('nonexistent');
assert.strictEqual(count, 0);
});
it('should return 0 for scalar values', async function() {
const driver = new MockDriver();
const db = new DeepBase([driver]);
await db.set('name', 'Alice');
const count = await db.len('name');
assert.strictEqual(count, 0);
});
it('should count top-level keys when called with no args', async function() {
const driver = new MockDriver();
const db = new DeepBase([driver]);
await db.set('a', 1);
await db.set('b', 2);
await db.set('c', 3);
const count = await db.len();
assert.strictEqual(count, 3);
});
it('should count nested keys', async function() {
const driver = new MockDriver();
const db = new DeepBase([driver]);
await db.set('data', 'items', { x: 1, y: 2, z: 3, w: 4 });
const count = await db.len('data', 'items');
assert.strictEqual(count, 4);
});
});
describe('first/last', function() {
it('should match keys() boundaries', async function() {
const driver = new MockDriver();
const db = new DeepBase([driver]);
await db.set('items', 'a', 1);
await db.set('items', 'b', 2);
await db.set('items', 'c', 3);
const keys = await db.keys('items');
assert.strictEqual(await db.first('items'), keys[0]);
assert.strictEqual(await db.last('items'), keys[keys.length - 1]);
});
it('should return undefined for missing path', async function() {
const driver = new MockDriver();
const db = new DeepBase([driver]);
await db.set('items', 'a', 1);
assert.strictEqual(await db.first('missing'), undefined);
assert.strictEqual(await db.last('missing'), undefined);
});
});
describe('Array Operations', function() {
it('should pop last element from array', async function() {
const driver = new MockDriver();
const db = new DeepBase([driver]);
await db.set('items', ['a', 'b', 'c', 'd']);
const popped = await db.pop('items');
assert.strictEqual(popped, 'd');
const remaining = await db.get('items');
assert.strictEqual(remaining['0'], 'a');
assert.strictEqual(remaining['1'], 'b');
assert.strictEqual(remaining['2'], 'c');
assert.strictEqual(remaining['3'], undefined);
});
it('should shift first element from array', async function() {
const driver = new MockDriver();
const db = new DeepBase([driver]);
await db.set('items', ['a', 'b', 'c', 'd']);
const shifted = await db.shift('items');
assert.strictEqual(shifted, 'a');
const remaining = await db.get('items');
assert.strictEqual(remaining['0'], undefined);
assert.strictEqual(remaining['1'], 'b');
assert.strictEqual(remaining['2'], 'c');
assert.strictEqual(remaining['3'], 'd');
});
it('should return undefined when popping from empty array', async function() {
const driver = new MockDriver();
const db = new DeepBase([driver]);
await db.set('items', []);
const result = await db.pop('items');
assert.strictEqual(result, undefined);
});
it('should return undefined when shifting from empty array', async function() {
const driver = new MockDriver();
const db = new DeepBase([driver]);
await db.set('items', []);
const result = await db.shift('items');
assert.strictEqual(result, undefined);
});
it('should pop from nested array', async function() {
const driver = new MockDriver();
const db = new DeepBase([driver]);
await db.set('data', {
queue: ['task1', 'task2', 'task3']
});
const popped = await db.pop('data', 'queue');
assert.strictEqual(popped, 'task3');
const queue = await db.get('data', 'queue');
assert.strictEqual(queue['0'], 'task1');
assert.strictEqual(queue['1'], 'task2');
assert.strictEqual(queue['2'], undefined);
});
it('should shift from nested array', async function() {
const driver = new MockDriver();
const db = new DeepBase([driver]);
await db.set('data', {
queue: ['task1', 'task2', 'task3']
});
const shifted = await db.shift('data', 'queue');
assert.strictEqual(shifted, 'task1');
const queue = await db.get('data', 'queue');
assert.strictEqual(queue['0'], undefined);
assert.strictEqual(queue['1'], 'task2');
assert.strictEqual(queue['2'], 'task3');
});
it('should handle multiple pops', async function() {
const driver = new MockDriver();
const db = new DeepBase([driver]);
await db.set('stack', ['one', 'two', 'three']);
assert.strictEqual(await db.pop('stack'), 'three');
assert.strictEqual(await db.pop('stack'), 'two');
assert.strictEqual(await db.pop('stack'), 'one');
assert.strictEqual(await db.pop('stack'), undefined);
});
it('should handle multiple shifts', async function() {
const driver = new MockDriver();
const db = new DeepBase([driver]);
await db.set('queue', ['first', 'second', 'third']);
assert.strictEqual(await db.shift('queue'), 'first');
assert.strictEqual(await db.shift('queue'), 'second');
assert.strictEqual(await db.shift('queue'), 'third');
assert.strictEqual(await db.shift('queue'), undefined);
});
it('should pop after add operations', async function() {
const driver = new MockDriver();
const db = new DeepBase([driver]);
// Add 5 items
await db.add('items', 1);
await db.add('items', 2);
await db.add('items', 3);
await db.add('items', 4);
await db.add('items', 5);
// Check initial values
let values = await db.values('items');
assert.strictEqual(values.length, 5);
assert.deepStrictEqual(values, [1, 2, 3, 4, 5]);
// Pop last item
const popped = await db.pop('items');
assert.strictEqual(popped, 5);
// Check remaining values
values = await db.values('items');
assert.strictEqual(values.length, 4);
assert.deepStrictEqual(values, [1, 2, 3, 4]);
});
it('should shift after add operations', async function() {
const driver = new MockDriver();
const db = new DeepBase([driver]);
// Add 5 items
await db.add('items', 1);
await db.add('items', 2);
await db.add('items', 3);
await db.add('items', 4);
await db.add('items', 5);
// Check initial values
let values = await db.values('items');
assert.strictEqual(values.length, 5);
assert.deepStrictEqual(values, [1, 2, 3, 4, 5]);
// Shift first item
const shifted = await db.shift('items');
assert.strictEqual(shifted, 1);
// Check remaining values
values = await db.values('items');
assert.strictEqual(values.length, 4);
assert.deepStrictEqual(values, [2, 3, 4, 5]);
});
it('should pop and shift together after add operations', async function() {
const driver = new MockDriver();
const db = new DeepBase([driver]);
// Add 5 items
await db.add('items', 1);
await db.add('items', 2);
await db.add('items', 3);
await db.add('items', 4);
await db.add('items', 5);
// Pop from end
const popped = await db.pop('items');
assert.strictEqual(popped, 5);
let values = await db.values('items');
assert.deepStrictEqual(values, [1, 2, 3, 4]);
// Shift from beginning
const shifted = await db.shift('items');
assert.strictEqual(shifted, 1);
values = await db.values('items');
assert.deepStrictEqual(values, [2, 3, 4]);
});
});
});