node-ejdb-lite
Version:
Blazing fast (installs in seconds) and lightweight EJDB2 bindings for NodeJS
333 lines (267 loc) • 9.53 kB
JavaScript
/**************************************************************************************************
* EJDB2 Node.js native API binding.
*
* MIT License
*
* Copyright (c) 2012-2022 Softmotions Ltd <info@softmotions.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*************************************************************************************************/
import assert from 'assert';
import test from 'basictap';
import { EJDB2, JBE } from '../src/index.cjs';
test('createQuery', async t => {
const db = await EJDB2.open('hello.db', { truncate: true });
const query = db.createQuery('@mycoll/*');
t.ok(query != null, 'query should be null');
t.equal(query.collection, 'mycoll', 'collection should be correct');
t.ok(query.db != null, 'db should be null');
await db.close();
});
test('put works successfully', async t => {
const db = await EJDB2.open('hello.db', { truncate: true });
const id = await db.put('mycoll', { foo: 'bar' });
t.equal(id, 1, 'should return the correct id');
await db.close();
});
test('put fails when given partial json', async (t) => {
const db = await EJDB2.open('hello.db', { truncate: true });
try {
await db.put('mycoll', '{"');
} catch (error) {
t.equal(error.code, '@ejdb IWRC:76005 put', 'error code should be correct');
t.equal(error.message, 'Unquoted JSON string (JBL_ERROR_PARSE_UNQUOTED_STRING)', 'error message should be correct');
}
await db.close();
});
test('get fails from none existing collection', async t => {
const db = await EJDB2.open('hello.db', { truncate: true });
try {
await db.get('mycoll', 1);
} catch (error) {
t.equal(error.message, 'Resource is not exists. (IW_ERROR_NOT_EXISTS)', 'should have correct error message');
t.equal(error.code, '@ejdb IWRC:70004 get', 'should have correct error code');
}
await db.close();
});
test('queries', async (t) => {
const db = await EJDB2.open('hello.db', { truncate: true });
let q = db.createQuery('@mycoll/*');
let id = await db.put('mycoll', { foo: 'bar' });
let doc = await db.get('mycoll', 1);
await db.put('mycoll', { foo: 'baz' });
const list = await db.createQuery('@mycoll/*').list({ limit: 1 });
t.equal(list.length, 1);
let first = await db.createQuery('@mycoll/*').first();
t.ok(first != null);
t.deepEqual(first.json, { foo: 'baz' });
t.equal(first._raw, null);
first = await db.createQuery('@mycoll/[zzz=bbb]').first();
t.equal(first, undefined);
let firstN = await db.createQuery('@mycoll/*').firstN(5);
t.ok(firstN != null && firstN.length == 2);
t.deepEqual(firstN[0].json, { foo: 'baz' });
t.deepEqual(firstN[1].json, { foo: 'bar' });
firstN = await db.createQuery('@mycoll/*').firstN(1);
t.ok(firstN != null && firstN.length == 1);
t.deepEqual(firstN[0].json, { foo: 'baz' });
// Query 1
const rbuf = [];
for await (const doc of q.stream()) {
rbuf.push(doc.id);
rbuf.push(doc._raw);
}
t.equal(rbuf.toString(), '2,{"foo":"baz"},1,{"foo":"bar"}');
// Query 2
rbuf.length = 0;
for await (const doc of db.createQuery('@mycoll/[foo=zaz]').stream()) {
rbuf.push(doc.id);
rbuf.push(doc._raw);
}
t.equal(rbuf.length, 0);
// Query 3
for await (const doc of db.createQuery('/[foo=bar]', 'mycoll').stream()) {
rbuf.push(doc.id);
rbuf.push(doc._raw);
}
t.equal(rbuf.toString(), '1,{"foo":"bar"}');
let error = null;
try {
await db.createQuery('@mycoll/[').stream();
} catch (e) {
error = e;
t.ok(JBE.isInvalidQuery(e));
}
t.ok(error != null);
error = null;
let count = await db.createQuery('@mycoll/* | count').scalarInt();
t.equal(count, 2);
// Del
await db.del('mycoll', 1);
error = null;
try {
await db.get('mycoll', 1);
} catch (e) {
error = e;
t.ok(JBE.isNotFound(e));
}
t.ok(error != null);
error = null;
count = await db.createQuery('@mycoll/* | count').scalarInt();
t.equal(count, 1);
// Patch
await db.patch('mycoll', '[{"op":"add", "path":"/baz", "value":"qux"}]', 2);
doc = await db.get('mycoll', 2);
t.deepEqual(doc, { foo: 'baz', baz: 'qux' });
// DB Info
doc = await db.info();
t.deepEqual(doc.collections[0], {
name: 'mycoll',
rnum: 1,
dbid: 3,
indexes: []
});
// Indexes
await db.ensureStringIndex('mycoll', '/foo', true);
doc = await db.info();
t.deepEqual(doc.collections[0].indexes[0], {
ptr: '/foo',
mode: 5,
idbf: 0,
dbid: 4,
rnum: 1
});
// Test JQL set
doc = await db.createQuery('@mycoll/[foo=:?]').setString(0, 'baz').first();
t.deepEqual(doc.json, {
foo: 'baz',
baz: 'qux'
});
let log = null;
// Test explain log
await db.createQuery('@mycoll/[foo=:?]').setString(0, 'baz').completionPromise({
explainCallback: (l) => {
log = l;
}
});
t.ok(typeof log === 'string');
t.ok(log.indexOf('[INDEX] MATCHED UNIQUE|STR|1 /foo EXPR1: \'foo = :?\' INIT: IWKV_CURSOR_EQ') != -1);
doc = await db.createQuery('@mycoll/[foo=:?] and /[baz=:?]')
.setString(0, 'baz')
.setString(1, 'qux')
.first();
t.deepEqual(doc.json, {
foo: 'baz',
baz: 'qux'
});
doc = await db.createQuery('@mycoll/[foo=:foo] and /[baz=:baz]')
.setString('foo', 'baz')
.setString('baz', 'qux')
.first();
t.deepEqual(doc.json, {
foo: 'baz',
baz: 'qux'
});
doc = await db.createQuery('@mycoll/[foo re :?]').setRegexp(0, '.*').first();
t.deepEqual(doc.json, {
foo: 'baz',
baz: 'qux'
});
doc = await db.createQuery('@mycoll/* | /foo').first();
t.deepEqual(doc.json, { foo: 'baz' });
await db.removeStringIndex('mycoll', '/foo', true);
doc = await db.info();
t.deepEqual(doc.collections[0], {
dbid: 3,
indexes: [],
name: 'mycoll',
rnum: 1,
indexes: []
});
await db.removeCollection('mycoll');
doc = await db.info();
t.deepEqual(doc.collections, []);
q = db.createQuery('@c1/* | limit 2');
t.equal(q.limit, 2);
q = db.createQuery('@c2/*');
t.equal(q.limit, 0);
await db.close();
});
test('unescaped characters', async t => {
const db = await EJDB2.open('hello.db', { truncate: true });
const colId = await db.put('mycollchars', { foo: String.fromCharCode(1) });
const result = await db.get('mycollchars', colId);
t.deepEqual(result, { foo: String.fromCharCode(1) }, 'should have the correct char code');
await db.close();
});
test('rename collection', async t => {
const db = await EJDB2.open('hello.db', { truncate: true });
const id = await db.put('cc1', { foo: 1 });
const doc1 = await db.get('cc1', id);
await db.renameCollection('cc1', 'cc2');
const doc2 = await db.get('cc2', id);
t.deepEqual(doc2, doc1, 'should be the same initial document');
await db.close();
});
test('stress test - series', async t => {
t.plan(1);
const db = await EJDB2.open('stress.db', { truncate: true });
const startTime = Date.now();
for (let x = 0; x < 10000; x++) {
const originalDocument = { foo: x };
const id = await db.put('cc1', originalDocument);
const returnedDocument = await db.get('cc1', id);
assert.deepEqual(originalDocument, returnedDocument);
}
const timeTaken = Date.now() - startTime;
t.ok(timeTaken < 5000, `should take less than 5000ms (took ${timeTaken}ms)`);
await db.close();
});
test('stress test - parallel', async t => {
t.plan(1);
const db = await EJDB2.open('stress.db', { truncate: true });
const startTime = Date.now();
const promises = [];
for (let x = 0; x < 10000; x++) {
const originalDocument = { foo: x };
promises.push(
db.put('cc1', originalDocument)
.then(id => db.get('cc1', id))
.then(returnedDocument =>
assert.deepEqual(returnedDocument, originalDocument)
)
);
}
await Promise.all(promises);
const timeTaken = Date.now() - startTime;
t.ok(timeTaken < 5000, `should take less than 5000ms (took ${timeTaken}ms)`);
await db.close();
});
test('backup and restore', async t => {
const db1 = await EJDB2.open('hello.db', { truncate: true });
const id = await db1.put('mycoll', { foo: 'bar' });
const ts0 = +new Date();
const ts = await db1.onlineBackup('hello-bkp.db');
t.ok(ts0 < ts, 'should not have timestamp in the past');
await db1.close();
const db2 = await EJDB2.open('hello-bkp.db', { truncate: false });
const doc = await db2.get('mycoll', id);
t.deepEqual(doc, { foo: 'bar' }, 'should have initial document');
await db2.close();
});