You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

202 lines
4.7 KiB
JavaScript

'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;
});
}