var assign = require('object-assign') var cloneDeep = require('lodash.clonedeep'); var isobject = require('lodash.isobject'); var format = require('util').format; var low = require('lowdb'); var Q = require('q'); low.mixin(low.mixin(require('underscore-db'))); /** * OKDB! * Minimal interface over a database of named collections of documents. */ function OKDB(options) { if (!(this instanceof OKDB)) return new OKDB(options); options = options || {}; var db; if (typeof options === 'string') db = options; else db = options.db; if (!db) throw new Error('No DB db provided to OKDB'); switch (db) { case 'fs': return FSDB(options); default: throw new Error('Invalid DB type'); } } /** * DB implementation backed by a JSON file. */ function FSDB(options) { if (!(this instanceof FSDB)) return new FSDB(options); options = options || {}; if (!options.schemas) throw new Error('No schemas provided to FSDB') this._schemas = options.schemas; var name = options.name || 'db'; var filename = name + '.json'; this._db = low(filename); } FSDB.prototype.get = function(collection, id) { var schema = this._schemas[collection]; if (!schema) return resolve(null, new Error('No such collection type')); var query = getQuery(schema, id); if (!query) return resolve(null, new Error('Bad query')); var result = this._db(collection).find(query); return resolve(result ? cloneDeep(result) : result); }; /** * Add a new document to the DB */ FSDB.prototype.insert = function(collection, data) { var schema = this._schemas[collection]; var wrapped = this._db(collection); if (!schema) return resolve(null, new Error('No such collection type')); // Get detached, clone deep, data sleep, beep beep data = cloneDeep(data); // Auto-increment fields data = autoincrement(wrapped, schema, data); // Record date created // TODO Should this meta prop logic be moved out of the DB? data.dateCreated = new Date().toUTCString(); var result = wrapped.chain().push(data).last().value(); if (result) { return resolve(cloneDeep(result)); } else { return resolve(null, new Error('Problem inserting document')); } }; /** * Update an existing document in the DB */ FSDB.prototype.update = function(collection, id, data) { var schema = this._schemas[collection]; if (!schema) return resolve(null, new Error('No such collection type')); var query = getQuery(schema, id); var wrapped = this._db(collection); var chain = wrapped.chain().find(query); if (!chain.value()) { return resolve(null, new Error('Cannot update nonexistent entry')); } var result = chain.assign(cloneDeep(data)).value(); if (result ) { return resolve(cloneDeep(result)); } else { return resolve(null, new Error('Problem updating document')); } }; /** * TODO Should be atomic ¯\_(ツ)_/¯ */ FSDB.prototype.updateBatch = function(collection, ids, datas) { var schema = this._schemas[collection]; if (!schema) return resolve(null, new Error('No such collection type')); if (!ids || !ids.length || !datas || !datas.length || ids.length !== datas.length) { return resolve(null, new Error('Bad input')); } var doc = this._db(collection); var results = ids.map(function(id, i) { return doc.chain().find(getQuery(schema, id)).assign(datas[i]).value(); }); return resolve(results); }; FSDB.prototype.remove = function(collection, id) { var schema = this._schemas[collection]; if (!schema) return resolve(null, new Error('No such collection type')); var query = getQuery(schema, id); var result = this._db(collection).removeWhere(query); if (result) { // Don't need to clone this ref, since it's removed anyway return resolve(result); } else { return resolve(null, new Error('Cannot remove nonexistent entry')); } }; FSDB.prototype.sortBy = function(collection, prop, descend) { var schema = this._schemas[collection]; if (!schema) return resolve(null, new Error('No such collection type')); if (!prop) return resolve(null, new Error('Bad input')); var result = this._db(collection).sortByOrder([prop], [!descend]); return resolve(result); }; FSDB.prototype.find = function(collection, query) { var schema = this._schemas[collection]; if (!schema) return resolve(null, new Error('No such collection type')); if (!query) return resolve(null, new Error('Bad input')); var result = this._db(collection).find(query); return resolve(cloneDeep(result)); }; FSDB.prototype.all = function(collection) { var schema = this._schemas[collection]; if (!schema) return resolve(null, new Error('No such collection type')); var data = this._db(collection).value(); return resolve(cloneDeep(data || [])); }; FSDB.prototype.getMeta = function() { var data = this._db('meta').first(); return resolve(data || {}); }; /** * Function implementing DB auto increment support * Naive implementation, assumes DB is relatively small. */ function autoincrement(wrapper, schema, data) { return schema.autoIncrementFields.reduce(function(data, field) { var last = wrapper.chain().sortByOrder([field], [true]).last().value(); var index = last ? last[field] : -1; var incremented = {}; incremented[field] = (parseInt(index) + 1); return assign(data, incremented); }, data); } function getQuery(schema, id) { if (schema && id) { var query = {}; query[schema.idField] = id; return query; } } /** * Helper function to create promises for DB data */ function resolve(data, error) { return Q.promise(function resolvePromise(resolve, reject) { if (error) { reject(error); } else { resolve(data); } }); }; module.exports = OKDB;