'use strict'; /* Rolling your own database is a bad idea. In this particular * case, I am doing it anyway, because there are very clear * constraints, no extreme performance requirements, and most * importantly, all the other in-memory databases seem to * -suck- from an API/usability point of view. * * Limitations: * - Only one process can use the DB at the same time. Yes, * it's a reading lock. * - Persistence is not guaranteed. If the process crashes, * writes may be lost. * - No performance guarantees, whatsoever. * - No schemas. */ const Promise = require("bluebird"); const fs = Promise.promisifyAll(require("fs")); const matchObject = require("match-object"); const uuid = require("uuid"); const pick = require("object-pick"); const rfr = require("rfr"); const promiseDebounce = rfr("lib/util/promise-debounce"); const escapeCollectionName = rfr("lib/db/escape-collection-name"); // FIXME: Actually split this up into collections... function getLockPath(path) { return `${path}.lock`; } function obtainLock(path) { return Promise.try(() => { return fs.writeFileAsync(getLockPath(path), "", { flag: "wx" }); }); } function releaseLock(path) { return Promise.try(() => { return fs.unlinkAsync(getLockPath(path)); }); } function loadDatabase(path) { return Promise.try(() => { return obtainLock(path); }).then(() => { return fs.readFileAsync(path); }).then((data) => { return JSON.parse(data); }).catch({code: "ENOENT"}, (err) => { /* Initialize a blank database */ return {}; }); } function saveDatabase(path, collections) { return Promise.try(() => { return fs.writeFileAsync(path, JSON.stringify(collections)); }); } module.exports = function(path) { return Promise.try(() => { return loadDatabase(path); }).then((collections) => { let queueWrite = promiseDebounce(function() { if (db.opened === true) { saveDatabase(path, collections); } }); function getCollection(name) { let indexes; // TODO function findIdIndex(id) { if (id == null) { return null; } return collections[name].findIndex((item) => item.$id === id); } return { find: function(query) { return collections[name].filter((item) => matchObject(query, item)); }, findOne: function(query) { let result = collections[name].find((item) => matchObject(query, item)); if (result == null) { throw new Error("No results found"); } else { return result; } }, insert: function(object, options = {}) { /* Intentional mutation. */ Object.assign(object, {$id: uuid.v4()}); collections[name].push(object); queueWrite(); }, update: function(object, options = {}) { let index; if (index = findIdIndex(object.$id)) { if (options.patch === true) { Object.assign(collections[name][index], object); } else { collections[name][index] = object; } queueWrite(); } else { throw new Error("No such object exists"); } }, upsert: function(object, options = {}) { try { this.update(object, options); } catch (err) { this.insert(object, options); } }, upsertBy: function(keys, object, options = {}) { let query = pick(object, keys); try { let result = this.findOne(query); // FIXME: The following shouldn't be in the try block... object.$id = result.$id; return this.update(object, options); } catch (err) { return this.insert(object, options); } }, delete: function(object) { let index; if (index = findIdIndex(object.$id)) { collections[name].splice(index, 1); queueWrite(); } else { throw new Error("No such object exists"); } }, deleteBy: function(query) { collections[name] = collections[name].filter((item) => !matchObject(query, item)); queueWrite(); }, ensureIndex: function(property) { // TODO } } } // TODO: Cache/TTL collections // TODO: .bak file let db = { opened: true, close: function() { return Promise.try(() => { return queueWrite(); }).then(() => { return releaseLock(path); }).then(() => { this.opened = false; }); }, collection: function(name) { if (collections[name] == null) { collections[name] = []; } return getCollection(name); } } function cleanup() { console.log("Doing cleanup..."); if (db.opened === true) { db.close(); } } if (process.versions.electron != null) { /* Running in Electron */ require("electron").app.on("will-quit", () => { cleanup(); }) } else { /* Running in a different Node-y environment */ process.on("beforeExit", () => { cleanup(); }); } return db; }); }