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