UNPKG

databank

Version:

Abstraction layer for JSON storage

531 lines (457 loc) 16.7 kB
// databank.js // // abstraction for storing JSON data in some kinda storage // // Copyright 2011,2012 E14N https://e14n.com/ // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // I'm too much of a wuss to commit to any JSON storage mechanism right // now, so I'm just going to declare the interface I need and try a few // different systems to implement it. // A thing that stores JSON. // Basically CRUD + Search. Recognizes types of data without // enforcing a schema. var Step = require('step'); function Databank(params) { } Databank.localDriver = function(driver) { return './drivers/' + driver.toLowerCase(); }; Databank.drivers = {}; Databank.register = function(name, cls) { Databank.drivers[name] = cls; return; }; Databank.deepProperty = function(object, property) { var i = property.indexOf('.'); if (!object) { return null; } else if (i == -1) { // no dots return object[property]; } else { return Databank.deepProperty(object[property.substr(0, i)], property.substr(i + 1)); } }; Databank.get = function(driver, params) { var core = ['memory'], canon = driver.toLowerCase(), cls; if (core.indexOf(canon) !== -1) { cls = require(Databank.localDriver(driver)); } else if (Databank.drivers.hasOwnProperty(driver)) { cls = Databank.drivers[driver]; } else { cls = require("databank-" + canon); } return new cls(params); }; Databank.prototype = { // Connect yourself on up. // params: object containing any params you need // onCompletion(err): function to call on completion connect: function(params, onCompletion) { if (onCompletion) { onCompletion(new NotImplementedError()); } }, // Disconnect yourself. // onCompletion(err): function to call on completion disconnect: function(onCompletion) { if (onCompletion) { onCompletion(new NotImplementedError()); } }, // Create a new thing // type: string, type of thing, usually 'user' or 'activity' // id: a unique ID, like a nickname or a UUID // value: JavaScript value; will be JSONified // onCompletion(err, value): function to call on completion create: function(type, id, value, onCompletion) { if (onCompletion) { onCompletion(new NotImplementedError(), null); } }, // Read an existing thing // type: the type of thing; 'user', 'activity' // id: a unique ID -- nickname or UUID or URI // onCompletion(err, value): function to call on completion read: function(type, id, onCompletion) { if (onCompletion) { onCompletion(new NotImplementedError(), null); } }, // Update an existing thing // type: the type of thing; 'user', 'activity' // id: a unique ID -- nickname or UUID or URI // value: the new value of the thing // onCompletion(err, value): function to call on completion update: function(type, id, value, onCompletion) { if (onCompletion) { onCompletion(new NotImplementedError(), null); } }, // Delete an existing thing // type: the type of thing; 'user', 'activity' // id: a unique ID -- nickname or UUID or URI // value: the new value of the thing // onCompletion(err): function to call on completion del: function(type, id, onCompletion) { if (onCompletion) { onCompletion(new NotImplementedError()); } }, // Search for things // type: type of thing // criteria: map of criteria, with exact matches, // like {'subject.id':'tag:example.org,2011:evan' } // onResult(value): called once per result found // onCompletion(err): called once at the end of results search: function(type, criteria, onResult, onCompletion) { if (onCompletion) { onCompletion(new NotImplementedError()); } }, // Get all things of a particular type // type: type of thing // onResult(value): called once per result found // onCompletion(err): called once at the end of results scan: function(type, onResult, onCompletion) { if (onCompletion) { onCompletion(new NotImplementedError()); } }, // Update an existing thing // type: the type of thing; 'user', 'activity' // id: a unique ID -- nickname or UUID or URI // value: the new value of the thing // onCompletion(err, value): function to call on completion save: function(type, id, value, onCompletion) { var bank = this; bank.update(type, id, value, function(err, result) { if (err instanceof NoSuchThingError) { bank.create(type, id, value, function(err, result) { onCompletion(err, result); }); } else { onCompletion(err, result); } }); }, // Read a bunch of things from the db // type: the type of thing; 'user', 'activity' // id: array of IDs // onCompletion(err, results): function to call on completion; // err: an error or null if none // results: map of id (maybe stringified) to value readAll: function(type, ids, onCompletion) { var bank = this, readNice = function(type, id, callback) { bank.read(type, id, function(err, value) { if (err) { if (err instanceof NoSuchThingError) { callback(null, null); } else { callback(err, null); } } else { callback(null, value); } }); }; Step( function() { var i, group = this.group(); for (i = 0; i < ids.length; i++) { readNice(type, ids[i], group()); } }, function(err, items) { var i, results; if (err) { onCompletion(err, null); } else { results = {}; for (i = 0; i < ids.length; i++) { results[ids[i]] = items[i]; } onCompletion(null, results); } } ); }, readAndModify: function(type, id, def, modify, onCompletion) { var bank = this; bank.read(type, id, function(err, value) { if (err) { // Set to def if not exist if (err instanceof NoSuchThingError) { bank.create(type, id, def, function(err, created) { if (err) { if (err instanceof AlreadyExistsError) { // Race condition; try again bank.readAndModify(type, id, def, modify, onCompletion); } else { onCompletion(err, null); } } else { onCompletion(null, created); } }); } else { onCompletion(err, null); } } else { bank.update(type, id, modify(value), onCompletion); } }); }, incr: function(type, id, onCompletion) { this.incrBy(type, id, 1, onCompletion); }, decr: function(type, id, onCompletion) { this.decrBy(type, id, 1, onCompletion); }, incrBy: function(type, id, n, onCompletion) { this.readAndModify(type, id, n, function(value) { return value+n; }, onCompletion); }, decrBy: function(type, id, n, onCompletion) { this.incrBy(type, id, -1 * n, onCompletion); }, appendAll: function(type, id, items, onCompletion) { this.readAndModify(type, id, items, function(value) { return value.concat(items); }, function(err, result) { onCompletion(err); }); }, prependAll: function(type, id, items, onCompletion) { this.readAndModify(type, id, items, function(value) { return items.concat(value); }, function(err, result) { onCompletion(err); }); }, append: function(type, id, item, onCompletion) { this.appendAll(type, id, [item], onCompletion); }, prepend: function(type, id, item, onCompletion) { this.prependAll(type, id, [item], onCompletion); }, item: function(type, id, index, onCompletion) { this.read(type, id, function(err, value) { if (err) { onCompletion(err, null); } else { if (Array.isArray(value)) { onCompletion(null, value[index]); } else { onCompletion(new WrongTypeError(type, id, "array"), null); } } }); }, slice: function(type, id, start, length, onCompletion) { this.read(type, id, function(err, value) { if (err) { onCompletion(err, null); } else { if (Array.isArray(value)) { onCompletion(null, value.slice(start, length)); } else { onCompletion(new WrongTypeError(type, id, "array"), null); } } }); }, indexOf: function(type, id, item, onCompletion) { this.read(type, id, function(err, value) { if (err) { onCompletion(err, null); } else { if (Array.isArray(value)) { onCompletion(null, value.indexOf(item)); } else { onCompletion(new WrongTypeError(type, id, "array"), null); } } }); }, remove: function(type, id, item, onCompletion) { this.removeAll(type, id, [item], onCompletion); }, removeAll: function(type, id, items, onCompletion) { var bank = this; Step( function() { bank.read(type, id, this); }, function(err, value) { var i; if (err) throw err; if (!Array.isArray(value)) { throw new WrongTypeError(type, id, "array"); } for (var i = 0; i < items.length; i++) { var item = items[i]; var j = value.indexOf(item); if (j !== -1) { value.splice(j, 1); } } bank.update(type, id, value, this); }, function(err, value) { if (err) throw err; this(null); }, onCompletion ); }, length: function(type, id, onCompletion) { bank = this; bank.read(type, id, function(err, value) { if (err) { onCompletion(err); } else if (!Array.isArray(value)) { onCompletion(new WrongTypeError(type, id, "array")); } else { onCompletion(null, value.length); } }); }, truncate: function(type, id, length, onCompletion) { bank = this; bank.read(type, id, function(err, value) { var newValue = null; if (err) { onCompletion(err); } else if (!Array.isArray(value)) { onCompletion(new WrongTypeError(type, id, "array")); } else if (value.length <= length) { onCompletion(null); } else { newValue = value.slice(0, length); bank.update(type, id, newValue, function(err, results) { if (err) { onCompletion(err); } else { onCompletion(null); } }); } }); }, // utility for searches matchesCriteria: function(value, criteria) { var property; for (property in criteria) { if (Databank.deepProperty(value, property) != criteria[property]) { return false; } } return true; } }; // A custom error for Databank schtuff. var DatabankError = function(message) { Error.captureStackTrace(this, DatabankError); this.name = 'DatabankError'; this.message = message || "Databank error"; }; DatabankError.prototype = new Error(); DatabankError.prototype.constructor = DatabankError; var NoSuchThingError = function(type, id) { Error.captureStackTrace(this, NoSuchThingError); this.name = 'NoSuchThingError'; this.type = type; this.id = id; this.message = "No such '" + type + "' with id '" + id + "'"; }; NoSuchThingError.prototype = new DatabankError(); NoSuchThingError.prototype.constructor = NoSuchThingError; var AlreadyExistsError = function(type, id) { Error.captureStackTrace(this, AlreadyExistsError); this.name = 'AlreadyExistsError'; this.type = type; this.id = id; this.message = "Already have a(n) '" + type + "' with id '" + id + "'"; }; AlreadyExistsError.prototype = new DatabankError(); AlreadyExistsError.prototype.constructor = AlreadyExistsError; var NoSuchItemError = function(type, id, item) { Error.captureStackTrace(this, NoSuchItemError); this.name = 'NoSuchItemError'; this.type = type; this.id = id; this.item = item; this.message = "No such index '" + item + "' in '" + type + "' with key '" + id + "'"; }; NoSuchItemError.prototype = new DatabankError(); NoSuchItemError.prototype.constructor = NoSuchItemError; var WrongTypeError = function(type, id, expected) { Error.captureStackTrace(this, WrongTypeError); this.name = 'WrongTypeError'; this.type = type; this.id = id; this.expected = expected; this.message = "The '" + type + "' with key '" + id + "' should be a '" + expected + "'"; }; WrongTypeError.prototype = new DatabankError(); WrongTypeError.prototype.constructor = WrongTypeError; var NotImplementedError = function() { Error.captureStackTrace(this, NotImplementedError); this.name = 'NotImplementedError'; this.message = "Method not yet implemented."; }; NotImplementedError.prototype = new DatabankError(); NotImplementedError.prototype.constructor = NotImplementedError; var NotConnectedError = function() { Error.captureStackTrace(this, NotConnectedError); this.name = 'NotConnectedError'; this.message = "Not connected to a server."; }; NotConnectedError.prototype = new DatabankError(); NotConnectedError.prototype.constructor = NotConnectedError; var AlreadyConnectedError = function() { Error.captureStackTrace(this, AlreadyConnectedError); this.name = 'AlreadyConnectedError'; this.message = "Already connected to a server."; }; AlreadyConnectedError.prototype = new DatabankError(); AlreadyConnectedError.prototype.constructor = AlreadyConnectedError; exports.Databank = Databank; exports.DatabankError = DatabankError; exports.NotImplementedError = NotImplementedError; exports.NoSuchThingError = NoSuchThingError; exports.AlreadyExistsError = AlreadyExistsError; exports.AlreadyConnectedError = AlreadyConnectedError; exports.NotConnectedError = NotConnectedError; exports.NoSuchItemError = NoSuchItemError; exports.WrongTypeError = WrongTypeError;