lokijs
Version:
A document oriented javascript in-memory database
1,438 lines (1,192 loc) • 39.9 kB
JavaScript
/**
* LokiJS
* @author Joe Minichino <joe@dsforge.net>
*
* A lightweight document oriented javascript database
*/
/**
* Define library loki
*/
/*jslint browser: true, node: true, plusplus: true, indent: 2 */
var loki = (function () {
'use strict';
function clone(data, method) {
var cloneMethod = method || 'parse-stringify',
cloned;
if (cloneMethod === 'parse-stringify') {
cloned = JSON.parse(JSON.stringify(data));
}
return cloned;
}
/**
* @constructor
* The main database class
*/
function Loki(filename) {
this.filename = filename || 'loki.db';
this.collections = [];
var getENV = function () {
if ((typeof module !== 'undefined') && module.exports) {
return 'NODEJS';
}
if (!(document === undefined)) {
if (document.URL.indexOf('http://') === -1 && document.URL.indexOf('https://') === -1) {
return 'CORDOVA';
}
return 'BROWSER';
}
return 'CORDOVA';
};
this.ENV = getENV();
if (this.ENV === 'NODEJS') {
this.fs = require('fs');
}
}
/**
* @constructor
* Resultset class allowing chainable queries. Intended to be instanced internally.
*
* Collection.find(), Collection.view(), and Collection.chain() instantiate this resultset
* Examples:
* mycollection.chain().view("Toyota").find({ "doors" : 4 }).data();
* mycollection.view("Toyota");
* mycollection.find({ "doors": 4 });
* When using .chain(), any number of view() and data() calls can be chained together to further filter
* resultset, ending the chain with a .data() call to return as an array of collection document objects.
*/
function Resultset(collection, queryObj, queryFunc) {
// retain reference to collection we are querying against
this.collection = collection;
// if chain() instantiates with null queryObj and queryFunc, so we will keep flag for later
this.searchIsChained = (!queryObj && !queryFunc);
this.filteredrows = [];
this.filterInitialized = false;
// if user supplied initial queryObj or queryFunc, apply it
if (queryObj != null) return this.find(queryObj);
if (queryFunc != null) return this.where(queryFunc);
// otherwise return unfiltered Resultset for future filtering
return this;
}
Resultset.prototype.toJSON = function () {
var copy = this.copy();
copy.collection = null;
return copy;
}
// limit() : allows you to limit the number of documents in the resultset (starting at 0).
// A resultset copy() is made to avoid altering original resultset,
// so future chain ops will not propagate back to original resultset
//
// Example :
// - You establish your resultset (directly or via a DynamicView)
// - You can then get documents 10-15 (array pos 9..14) via : results.offset(10).limit(5).data();
Resultset.prototype.limit = function (qty) {
// if this is chained resultset with no filters applied, just we need to populate filteredrows first
if (this.searchIsChained && !this.filterInitialized && this.filteredrows.length == 0) {
this.filteredrows = Object.keys(this.collection.data);
}
var rscopy = this.copy();
rscopy.filteredrows = rscopy.filteredrows.slice(0, qty);
return rscopy;
}
// offset() : zero based pos allows you to skip the first pos+1 documents in the resultset
// An offset(5) will start at the sixth document at array resultset.filteredrows[5]
Resultset.prototype.offset = function (pos) {
// if this is chained resultset with no filters applied, just we need to populate filteredrows first
if (this.searchIsChained && !this.filterInitialized && this.filteredrows.length == 0) {
this.filteredrows = Object.keys(this.collection.data);
}
var rscopy = this.copy();
rscopy.filteredrows = rscopy.filteredrows.splice(pos);
return rscopy;
}
// To support reuse of resultset in forked query situations use copy()
Resultset.prototype.copy = function () {
var result = new Resultset(this.collection, null, null);
result.filteredrows = this.filteredrows.slice();
result.filterInitialized = this.filterInitialized;
return result;
}
// User supplied compare function is provided two documents to compare. (chainable)
// Example:
// rslt.sort(function(obj1, obj2) {
// if (obj1.name == obj2.name) return 0;
// if (obj1.name > obj2.name) return 1;
// if (obj1.name < obj2.name) return -1;
// });
Resultset.prototype.sort = function (comparefun) {
// if this is chained resultset with no filters applied, just we need to populate filteredrows first
if (this.searchIsChained && !this.filterInitialized && this.filteredrows.length == 0) {
this.filteredrows = Object.keys(this.collection.data);
}
var wrappedComparer =
(function (userComparer, rslt) {
return function (a, b) {
var obj1 = rslt.collection.data[a];
var obj2 = rslt.collection.data[b];
return userComparer(obj1, obj2);
}
})(comparefun, this);
this.filteredrows.sort(wrappedComparer);
return this;
}
// Simpler, loose evaluation for user to sort based on a property name. (chainable)
// Example :
// rslt.simplesort("name");
Resultset.prototype.simplesort = function (propname, isdesc) {
// if this is chained resultset with no filters applied, just we need to populate filteredrows first
if (this.searchIsChained && !this.filterInitialized && this.filteredrows.length == 0) {
this.filteredrows = Object.keys(this.collection.data);
}
if (typeof (isdesc) == "undefined") isdesc = false;
var wrappedComparer =
(function (prop, desc, rslt) {
return function (a, b) {
var obj1 = rslt.collection.data[a];
var obj2 = rslt.collection.data[b];
if (obj1[prop] == obj2[prop]) return 0;
if (desc) {
if (obj1[prop] < obj2[prop]) return 1;
if (obj1[prop] > obj2[prop]) return -1;
} else {
if (obj1[prop] > obj2[prop]) return 1;
if (obj1[prop] < obj2[prop]) return -1;
}
}
})(propname, isdesc, this);
this.filteredrows.sort(wrappedComparer);
return this;
}
// Resultset.find() returns reference to 'this' Resultset, use data() to get rowdata
Resultset.prototype.find = function (query) {
// comparison operators
function $eq(a, b) {
return a === b;
}
function $gt(a, b) {
return a > b;
}
function $gte(a, b) {
return a >= b;
}
function $lt(a, b) {
return a < b;
}
function $lte(a, b) {
return a <= b;
}
function $ne(a, b) {
return a !== b;
}
var queryObject = query || 'getAll',
property,
value,
operator,
p,
key,
operators = {
'$eq': $eq,
'$gt': $gt,
'$gte': $gte,
'$lt': $lt,
'$lte': $lte,
'$ne': $ne
},
searchByIndex = false,
result = [],
index = null,
// comparison function
fun,
// collection data
t,
// collection data length
i;
// apply no filters if they want all
if (queryObject === 'getAll') {
return this;
}
for (p in queryObject) {
if (queryObject.hasOwnProperty(p)) {
property = p;
if (typeof queryObject[p] !== 'object') {
operator = '$eq';
value = queryObject[p];
} else if (typeof queryObject[p] === 'object') {
for (key in queryObject[p]) {
if (queryObject[p].hasOwnProperty(key)) {
operator = key;
value = queryObject[p][key];
}
}
} else {
throw 'Do not know what you want to do.';
}
break;
}
}
if (this.collection.data === null) {
throw new TypeError();
}
// if an index exists for the property being queried against, use it
if (this.collection.indices.hasOwnProperty(property)) {
searchByIndex = true;
index = this.collection.indices[property];
}
// the comparison function
fun = operators[operator];
// Query executed differently depending on :
// - whether it is chained or not
// - whether the property being queried has an index defined
// - if chained, we handle first pass differently for initial filteredrows[] population
//
// For performance reasons, each case has its own if block to minimize in-loop calculations
// If not a chained query, bypass filteredrows and work directly against data
if (!this.searchIsChained) {
if (!searchByIndex) {
t = this.collection.data;
i = t.length;
while (i--) {
if (fun(t[i][property], value)) {
result.push(t[i]);
}
}
} else {
t = index;
i = index.length;
while (i--) {
if (fun(t[i], value)) {
result.push(this.collection.data[i]);
}
}
}
// not a chained query so return result as data[]
return result;
}
// Otherwise this is a chained query
else {
// If the filteredrows[] is already initialized, use it
if (this.filterInitialized) {
if (!searchByIndex) {
t = this.collection.data;
i = this.filteredrows.length;
while (i--) {
if (fun(t[this.filteredrows[i]][property], value)) {
result.push(this.filteredrows[i]);
}
}
} else {
t = index;
i = this.filteredrows.length; //t.length;
while (i--) {
if (fun(t[this.filteredrows[i]], value)) {
result.push(this.filteredrows[i]);
}
}
}
this.filteredrows = result;
return this;
}
// first chained query so work against data[] but put results in filteredrows
else {
if (!searchByIndex) {
t = this.collection.data;
i = t.length;
while (i--) {
if (fun(t[i][property], value)) {
result.push(i);
}
}
} else {
t = index;
i = t.length;
while (i--) {
if (fun(t[i], value)) {
result.push(i);
}
}
}
this.filteredrows = result;
this.filterInitialized = true; // next time work against filteredrows[]
return this;
}
}
}
// Resultset.where() returns reference to 'this' Resultset, use data() to get rowdata
Resultset.prototype.where = function (fun) {
var viewFunction,
result = [];
if (('string' === typeof fun) && ('function' === typeof this.collection.Views[fun])) {
viewFunction = this.collection.Views[fun];
} else if ('function' === typeof fun) {
viewFunction = fun;
} else {
throw 'Argument is not a stored view or a function';
}
try {
// if not a chained query then run directly against data[] and return object []
if (!this.searchIsChained) {
var i = this.collection.data.length;
while (i--) {
if (viewFunction(this.collection.data[i]) === true) {
result.push(this.collection.data[i]);
}
}
// not a chained query so returning result as data[]
return result;
}
// else chained query, so run against filteredrows
else {
// If the filteredrows[] is already initialized, use it
if (this.filterInitialized) {
var i = this.filteredrows.length;
while (i--) {
if (viewFunction(this.collection.data[this.filteredrows[i]]) === true) {
result.push(this.filteredrows[i]);
}
}
this.filteredrows = result;
return this;
}
// otherwise this is initial chained op, work against data, push into filteredrows[]
else {
var i = this.collection.data.length;
while (i--) {
if (viewFunction(this.collection.data[i]) === true) {
result.push(i);
}
}
this.filteredrows = result;
this.filterInitialized = true;
return this;
}
}
} catch (err) {
throw err;
}
}
// Resultset.data() returns array or filtered documents
Resultset.prototype.data = function () {
var result = [];
// if this is chained resultset with no filters applied, just return collection.data
if (this.searchIsChained && !this.filterInitialized) {
if (this.filteredrows.length == 0) {
return this.collection.data;
} else {
// filteredrows must have been set manually, so use it
this.filterInitialized = true;
}
}
for (var i in this.filteredrows) {
result.push(this.collection.data[this.filteredrows[i]]);
}
return result;
}
/**
* @constructor
* DynamicView class is a versatile 'live' view class which is optionally persistent
*
* Collection.addDynamicView(name) instantiates this DynamicView object
*
* Examples:
* var mydv = mycollection.addDynamicView("test"); // default is non-persistent
* mydv.applyWhere(function(obj) { return obj.name == "Toyota"; });
* mydv.applyFind({ "doors" : 4 });
* var results = mydv.data();
*
* Chaining is supported on apply functions : applyWhere().applyFind().data() is valid
*/
function DynamicView(collection, name, persistent) {
this.collection = collection;
this.name = name;
this.persistent = false;
if (typeof (persistent) != "undefined") this.persistent = persistent;
this.resultset = new Resultset(collection)
this.resultdata = [];
this.resultsdirty = false;
this.cachedresultset = null;
// keep ordered filter pipeline
this.filterPipeline = [];
// sorting member variables
// we only support one active search, applied using applySort() or applySimpleSort()
this.sortFunction = null;
this.sortColumn = null;
this.sortColumnDesc = false;
this.sortDirty = false;
// may add map and reduce phases later
}
DynamicView.prototype.toJSON = function () {
var copy = new DynamicView(this.collection, this.name, this.persistent);
copy.resultset = this.resultset;
copy.resultdata = this.resultdata;
copy.resultsdirty = this.resultsdirty;
copy.filterPipeline = this.filterPipeline;
copy.sortFunction = this.sortFunction;
copy.sortColumn = this.sortColumn;
copy.sortColumnDesc = this.sortColumnDesc;
copy.sortDirty = this.sortDirty;
// avoid circular reference, reapply in db.loadJSON()
copy.collection = null;
return copy;
}
DynamicView.prototype.applySort = function (comparefun) {
this.sortFunction = comparefun;
this.sortColumn = null;
this.sortColumnDesc = false;
this.resultset.sort(comparefun);
this.sortDirty = false;
return this;
}
DynamicView.prototype.applySimpleSort = function (propname, isdesc) {
if (typeof (isdesc) == "undefined") isdesc = false;
this.sortColumn = propname;
this.sortColumnDesc = isdesc;
this.sortFunction = null;
this.resultset.simplesort(propname, isdesc);
this.sortDirty = false;
return this;
}
DynamicView.prototype.startTransaction = function () {
this.cachedresultset = this.resultset.copy();
}
DynamicView.prototype.commit = function () {
this.cachedresultset = null;
}
DynamicView.prototype.rollback = function () {
this.resultset = this.cachedresultset;
if (this.persistent) {
// i don't like the idea of keeping duplicate cached rows for each (possibly) persistent view
// so we will for now just rebuild the persistent dynamic view data in this worst case scenario
// (a persistent view utilizing transactions which get rolled back), we already know the filter so not too bad.
this.resultdata = this.resultset.data();
}
}
DynamicView.prototype.applyFind = function (query) {
this.filterPipeline.push({
type: 'find',
val: query
});
// Apply immediately to Resultset; if persistent we will wait until later to build internal data
this.resultset.find(query);
this.sortDirty = true;
if (this.persistent) this.resultsdirty = true;
return this;
}
DynamicView.prototype.applyWhere = function (fun) {
this.filterPipeline.push({
type: 'where',
val: fun
});
// Apply immediately to Resultset; if persistent we will wait until later to build internal data
this.resultset.where(fun);
this.sortDirty = true;
if (this.persistent) this.resultsdirty = true;
return this;
}
// will either build a resultset array or (if persistent) return reference to persistent data array
DynamicView.prototype.data = function () {
if (this.sortDirty) {
if (this.sortFunction) this.resultset.sort(this.sortFunction);
if (this.sortColumn) this.resultset.simplesort(this.sortColumn, this.sortColumnDesc);
this.sortDirty = false;
if (this.persistent) this.resultsdirty = true; // newly sorted, if persistent we need to rebuild resultdata
}
// if nonpersistent return resultset data evaluation
if (!this.persistent) {
return this.resultset.data();
}
// Persistent Views - we pay price of bulk row copy on first data() access after new filters applied
if (this.resultsdirty) {
this.resultdata = this.resultset.data();
this.resultsdirty = false;
}
return this.resultdata;
}
// internal function called on collection.insert() and collection.update()
DynamicView.prototype.evaluateDocument = function (objIndex) {
var ofr = this.resultset.filteredrows;
var oldPos = ofr.indexOf(objIndex);
var oldlen = ofr.length;
// creating a 1-element resultset to run filter chain ops on to see if that doc passes filters;
// mostly efficient algorithm, slight stack overhead price (this function is called on inserts and updates)
var evalResultset = new Resultset(this.collection);
evalResultset.filteredrows = [objIndex];
evalResultset.filterInitialized = true;
for (var idx = 0; idx < this.filterPipeline.length; idx++) {
switch (this.filterPipeline[idx].type) {
case "find":
evalResultset.find(this.filterPipeline[idx].val);
break;
case "where":
evalResultset.where(this.filterPipeline[idx].val);
break;
}
}
// not a true position, but -1 if not pass our filter(s), 0 if passed filter(s)
var newPos = (evalResultset.filteredrows.length == 0) ? -1 : 0;
// wasn't in old, shouldn't be now... do nothing
if (oldPos == -1 && newPos == -1) return;
// wasn't in resultset, should be now... add
if (oldPos == -1 && newPos != -1) {
ofr.push(objIndex);
if (this.persistent) this.resultdata.push(this.collection.data[objIndex]);
// need to re-sort to sort new document
if (this.sortFunction || this.sortColumn) this.sortDirty = true;
return;
}
// was in resultset, shouldn't be now... delete
if (oldPos != -1 && newPos == -1) {
if (oldPos < oldlen - 1) {
// http://dvolvr.davidwaterston.com/2013/06/09/restating-the-obvious-the-fastest-way-to-truncate-an-array-in-javascript/comment-page-1/
ofr[oldPos] = ofr[oldlen - 1];
ofr.length = oldlen - 1;
if (this.persistent) {
this.resultdata[oldPos] = this.resultdata[oldlen - 1];
this.resultdata.length = oldlen - 1;
}
} else {
ofr.length = oldlen - 1;
if (this.persistent) this.resultdata.length = oldlen - 1;
}
return;
}
// was in resultset, should still be now... (update persistent only?)
if (oldPos != -1 && newPos != -1) {
if (this.persistent) {
// in case document changed, replace persistent view data with the latest collection.data document
this.resultdata[oldPos] = this.collection.data[objIndex];
}
// in case changes to data altered a sort column
if (this.sortFunction || this.sortColumn) this.sortDirty = true;
return;
}
}
// internal function called on collection.delete()
DynamicView.prototype.removeDocument = function (objIndex) {
var ofr = this.resultset.filteredrows;
var oldPos = ofr.indexOf(objIndex);
var oldlen = ofr.length;
if (oldPos != -1) {
// if not last row in resultdata, swap last to hole and truncate last row
if (oldPos < oldlen - 1) {
ofr[oldPos] = ofr[oldlen - 1];
ofr.length = oldlen - 1;
this.resultdata[oldPos] = this.resultdata[oldlen - 1];
this.resultdata.length = oldlen - 1;
}
// last row, so just truncate last row
else {
ofr.length = oldlen - 1;
this.resultdata.length = oldlen - 1;
}
}
}
/**
* @constructor
* Collection class that handles documents of same type
*/
function Collection(name, objType, indices, transactionOptions) {
// the name of the collection
this.name = name;
// the data held by the collection
this.data = [];
// indices multi-dimensional array
this.indices = {};
this.idIndex = {}; // index of idx
// the object type of the collection
this.objType = objType || "";
/** Transactions properties */
// is collection transactional
this.transactional = transactionOptions || false;
// private holders for cached data
this.cachedIndex = null;
this.cachedData = null;
// currentMaxId - change manually at your own peril!
this.maxId = 0;
// view container is an object because each views gets a name
this.Views = {};
this.DynamicViews = [];
// pointer to self to avoid this tricks
var indexesArray = indices || ['id'],
i = indexesArray.length;
while (i--) {
this.ensureIndex(indexesArray[i]);
}
// initialize the id index
this.ensureIndex('id');
}
Loki.prototype.addCollection = function (name, objType, indexesArray, transactional) {
var collection = new Collection(name, objType, indexesArray, transactional);
this.collections.push(collection);
return collection;
};
Loki.prototype.loadCollection = function (collection) {
this.collections.push(collection);
};
Loki.prototype.getCollection = function (collectionName) {
var found = false,
len = this.collections.length,
i;
for (i = 0; i < len; i += 1) {
if (this.collections[i].name === collectionName) {
found = true;
return this.collections[i];
}
}
if (!found) {
throw 'No such collection';
}
};
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;
};
Loki.prototype.removeCollection = function (name) {
var i = 0,
len = this.collections.length;
for (i; i < len; i += 1) {
if (this.collections[i].name === name) {
this.collections.splice(i, 1);
break;
}
}
};
Loki.prototype.getName = function () {
return this.name;
};
// toJson
Loki.prototype.serialize = function () {
return JSON.stringify(this);
};
// alias of serialize
Loki.prototype.toJson = Loki.prototype.serialize;
// load Json function - db is saved to disk as json
Loki.prototype.loadJSON = function (serializedDb) {
var obj = JSON.parse(serializedDb),
i = 0,
len = obj.collections.length,
coll,
copyColl,
clen,
j;
this.name = obj.name;
this.collections = [];
for (i; i < len; i += 1) {
coll = obj.collections[i];
copyColl = this.addCollection(coll.name, coll.objType);
// load each element individually
clen = coll.data.length;
j = 0;
for (j; j < clen; j++) {
copyColl.data[j] = coll.data[j];
}
copyColl.maxId = (coll.data.length == 0) ? 0 : coll.data.maxId;
copyColl.indices = coll.indices;
copyColl.idIndex = coll.indices.id;
copyColl.transactional = coll.transactional;
copyColl.ensureAllIndexes();
// in case they are loading a database created before we added dynamic views, handle undefined
if (typeof (coll.DynamicViews) == "undefined") continue;
// reinflate DynamicViews and attached Resultsets
for (var idx = 0; idx < coll.DynamicViews.length; idx++) {
var colldv = coll.DynamicViews[idx];
var dv = copyColl.addDynamicView(colldv.name, colldv.persistent);
dv.resultdata = colldv.resultdata;
dv.resultsdirty = colldv.resultsdirty;
dv.filterPipeline = colldv.filterPipeline;
dv.sortColumn = colldv.sortColumn;
dv.sortColumnDesc = colldv.sortColumnDesc;
dv.sortFunction = colldv.sortFunction;
dv.sortDirty = colldv.sortDirty;
dv.resultset.filteredrows = colldv.resultset.filteredrows;
dv.resultset.searchIsChained = colldv.resultset.searchIsChained;
dv.resultset.filterInitialized = colldv.resultset.filterInitialized;
}
}
};
// load db from a file
Loki.prototype.loadDatabase = function (callback) {
var cFun = callback || function () {
return;
},
self = this;
if (this.ENV === 'NODEJS') {
this.fs.readFile(this.filename, {
encoding: 'utf8'
}, function (err, data) {
if (err) {
throw err;
}
self.loadJSON(data);
cFun(data);
});
}
};
// save file to disk as json
Loki.prototype.saveToDisk = function (callback) {
var cFun = callback || function () {
return;
},
self = this;
// persist in nodejs
if (this.ENV === 'NODEJS') {
this.fs.exists(this.filename, function (exists) {
if (exists) {
self.fs.unlink(self.filename);
}
self.fs.writeFile(self.filename, self.serialize(), function (err) {
if (err) {
throw err;
}
cFun();
});
});
}
};
// alias
Loki.prototype.save = Loki.prototype.saveToDisk;
// future use for saving collections to remote db
// Loki.prototype.saveRemote = Loki.prototype.no_op;
/*----------------------------+
| INDEXING |
+----------------------------*/
/**
* Ensure indexes on a certain field
*/
Collection.prototype.ensureIndex = function (property) {
if (property === null || property === undefined) {
throw 'Attempting to set index without an associated property';
}
var index, len = this.data.length,
i = 0;
if (this.indices.hasOwnProperty(property)) {
index = this.indices[property];
} else {
this.indices[property] = [];
index = this.indices[property];
}
for (i; i < len; i += 1) {
index.push(this.data[i][property]);
}
if (property === 'id') {
this.idIndex = index;
}
};
/**
* Ensure index async with callback - useful for background syncing with a remote server
*/
Collection.prototype.ensureIndexAsync = function (property, callback) {
this.async(function () {
this.ensureIndex(property);
}, callback);
};
/**
* Ensure all indexes
*/
Collection.prototype.ensureAllIndexes = function () {
var i = this.indices.length;
while (i--) {
this.ensureIndex(this.indices[i].name);
}
if (i === 0) {
this.ensureIndex('id');
}
};
Collection.prototype.ensureAllIndexesAsync = function (callback) {
this.async(function () {
this.ensureAllIndexes();
}, callback);
};
/**
* Each collection maintains a list of DynamicViews associated with it
**/
Collection.prototype.addDynamicView = function (name, persistent) {
var dv = new DynamicView(this, name, persistent);
this.DynamicViews.push(dv);
return dv;
}
Collection.prototype.removeDynamicView = function (name) {
for (var idx = 0; idx < this.DynamicViews.length; idx++) {
if (this.DynamicViews[idx].name == name) {
this.DynamicViews.splice(idx, 1);
}
}
}
Collection.prototype.getDynamicView = function (name) {
for (var idx = 0; idx < this.DynamicViews.length; idx++) {
if (this.DynamicViews[idx].name == name) {
return this.DynamicViews[idx];
}
}
}
/**
* find and update: pass a filtering function to select elements to be updated
* and apply the updatefunctino to those elements iteratively
*/
Collection.prototype.findAndUpdate = function (filterFunction, updateFunction) {
var results = this.view(filterFunction),
i = 0,
obj;
try {
for (i; i < results.length; i++) {
obj = updateFunction(results[i]);
this.update(obj);
}
} catch (err) {
this.rollback();
console.error(err.message);
}
};
/**
* generate document method - ensure objects have id and objType properties
* Come to think of it, really unfortunate name because of what document normally refers to in js.
* that's why there's an alias below but until I have this implemented
*/
Collection.prototype.insert = function (doc) {
var self = this;
if (Array.isArray(doc)) {
doc.forEach(function (d) {
d.id = null;
d.objType = self.objType;
self.add(d);
});
return doc;
} else {
if (typeof doc !== 'object') {
throw new TypeError("Document needs to be an object");
return;
}
doc.id = null;
doc.objType = this.objType;
this.add(doc);
return doc;
}
};
Collection.prototype.clear = function () {
this.data = [];
this.indices = {};
this.idIndex = {};
this.cachedIndex = null;
this.cachedData = null;
this.maxId = 0;
this.Views = {};
};
/**
* Update method
*/
Collection.prototype.update = function (doc) {
// verify object is a properly formed document
if (!doc.hasOwnProperty('id')) {
throw 'Trying to update unsynced document. Please save the document first by using add() or addMany()';
}
try {
this.startTransaction();
var i, arr = this.get(doc.id, true),
obj = arr[0],
// get current position in data array
position = arr[1];
// operate the update
this.data[position] = doc;
// now that we can efficiently determine the data[] position of newly added document,
// submit it for all registered DynamicViews to evaluate for inclusion/exclusion
for (var idx = 0; idx < this.DynamicViews.length; idx++) {
this.DynamicViews[idx].evaluateDocument(position);
}
for (i in this.indices) {
if (this.indices.hasOwnProperty(i)) {
this.indices[i][position] = obj[i];
}
}
this.commit();
} catch (err) {
this.rollback();
console.error(err.message);
}
};
/**
* Add object to collection
*/
Collection.prototype.add = function (obj) {
// if parameter isn't object exit with throw
if ('object' !== typeof obj) {
throw 'Object being added needs to be an object';
}
/*
* try adding object to collection
*/
if (this.objType === "" && this.data.length === 0) {
// set object type to that of the first object added to collection
this.objType = obj.objType;
} else {
// throw an error if the object added is not the same type as the collection's
if (this.objType !== obj.objType) {
throw 'Object type [' + obj.objType + '] is incongruent with collection type [' + this.objType + ']';
}
if (this.objType === '') {
throw 'Object is not a model';
}
if (obj.id !== null && obj.id > 0) {
throw 'Document is already in collection, please use update()';
}
try {
this.startTransaction();
this.maxId++;
var i;
if (isNaN(this.maxId)) {
this.maxId = (this.data[this.data.length - 1].id + 1);
}
obj.id = this.maxId;
// add the object
this.data.push(obj);
// now that we can efficiently determine the data[] position of newly added document,
// submit it for all registered DynamicViews to evaluate for inclusion/exclusion
for (var idx = 0; idx < this.DynamicViews.length; idx++) {
this.DynamicViews[idx].evaluateDocument(this.data.length - 1);
}
// now that we can efficiently determine the data[] position of newly added document,
// submit it for all registered DynamicViews to evaluate for inclusion/exclusion
for (var idx = 0; idx < this.DynamicViews.length; idx++) {
this.DynamicViews[idx].evaluateDocument(this.data.length - 1);
}
// resync indexes to make sure all IDs are there
for (i in this.indices) {
if (this.indices.hasOwnProperty(i)) {
this.indices[i].push(obj[i]);
}
}
this.commit();
return obj;
} catch (err) {
this.rollback();
console.error(err.message);
}
}
};
/**
* iterate through arguments and add indexes
*/
Collection.prototype.addMany = function () {
var i = arguments.length;
while (i--) {
this.add(arguments[i]);
}
};
/**
* delete wrapped
*/
Collection.prototype.remove = function (doc) {
if ('object' !== typeof doc) {
throw 'Parameter is not an object';
}
if (!doc.hasOwnProperty('id')) {
throw 'Object is not a document stored in the collection';
}
try {
this.startTransaction();
var arr = this.get(doc.id, true),
// obj = arr[0],
position = arr[1],
i;
// now that we can efficiently determine the data[] position of newly added document,
// submit it for all registered DynamicViews to remove
for (var idx = 0; idx < this.DynamicViews.length; idx++) {
this.DynamicViews[idx].removeDocument(position);
}
this.data.splice(position, 1);
for (i in this.indices) {
if (this.indices.hasOwnProperty(i)) {
this.indices[i].splice(position, 1);
}
}
this.commit();
} catch (err) {
this.rollback();
console.error(err.message);
}
};
/*---------------------+
| Finding methods |
+----------------------*/
/**
* Get by Id - faster than other methods because of the searching algorithm
*/
Collection.prototype.get = function (id, returnPosition) {
var retpos = returnPosition || false,
data = this.indices.id,
max = data.length - 1,
min = 0,
mid = Math.floor(min + (max - min) / 2);
if (isNaN(id)) {
id = parseInt(id);
if (isNaN(id)) {
throw 'Passed id is not an integer';
}
}
while (data[min] < data[max]) {
mid = Math.floor((min + max) / 2);
if (data[mid] < id) {
min = mid + 1;
} else {
max = mid;
}
}
if (max === min && data[min] === id) {
if (retpos) {
return [this.data[min], min];
}
return this.data[min];
}
return null;
};
/**
* Find one object by index property, by property equal to value
*/
Collection.prototype.findOne = function (prop, value) {
var searchByIndex = false,
indexObject = null,
// iterate the indices to ascertain whether property is indexed
i = this.indices.length,
len,
doc;
for (i in this.indices) {
if (this.indices.hasOwnProperty(i)) {
if (i === prop) {
searchByIndex = true;
indexObject = this.indices[i];
break;
}
}
}
if (searchByIndex) {
// perform search based on index
len = indexObject.data.length;
while (len--) {
if (indexObject.data[len] === value) {
doc = this.data[len];
return doc;
}
}
} else {
// search all collection and find first matching result
return this.findOneUnindexed(prop, value);
}
return null;
};
/**
* Chain method, used for beginning a series of chained find() and/or view() operations
* on a collection.
*/
Collection.prototype.chain = function () {
return new Resultset(this, null, null);
};
/**
* Find method, api is similar to mongodb except for now it only supports one search parameter.
* for more complex queries use view() and storeView()
*/
Collection.prototype.find = function (query) {
// find logic moved into Resultset class
return new Resultset(this, query, null);
};
/**
* Find object by unindexed field by property equal to value,
* simply iterates and returns the first element matching the query
*/
Collection.prototype.findOneUnindexed = function (prop, value) {
var i = this.data.length,
doc;
while (i--) {
if (this.data[i][prop] === value) {
doc = this.data[i];
return doc;
}
}
return null;
};
/**
* Transaction methods
*/
/** start the transation */
Collection.prototype.startTransaction = function () {
if (this.transactional) {
this.cachedData = clone(this.data, 'parse-stringify');
this.cachedIndex = this.indices;
// propagate startTransaction to dynamic views
for (var idx = 0; idx < this.DynamicViews.length; idx++) {
this.DynamicViews[idx].startTransaction();
}
}
};
/** commit the transation */
Collection.prototype.commit = function () {
if (this.transactional) {
this.cachedData = null;
this.cachedIndex = null;
// propagate commit to dynamic views
for (var idx = 0; idx < this.DynamicViews.length; idx++) {
this.DynamicViews[idx].commit();
}
}
};
/** roll back the transation */
Collection.prototype.rollback = function () {
if (this.transactional) {
if (this.cachedData !== null && this.cachedIndex !== null) {
this.data = this.cachedData;
this.indices = this.cachedIndex;
}
// propagate rollback to dynamic views
for (var idx = 0; idx < this.DynamicViews.length; idx++) {
this.DynamicViews[idx].rollback();
}
}
};
// async executor. This is only to enable callbacks at the end of the execution.
Collection.prototype.async = function (fun, callback) {
setTimeout(function () {
if (typeof fun === 'function') {
fun();
callback();
} else {
throw 'Argument passed for async execution is not a function';
}
}, 0);
};
/**
* Create view function - CouchDB style
*/
Collection.prototype.view = function (fun) {
// find logic moved into Resultset class
return new Resultset(this, null, fun);
};
/**
* store a view in the collection for later reuse
*/
Collection.prototype.storeView = function (name, fun) {
if (typeof fun === 'function') {
this.Views[name] = fun;
}
};
/**
* Map Reduce
*/
Collection.prototype.mapReduce = function (mapFunction, reduceFunction) {
try {
return reduceFunction(this.data.map(mapFunction));
} catch (err) {
throw err;
}
};
Collection.prototype.no_op = function () {
return;
};
return Loki;
}());
if ('undefined' !== typeof module && module.exports) {
module.exports = loki;
}