@stefanprobst/lokijs
Version:
Fast document oriented javascript in-memory database
1,452 lines (1,236 loc) • 258 kB
JavaScript
/**
* 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]));