'use strict'; const Promise = require("bluebird"); const knex = require("knex"); const fs = Promise.promisifyAll(require("fs")); const matchObject = require("match-object"); const uuid = require("uuid"); const pick = require("object-pick"); const snakeCase = require("snake-case"); const rfr = require("rfr"); const knexErrors = rfr("lib/db/knex-error-type"); function loadCollection(db, collection) { return Promise.try(() => { return db(collection).select(); }).then((results) => { return results.map((result) => { return Object.assign(JSON.parse(result.value), { $id: result.id }); }); }); } module.exports = function(path) { let collections = {}; let db = knex({ client: "sqlite3", connection: { filename: path }, useNullAsDefault: true, debug: true }); function createTable(name) { return db.schema.createTable(name, function(table) { table.text("id"); table.text("value"); }); } 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); return db(name).insert({ id: object.$id, value: JSON.stringify(object) }).then(() => {}); }, 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; } return db(name).where({id: object.$id}).update({ value: JSON.stringify(collections[name][index]) }).then(() => {}); } 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); return db(name).where({id: object.$id}).delete().then(() => {}); } else { throw new Error("No such object exists"); } }, deleteBy: function(query) { let toRemove = collections[name].filter((item) => matchObject(query, item)); collections[name] = collections[name].filter((item) => (toRemove.indexOf(item) !== -1)); return Promise.map(toRemove, (item) => { return db(name).where({id: item.$id}).delete(); }); }, ensureIndex: function(property) { // TODO } } } let dbAPI = { close: function() {}, collection: function(name) { return Promise.try(() => { let snakeCasedName = snakeCase(name); if (collections[snakeCasedName] != null) { return getCollection(snakeCasedName); } else { return Promise.try(() => { return loadCollection(db, snakeCasedName); }).then((collectionData) => { collections[snakeCasedName] = collectionData; }).catch(knexErrors.sqlite.NoSuchTable, (err) => { collections[snakeCasedName] = []; return createTable(snakeCasedName); }).then(() => { return getCollection(snakeCasedName); }); } }); } } return dbAPI; }