UNPKG

@stefanprobst/lokijs

Version:

Fast document oriented javascript in-memory database

1,452 lines (1,236 loc) 258 kB
/** * LokiJS * @author Joe Minichino <joe.minichino@gmail.com> * * A lightweight document oriented javascript database */ (function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD define([], factory); } else if (typeof exports === 'object') { // CommonJS module.exports = factory(); } else { // Browser globals root.loki = factory(); } }(this, function () { return (function () { 'use strict'; var hasOwnProperty = Object.prototype.hasOwnProperty; var Utils = { copyProperties: function (src, dest) { var prop; for (prop in src) { dest[prop] = src[prop]; } }, // used to recursively scan hierarchical transform step object for param substitution resolveTransformObject: function (subObj, params, depth) { var prop, pname; if (typeof depth !== 'number') { depth = 0; } if (++depth >= 10) return subObj; for (prop in subObj) { if (typeof subObj[prop] === 'string' && subObj[prop].indexOf("[%lktxp]") === 0) { pname = subObj[prop].substring(8); if (params.hasOwnProperty(pname)) { subObj[prop] = params[pname]; } } else if (typeof subObj[prop] === "object") { subObj[prop] = Utils.resolveTransformObject(subObj[prop], params, depth); } } return subObj; }, // top level utility to resolve an entire (single) transform (array of steps) for parameter substitution resolveTransformParams: function (transform, params) { var idx, clonedStep, resolvedTransform = []; if (typeof params === 'undefined') return transform; // iterate all steps in the transform array for (idx = 0; idx < transform.length; idx++) { // clone transform so our scan/replace can operate directly on cloned transform clonedStep = clone(transform[idx], "shallow-recurse-objects"); resolvedTransform.push(Utils.resolveTransformObject(clonedStep, params)); } return resolvedTransform; }, // By default (if usingDotNotation is false), looks up path in // object via `object[path]` // // If `usingDotNotation` is true, then the path is assumed to // represent a nested path. It can be in the form of an array of // field names, or a period delimited string. The function will // look up the value of object[path[0]], and then call // result[path[1]] on the result, etc etc. // // If `usingDotNotation` is true, this function still supports // non nested fields. // // `usingDotNotation` is a performance optimization. The caller // may know that a path is *not* nested. In which case, this // function avoids a costly string.split('.') // // examples: // getIn({a: 1}, "a") => 1 // getIn({a: 1}, "a", true) => 1 // getIn({a: {b: 1}}, ["a", "b"], true) => 1 // getIn({a: {b: 1}}, "a.b", true) => 1 getIn: function (object, path, usingDotNotation) { if (object == null) { return undefined; } if (!usingDotNotation) { return object[path]; } if (typeof(path) === "string") { path = path.split("."); } if (!Array.isArray(path)) { throw new Error("path must be a string or array. Found " + typeof(path)); } var index = 0, length = path.length; while (object != null && index < length) { object = object[path[index++]]; } return (index && index == length) ? object : undefined; } }; // wrapping in object to expose to default export for potential user override. // warning: overriding these methods will override behavior for all loki db instances in memory. // warning: if you use binary indices these comparators should be the same for all inserts/updates/removes. var Comparators = { aeq: aeqHelper, lt: ltHelper, gt: gtHelper }; /** Helper function for determining 'loki' abstract equality which is a little more abstract than == * aeqHelper(5, '5') === true * aeqHelper(5.0, '5') === true * aeqHelper(new Date("1/1/2011"), new Date("1/1/2011")) === true * aeqHelper({a:1}, {z:4}) === true (all objects sorted equally) * aeqHelper([1, 2, 3], [1, 3]) === false * aeqHelper([1, 2, 3], [1, 2, 3]) === true * aeqHelper(undefined, null) === true */ function aeqHelper(prop1, prop2) { var cv1, cv2, t1, t2; if (prop1 === prop2) return true; // 'falsy' and Boolean handling if (!prop1 || !prop2 || prop1 === true || prop2 === true || prop1 !== prop1 || prop2 !== prop2) { // dates and NaN conditions (typed dates before serialization) switch (prop1) { case undefined: t1 = 1; break; case null: t1 = 1; break; case false: t1 = 3; break; case true: t1 = 4; break; case "": t1 = 5; break; default: t1 = (prop1 === prop1)?9:0; break; } switch (prop2) { case undefined: t2 = 1; break; case null: t2 = 1; break; case false: t2 = 3; break; case true: t2 = 4; break; case "": t2 = 5; break; default: t2 = (prop2 === prop2)?9:0; break; } // one or both is edge case if (t1 !== 9 || t2 !== 9) { return (t1===t2); } } // Handle 'Number-like' comparisons cv1 = Number(prop1); cv2 = Number(prop2); // if one or both are 'number-like'... if (cv1 === cv1 || cv2 === cv2) { return (cv1 === cv2); } // not strict equal nor less than nor gt so must be mixed types, convert to string and use that to compare cv1 = prop1.toString(); cv2 = prop2.toString(); return (cv1 == cv2); } /** Helper function for determining 'less-than' conditions for ops, sorting, and binary indices. * In the future we might want $lt and $gt ops to use their own functionality/helper. * Since binary indices on a property might need to index [12, NaN, new Date(), Infinity], we * need this function (as well as gtHelper) to always ensure one value is LT, GT, or EQ to another. */ function ltHelper(prop1, prop2, equal) { var cv1, cv2, t1, t2; // if one of the params is falsy or strictly true or not equal to itself // 0, 0.0, "", NaN, null, undefined, not defined, false, true if (!prop1 || !prop2 || prop1 === true || prop2 === true || prop1 !== prop1 || prop2 !== prop2) { switch (prop1) { case undefined: t1 = 1; break; case null: t1 = 1; break; case false: t1 = 3; break; case true: t1 = 4; break; case "": t1 = 5; break; // if strict equal probably 0 so sort higher, otherwise probably NaN so sort lower than even null default: t1 = (prop1 === prop1)?9:0; break; } switch (prop2) { case undefined: t2 = 1; break; case null: t2 = 1; break; case false: t2 = 3; break; case true: t2 = 4; break; case "": t2 = 5; break; default: t2 = (prop2 === prop2)?9:0; break; } // one or both is edge case if (t1 !== 9 || t2 !== 9) { return (t1===t2)?equal:(t1<t2); } } // if both are numbers (string encoded or not), compare as numbers cv1 = Number(prop1); cv2 = Number(prop2); if (cv1 === cv1 && cv2 === cv2) { if (cv1 < cv2) return true; if (cv1 > cv2) return false; return equal; } if (cv1 === cv1 && cv2 !== cv2) { return true; } if (cv2 === cv2 && cv1 !== cv1) { return false; } if (prop1 < prop2) return true; if (prop1 > prop2) return false; if (prop1 == prop2) return equal; // not strict equal nor less than nor gt so must be mixed types, convert to string and use that to compare cv1 = prop1.toString(); cv2 = prop2.toString(); if (cv1 < cv2) { return true; } if (cv1 == cv2) { return equal; } return false; } function gtHelper(prop1, prop2, equal) { var cv1, cv2, t1, t2; // 'falsy' and Boolean handling if (!prop1 || !prop2 || prop1 === true || prop2 === true || prop1 !== prop1 || prop2 !== prop2) { switch (prop1) { case undefined: t1 = 1; break; case null: t1 = 1; break; case false: t1 = 3; break; case true: t1 = 4; break; case "": t1 = 5; break; // NaN 0 default: t1 = (prop1 === prop1)?9:0; break; } switch (prop2) { case undefined: t2 = 1; break; case null: t2 = 1; break; case false: t2 = 3; break; case true: t2 = 4; break; case "": t2 = 5; break; default: t2 = (prop2 === prop2)?9:0; break; } // one or both is edge case if (t1 !== 9 || t2 !== 9) { return (t1===t2)?equal:(t1>t2); } } // if both are numbers (string encoded or not), compare as numbers cv1 = Number(prop1); cv2 = Number(prop2); if (cv1 === cv1 && cv2 === cv2) { if (cv1 > cv2) return true; if (cv1 < cv2) return false; return equal; } if (cv1 === cv1 && cv2 !== cv2) { return false; } if (cv2 === cv2 && cv1 !== cv1) { return true; } if (prop1 > prop2) return true; if (prop1 < prop2) return false; if (prop1 == prop2) return equal; // not strict equal nor less than nor gt so must be dates or mixed types // convert to string and use that to compare cv1 = prop1.toString(); cv2 = prop2.toString(); if (cv1 > cv2) { return true; } if (cv1 == cv2) { return equal; } return false; } function sortHelper(prop1, prop2, desc) { if (Comparators.aeq(prop1, prop2)) return 0; if (Comparators.lt(prop1, prop2, false)) { return (desc) ? (1) : (-1); } if (Comparators.gt(prop1, prop2, false)) { return (desc) ? (-1) : (1); } // not lt, not gt so implied equality-- date compatible return 0; } /** * compoundeval() - helper function for compoundsort(), performing individual object comparisons * * @param {array} properties - array of property names, in order, by which to evaluate sort order * @param {object} obj1 - first object to compare * @param {object} obj2 - second object to compare * @returns {integer} 0, -1, or 1 to designate if identical (sortwise) or which should be first */ function compoundeval(properties, obj1, obj2) { var res = 0; var prop, field, val1, val2, arr, path; for (var i = 0, len = properties.length; i < len; i++) { prop = properties[i]; field = prop[0]; if (~field.indexOf('.')) { arr = field.split('.'); val1 = Utils.getIn(obj1, arr, true); val2 = Utils.getIn(obj2, arr, true); } else { val1 = obj1[field]; val2 = obj2[field]; } res = sortHelper(val1, val2, prop[1]); if (res !== 0) { return res; } } return 0; } /** * dotSubScan - helper function used for dot notation queries. * * @param {object} root - object to traverse * @param {array} paths - array of properties to drill into * @param {function} fun - evaluation function to test with * @param {any} value - comparative value to also pass to (compare) fun * @param {number} poffset - index of the item in 'paths' to start the sub-scan from */ function dotSubScan(root, paths, fun, value, poffset) { var pathOffset = poffset || 0; var path = paths[pathOffset]; var valueFound = false; var element; if (typeof root === 'object' && path in root) { element = root[path]; } if (pathOffset + 1 >= paths.length) { // if we have already expanded out the dot notation, // then just evaluate the test function and value on the element valueFound = fun(element, value); } else if (Array.isArray(element)) { for (var index = 0, len = element.length; index < len; index += 1) { valueFound = dotSubScan(element[index], paths, fun, value, pathOffset + 1); if (valueFound === true) { break; } } } else { valueFound = dotSubScan(element, paths, fun, value, pathOffset + 1); } return valueFound; } function containsCheckFn(a) { if (typeof a === 'string' || Array.isArray(a)) { return function (b) { return a.indexOf(b) !== -1; }; } else if (typeof a === 'object' && a !== null) { return function (b) { return hasOwnProperty.call(a, b); }; } return null; } function doQueryOp(val, op) { for (var p in op) { if (hasOwnProperty.call(op, p)) { return LokiOps[p](val, op[p]); } } return false; } var LokiOps = { // comparison operators // a is the value in the collection // b is the query value $eq: function (a, b) { return a === b; }, // abstract/loose equality $aeq: function (a, b) { return a == b; }, $ne: function (a, b) { // ecma 5 safe test for NaN if (b !== b) { // ecma 5 test value is not NaN return (a === a); } return a !== b; }, // date equality / loki abstract equality test $dteq: function (a, b) { return Comparators.aeq(a, b); }, // loki comparisons: return identical unindexed results as indexed comparisons $gt: function (a, b) { return Comparators.gt(a, b, false); }, $gte: function (a, b) { return Comparators.gt(a, b, true); }, $lt: function (a, b) { return Comparators.lt(a, b, false); }, $lte: function (a, b) { return Comparators.lt(a, b, true); }, // lightweight javascript comparisons $jgt: function (a, b) { return a > b; }, $jgte: function (a, b) { return a >= b; }, $jlt: function (a, b) { return a < b; }, $jlte: function (a, b) { return a <= b; }, // ex : coll.find({'orderCount': {$between: [10, 50]}}); $between: function (a, vals) { if (a === undefined || a === null) return false; return (Comparators.gt(a, vals[0], true) && Comparators.lt(a, vals[1], true)); }, $jbetween: function (a, vals) { if (a === undefined || a === null) return false; return (a >= vals[0] && a <= vals[1]); }, $in: function (a, b) { return b.indexOf(a) !== -1; }, $nin: function (a, b) { return b.indexOf(a) === -1; }, $keyin: function (a, b) { return a in b; }, $nkeyin: function (a, b) { return !(a in b); }, $definedin: function (a, b) { return b[a] !== undefined; }, $undefinedin: function (a, b) { return b[a] === undefined; }, $regex: function (a, b) { return b.test(a); }, $containsString: function (a, b) { return (typeof a === 'string') && (a.indexOf(b) !== -1); }, $containsNone: function (a, b) { return !LokiOps.$containsAny(a, b); }, $containsAny: function (a, b) { var checkFn = containsCheckFn(a); if (checkFn !== null) { return (Array.isArray(b)) ? (b.some(checkFn)) : (checkFn(b)); } return false; }, $contains: function (a, b) { var checkFn = containsCheckFn(a); if (checkFn !== null) { return (Array.isArray(b)) ? (b.every(checkFn)) : (checkFn(b)); } return false; }, $elemMatch: function (a, b) { if (Array.isArray(a)) { return a.some(function(item){ return Object.keys(b).every(function(property) { var filter = b[property]; if (!(typeof filter === 'object' && filter)) { filter = { $eq: filter }; } if (property.indexOf('.') !== -1) { return dotSubScan(item, property.split('.'), doQueryOp, b[property]); } return doQueryOp(item[property], filter); }); }); } return false; }, $type: function (a, b) { var type = typeof a; if (type === 'object') { if (Array.isArray(a)) { type = 'array'; } else if (a instanceof Date) { type = 'date'; } } return (typeof b !== 'object') ? (type === b) : doQueryOp(type, b); }, $finite: function(a, b) { return (b === isFinite(a)); }, $size: function (a, b) { if (Array.isArray(a)) { return (typeof b !== 'object') ? (a.length === b) : doQueryOp(a.length, b); } return false; }, $len: function (a, b) { if (typeof a === 'string') { return (typeof b !== 'object') ? (a.length === b) : doQueryOp(a.length, b); } return false; }, $where: function (a, b) { return b(a) === true; }, // field-level logical operators // a is the value in the collection // b is the nested query operation (for '$not') // or an array of nested query operations (for '$and' and '$or') $not: function (a, b) { return !doQueryOp(a, b); }, $and: function (a, b) { for (var idx = 0, len = b.length; idx < len; idx += 1) { if (!doQueryOp(a, b[idx])) { return false; } } return true; }, $or: function (a, b) { for (var idx = 0, len = b.length; idx < len; idx += 1) { if (doQueryOp(a, b[idx])) { return true; } } return false; }, $exists: function (a, b) { if (b) { return a !== undefined; } else { return a === undefined; } } }; // if an op is registered in this object, our 'calculateRange' can use it with our binary indices. // if the op is registered to a function, we will run that function/op as a 2nd pass filter on results. // those 2nd pass filter functions should be similar to LokiOps functions, accepting 2 vals to compare. var indexedOps = { $eq: LokiOps.$eq, $aeq: true, $dteq: true, $gt: true, $gte: true, $lt: true, $lte: true, $in: true, $between: true }; function clone(data, method) { if (data === null || data === undefined) { return null; } var cloneMethod = method || 'parse-stringify', cloned; switch (cloneMethod) { case "parse-stringify": cloned = JSON.parse(JSON.stringify(data)); break; case "jquery-extend-deep": cloned = jQuery.extend(true, {}, data); break; case "shallow": // more compatible method for older browsers cloned = Object.create(data.constructor.prototype); Object.keys(data).map(function (i) { cloned[i] = data[i]; }); break; case "shallow-assign": // should be supported by newer environments/browsers cloned = Object.create(data.constructor.prototype); Object.assign(cloned, data); break; case "shallow-recurse-objects": // shallow clone top level properties cloned = clone(data, "shallow"); var keys = Object.keys(data); // for each of the top level properties which are object literals, recursively shallow copy keys.forEach(function(key) { if (typeof data[key] === "object" && data[key].constructor.name === "Object") { cloned[key] = clone(data[key], "shallow-recurse-objects"); }else if(Array.isArray(data[key])){ cloned[key] = cloneObjectArray(data[key], "shallow-recurse-objects"); } }); break; default: break; } return cloned; } function cloneObjectArray(objarray, method) { if (method == "parse-stringify") { return clone(objarray, method); } var result = []; for (var i = 0, len = objarray.length; i < len; i++) { result[i] = clone(objarray[i], method); } return result; } function localStorageAvailable() { try { return (window && window.localStorage !== undefined && window.localStorage !== null); } catch (e) { return false; } } /** * LokiEventEmitter is a minimalist version of EventEmitter. It enables any * constructor that inherits EventEmitter to emit events and trigger * listeners that have been added to the event through the on(event, callback) method * * @constructor LokiEventEmitter */ function LokiEventEmitter() {} /** * @prop {hashmap} events - a hashmap, with each property being an array of callbacks * @memberof LokiEventEmitter */ LokiEventEmitter.prototype.events = {}; /** * @prop {boolean} asyncListeners - boolean determines whether or not the callbacks associated with each event * should happen in an async fashion or not * Default is false, which means events are synchronous * @memberof LokiEventEmitter */ LokiEventEmitter.prototype.asyncListeners = false; /** * on(eventName, listener) - adds a listener to the queue of callbacks associated to an event * @param {string|string[]} eventName - the name(s) of the event(s) to listen to * @param {function} listener - callback function of listener to attach * @returns {int} the index of the callback in the array of listeners for a particular event * @memberof LokiEventEmitter */ LokiEventEmitter.prototype.on = function (eventName, listener) { var event; var self = this; if (Array.isArray(eventName)) { eventName.forEach(function(currentEventName) { self.on(currentEventName, listener); }); return listener; } event = this.events[eventName]; if (!event) { event = this.events[eventName] = []; } event.push(listener); return listener; }; /** * emit(eventName, data) - emits a particular event * with the option of passing optional parameters which are going to be processed by the callback * provided signatures match (i.e. if passing emit(event, arg0, arg1) the listener should take two parameters) * @param {string} eventName - the name of the event * @param {object=} data - optional object passed with the event * @memberof LokiEventEmitter */ LokiEventEmitter.prototype.emit = function (eventName) { var self = this; var selfArgs = Array.prototype.slice.call(arguments, 1); if (eventName && this.events[eventName]) { this.events[eventName].forEach(function (listener) { if (self.asyncListeners) { setTimeout(function () { listener.apply(self, selfArgs); }, 1); } else { listener.apply(self, selfArgs); } }); } else { throw new Error('No event ' + eventName + ' defined'); } }; /** * Alias of LokiEventEmitter.prototype.on * addListener(eventName, listener) - adds a listener to the queue of callbacks associated to an event * @param {string|string[]} eventName - the name(s) of the event(s) to listen to * @param {function} listener - callback function of listener to attach * @returns {int} the index of the callback in the array of listeners for a particular event * @memberof LokiEventEmitter */ LokiEventEmitter.prototype.addListener = LokiEventEmitter.prototype.on; /** * removeListener() - removes the listener at position 'index' from the event 'eventName' * @param {string|string[]} eventName - the name(s) of the event(s) which the listener is attached to * @param {function} listener - the listener callback function to remove from emitter * @memberof LokiEventEmitter */ LokiEventEmitter.prototype.removeListener = function (eventName, listener) { var self = this; if (Array.isArray(eventName)) { eventName.forEach(function(currentEventName) { self.removeListener(currentEventName, listener); }); return; } if (this.events[eventName]) { var listeners = this.events[eventName]; listeners.splice(listeners.indexOf(listener), 1); } }; /** * Loki: The main database class * @constructor Loki * @implements LokiEventEmitter * @param {string} filename - name of the file to be saved to * @param {object=} options - (Optional) config options object * @param {string} options.env - override environment detection as 'NODEJS', 'BROWSER', 'CORDOVA' * @param {boolean} [options.verbose=false] - enable console output * @param {boolean} [options.autosave=false] - enables autosave * @param {int} [options.autosaveInterval=5000] - time interval (in milliseconds) between saves (if dirty) * @param {boolean} [options.autoload=false] - enables autoload on loki instantiation * @param {function} options.autoloadCallback - user callback called after database load * @param {adapter} options.adapter - an instance of a loki persistence adapter * @param {string} [options.serializationMethod='normal'] - ['normal', 'pretty', 'destructured'] * @param {string} options.destructureDelimiter - string delimiter used for destructured serialization * @param {boolean} [options.throttledSaves=true] - debounces multiple calls to to saveDatabase reducing number of disk I/O operations and guaranteeing proper serialization of the calls. */ function Loki(filename, options) { this.filename = filename || 'loki.db'; this.collections = []; // persist version of code which created the database to the database. // could use for upgrade scenarios this.databaseVersion = 1.5; this.engineVersion = 1.5; // autosave support (disabled by default) // pass autosave: true, autosaveInterval: 6000 in options to set 6 second autosave this.autosave = false; this.autosaveInterval = 5000; this.autosaveHandle = null; this.throttledSaves = true; this.options = {}; // currently keeping persistenceMethod and persistenceAdapter as loki level properties that // will not or cannot be deserialized. You are required to configure persistence every time // you instantiate a loki object (or use default environment detection) in order to load the database anyways. // persistenceMethod could be 'fs', 'localStorage', or 'adapter' // this is optional option param, otherwise environment detection will be used // if user passes their own adapter we will force this method to 'adapter' later, so no need to pass method option. this.persistenceMethod = null; // retain reference to optional (non-serializable) persistenceAdapter 'instance' this.persistenceAdapter = null; // flags used to throttle saves this.throttledSavePending = false; this.throttledCallbacks = []; // enable console output if verbose flag is set (disabled by default) this.verbose = options && options.hasOwnProperty('verbose') ? options.verbose : false; this.events = { 'init': [], 'loaded': [], 'flushChanges': [], 'close': [], 'changes': [], 'warning': [] }; var getENV = function () { if (typeof global !== 'undefined' && (global.android || global.NSObject)) { // If no adapter assume nativescript which needs adapter to be passed manually return 'NATIVESCRIPT'; //nativescript } if (typeof window === 'undefined') { return 'NODEJS'; } if (typeof global !== 'undefined' && global.window && typeof process !== 'undefined') { return 'NODEJS'; //node-webkit } if (typeof document !== 'undefined') { if (document.URL.indexOf('http://') === -1 && document.URL.indexOf('https://') === -1) { return 'CORDOVA'; } return 'BROWSER'; } return 'CORDOVA'; }; // refactored environment detection due to invalid detection for browser environments. // if they do not specify an options.env we want to detect env rather than default to nodejs. // currently keeping two properties for similar thing (options.env and options.persistenceMethod) // might want to review whether we can consolidate. if (options && options.hasOwnProperty('env')) { this.ENV = options.env; } else { this.ENV = getENV(); } // not sure if this is necessary now that i have refactored the line above if (this.ENV === 'undefined') { this.ENV = 'NODEJS'; } this.configureOptions(options, true); this.on('init', this.clearChanges); } // db class is an EventEmitter Loki.prototype = new LokiEventEmitter(); Loki.prototype.constructor = Loki; // experimental support for browserify's abstract syntax scan to pick up dependency of indexed adapter. // Hopefully, once this hits npm a browserify require of lokijs should scan the main file and detect this indexed adapter reference. Loki.prototype.getIndexedAdapter = function () { var adapter; if (typeof require === 'function') { adapter = require("./loki-indexed-adapter.js"); } return adapter; }; /** * Allows reconfiguring database options * * @param {object} options - configuration options to apply to loki db object * @param {string} options.env - override environment detection as 'NODEJS', 'BROWSER', 'CORDOVA' * @param {boolean} options.verbose - enable console output (default is 'false') * @param {boolean} options.autosave - enables autosave * @param {int} options.autosaveInterval - time interval (in milliseconds) between saves (if dirty) * @param {boolean} options.autoload - enables autoload on loki instantiation * @param {function} options.autoloadCallback - user callback called after database load * @param {adapter} options.adapter - an instance of a loki persistence adapter * @param {string} options.serializationMethod - ['normal', 'pretty', 'destructured'] * @param {string} options.destructureDelimiter - string delimiter used for destructured serialization * @param {boolean} initialConfig - (internal) true is passed when loki ctor is invoking * @memberof Loki */ Loki.prototype.configureOptions = function (options, initialConfig) { var defaultPersistence = { 'NODEJS': 'fs', 'BROWSER': 'localStorage', 'CORDOVA': 'localStorage', 'MEMORY': 'memory' }, persistenceMethods = { 'fs': LokiFsAdapter, 'localStorage': LokiLocalStorageAdapter, 'memory': LokiMemoryAdapter }; this.options = {}; this.persistenceMethod = null; // retain reference to optional persistence adapter 'instance' // currently keeping outside options because it can't be serialized this.persistenceAdapter = null; // process the options if (typeof (options) !== 'undefined') { this.options = options; if (this.options.hasOwnProperty('persistenceMethod')) { // check if the specified persistence method is known if (typeof (persistenceMethods[options.persistenceMethod]) == 'function') { this.persistenceMethod = options.persistenceMethod; this.persistenceAdapter = new persistenceMethods[options.persistenceMethod](); } // should be throw an error here, or just fall back to defaults ?? } // if user passes adapter, set persistence mode to adapter and retain persistence adapter instance if (this.options.hasOwnProperty('adapter')) { this.persistenceMethod = 'adapter'; this.persistenceAdapter = options.adapter; this.options.adapter = null; } // if they want to load database on loki instantiation, now is a good time to load... after adapter set and before possible autosave initiation if (options.autoload && initialConfig) { // for autoload, let the constructor complete before firing callback var self = this; setTimeout(function () { self.loadDatabase(options, options.autoloadCallback); }, 1); } if (this.options.hasOwnProperty('autosaveInterval')) { this.autosaveDisable(); this.autosaveInterval = parseInt(this.options.autosaveInterval, 10); } if (this.options.hasOwnProperty('autosave') && this.options.autosave) { this.autosaveDisable(); this.autosave = true; if (this.options.hasOwnProperty('autosaveCallback')) { this.autosaveEnable(options, options.autosaveCallback); } else { this.autosaveEnable(); } } if (this.options.hasOwnProperty('throttledSaves')) { this.throttledSaves = this.options.throttledSaves; } } // end of options processing // ensure defaults exists for options which were not set if (!this.options.hasOwnProperty('serializationMethod')) { this.options.serializationMethod = 'normal'; } // ensure passed or default option exists if (!this.options.hasOwnProperty('destructureDelimiter')) { this.options.destructureDelimiter = '$<\n'; } // if by now there is no adapter specified by user nor derived from persistenceMethod: use sensible defaults if (this.persistenceAdapter === null) { this.persistenceMethod = defaultPersistence[this.ENV]; if (this.persistenceMethod) { this.persistenceAdapter = new persistenceMethods[this.persistenceMethod](); } } }; /** * Copies 'this' database into a new Loki instance. Object references are shared to make lightweight. * * @param {object} options - apply or override collection level settings * @param {bool} options.removeNonSerializable - nulls properties not safe for serialization. * @memberof Loki */ Loki.prototype.copy = function(options) { // in case running in an environment without accurate environment detection, pass 'NA' var databaseCopy = new Loki(this.filename, { env: "NA" }); var clen, idx; options = options || {}; // currently inverting and letting loadJSONObject do most of the work databaseCopy.loadJSONObject(this, { retainDirtyFlags: true }); // since our JSON serializeReplacer is not invoked for reference database adapters, this will let us mimic if(options.hasOwnProperty("removeNonSerializable") && options.removeNonSerializable === true) { databaseCopy.autosaveHandle = null; databaseCopy.persistenceAdapter = null; clen = databaseCopy.collections.length; for (idx=0; idx<clen; idx++) { databaseCopy.collections[idx].constraints = null; databaseCopy.collections[idx].ttl = null; } } return databaseCopy; }; /** * Adds a collection to the database. * @param {string} name - name of collection to add * @param {object=} options - (optional) options to configure collection with. * @param {array=} [options.unique=[]] - array of property names to define unique constraints for * @param {array=} [options.exact=[]] - array of property names to define exact constraints for * @param {array=} [options.indices=[]] - array property names to define binary indexes for * @param {boolean} [options.asyncListeners=false] - whether listeners are called asynchronously * @param {boolean} [options.disableMeta=false] - set to true to disable meta property on documents * @param {boolean} [options.disableChangesApi=true] - set to false to enable Changes Api * @param {boolean} [options.disableDeltaChangesApi=true] - set to false to enable Delta Changes API (requires Changes API, forces cloning) * @param {boolean} [options.autoupdate=false] - use Object.observe to update objects automatically * @param {boolean} [options.clone=false] - specify whether inserts and queries clone to/from user * @param {string} [options.cloneMethod='parse-stringify'] - 'parse-stringify', 'jquery-extend-deep', 'shallow, 'shallow-assign' * @param {int=} options.ttl - age of document (in ms.) before document is considered aged/stale. * @param {int=} options.ttlInterval - time interval for clearing out 'aged' documents; not set by default. * @returns {Collection} a reference to the collection which was just added * @memberof Loki */ Loki.prototype.addCollection = function (name, options) { var i, len = this.collections.length; if (options && options.disableMeta === true) { if (options.disableChangesApi === false) { throw new Error("disableMeta option cannot be passed as true when disableChangesApi is passed as false"); } if (options.disableDeltaChangesApi === false) { throw new Error("disableMeta option cannot be passed as true when disableDeltaChangesApi is passed as false"); } if (typeof options.ttl === "number" && options.ttl > 0) { throw new Error("disableMeta option cannot be passed as true when ttl is enabled"); } } for (i = 0; i < len; i += 1) { if (this.collections[i].name === name) { return this.collections[i]; } } var collection = new Collection(name, options); this.collections.push(collection); if (this.verbose) collection.console = console; return collection; }; Loki.prototype.loadCollection = function (collection) { if (!collection.name) { throw new Error('Collection must have a name property to be loaded'); } this.collections.push(collection); }; /** * Retrieves reference to a collection by name. * @param {string} collectionName - name of collection to look up * @returns {Collection} Reference to collection in database by that name, or null if not found * @memberof Loki */ Loki.prototype.getCollection = function (collectionName) { var i, len = this.collections.length; for (i = 0; i < len; i += 1) { if (this.collections[i].name === collectionName) { return this.collections[i]; } } // no such collection this.emit('warning', 'collection ' + collectionName + ' not found'); return null; }; /** * Renames an existing loki collection * @param {string} oldName - name of collection to rename * @param {string} newName - new name of collection * @returns {Collection} reference to the newly renamed collection * @memberof Loki */ Loki.prototype.renameCollection = function (oldName, newName) { var c = this.getCollection(oldName); if (c) { c.name = newName; } return c; }; /** * Returns a list of collections in the database. * @returns {object[]} array of objects containing 'name', 'type', and 'count' properties. * @memberof Loki */ Loki.prototype.listCollections = function () { var i = this.collections.length, colls = []; while (i--) { colls.push({ name: this.collections[i].name, type: this.collections[i].objType, count: this.collections[i].data.length }); } return colls; }; /** * Removes a collection from the database. * @param {string} collectionName - name of collection to remove * @memberof Loki */ Loki.prototype.removeCollection = function (collectionName) { var i, len = this.collections.length; for (i = 0; i < len; i += 1) { if (this.collections[i].name === collectionName) { var tmpcol = new Collection(collectionName, {}); var curcol = this.collections[i]; for (var prop in curcol) { if (curcol.hasOwnProperty(prop) && tmpcol.hasOwnProperty(prop)) { curcol[prop] = tmpcol[prop]; } } this.collections.splice(i, 1); return; } } }; Loki.prototype.getName = function () { return this.name; }; /** * serializeReplacer - used to prevent certain properties from being serialized * */ Loki.prototype.serializeReplacer = function (key, value) { switch (key) { case 'autosaveHandle': case 'persistenceAdapter': case 'constraints': case 'ttl': return null; case 'throttledSavePending': case 'throttledCallbacks': return undefined; default: return value; } }; /** * Serialize database to a string which can be loaded via {@link Loki#loadJSON} * * @returns {string} Stringified representation of the loki database. * @memberof Loki */ Loki.prototype.serialize = function (options) { options = options || {}; if (!options.hasOwnProperty("serializationMethod")) { options.serializationMethod = this.options.serializationMethod; } switch(options.serializationMethod) { case "normal": return JSON.stringify(this, this.serializeReplacer); case "pretty": return JSON.stringify(this, this.serializeReplacer, 2); case "destructured": return this.serializeDestructured(); // use default options default: return JSON.stringify(this, this.serializeReplacer); } }; // alias of serialize Loki.prototype.toJson = Loki.prototype.serialize; /** * Database level destructured JSON serialization routine to allow alternate serialization methods. * Internally, Loki supports destructuring via loki "serializationMethod' option and * the optional LokiPartitioningAdapter class. It is also available if you wish to do * your own structured persistence or data exchange. * * @param {object=} options - output format options for use externally to loki * @param {bool=} options.partitioned - (default: false) whether db and each collection are separate * @param {int=} options.partition - can be used to only output an individual collection or db (-1) * @param {bool=} options.delimited - (default: true) whether subitems are delimited or subarrays * @param {string=} options.delimiter - override default delimiter * * @returns {string|array} A custom, restructured aggregation of independent serializations. * @memberof Loki */ Loki.prototype.serializeDestructured = function(options) { var idx, sidx, result, resultlen; var reconstruct = []; var dbcopy; options = options || {}; if (!options.hasOwnProperty("partitioned")) { options.partitioned = false; } if (!options.hasOwnProperty("delimited")) { options.delimited = true; } if (!options.hasOwnProperty("delimiter")) { options.delimiter = this.options.destructureDelimiter; } // 'partitioned' along with 'partition' of 0 or greater is a request for single collection serialization if (options.partitioned === true && options.hasOwnProperty("partition") && options.partition >= 0) { return this.serializeCollection({ delimited: options.delimited, delimiter: options.delimiter, collectionIndex: options.partition }); } // not just an individual collection, so we will need to serialize db container via shallow copy dbcopy = new Loki(this.filename); dbcopy.loadJSONObject(this); for(idx=0; idx < dbcopy.collections.length; idx++) { dbcopy.collections[idx].data = []; } // if we -only- wanted the db container portion, return it now if (options.partitioned === true && options.partition === -1) { // since we are deconstructing, override serializationMethod to normal for here return dbcopy.serialize({ serializationMethod: "normal" }); } // at this point we must be deconstructing the entire database // start by pushing db serialization into first array element reconstruct.push(dbcopy.serialize({ serializationMethod: "normal" })); dbcopy = null; // push collection data into subsequent elements for(idx=0; idx < this.collections.length; idx++) { result = this.serializeCollection({ delimited: options.delimited, delimiter: options.delimiter, collectionIndex: idx }); // NDA : Non-Delimited Array : one iterable concatenated array with empty string collection partitions if (options.partitioned === false && options.delimited === false) { if (!Array.isArray(result)) { throw new Error("a nondelimited, non partitioned collection serialization did not return an expected array"); } // Array.concat would probably duplicate memory overhead for copying strings. // Instead copy each individually, and clear old value after each copy. // Hopefully this will allow g.c. to reduce memory pressure, if needed. resultlen = result.length; for (sidx=0; sidx < resultlen; sidx++) { reconstruct.push(result[sidx]); result[sidx] = null; } reconstruct.push(""); } else { reconstruct.push(result); } } // Reconstruct / present results according to four combinations : D, DA, NDA, NDAA if (options.partitioned) { // DA : Delimited Array of strings [0] db [1] collection [n] collection { partitioned: true, delimited: true } // useful for simple future adaptations of existing persistence adapters to save collections separately if (options.delimited) { return reconstruct; } // NDAA : Non-Delimited Array with subArrays. db at [0] and collection subarrays at [n] { partitioned: true, delimited : false } // This format might be the most versatile for 'rolling your own' partitioned sync or save. // Memory overhead can be reduced by specifying a specific partition, but at this code path they did not, so its all. else { return reconstruct; } } else { // D : one big Delimited string { partitioned: false, delimited : true } // This is the method Loki will use internally if 'destructured'. // Little memory overhead improvements but does not require multiple asynchronous adapter call scheduling if (options.delimited) { // indicate no more collections reconstruct.push(""); return reconstruct.join(options.delimiter); } // NDA : Non-Delimited Array : one iterable array with empty string collection partitions { partitioned: false, delimited: false } // This format might be best candidate for custom synchronous syncs or saves else { // indicate no more collections reconstruct.push(""); return reconstruct; } } reconstruct.push(""); return reconstruct.join(delim); }; /** * Collection level utility method to serialize a collection in a 'destructured' format * * @param {object=} options - used to determine output of method * @param {int} options.delimited - whether to return single delimited string or an array * @param {string} options.delimiter - (optional) if delimited, this is delimiter to use * @param {int} options.collectionIndex - specify which collection to serialize data for * * @returns {string|array} A custom, restructured aggregation of independent serializations for a single collection. * @memberof Loki */ Loki.prototype.serializeCollection = function(options) { var doccount, docidx, resultlines = []; options = options || {}; if (!options.hasOwnProperty("delimited")) { options.delimited = true; } if (!options.hasOwnProperty("collectionIndex")) { throw new Error("serializeCollection called without 'collectionIndex' option"); } doccount = this.collections[options.collectionIndex].data.length; resultlines = []; for(docidx=0; docidx<doccount; docidx++) { resultlines.push(JSON.stringify(this.collections[options.collectionIndex].data[docidx]));