UNPKG

231 kBJavaScriptView Raw
1/**
2 * LokiJS
3 * @author Joe Minichino <joe.minichino@gmail.com>
4 *
5 * A lightweight document oriented javascript database
6 */
7(function (root, factory) {
8 if (typeof define === 'function' && define.amd) {
9 // AMD
10 define([], factory);
11 } else if (typeof exports === 'object') {
12 // CommonJS
13 module.exports = factory();
14 } else {
15 // Browser globals
16 root.loki = factory();
17 }
18}(this, function () {
19
20 return (function () {
21 'use strict';
22
23 var hasOwnProperty = Object.prototype.hasOwnProperty;
24
25 var Utils = {
26 copyProperties: function (src, dest) {
27 var prop;
28 for (prop in src) {
29 dest[prop] = src[prop];
30 }
31 },
32 // used to recursively scan hierarchical transform step object for param substitution
33 resolveTransformObject: function (subObj, params, depth) {
34 var prop,
35 pname;
36
37 if (typeof depth !== 'number') {
38 depth = 0;
39 }
40
41 if (++depth >= 10) return subObj;
42
43 for (prop in subObj) {
44 if (typeof subObj[prop] === 'string' && subObj[prop].indexOf("[%lktxp]") === 0) {
45 pname = subObj[prop].substring(8);
46 if (params.hasOwnProperty(pname)) {
47 subObj[prop] = params[pname];
48 }
49 } else if (typeof subObj[prop] === "object") {
50 subObj[prop] = Utils.resolveTransformObject(subObj[prop], params, depth);
51 }
52 }
53
54 return subObj;
55 },
56 // top level utility to resolve an entire (single) transform (array of steps) for parameter substitution
57 resolveTransformParams: function (transform, params) {
58 var idx,
59 clonedStep,
60 resolvedTransform = [];
61
62 if (typeof params === 'undefined') return transform;
63
64 // iterate all steps in the transform array
65 for (idx = 0; idx < transform.length; idx++) {
66 // clone transform so our scan/replace can operate directly on cloned transform
67 clonedStep = clone(transform[idx], "shallow-recurse-objects");
68 resolvedTransform.push(Utils.resolveTransformObject(clonedStep, params));
69 }
70
71 return resolvedTransform;
72 }
73 };
74
75 /** Helper function for determining 'loki' abstract equality which is a little more abstract than ==
76 * aeqHelper(5, '5') === true
77 * aeqHelper(5.0, '5') === true
78 * aeqHelper(new Date("1/1/2011"), new Date("1/1/2011")) === true
79 * aeqHelper({a:1}, {z:4}) === true (all objects sorted equally)
80 * aeqHelper([1, 2, 3], [1, 3]) === false
81 * aeqHelper([1, 2, 3], [1, 2, 3]) === true
82 * aeqHelper(undefined, null) === true
83 */
84 function aeqHelper(prop1, prop2) {
85 var cv1, cv2, t1, t2;
86
87 if (prop1 === prop2) return true;
88
89 // 'falsy' and Boolean handling
90 if (!prop1 || !prop2 || prop1 === true || prop2 === true || prop1 !== prop1 || prop2 !== prop2) {
91 // dates and NaN conditions (typed dates before serialization)
92 switch (prop1) {
93 case undefined: t1 = 1; break;
94 case null: t1 = 1; break;
95 case false: t1 = 3; break;
96 case true: t1 = 4; break;
97 case "": t1 = 5; break;
98 default: t1 = (prop1 === prop1)?9:0; break;
99 }
100
101 switch (prop2) {
102 case undefined: t2 = 1; break;
103 case null: t2 = 1; break;
104 case false: t2 = 3; break;
105 case true: t2 = 4; break;
106 case "": t2 = 5; break;
107 default: t2 = (prop2 === prop2)?9:0; break;
108 }
109
110 // one or both is edge case
111 if (t1 !== 9 || t2 !== 9) {
112 return (t1===t2);
113 }
114 }
115
116 // Handle 'Number-like' comparisons
117 cv1 = Number(prop1);
118 cv2 = Number(prop2);
119
120 // if one or both are 'number-like'...
121 if (cv1 === cv1 || cv2 === cv2) {
122 return (cv1 === cv2);
123 }
124
125 // not strict equal nor less than nor gt so must be mixed types, convert to string and use that to compare
126 cv1 = prop1.toString();
127 cv2 = prop2.toString();
128
129 return (cv1 == cv2);
130 }
131
132 /** Helper function for determining 'less-than' conditions for ops, sorting, and binary indices.
133 * In the future we might want $lt and $gt ops to use their own functionality/helper.
134 * Since binary indices on a property might need to index [12, NaN, new Date(), Infinity], we
135 * need this function (as well as gtHelper) to always ensure one value is LT, GT, or EQ to another.
136 */
137 function ltHelper(prop1, prop2, equal) {
138 var cv1, cv2, t1, t2;
139
140 // if one of the params is falsy or strictly true or not equal to itself
141 // 0, 0.0, "", NaN, null, undefined, not defined, false, true
142 if (!prop1 || !prop2 || prop1 === true || prop2 === true || prop1 !== prop1 || prop2 !== prop2) {
143 switch (prop1) {
144 case undefined: t1 = 1; break;
145 case null: t1 = 1; break;
146 case false: t1 = 3; break;
147 case true: t1 = 4; break;
148 case "": t1 = 5; break;
149 // if strict equal probably 0 so sort higher, otherwise probably NaN so sort lower than even null
150 default: t1 = (prop1 === prop1)?9:0; break;
151 }
152
153 switch (prop2) {
154 case undefined: t2 = 1; break;
155 case null: t2 = 1; break;
156 case false: t2 = 3; break;
157 case true: t2 = 4; break;
158 case "": t2 = 5; break;
159 default: t2 = (prop2 === prop2)?9:0; break;
160 }
161
162 // one or both is edge case
163 if (t1 !== 9 || t2 !== 9) {
164 return (t1===t2)?equal:(t1<t2);
165 }
166 }
167
168 // if both are numbers (string encoded or not), compare as numbers
169 cv1 = Number(prop1);
170 cv2 = Number(prop2);
171
172 if (cv1 === cv1 && cv2 === cv2) {
173 if (cv1 < cv2) return true;
174 if (cv1 > cv2) return false;
175 return equal;
176 }
177
178 if (cv1 === cv1 && cv2 !== cv2) {
179 return true;
180 }
181
182 if (cv2 === cv2 && cv1 !== cv1) {
183 return false;
184 }
185
186 if (prop1 < prop2) return true;
187 if (prop1 > prop2) return false;
188 if (prop1 == prop2) return equal;
189
190 // not strict equal nor less than nor gt so must be mixed types, convert to string and use that to compare
191 cv1 = prop1.toString();
192 cv2 = prop2.toString();
193
194 if (cv1 < cv2) {
195 return true;
196 }
197
198 if (cv1 == cv2) {
199 return equal;
200 }
201
202 return false;
203 }
204
205 function gtHelper(prop1, prop2, equal) {
206 var cv1, cv2, t1, t2;
207
208 // 'falsy' and Boolean handling
209 if (!prop1 || !prop2 || prop1 === true || prop2 === true || prop1 !== prop1 || prop2 !== prop2) {
210 switch (prop1) {
211 case undefined: t1 = 1; break;
212 case null: t1 = 1; break;
213 case false: t1 = 3; break;
214 case true: t1 = 4; break;
215 case "": t1 = 5; break;
216 // NaN 0
217 default: t1 = (prop1 === prop1)?9:0; break;
218 }
219
220 switch (prop2) {
221 case undefined: t2 = 1; break;
222 case null: t2 = 1; break;
223 case false: t2 = 3; break;
224 case true: t2 = 4; break;
225 case "": t2 = 5; break;
226 default: t2 = (prop2 === prop2)?9:0; break;
227 }
228
229 // one or both is edge case
230 if (t1 !== 9 || t2 !== 9) {
231 return (t1===t2)?equal:(t1>t2);
232 }
233 }
234
235 // if both are numbers (string encoded or not), compare as numbers
236 cv1 = Number(prop1);
237 cv2 = Number(prop2);
238 if (cv1 === cv1 && cv2 === cv2) {
239 if (cv1 > cv2) return true;
240 if (cv1 < cv2) return false;
241 return equal;
242 }
243
244 if (cv1 === cv1 && cv2 !== cv2) {
245 return false;
246 }
247
248 if (cv2 === cv2 && cv1 !== cv1) {
249 return true;
250 }
251
252 if (prop1 > prop2) return true;
253 if (prop1 < prop2) return false;
254 if (prop1 == prop2) return equal;
255
256 // not strict equal nor less than nor gt so must be dates or mixed types
257 // convert to string and use that to compare
258 cv1 = prop1.toString();
259 cv2 = prop2.toString();
260
261 if (cv1 > cv2) {
262 return true;
263 }
264
265 if (cv1 == cv2) {
266 return equal;
267 }
268
269 return false;
270 }
271
272 function sortHelper(prop1, prop2, desc) {
273 if (aeqHelper(prop1, prop2)) return 0;
274
275 if (ltHelper(prop1, prop2, false)) {
276 return (desc) ? (1) : (-1);
277 }
278
279 if (gtHelper(prop1, prop2, false)) {
280 return (desc) ? (-1) : (1);
281 }
282
283 // not lt, not gt so implied equality-- date compatible
284 return 0;
285 }
286
287 /**
288 * compoundeval() - helper function for compoundsort(), performing individual object comparisons
289 *
290 * @param {array} properties - array of property names, in order, by which to evaluate sort order
291 * @param {object} obj1 - first object to compare
292 * @param {object} obj2 - second object to compare
293 * @returns {integer} 0, -1, or 1 to designate if identical (sortwise) or which should be first
294 */
295 function compoundeval(properties, obj1, obj2) {
296 var res = 0;
297 var prop, field, val1, val2, arr;
298 for (var i = 0, len = properties.length; i < len; i++) {
299 prop = properties[i];
300 field = prop[0];
301 if (~field.indexOf('.')) {
302 arr = field.split('.');
303 val1 = arr.reduce(function(obj, i) { return obj && obj[i] || undefined; }, obj1);
304 val2 = arr.reduce(function(obj, i) { return obj && obj[i] || undefined; }, obj2);
305 } else {
306 val1 = obj1[field];
307 val2 = obj2[field];
308 }
309 res = sortHelper(val1, val2, prop[1]);
310 if (res !== 0) {
311 return res;
312 }
313 }
314 return 0;
315 }
316
317 /**
318 * dotSubScan - helper function used for dot notation queries.
319 *
320 * @param {object} root - object to traverse
321 * @param {array} paths - array of properties to drill into
322 * @param {function} fun - evaluation function to test with
323 * @param {any} value - comparative value to also pass to (compare) fun
324 * @param {number} poffset - index of the item in 'paths' to start the sub-scan from
325 */
326 function dotSubScan(root, paths, fun, value, poffset) {
327 var pathOffset = poffset || 0;
328 var path = paths[pathOffset];
329 if (root === undefined || root === null || !hasOwnProperty.call(root, path)) {
330 return false;
331 }
332
333 var valueFound = false;
334 var element = root[path];
335 if (pathOffset + 1 >= paths.length) {
336 // if we have already expanded out the dot notation,
337 // then just evaluate the test function and value on the element
338 valueFound = fun(element, value);
339 } else if (Array.isArray(element)) {
340 for (var index = 0, len = element.length; index < len; index += 1) {
341 valueFound = dotSubScan(element[index], paths, fun, value, pathOffset + 1);
342 if (valueFound === true) {
343 break;
344 }
345 }
346 } else {
347 valueFound = dotSubScan(element, paths, fun, value, pathOffset + 1);
348 }
349
350 return valueFound;
351 }
352
353 function containsCheckFn(a) {
354 if (typeof a === 'string' || Array.isArray(a)) {
355 return function (b) {
356 return a.indexOf(b) !== -1;
357 };
358 } else if (typeof a === 'object' && a !== null) {
359 return function (b) {
360 return hasOwnProperty.call(a, b);
361 };
362 }
363 return null;
364 }
365
366 function doQueryOp(val, op) {
367 for (var p in op) {
368 if (hasOwnProperty.call(op, p)) {
369 return LokiOps[p](val, op[p]);
370 }
371 }
372 return false;
373 }
374
375 var LokiOps = {
376 // comparison operators
377 // a is the value in the collection
378 // b is the query value
379 $eq: function (a, b) {
380 return a === b;
381 },
382
383 // abstract/loose equality
384 $aeq: function (a, b) {
385 return a == b;
386 },
387
388 $ne: function (a, b) {
389 // ecma 5 safe test for NaN
390 if (b !== b) {
391 // ecma 5 test value is not NaN
392 return (a === a);
393 }
394
395 return a !== b;
396 },
397 // date equality / loki abstract equality test
398 $dteq: function (a, b) {
399 return aeqHelper(a, b);
400 },
401
402 $gt: function (a, b) {
403 return gtHelper(a, b, false);
404 },
405
406 $gte: function (a, b) {
407 return gtHelper(a, b, true);
408 },
409
410 $lt: function (a, b) {
411 return ltHelper(a, b, false);
412 },
413
414 $lte: function (a, b) {
415 return ltHelper(a, b, true);
416 },
417
418 // ex : coll.find({'orderCount': {$between: [10, 50]}});
419 $between: function (a, vals) {
420 if (a === undefined || a === null) return false;
421 return (gtHelper(a, vals[0], true) && ltHelper(a, vals[1], true));
422 },
423
424 $in: function (a, b) {
425 return b.indexOf(a) !== -1;
426 },
427
428 $nin: function (a, b) {
429 return b.indexOf(a) === -1;
430 },
431
432 $keyin: function (a, b) {
433 return a in b;
434 },
435
436 $nkeyin: function (a, b) {
437 return !(a in b);
438 },
439
440 $definedin: function (a, b) {
441 return b[a] !== undefined;
442 },
443
444 $undefinedin: function (a, b) {
445 return b[a] === undefined;
446 },
447
448 $regex: function (a, b) {
449 return b.test(a);
450 },
451
452 $containsString: function (a, b) {
453 return (typeof a === 'string') && (a.indexOf(b) !== -1);
454 },
455
456 $containsNone: function (a, b) {
457 return !LokiOps.$containsAny(a, b);
458 },
459
460 $containsAny: function (a, b) {
461 var checkFn = containsCheckFn(a);
462 if (checkFn !== null) {
463 return (Array.isArray(b)) ? (b.some(checkFn)) : (checkFn(b));
464 }
465 return false;
466 },
467
468 $contains: function (a, b) {
469 var checkFn = containsCheckFn(a);
470 if (checkFn !== null) {
471 return (Array.isArray(b)) ? (b.every(checkFn)) : (checkFn(b));
472 }
473 return false;
474 },
475
476 $type: function (a, b) {
477 var type = typeof a;
478 if (type === 'object') {
479 if (Array.isArray(a)) {
480 type = 'array';
481 } else if (a instanceof Date) {
482 type = 'date';
483 }
484 }
485 return (typeof b !== 'object') ? (type === b) : doQueryOp(type, b);
486 },
487
488 $finite: function(a, b) {
489 return (b === isFinite(a));
490 },
491
492 $size: function (a, b) {
493 if (Array.isArray(a)) {
494 return (typeof b !== 'object') ? (a.length === b) : doQueryOp(a.length, b);
495 }
496 return false;
497 },
498
499 $len: function (a, b) {
500 if (typeof a === 'string') {
501 return (typeof b !== 'object') ? (a.length === b) : doQueryOp(a.length, b);
502 }
503 return false;
504 },
505
506 $where: function (a, b) {
507 return b(a) === true;
508 },
509
510 // field-level logical operators
511 // a is the value in the collection
512 // b is the nested query operation (for '$not')
513 // or an array of nested query operations (for '$and' and '$or')
514 $not: function (a, b) {
515 return !doQueryOp(a, b);
516 },
517
518 $and: function (a, b) {
519 for (var idx = 0, len = b.length; idx < len; idx += 1) {
520 if (!doQueryOp(a, b[idx])) {
521 return false;
522 }
523 }
524 return true;
525 },
526
527 $or: function (a, b) {
528 for (var idx = 0, len = b.length; idx < len; idx += 1) {
529 if (doQueryOp(a, b[idx])) {
530 return true;
531 }
532 }
533 return false;
534 }
535 };
536
537 // if an op is registered in this object, our 'calculateRange' can use it with our binary indices.
538 // if the op is registered to a function, we will run that function/op as a 2nd pass filter on results.
539 // those 2nd pass filter functions should be similar to LokiOps functions, accepting 2 vals to compare.
540 var indexedOps = {
541 $eq: LokiOps.$eq,
542 $aeq: true,
543 $dteq: true,
544 $gt: true,
545 $gte: true,
546 $lt: true,
547 $lte: true,
548 $in: true,
549 $between: true
550 };
551
552 function clone(data, method) {
553 if (data === null || data === undefined) {
554 return null;
555 }
556
557 var cloneMethod = method || 'parse-stringify',
558 cloned;
559
560 switch (cloneMethod) {
561 case "parse-stringify":
562 cloned = JSON.parse(JSON.stringify(data));
563 break;
564 case "jquery-extend-deep":
565 cloned = jQuery.extend(true, {}, data);
566 break;
567 case "shallow":
568 // more compatible method for older browsers
569 cloned = Object.create(data.constructor.prototype);
570 Object.keys(data).map(function (i) {
571 cloned[i] = data[i];
572 });
573 break;
574 case "shallow-assign":
575 // should be supported by newer environments/browsers
576 cloned = Object.create(data.constructor.prototype);
577 Object.assign(cloned, data);
578 break;
579 case "shallow-recurse-objects":
580 // shallow clone top level properties
581 cloned = clone(data, "shallow");
582 var keys = Object.keys(data);
583 // for each of the top level properties which are object literals, recursively shallow copy
584 keys.forEach(function(key) {
585 if (typeof data[key] === "object" && data[key].constructor.name === "Object") {
586 cloned[key] = clone(data[key], "shallow-recurse-objects");
587 }
588 });
589 break;
590 default:
591 break;
592 }
593
594 return cloned;
595 }
596
597 function cloneObjectArray(objarray, method) {
598 var i,
599 result = [];
600
601 if (method == "parse-stringify") {
602 return clone(objarray, method);
603 }
604
605 i = objarray.length - 1;
606
607 for (; i <= 0; i--) {
608 result.push(clone(objarray[i], method));
609 }
610
611 return result;
612 }
613
614 function localStorageAvailable() {
615 try {
616 return (window && window.localStorage !== undefined && window.localStorage !== null);
617 } catch (e) {
618 return false;
619 }
620 }
621
622
623 /**
624 * LokiEventEmitter is a minimalist version of EventEmitter. It enables any
625 * constructor that inherits EventEmitter to emit events and trigger
626 * listeners that have been added to the event through the on(event, callback) method
627 *
628 * @constructor LokiEventEmitter
629 */
630 function LokiEventEmitter() {}
631
632 /**
633 * @prop {hashmap} events - a hashmap, with each property being an array of callbacks
634 * @memberof LokiEventEmitter
635 */
636 LokiEventEmitter.prototype.events = {};
637
638 /**
639 * @prop {boolean} asyncListeners - boolean determines whether or not the callbacks associated with each event
640 * should happen in an async fashion or not
641 * Default is false, which means events are synchronous
642 * @memberof LokiEventEmitter
643 */
644 LokiEventEmitter.prototype.asyncListeners = false;
645
646 /**
647 * on(eventName, listener) - adds a listener to the queue of callbacks associated to an event
648 * @param {string|string[]} eventName - the name(s) of the event(s) to listen to
649 * @param {function} listener - callback function of listener to attach
650 * @returns {int} the index of the callback in the array of listeners for a particular event
651 * @memberof LokiEventEmitter
652 */
653 LokiEventEmitter.prototype.on = function (eventName, listener) {
654 var event;
655 var self = this;
656
657 if (Array.isArray(eventName)) {
658 eventName.forEach(function(currentEventName) {
659 self.on(currentEventName, listener);
660 });
661 return listener;
662 }
663
664 event = this.events[eventName];
665 if (!event) {
666 event = this.events[eventName] = [];
667 }
668 event.push(listener);
669 return listener;
670 };
671
672 /**
673 * emit(eventName, data) - emits a particular event
674 * with the option of passing optional parameters which are going to be processed by the callback
675 * provided signatures match (i.e. if passing emit(event, arg0, arg1) the listener should take two parameters)
676 * @param {string} eventName - the name of the event
677 * @param {object=} data - optional object passed with the event
678 * @memberof LokiEventEmitter
679 */
680 LokiEventEmitter.prototype.emit = function (eventName) {
681 var self = this;
682 var selfArgs = Array.prototype.slice.call(arguments, 1);
683 if (eventName && this.events[eventName]) {
684 this.events[eventName].forEach(function (listener) {
685 if (self.asyncListeners) {
686 setTimeout(function () {
687 listener.apply(self, selfArgs);
688 }, 1);
689 } else {
690 listener.apply(self, selfArgs);
691 }
692
693 });
694 } else {
695 throw new Error('No event ' + eventName + ' defined');
696 }
697 };
698
699 /**
700 * Alias of LokiEventEmitter.prototype.on
701 * addListener(eventName, listener) - adds a listener to the queue of callbacks associated to an event
702 * @param {string|string[]} eventName - the name(s) of the event(s) to listen to
703 * @param {function} listener - callback function of listener to attach
704 * @returns {int} the index of the callback in the array of listeners for a particular event
705 * @memberof LokiEventEmitter
706 */
707 LokiEventEmitter.prototype.addListener = LokiEventEmitter.prototype.on;
708
709 /**
710 * removeListener() - removes the listener at position 'index' from the event 'eventName'
711 * @param {string|string[]} eventName - the name(s) of the event(s) which the listener is attached to
712 * @param {function} listener - the listener callback function to remove from emitter
713 * @memberof LokiEventEmitter
714 */
715 LokiEventEmitter.prototype.removeListener = function (eventName, listener) {
716 var self = this;
717
718 if (Array.isArray(eventName)) {
719 eventName.forEach(function(currentEventName) {
720 self.removeListener(currentEventName, listener);
721 });
722
723 return;
724 }
725
726 if (this.events[eventName]) {
727 var listeners = this.events[eventName];
728 listeners.splice(listeners.indexOf(listener), 1);
729 }
730 };
731
732 /**
733 * Loki: The main database class
734 * @constructor Loki
735 * @implements LokiEventEmitter
736 * @param {string} filename - name of the file to be saved to
737 * @param {object=} options - (Optional) config options object
738 * @param {string} options.env - override environment detection as 'NODEJS', 'BROWSER', 'CORDOVA'
739 * @param {boolean} [options.verbose=false] - enable console output
740 * @param {boolean} [options.autosave=false] - enables autosave
741 * @param {int} [options.autosaveInterval=5000] - time interval (in milliseconds) between saves (if dirty)
742 * @param {boolean} [options.autoload=false] - enables autoload on loki instantiation
743 * @param {function} options.autoloadCallback - user callback called after database load
744 * @param {adapter} options.adapter - an instance of a loki persistence adapter
745 * @param {string} [options.serializationMethod='normal'] - ['normal', 'pretty', 'destructured']
746 * @param {string} options.destructureDelimiter - string delimiter used for destructured serialization
747 * @param {boolean} [options.throttledSaves=true] - debounces multiple calls to to saveDatabase reducing number of disk I/O operations
748 and guaranteeing proper serialization of the calls.
749 */
750 function Loki(filename, options) {
751 this.filename = filename || 'loki.db';
752 this.collections = [];
753
754 // persist version of code which created the database to the database.
755 // could use for upgrade scenarios
756 this.databaseVersion = 1.5;
757 this.engineVersion = 1.5;
758
759 // autosave support (disabled by default)
760 // pass autosave: true, autosaveInterval: 6000 in options to set 6 second autosave
761 this.autosave = false;
762 this.autosaveInterval = 5000;
763 this.autosaveHandle = null;
764 this.throttledSaves = true;
765
766 this.options = {};
767
768 // currently keeping persistenceMethod and persistenceAdapter as loki level properties that
769 // will not or cannot be deserialized. You are required to configure persistence every time
770 // you instantiate a loki object (or use default environment detection) in order to load the database anyways.
771
772 // persistenceMethod could be 'fs', 'localStorage', or 'adapter'
773 // this is optional option param, otherwise environment detection will be used
774 // if user passes their own adapter we will force this method to 'adapter' later, so no need to pass method option.
775 this.persistenceMethod = null;
776
777 // retain reference to optional (non-serializable) persistenceAdapter 'instance'
778 this.persistenceAdapter = null;
779
780 // flags used to throttle saves
781 this.throttledSavePending = false;
782 this.throttledCallbacks = [];
783
784 // enable console output if verbose flag is set (disabled by default)
785 this.verbose = options && options.hasOwnProperty('verbose') ? options.verbose : false;
786
787 this.events = {
788 'init': [],
789 'loaded': [],
790 'flushChanges': [],
791 'close': [],
792 'changes': [],
793 'warning': []
794 };
795
796 var getENV = function () {
797 if (typeof global !== 'undefined' && (global.android || global.NSObject)) {
798 // If no adapter assume nativescript which needs adapter to be passed manually
799 return 'NATIVESCRIPT'; //nativescript
800 }
801
802 if (typeof window === 'undefined') {
803 return 'NODEJS';
804 }
805
806 if (typeof global !== 'undefined' && global.window) {
807 return 'NODEJS'; //node-webkit
808 }
809
810 if (typeof document !== 'undefined') {
811 if (document.URL.indexOf('http://') === -1 && document.URL.indexOf('https://') === -1) {
812 return 'CORDOVA';
813 }
814 return 'BROWSER';
815 }
816 return 'CORDOVA';
817 };
818
819 // refactored environment detection due to invalid detection for browser environments.
820 // if they do not specify an options.env we want to detect env rather than default to nodejs.
821 // currently keeping two properties for similar thing (options.env and options.persistenceMethod)
822 // might want to review whether we can consolidate.
823 if (options && options.hasOwnProperty('env')) {
824 this.ENV = options.env;
825 } else {
826 this.ENV = getENV();
827 }
828
829 // not sure if this is necessary now that i have refactored the line above
830 if (this.ENV === 'undefined') {
831 this.ENV = 'NODEJS';
832 }
833
834 this.configureOptions(options, true);
835
836 this.on('init', this.clearChanges);
837
838 }
839
840 // db class is an EventEmitter
841 Loki.prototype = new LokiEventEmitter();
842 Loki.prototype.constructor = Loki;
843
844 // experimental support for browserify's abstract syntax scan to pick up dependency of indexed adapter.
845 // Hopefully, once this hits npm a browserify require of lokijs should scan the main file and detect this indexed adapter reference.
846 Loki.prototype.getIndexedAdapter = function () {
847 var adapter;
848
849 if (typeof require === 'function') {
850 adapter = require("./loki-indexed-adapter.js");
851 }
852
853 return adapter;
854 };
855
856
857 /**
858 * Allows reconfiguring database options
859 *
860 * @param {object} options - configuration options to apply to loki db object
861 * @param {string} options.env - override environment detection as 'NODEJS', 'BROWSER', 'CORDOVA'
862 * @param {boolean} options.verbose - enable console output (default is 'false')
863 * @param {boolean} options.autosave - enables autosave
864 * @param {int} options.autosaveInterval - time interval (in milliseconds) between saves (if dirty)
865 * @param {boolean} options.autoload - enables autoload on loki instantiation
866 * @param {function} options.autoloadCallback - user callback called after database load
867 * @param {adapter} options.adapter - an instance of a loki persistence adapter
868 * @param {string} options.serializationMethod - ['normal', 'pretty', 'destructured']
869 * @param {string} options.destructureDelimiter - string delimiter used for destructured serialization
870 * @param {boolean} initialConfig - (internal) true is passed when loki ctor is invoking
871 * @memberof Loki
872 */
873 Loki.prototype.configureOptions = function (options, initialConfig) {
874 var defaultPersistence = {
875 'NODEJS': 'fs',
876 'BROWSER': 'localStorage',
877 'CORDOVA': 'localStorage',
878 'MEMORY': 'memory'
879 },
880 persistenceMethods = {
881 'fs': LokiFsAdapter,
882 'localStorage': LokiLocalStorageAdapter,
883 'memory': LokiMemoryAdapter
884 };
885
886 this.options = {};
887
888 this.persistenceMethod = null;
889 // retain reference to optional persistence adapter 'instance'
890 // currently keeping outside options because it can't be serialized
891 this.persistenceAdapter = null;
892
893 // process the options
894 if (typeof (options) !== 'undefined') {
895 this.options = options;
896
897 if (this.options.hasOwnProperty('persistenceMethod')) {
898 // check if the specified persistence method is known
899 if (typeof (persistenceMethods[options.persistenceMethod]) == 'function') {
900 this.persistenceMethod = options.persistenceMethod;
901 this.persistenceAdapter = new persistenceMethods[options.persistenceMethod]();
902 }
903 // should be throw an error here, or just fall back to defaults ??
904 }
905
906 // if user passes adapter, set persistence mode to adapter and retain persistence adapter instance
907 if (this.options.hasOwnProperty('adapter')) {
908 this.persistenceMethod = 'adapter';
909 this.persistenceAdapter = options.adapter;
910 this.options.adapter = null;
911 }
912
913
914 // if they want to load database on loki instantiation, now is a good time to load... after adapter set and before possible autosave initiation
915 if (options.autoload && initialConfig) {
916 // for autoload, let the constructor complete before firing callback
917 var self = this;
918 setTimeout(function () {
919 self.loadDatabase(options, options.autoloadCallback);
920 }, 1);
921 }
922
923 if (this.options.hasOwnProperty('autosaveInterval')) {
924 this.autosaveDisable();
925 this.autosaveInterval = parseInt(this.options.autosaveInterval, 10);
926 }
927
928 if (this.options.hasOwnProperty('autosave') && this.options.autosave) {
929 this.autosaveDisable();
930 this.autosave = true;
931
932 if (this.options.hasOwnProperty('autosaveCallback')) {
933 this.autosaveEnable(options, options.autosaveCallback);
934 } else {
935 this.autosaveEnable();
936 }
937 }
938
939 if (this.options.hasOwnProperty('throttledSaves')) {
940 this.throttledSaves = this.options.throttledSaves;
941 }
942 } // end of options processing
943
944 // ensure defaults exists for options which were not set
945 if (!this.options.hasOwnProperty('serializationMethod')) {
946 this.options.serializationMethod = 'normal';
947 }
948
949 // ensure passed or default option exists
950 if (!this.options.hasOwnProperty('destructureDelimiter')) {
951 this.options.destructureDelimiter = '$<\n';
952 }
953
954 // if by now there is no adapter specified by user nor derived from persistenceMethod: use sensible defaults
955 if (this.persistenceAdapter === null) {
956 this.persistenceMethod = defaultPersistence[this.ENV];
957 if (this.persistenceMethod) {
958 this.persistenceAdapter = new persistenceMethods[this.persistenceMethod]();
959 }
960 }
961
962 };
963
964 /**
965 * Copies 'this' database into a new Loki instance. Object references are shared to make lightweight.
966 *
967 * @param {object} options - apply or override collection level settings
968 * @param {bool} options.removeNonSerializable - nulls properties not safe for serialization.
969 * @memberof Loki
970 */
971 Loki.prototype.copy = function(options) {
972 // in case running in an environment without accurate environment detection, pass 'NA'
973 var databaseCopy = new Loki(this.filename, { env: "NA" });
974 var clen, idx;
975
976 options = options || {};
977
978 // currently inverting and letting loadJSONObject do most of the work
979 databaseCopy.loadJSONObject(this, { retainDirtyFlags: true });
980
981 // since our JSON serializeReplacer is not invoked for reference database adapters, this will let us mimic
982 if(options.hasOwnProperty("removeNonSerializable") && options.removeNonSerializable === true) {
983 databaseCopy.autosaveHandle = null;
984 databaseCopy.persistenceAdapter = null;
985
986 clen = databaseCopy.collections.length;
987 for (idx=0; idx<clen; idx++) {
988 databaseCopy.collections[idx].constraints = null;
989 databaseCopy.collections[idx].ttl = null;
990 }
991 }
992
993 return databaseCopy;
994 };
995
996 /**
997 * Adds a collection to the database.
998 * @param {string} name - name of collection to add
999 * @param {object=} options - (optional) options to configure collection with.
1000 * @param {array=} [options.unique=[]] - array of property names to define unique constraints for
1001 * @param {array=} [options.exact=[]] - array of property names to define exact constraints for
1002 * @param {array=} [options.indices=[]] - array property names to define binary indexes for
1003 * @param {boolean} [options.asyncListeners=false] - whether listeners are called asynchronously
1004 * @param {boolean} [options.disableMeta=false] - set to true to disable meta property on documents
1005 * @param {boolean} [options.disableChangesApi=true] - set to false to enable Changes Api
1006 * @param {boolean} [options.disableDeltaChangesApi=true] - set to false to enable Delta Changes API (requires Changes API, forces cloning)
1007 * @param {boolean} [options.autoupdate=false] - use Object.observe to update objects automatically
1008 * @param {boolean} [options.clone=false] - specify whether inserts and queries clone to/from user
1009 * @param {string} [options.cloneMethod='parse-stringify'] - 'parse-stringify', 'jquery-extend-deep', 'shallow, 'shallow-assign'
1010 * @param {int=} options.ttl - age of document (in ms.) before document is considered aged/stale.
1011 * @param {int=} options.ttlInterval - time interval for clearing out 'aged' documents; not set by default.
1012 * @returns {Collection} a reference to the collection which was just added
1013 * @memberof Loki
1014 */
1015 Loki.prototype.addCollection = function (name, options) {
1016 var i,
1017 len = this.collections.length;
1018
1019 if (options && options.disableMeta === true) {
1020 if (options.disableChangesApi === false) {
1021 throw new Error("disableMeta option cannot be passed as true when disableChangesApi is passed as false");
1022 }
1023 if (options.disableDeltaChangesApi === false) {
1024 throw new Error("disableMeta option cannot be passed as true when disableDeltaChangesApi is passed as false");
1025 }
1026 if (typeof options.ttl === "number" && options.ttl > 0) {
1027 throw new Error("disableMeta option cannot be passed as true when ttl is enabled");
1028 }
1029 }
1030
1031 for (i = 0; i < len; i += 1) {
1032 if (this.collections[i].name === name) {
1033 return this.collections[i];
1034 }
1035 }
1036
1037 var collection = new Collection(name, options);
1038 this.collections.push(collection);
1039
1040 if (this.verbose)
1041 collection.console = console;
1042
1043 return collection;
1044 };
1045
1046 Loki.prototype.loadCollection = function (collection) {
1047 if (!collection.name) {
1048 throw new Error('Collection must have a name property to be loaded');
1049 }
1050 this.collections.push(collection);
1051 };
1052
1053 /**
1054 * Retrieves reference to a collection by name.
1055 * @param {string} collectionName - name of collection to look up
1056 * @returns {Collection} Reference to collection in database by that name, or null if not found
1057 * @memberof Loki
1058 */
1059 Loki.prototype.getCollection = function (collectionName) {
1060 var i,
1061 len = this.collections.length;
1062
1063 for (i = 0; i < len; i += 1) {
1064 if (this.collections[i].name === collectionName) {
1065 return this.collections[i];
1066 }
1067 }
1068
1069 // no such collection
1070 this.emit('warning', 'collection ' + collectionName + ' not found');
1071 return null;
1072 };
1073
1074 /**
1075 * Renames an existing loki collection
1076 * @param {string} oldName - name of collection to rename
1077 * @param {string} newName - new name of collection
1078 * @returns {Collection} reference to the newly renamed collection
1079 * @memberof Loki
1080 */
1081 Loki.prototype.renameCollection = function (oldName, newName) {
1082 var c = this.getCollection(oldName);
1083
1084 if (c) {
1085 c.name = newName;
1086 }
1087
1088 return c;
1089 };
1090
1091 /**
1092 * Returns a list of collections in the database.
1093 * @returns {object[]} array of objects containing 'name', 'type', and 'count' properties.
1094 * @memberof Loki
1095 */
1096 Loki.prototype.listCollections = function () {
1097
1098 var i = this.collections.length,
1099 colls = [];
1100
1101 while (i--) {
1102 colls.push({
1103 name: this.collections[i].name,
1104 type: this.collections[i].objType,
1105 count: this.collections[i].data.length
1106 });
1107 }
1108 return colls;
1109 };
1110
1111 /**
1112 * Removes a collection from the database.
1113 * @param {string} collectionName - name of collection to remove
1114 * @memberof Loki
1115 */
1116 Loki.prototype.removeCollection = function (collectionName) {
1117 var i,
1118 len = this.collections.length;
1119
1120 for (i = 0; i < len; i += 1) {
1121 if (this.collections[i].name === collectionName) {
1122 var tmpcol = new Collection(collectionName, {});
1123 var curcol = this.collections[i];
1124 for (var prop in curcol) {
1125 if (curcol.hasOwnProperty(prop) && tmpcol.hasOwnProperty(prop)) {
1126 curcol[prop] = tmpcol[prop];
1127 }
1128 }
1129 this.collections.splice(i, 1);
1130 return;
1131 }
1132 }
1133 };
1134
1135 Loki.prototype.getName = function () {
1136 return this.name;
1137 };
1138
1139 /**
1140 * serializeReplacer - used to prevent certain properties from being serialized
1141 *
1142 */
1143 Loki.prototype.serializeReplacer = function (key, value) {
1144 switch (key) {
1145 case 'autosaveHandle':
1146 case 'persistenceAdapter':
1147 case 'constraints':
1148 case 'ttl':
1149 return null;
1150 case 'throttledSavePending':
1151 case 'throttledCallbacks':
1152 return undefined;
1153 default:
1154 return value;
1155 }
1156 };
1157
1158 /**
1159 * Serialize database to a string which can be loaded via {@link Loki#loadJSON}
1160 *
1161 * @returns {string} Stringified representation of the loki database.
1162 * @memberof Loki
1163 */
1164 Loki.prototype.serialize = function (options) {
1165 options = options || {};
1166
1167 if (!options.hasOwnProperty("serializationMethod")) {
1168 options.serializationMethod = this.options.serializationMethod;
1169 }
1170
1171 switch(options.serializationMethod) {
1172 case "normal": return JSON.stringify(this, this.serializeReplacer);
1173 case "pretty": return JSON.stringify(this, this.serializeReplacer, 2);
1174 case "destructured": return this.serializeDestructured(); // use default options
1175 default: return JSON.stringify(this, this.serializeReplacer);
1176 }
1177 };
1178
1179 // alias of serialize
1180 Loki.prototype.toJson = Loki.prototype.serialize;
1181
1182 /**
1183 * Database level destructured JSON serialization routine to allow alternate serialization methods.
1184 * Internally, Loki supports destructuring via loki "serializationMethod' option and
1185 * the optional LokiPartitioningAdapter class. It is also available if you wish to do
1186 * your own structured persistence or data exchange.
1187 *
1188 * @param {object=} options - output format options for use externally to loki
1189 * @param {bool=} options.partitioned - (default: false) whether db and each collection are separate
1190 * @param {int=} options.partition - can be used to only output an individual collection or db (-1)
1191 * @param {bool=} options.delimited - (default: true) whether subitems are delimited or subarrays
1192 * @param {string=} options.delimiter - override default delimiter
1193 *
1194 * @returns {string|array} A custom, restructured aggregation of independent serializations.
1195 * @memberof Loki
1196 */
1197 Loki.prototype.serializeDestructured = function(options) {
1198 var idx, sidx, result, resultlen;
1199 var reconstruct = [];
1200 var dbcopy;
1201
1202 options = options || {};
1203
1204 if (!options.hasOwnProperty("partitioned")) {
1205 options.partitioned = false;
1206 }
1207
1208 if (!options.hasOwnProperty("delimited")) {
1209 options.delimited = true;
1210 }
1211
1212 if (!options.hasOwnProperty("delimiter")) {
1213 options.delimiter = this.options.destructureDelimiter;
1214 }
1215
1216 // 'partitioned' along with 'partition' of 0 or greater is a request for single collection serialization
1217 if (options.partitioned === true && options.hasOwnProperty("partition") && options.partition >= 0) {
1218 return this.serializeCollection({
1219 delimited: options.delimited,
1220 delimiter: options.delimiter,
1221 collectionIndex: options.partition
1222 });
1223 }
1224
1225 // not just an individual collection, so we will need to serialize db container via shallow copy
1226 dbcopy = new Loki(this.filename);
1227 dbcopy.loadJSONObject(this);
1228
1229 for(idx=0; idx < dbcopy.collections.length; idx++) {
1230 dbcopy.collections[idx].data = [];
1231 }
1232
1233 // if we -only- wanted the db container portion, return it now
1234 if (options.partitioned === true && options.partition === -1) {
1235 // since we are deconstructing, override serializationMethod to normal for here
1236 return dbcopy.serialize({
1237 serializationMethod: "normal"
1238 });
1239 }
1240
1241 // at this point we must be deconstructing the entire database
1242 // start by pushing db serialization into first array element
1243 reconstruct.push(dbcopy.serialize({
1244 serializationMethod: "normal"
1245 }));
1246
1247 dbcopy = null;
1248
1249 // push collection data into subsequent elements
1250 for(idx=0; idx < this.collections.length; idx++) {
1251 result = this.serializeCollection({
1252 delimited: options.delimited,
1253 delimiter: options.delimiter,
1254 collectionIndex: idx
1255 });
1256
1257 // NDA : Non-Delimited Array : one iterable concatenated array with empty string collection partitions
1258 if (options.partitioned === false && options.delimited === false) {
1259 if (!Array.isArray(result)) {
1260 throw new Error("a nondelimited, non partitioned collection serialization did not return an expected array");
1261 }
1262
1263 // Array.concat would probably duplicate memory overhead for copying strings.
1264 // Instead copy each individually, and clear old value after each copy.
1265 // Hopefully this will allow g.c. to reduce memory pressure, if needed.
1266 resultlen = result.length;
1267
1268 for (sidx=0; sidx < resultlen; sidx++) {
1269 reconstruct.push(result[sidx]);
1270 result[sidx] = null;
1271 }
1272
1273 reconstruct.push("");
1274 }
1275 else {
1276 reconstruct.push(result);
1277 }
1278 }
1279
1280 // Reconstruct / present results according to four combinations : D, DA, NDA, NDAA
1281 if (options.partitioned) {
1282 // DA : Delimited Array of strings [0] db [1] collection [n] collection { partitioned: true, delimited: true }
1283 // useful for simple future adaptations of existing persistence adapters to save collections separately
1284 if (options.delimited) {
1285 return reconstruct;
1286 }
1287 // NDAA : Non-Delimited Array with subArrays. db at [0] and collection subarrays at [n] { partitioned: true, delimited : false }
1288 // This format might be the most versatile for 'rolling your own' partitioned sync or save.
1289 // Memory overhead can be reduced by specifying a specific partition, but at this code path they did not, so its all.
1290 else {
1291 return reconstruct;
1292 }
1293 }
1294 else {
1295 // D : one big Delimited string { partitioned: false, delimited : true }
1296 // This is the method Loki will use internally if 'destructured'.
1297 // Little memory overhead improvements but does not require multiple asynchronous adapter call scheduling
1298 if (options.delimited) {
1299 // indicate no more collections
1300 reconstruct.push("");
1301
1302 return reconstruct.join(options.delimiter);
1303 }
1304 // NDA : Non-Delimited Array : one iterable array with empty string collection partitions { partitioned: false, delimited: false }
1305 // This format might be best candidate for custom synchronous syncs or saves
1306 else {
1307 // indicate no more collections
1308 reconstruct.push("");
1309
1310 return reconstruct;
1311 }
1312 }
1313
1314 reconstruct.push("");
1315
1316 return reconstruct.join(delim);
1317 };
1318
1319 /**
1320 * Collection level utility method to serialize a collection in a 'destructured' format
1321 *
1322 * @param {object=} options - used to determine output of method
1323 * @param {int} options.delimited - whether to return single delimited string or an array
1324 * @param {string} options.delimiter - (optional) if delimited, this is delimiter to use
1325 * @param {int} options.collectionIndex - specify which collection to serialize data for
1326 *
1327 * @returns {string|array} A custom, restructured aggregation of independent serializations for a single collection.
1328 * @memberof Loki
1329 */
1330 Loki.prototype.serializeCollection = function(options) {
1331 var doccount,
1332 docidx,
1333 resultlines = [];
1334
1335 options = options || {};
1336
1337 if (!options.hasOwnProperty("delimited")) {
1338 options.delimited = true;
1339 }
1340
1341 if (!options.hasOwnProperty("collectionIndex")) {
1342 throw new Error("serializeCollection called without 'collectionIndex' option");
1343 }
1344
1345 doccount = this.collections[options.collectionIndex].data.length;
1346
1347 resultlines = [];
1348
1349 for(docidx=0; docidx<doccount; docidx++) {
1350 resultlines.push(JSON.stringify(this.collections[options.collectionIndex].data[docidx]));
1351 }
1352
1353 // D and DA
1354 if (options.delimited) {
1355 // indicate no more documents in collection (via empty delimited string)
1356 resultlines.push("");
1357
1358 return resultlines.join(options.delimiter);
1359 }
1360 else {
1361 // NDAA and NDA
1362 return resultlines;
1363 }
1364 };
1365
1366 /**
1367 * Database level destructured JSON deserialization routine to minimize memory overhead.
1368 * Internally, Loki supports destructuring via loki "serializationMethod' option and
1369 * the optional LokiPartitioningAdapter class. It is also available if you wish to do
1370 * your own structured persistence or data exchange.
1371 *
1372 * @param {string|array} destructuredSource - destructured json or array to deserialize from
1373 * @param {object=} options - source format options
1374 * @param {bool=} [options.partitioned=false] - whether db and each collection are separate
1375 * @param {int=} options.partition - can be used to deserialize only a single partition
1376 * @param {bool=} [options.delimited=true] - whether subitems are delimited or subarrays
1377 * @param {string=} options.delimiter - override default delimiter
1378 *
1379 * @returns {object|array} An object representation of the deserialized database, not yet applied to 'this' db or document array
1380 * @memberof Loki
1381 */
1382 Loki.prototype.deserializeDestructured = function(destructuredSource, options) {
1383 var workarray=[];
1384 var len, cdb;
1385 var idx, collIndex=0, collCount, lineIndex=1, done=false;
1386 var currLine, currObject;
1387
1388 options = options || {};
1389
1390 if (!options.hasOwnProperty("partitioned")) {
1391 options.partitioned = false;
1392 }
1393
1394 if (!options.hasOwnProperty("delimited")) {
1395 options.delimited = true;
1396 }
1397
1398 if (!options.hasOwnProperty("delimiter")) {
1399 options.delimiter = this.options.destructureDelimiter;
1400 }
1401
1402 // Partitioned
1403 // DA : Delimited Array of strings [0] db [1] collection [n] collection { partitioned: true, delimited: true }
1404 // NDAA : Non-Delimited Array with subArrays. db at [0] and collection subarrays at [n] { partitioned: true, delimited : false }
1405 // -or- single partition
1406 if (options.partitioned) {
1407 // handle single partition
1408 if (options.hasOwnProperty('partition')) {
1409 // db only
1410 if (options.partition === -1) {
1411 cdb = JSON.parse(destructuredSource[0]);
1412
1413 return cdb;
1414 }
1415
1416 // single collection, return doc array
1417 return this.deserializeCollection(destructuredSource[options.partition+1], options);
1418 }
1419
1420 // Otherwise we are restoring an entire partitioned db
1421 cdb = JSON.parse(destructuredSource[0]);
1422 collCount = cdb.collections.length;
1423 for(collIndex=0; collIndex<collCount; collIndex++) {
1424 // attach each collection docarray to container collection data, add 1 to collection array index since db is at 0
1425 cdb.collections[collIndex].data = this.deserializeCollection(destructuredSource[collIndex+1], options);
1426 }
1427
1428 return cdb;
1429 }
1430
1431 // Non-Partitioned
1432 // D : one big Delimited string { partitioned: false, delimited : true }
1433 // NDA : Non-Delimited Array : one iterable array with empty string collection partitions { partitioned: false, delimited: false }
1434
1435 // D
1436 if (options.delimited) {
1437 workarray = destructuredSource.split(options.delimiter);
1438 destructuredSource = null; // lower memory pressure
1439 len = workarray.length;
1440
1441 if (len === 0) {
1442 return null;
1443 }
1444 }
1445 // NDA
1446 else {
1447 workarray = destructuredSource;
1448 }
1449
1450 // first line is database and collection shells
1451 cdb = JSON.parse(workarray[0]);
1452 collCount = cdb.collections.length;
1453 workarray[0] = null;
1454
1455 while (!done) {
1456 currLine = workarray[lineIndex];
1457
1458 // empty string indicates either end of collection or end of file
1459 if (workarray[lineIndex] === "") {
1460 // if no more collections to load into, we are done
1461 if (++collIndex > collCount) {
1462 done = true;
1463 }
1464 }
1465 else {
1466 currObject = JSON.parse(workarray[lineIndex]);
1467 cdb.collections[collIndex].data.push(currObject);
1468 }
1469
1470 // lower memory pressure and advance iterator
1471 workarray[lineIndex++] = null;
1472 }
1473
1474 return cdb;
1475 };
1476
1477 /**
1478 * Collection level utility function to deserializes a destructured collection.
1479 *
1480 * @param {string|array} destructuredSource - destructured representation of collection to inflate
1481 * @param {object=} options - used to describe format of destructuredSource input
1482 * @param {int=} [options.delimited=false] - whether source is delimited string or an array
1483 * @param {string=} options.delimiter - if delimited, this is delimiter to use (if other than default)
1484 *
1485 * @returns {array} an array of documents to attach to collection.data.
1486 * @memberof Loki
1487 */
1488 Loki.prototype.deserializeCollection = function(destructuredSource, options) {
1489 var workarray=[];
1490 var idx, len;
1491
1492 options = options || {};
1493
1494 if (!options.hasOwnProperty("partitioned")) {
1495 options.partitioned = false;
1496 }
1497
1498 if (!options.hasOwnProperty("delimited")) {
1499 options.delimited = true;
1500 }
1501
1502 if (!options.hasOwnProperty("delimiter")) {
1503 options.delimiter = this.options.destructureDelimiter;
1504 }
1505
1506 if (options.delimited) {
1507 workarray = destructuredSource.split(options.delimiter);
1508 workarray.pop();
1509 }
1510 else {
1511 workarray = destructuredSource;
1512 }
1513
1514 len = workarray.length;
1515 for (idx=0; idx < len; idx++) {
1516 workarray[idx] = JSON.parse(workarray[idx]);
1517 }
1518
1519 return workarray;
1520 };
1521
1522 /**
1523 * Inflates a loki database from a serialized JSON string
1524 *
1525 * @param {string} serializedDb - a serialized loki database string
1526 * @param {object=} options - apply or override collection level settings
1527 * @param {bool} options.retainDirtyFlags - whether collection dirty flags will be preserved
1528 * @memberof Loki
1529 */
1530 Loki.prototype.loadJSON = function (serializedDb, options) {
1531 var dbObject;
1532 if (serializedDb.length === 0) {
1533 dbObject = {};
1534 } else {
1535 // using option defined in instantiated db not what was in serialized db
1536 switch (this.options.serializationMethod) {
1537 case "normal":
1538 case "pretty": dbObject = JSON.parse(serializedDb); break;
1539 case "destructured": dbObject = this.deserializeDestructured(serializedDb); break;
1540 default: dbObject = JSON.parse(serializedDb); break;
1541 }
1542 }
1543
1544 this.loadJSONObject(dbObject, options);
1545 };
1546
1547 /**
1548 * Inflates a loki database from a JS object
1549 *
1550 * @param {object} dbObject - a serialized loki database string
1551 * @param {object=} options - apply or override collection level settings
1552 * @param {bool} options.retainDirtyFlags - whether collection dirty flags will be preserved
1553 * @memberof Loki
1554 */
1555 Loki.prototype.loadJSONObject = function (dbObject, options) {
1556 var i = 0,
1557 len = dbObject.collections ? dbObject.collections.length : 0,
1558 coll,
1559 copyColl,
1560 clen,
1561 j,
1562 loader,
1563 collObj;
1564
1565 this.name = dbObject.name;
1566
1567 // restore save throttled boolean only if not defined in options
1568 if (dbObject.hasOwnProperty('throttledSaves') && options && !options.hasOwnProperty('throttledSaves')) {
1569 this.throttledSaves = dbObject.throttledSaves;
1570 }
1571
1572 this.collections = [];
1573
1574 function makeLoader(coll) {
1575 var collOptions = options[coll.name];
1576 var inflater;
1577
1578 if(collOptions.proto) {
1579 inflater = collOptions.inflate || Utils.copyProperties;
1580
1581 return function(data) {
1582 var collObj = new(collOptions.proto)();
1583 inflater(data, collObj);
1584 return collObj;
1585 };
1586 }
1587
1588 return collOptions.inflate;
1589 }
1590
1591 for (i; i < len; i += 1) {
1592 coll = dbObject.collections[i];
1593
1594 copyColl = this.addCollection(coll.name, { disableChangesApi: coll.disableChangesApi, disableDeltaChangesApi: coll.disableDeltaChangesApi, disableMeta: coll.disableMeta });
1595
1596 copyColl.adaptiveBinaryIndices = coll.hasOwnProperty('adaptiveBinaryIndices')?(coll.adaptiveBinaryIndices === true): false;
1597 copyColl.transactional = coll.transactional;
1598 copyColl.asyncListeners = coll.asyncListeners;
1599 copyColl.cloneObjects = coll.cloneObjects;
1600 copyColl.cloneMethod = coll.cloneMethod || "parse-stringify";
1601 copyColl.autoupdate = coll.autoupdate;
1602 copyColl.changes = coll.changes;
1603
1604 if (options && options.retainDirtyFlags === true) {
1605 copyColl.dirty = coll.dirty;
1606 }
1607 else {
1608 copyColl.dirty = false;
1609 }
1610
1611 // load each element individually
1612 clen = coll.data.length;
1613 j = 0;
1614 if (options && options.hasOwnProperty(coll.name)) {
1615 loader = makeLoader(coll);
1616
1617 for (j; j < clen; j++) {
1618 collObj = loader(coll.data[j]);
1619 copyColl.data[j] = collObj;
1620 copyColl.addAutoUpdateObserver(collObj);
1621 }
1622 } else {
1623
1624 for (j; j < clen; j++) {
1625 copyColl.data[j] = coll.data[j];
1626 copyColl.addAutoUpdateObserver(copyColl.data[j]);
1627 }
1628 }
1629
1630 copyColl.maxId = (typeof coll.maxId === 'undefined') ? 0 : coll.maxId;
1631 copyColl.idIndex = coll.idIndex;
1632 if (typeof (coll.binaryIndices) !== 'undefined') {
1633 copyColl.binaryIndices = coll.binaryIndices;
1634 }
1635 if (typeof coll.transforms !== 'undefined') {
1636 copyColl.transforms = coll.transforms;
1637 }
1638
1639 copyColl.ensureId();
1640
1641 // regenerate unique indexes
1642 copyColl.uniqueNames = [];
1643 if (coll.hasOwnProperty("uniqueNames")) {
1644 copyColl.uniqueNames = coll.uniqueNames;
1645 for (j = 0; j < copyColl.uniqueNames.length; j++) {
1646 copyColl.ensureUniqueIndex(copyColl.uniqueNames[j]);
1647 }
1648 }
1649
1650 // in case they are loading a database created before we added dynamic views, handle undefined
1651 if (typeof (coll.DynamicViews) === 'undefined') continue;
1652
1653 // reinflate DynamicViews and attached Resultsets
1654 for (var idx = 0; idx < coll.DynamicViews.length; idx++) {
1655 var colldv = coll.DynamicViews[idx];
1656
1657 var dv = copyColl.addDynamicView(colldv.name, colldv.options);
1658 dv.resultdata = colldv.resultdata;
1659 dv.resultsdirty = colldv.resultsdirty;
1660 dv.filterPipeline = colldv.filterPipeline;
1661
1662 dv.sortCriteria = colldv.sortCriteria;
1663 dv.sortFunction = null;
1664
1665 dv.sortDirty = colldv.sortDirty;
1666 dv.resultset.filteredrows = colldv.resultset.filteredrows;
1667 dv.resultset.filterInitialized = colldv.resultset.filterInitialized;
1668
1669 dv.rematerialize({
1670 removeWhereFilters: true
1671 });
1672 }
1673
1674 // Upgrade Logic for binary index refactoring at version 1.5
1675 if (dbObject.databaseVersion < 1.5) {
1676 // rebuild all indices
1677 copyColl.ensureAllIndexes(true);
1678 copyColl.dirty = true;
1679 }
1680 }
1681 };
1682
1683 /**
1684 * Emits the close event. In autosave scenarios, if the database is dirty, this will save and disable timer.
1685 * Does not actually destroy the db.
1686 *
1687 * @param {function=} callback - (Optional) if supplied will be registered with close event before emitting.
1688 * @memberof Loki
1689 */
1690 Loki.prototype.close = function (callback) {
1691 // for autosave scenarios, we will let close perform final save (if dirty)
1692 // For web use, you might call from window.onbeforeunload to shutdown database, saving pending changes
1693 if (this.autosave) {
1694 this.autosaveDisable();
1695 if (this.autosaveDirty()) {
1696 this.saveDatabase(callback);
1697 callback = undefined;
1698 }
1699 }
1700
1701 if (callback) {
1702 this.on('close', callback);
1703 }
1704 this.emit('close');
1705 };
1706
1707 /**-------------------------+
1708 | Changes API |
1709 +--------------------------*/
1710
1711 /**
1712 * The Changes API enables the tracking the changes occurred in the collections since the beginning of the session,
1713 * so it's possible to create a differential dataset for synchronization purposes (possibly to a remote db)
1714 */
1715
1716 /**
1717 * (Changes API) : takes all the changes stored in each
1718 * collection and creates a single array for the entire database. If an array of names
1719 * of collections is passed then only the included collections will be tracked.
1720 *
1721 * @param {array=} optional array of collection names. No arg means all collections are processed.
1722 * @returns {array} array of changes
1723 * @see private method createChange() in Collection
1724 * @memberof Loki
1725 */
1726 Loki.prototype.generateChangesNotification = function (arrayOfCollectionNames) {
1727 function getCollName(coll) {
1728 return coll.name;
1729 }
1730 var changes = [],
1731 selectedCollections = arrayOfCollectionNames || this.collections.map(getCollName);
1732
1733 this.collections.forEach(function (coll) {
1734 if (selectedCollections.indexOf(getCollName(coll)) !== -1) {
1735 changes = changes.concat(coll.getChanges());
1736 }
1737 });
1738 return changes;
1739 };
1740
1741 /**
1742 * (Changes API) - stringify changes for network transmission
1743 * @returns {string} string representation of the changes
1744 * @memberof Loki
1745 */
1746 Loki.prototype.serializeChanges = function (collectionNamesArray) {
1747 return JSON.stringify(this.generateChangesNotification(collectionNamesArray));
1748 };
1749
1750 /**
1751 * (Changes API) : clears all the changes in all collections.
1752 * @memberof Loki
1753 */
1754 Loki.prototype.clearChanges = function () {
1755 this.collections.forEach(function (coll) {
1756 if (coll.flushChanges) {
1757 coll.flushChanges();
1758 }
1759 });
1760 };
1761
1762 /*------------------+
1763 | PERSISTENCE |
1764 -------------------*/
1765
1766 /** there are two build in persistence adapters for internal use
1767 * fs for use in Nodejs type environments
1768 * localStorage for use in browser environment
1769 * defined as helper classes here so its easy and clean to use
1770 */
1771
1772 /**
1773 * In in-memory persistence adapter for an in-memory database.
1774 * This simple 'key/value' adapter is intended for unit testing and diagnostics.
1775 *
1776 * @param {object=} options - memory adapter options
1777 * @param {boolean} [options.asyncResponses=false] - whether callbacks are invoked asynchronously
1778 * @param {int} [options.asyncTimeout=50] - timeout in ms to queue callbacks
1779 * @constructor LokiMemoryAdapter
1780 */
1781 function LokiMemoryAdapter(options) {
1782 this.hashStore = {};
1783 this.options = options || {};
1784
1785 if (!this.options.hasOwnProperty('asyncResponses')) {
1786 this.options.asyncResponses = false;
1787 }
1788
1789 if (!this.options.hasOwnProperty('asyncTimeout')) {
1790 this.options.asyncTimeout = 50; // 50 ms default
1791 }
1792 }
1793
1794 /**
1795 * Loads a serialized database from its in-memory store.
1796 * (Loki persistence adapter interface function)
1797 *
1798 * @param {string} dbname - name of the database (filename/keyname)
1799 * @param {function} callback - adapter callback to return load result to caller
1800 * @memberof LokiMemoryAdapter
1801 */
1802 LokiMemoryAdapter.prototype.loadDatabase = function (dbname, callback) {
1803 var self=this;
1804
1805 if (this.options.asyncResponses) {
1806 setTimeout(function() {
1807 if (self.hashStore.hasOwnProperty(dbname)) {
1808 callback(self.hashStore[dbname].value);
1809 }
1810 else {
1811 // database doesn't exist, return falsy
1812 callback (null);
1813 }
1814 }, this.options.asyncTimeout);
1815 }
1816 else {
1817 if (this.hashStore.hasOwnProperty(dbname)) {
1818 // database doesn't exist, return falsy
1819 callback(this.hashStore[dbname].value);
1820 }
1821 else {
1822 callback (null);
1823 }
1824 }
1825 };
1826
1827 /**
1828 * Saves a serialized database to its in-memory store.
1829 * (Loki persistence adapter interface function)
1830 *
1831 * @param {string} dbname - name of the database (filename/keyname)
1832 * @param {function} callback - adapter callback to return load result to caller
1833 * @memberof LokiMemoryAdapter
1834 */
1835 LokiMemoryAdapter.prototype.saveDatabase = function (dbname, dbstring, callback) {
1836 var self=this;
1837 var saveCount;
1838
1839 if (this.options.asyncResponses) {
1840 setTimeout(function() {
1841 saveCount = (self.hashStore.hasOwnProperty(dbname)?self.hashStore[dbname].savecount:0);
1842
1843 self.hashStore[dbname] = {
1844 savecount: saveCount+1,
1845 lastsave: new Date(),
1846 value: dbstring
1847 };
1848
1849 callback();
1850 }, this.options.asyncTimeout);
1851 }
1852 else {
1853 saveCount = (this.hashStore.hasOwnProperty(dbname)?this.hashStore[dbname].savecount:0);
1854
1855 this.hashStore[dbname] = {
1856 savecount: saveCount+1,
1857 lastsave: new Date(),
1858 value: dbstring
1859 };
1860
1861 callback();
1862 }
1863 };
1864
1865 /**
1866 * Deletes a database from its in-memory store.
1867 *
1868 * @param {string} dbname - name of the database (filename/keyname)
1869 * @param {function} callback - function to call when done
1870 * @memberof LokiMemoryAdapter
1871 */
1872 LokiMemoryAdapter.prototype.deleteDatabase = function(dbname, callback) {
1873 if (this.hashStore.hasOwnProperty(dbname)) {
1874 delete this.hashStore[dbname];
1875 }
1876
1877 if (typeof callback === "function") {
1878 callback();
1879 }
1880 };
1881
1882 /**
1883 * An adapter for adapters. Converts a non reference mode adapter into a reference mode adapter
1884 * which can perform destructuring and partioning. Each collection will be stored in its own key/save and
1885 * only dirty collections will be saved. If you turn on paging with default page size of 25megs and save
1886 * a 75 meg collection it should use up roughly 3 save slots (key/value pairs sent to inner adapter).
1887 * A dirty collection that spans three pages will save all three pages again
1888 * Paging mode was added mainly because Chrome has issues saving 'too large' of a string within a
1889 * single indexeddb row. If a single document update causes the collection to be flagged as dirty, all
1890 * of that collection's pages will be written on next save.
1891 *
1892 * @param {object} adapter - reference to a 'non-reference' mode loki adapter instance.
1893 * @param {object=} options - configuration options for partitioning and paging
1894 * @param {bool} options.paging - (default: false) set to true to enable paging collection data.
1895 * @param {int} options.pageSize - (default : 25MB) you can use this to limit size of strings passed to inner adapter.
1896 * @param {string} options.delimiter - allows you to override the default delimeter
1897 * @constructor LokiPartitioningAdapter
1898 */
1899 function LokiPartitioningAdapter(adapter, options) {
1900 this.mode = "reference";
1901 this.adapter = null;
1902 this.options = options || {};
1903 this.dbref = null;
1904 this.dbname = "";
1905 this.pageIterator = {};
1906
1907 // verify user passed an appropriate adapter
1908 if (adapter) {
1909 if (adapter.mode === "reference") {
1910 throw new Error("LokiPartitioningAdapter cannot be instantiated with a reference mode adapter");
1911 }
1912 else {
1913 this.adapter = adapter;
1914 }
1915 }
1916 else {
1917 throw new Error("LokiPartitioningAdapter requires a (non-reference mode) adapter on construction");
1918 }
1919
1920 // set collection paging defaults
1921 if (!this.options.hasOwnProperty("paging")) {
1922 this.options.paging = false;
1923 }
1924
1925 // default to page size of 25 megs (can be up to your largest serialized object size larger than this)
1926 if (!this.options.hasOwnProperty("pageSize")) {
1927 this.options.pageSize = 25*1024*1024;
1928 }
1929
1930 if (!this.options.hasOwnProperty("delimiter")) {
1931 this.options.delimiter = '$<\n';
1932 }
1933 }
1934
1935 /**
1936 * Loads a database which was partitioned into several key/value saves.
1937 * (Loki persistence adapter interface function)
1938 *
1939 * @param {string} dbname - name of the database (filename/keyname)
1940 * @param {function} callback - adapter callback to return load result to caller
1941 * @memberof LokiPartitioningAdapter
1942 */
1943 LokiPartitioningAdapter.prototype.loadDatabase = function (dbname, callback) {
1944 var self=this;
1945 this.dbname = dbname;
1946 this.dbref = new Loki(dbname);
1947
1948 // load the db container (without data)
1949 this.adapter.loadDatabase(dbname, function(result) {
1950 // empty database condition is for inner adapter return null/undefined/falsy
1951 if (!result) {
1952 // partition 0 not found so new database, no need to try to load other partitions.
1953 // return same falsy result to loadDatabase to signify no database exists (yet)
1954 callback(result);
1955 return;
1956 }
1957
1958 if (typeof result !== "string") {
1959 callback(new Error("LokiPartitioningAdapter received an unexpected response from inner adapter loadDatabase()"));
1960 }
1961
1962 // I will want to use loki destructuring helper methods so i will inflate into typed instance
1963 var db = JSON.parse(result);
1964 self.dbref.loadJSONObject(db);
1965 db = null;
1966
1967 var clen = self.dbref.collections.length;
1968
1969 if (self.dbref.collections.length === 0) {
1970 callback(self.dbref);
1971 return;
1972 }
1973
1974 self.pageIterator = {
1975 collection: 0,
1976 pageIndex: 0
1977 };
1978
1979 self.loadNextPartition(0, function() {
1980 callback(self.dbref);
1981 });
1982 });
1983 };
1984
1985 /**
1986 * Used to sequentially load each collection partition, one at a time.
1987 *
1988 * @param {int} partition - ordinal collection position to load next
1989 * @param {function} callback - adapter callback to return load result to caller
1990 */
1991 LokiPartitioningAdapter.prototype.loadNextPartition = function(partition, callback) {
1992 var keyname = this.dbname + "." + partition;
1993 var self=this;
1994
1995 if (this.options.paging === true) {
1996 this.pageIterator.pageIndex = 0;
1997 this.loadNextPage(callback);
1998 return;
1999 }
2000
2001 this.adapter.loadDatabase(keyname, function(result) {
2002 var data = self.dbref.deserializeCollection(result, { delimited: true, collectionIndex: partition });
2003 self.dbref.collections[partition].data = data;
2004
2005 if (++partition < self.dbref.collections.length) {
2006 self.loadNextPartition(partition, callback);
2007 }
2008 else {
2009 callback();
2010 }
2011 });
2012 };
2013
2014 /**
2015 * Used to sequentially load the next page of collection partition, one at a time.
2016 *
2017 * @param {function} callback - adapter callback to return load result to caller
2018 */
2019 LokiPartitioningAdapter.prototype.loadNextPage = function(callback) {
2020 // calculate name for next saved page in sequence
2021 var keyname = this.dbname + "." + this.pageIterator.collection + "." + this.pageIterator.pageIndex;
2022 var self=this;
2023
2024 // load whatever page is next in sequence
2025 this.adapter.loadDatabase(keyname, function(result) {
2026 var data = result.split(self.options.delimiter);
2027 result = ""; // free up memory now that we have split it into array
2028 var dlen = data.length;
2029 var idx;
2030
2031 // detect if last page by presence of final empty string element and remove it if so
2032 var isLastPage = (data[dlen-1] === "");
2033 if (isLastPage) {
2034 data.pop();
2035 dlen = data.length;
2036 // empty collections are just a delimiter meaning two blank items
2037 if (data[dlen-1] === "" && dlen === 1) {
2038 data.pop();
2039 dlen = data.length;
2040 }
2041 }
2042
2043 // convert stringified array elements to object instances and push to collection data
2044 for(idx=0; idx < dlen; idx++) {
2045 self.dbref.collections[self.pageIterator.collection].data.push(JSON.parse(data[idx]));
2046 data[idx] = null;
2047 }
2048 data = [];
2049
2050 // if last page, we are done with this partition
2051 if (isLastPage) {
2052
2053 // if there are more partitions, kick off next partition load
2054 if (++self.pageIterator.collection < self.dbref.collections.length) {
2055 self.loadNextPartition(self.pageIterator.collection, callback);
2056 }
2057 else {
2058 callback();
2059 }
2060 }
2061 else {
2062 self.pageIterator.pageIndex++;
2063 self.loadNextPage(callback);
2064 }
2065 });
2066 };
2067
2068 /**
2069 * Saves a database by partioning into separate key/value saves.
2070 * (Loki 'reference mode' persistence adapter interface function)
2071 *
2072 * @param {string} dbname - name of the database (filename/keyname)
2073 * @param {object} dbref - reference to database which we will partition and save.
2074 * @param {function} callback - adapter callback to return load result to caller
2075 *
2076 * @memberof LokiPartitioningAdapter
2077 */
2078 LokiPartitioningAdapter.prototype.exportDatabase = function(dbname, dbref, callback) {
2079 var self=this;
2080 var idx, clen = dbref.collections.length;
2081
2082 this.dbref = dbref;
2083 this.dbname = dbname;
2084
2085 // queue up dirty partitions to be saved
2086 this.dirtyPartitions = [-1];
2087 for(idx=0; idx<clen; idx++) {
2088 if (dbref.collections[idx].dirty) {
2089 this.dirtyPartitions.push(idx);
2090 }
2091 }
2092
2093 this.saveNextPartition(function(err) {
2094 callback(err);
2095 });
2096 };
2097
2098 /**
2099 * Helper method used internally to save each dirty collection, one at a time.
2100 *
2101 * @param {function} callback - adapter callback to return load result to caller
2102 */
2103 LokiPartitioningAdapter.prototype.saveNextPartition = function(callback) {
2104 var self=this;
2105 var partition = this.dirtyPartitions.shift();
2106 var keyname = this.dbname + ((partition===-1)?"":("." + partition));
2107
2108 // if we are doing paging and this is collection partition
2109 if (this.options.paging && partition !== -1) {
2110 this.pageIterator = {
2111 collection: partition,
2112 docIndex: 0,
2113 pageIndex: 0
2114 };
2115
2116 // since saveNextPage recursively calls itself until done, our callback means this whole paged partition is finished
2117 this.saveNextPage(function(err) {
2118 if (self.dirtyPartitions.length === 0) {
2119 callback(err);
2120 }
2121 else {
2122 self.saveNextPartition(callback);
2123 }
2124 });
2125 return;
2126 }
2127
2128 // otherwise this is 'non-paged' partioning...
2129 var result = this.dbref.serializeDestructured({
2130 partitioned : true,
2131 delimited: true,
2132 partition: partition
2133 });
2134
2135 this.adapter.saveDatabase(keyname, result, function(err) {
2136 if (err) {
2137 callback(err);
2138 return;
2139 }
2140
2141 if (self.dirtyPartitions.length === 0) {
2142 callback(null);
2143 }
2144 else {
2145 self.saveNextPartition(callback);
2146 }
2147 });
2148 };
2149
2150 /**
2151 * Helper method used internally to generate and save the next page of the current (dirty) partition.
2152 *
2153 * @param {function} callback - adapter callback to return load result to caller
2154 */
2155 LokiPartitioningAdapter.prototype.saveNextPage = function(callback) {
2156 var self=this;
2157 var coll = this.dbref.collections[this.pageIterator.collection];
2158 var keyname = this.dbname + "." + this.pageIterator.collection + "." + this.pageIterator.pageIndex;
2159 var pageLen=0,
2160 cdlen = coll.data.length,
2161 delimlen = this.options.delimiter.length;
2162 var serializedObject = "",
2163 pageBuilder = "";
2164 var doneWithPartition=false,
2165 doneWithPage=false;
2166
2167 var pageSaveCallback = function(err) {
2168 pageBuilder = "";
2169
2170 if (err) {
2171 callback(err);
2172 }
2173
2174 // update meta properties then continue process by invoking callback
2175 if (doneWithPartition) {
2176 callback(null);
2177 }
2178 else {
2179 self.pageIterator.pageIndex++;
2180 self.saveNextPage(callback);
2181 }
2182 };
2183
2184 if (coll.data.length === 0) {
2185 doneWithPartition = true;
2186 }
2187
2188 while (true) {
2189 if (!doneWithPartition) {
2190 // serialize object
2191 serializedObject = JSON.stringify(coll.data[this.pageIterator.docIndex]);
2192 pageBuilder += serializedObject;
2193 pageLen += serializedObject.length;
2194
2195 // if no more documents in collection to add, we are done with partition
2196 if (++this.pageIterator.docIndex >= cdlen) doneWithPartition = true;
2197 }
2198 // if our current page is bigger than defined pageSize, we are done with page
2199 if (pageLen >= this.options.pageSize) doneWithPage = true;
2200
2201 // if not done with current page, need delimiter before next item
2202 // if done with partition we also want a delmiter to indicate 'end of pages' final empty row
2203 if (!doneWithPage || doneWithPartition) {
2204 pageBuilder += this.options.delimiter;
2205 pageLen += delimlen;
2206 }
2207
2208 // if we are done with page save it and pass off to next recursive call or callback
2209 if (doneWithPartition || doneWithPage) {
2210 this.adapter.saveDatabase(keyname, pageBuilder, pageSaveCallback);
2211 return;
2212 }
2213 }
2214 };
2215
2216 /**
2217 * A loki persistence adapter which persists using node fs module
2218 * @constructor LokiFsAdapter
2219 */
2220 function LokiFsAdapter() {
2221 this.fs = require('fs');
2222 }
2223
2224 /**
2225 * loadDatabase() - Load data from file, will throw an error if the file does not exist
2226 * @param {string} dbname - the filename of the database to load
2227 * @param {function} callback - the callback to handle the result
2228 * @memberof LokiFsAdapter
2229 */
2230 LokiFsAdapter.prototype.loadDatabase = function loadDatabase(dbname, callback) {
2231 var self = this;
2232
2233 this.fs.stat(dbname, function (err, stats) {
2234 if (!err && stats.isFile()) {
2235 self.fs.readFile(dbname, {
2236 encoding: 'utf8'
2237 }, function readFileCallback(err, data) {
2238 if (err) {
2239 callback(new Error(err));
2240 } else {
2241 callback(data);
2242 }
2243 });
2244 }
2245 else {
2246 callback(null);
2247 }
2248 });
2249 };
2250
2251 /**
2252 * saveDatabase() - save data to file, will throw an error if the file can't be saved
2253 * might want to expand this to avoid dataloss on partial save
2254 * @param {string} dbname - the filename of the database to load
2255 * @param {function} callback - the callback to handle the result
2256 * @memberof LokiFsAdapter
2257 */
2258 LokiFsAdapter.prototype.saveDatabase = function saveDatabase(dbname, dbstring, callback) {
2259 var self = this;
2260 var tmpdbname = dbname + '~';
2261 this.fs.writeFile(tmpdbname, dbstring, function writeFileCallback(err) {
2262 if (err) {
2263 callback(new Error(err));
2264 } else {
2265 self.fs.rename(tmpdbname,dbname,callback);
2266 }
2267 });
2268 };
2269
2270 /**
2271 * deleteDatabase() - delete the database file, will throw an error if the
2272 * file can't be deleted
2273 * @param {string} dbname - the filename of the database to delete
2274 * @param {function} callback - the callback to handle the result
2275 * @memberof LokiFsAdapter
2276 */
2277 LokiFsAdapter.prototype.deleteDatabase = function deleteDatabase(dbname, callback) {
2278 this.fs.unlink(dbname, function deleteDatabaseCallback(err) {
2279 if (err) {
2280 callback(new Error(err));
2281 } else {
2282 callback();
2283 }
2284 });
2285 };
2286
2287
2288 /**
2289 * A loki persistence adapter which persists to web browser's local storage object
2290 * @constructor LokiLocalStorageAdapter
2291 */
2292 function LokiLocalStorageAdapter() {}
2293
2294 /**
2295 * loadDatabase() - Load data from localstorage
2296 * @param {string} dbname - the name of the database to load
2297 * @param {function} callback - the callback to handle the result
2298 * @memberof LokiLocalStorageAdapter
2299 */
2300 LokiLocalStorageAdapter.prototype.loadDatabase = function loadDatabase(dbname, callback) {
2301 if (localStorageAvailable()) {
2302 callback(localStorage.getItem(dbname));
2303 } else {
2304 callback(new Error('localStorage is not available'));
2305 }
2306 };
2307
2308 /**
2309 * saveDatabase() - save data to localstorage, will throw an error if the file can't be saved
2310 * might want to expand this to avoid dataloss on partial save
2311 * @param {string} dbname - the filename of the database to load
2312 * @param {function} callback - the callback to handle the result
2313 * @memberof LokiLocalStorageAdapter
2314 */
2315 LokiLocalStorageAdapter.prototype.saveDatabase = function saveDatabase(dbname, dbstring, callback) {
2316 if (localStorageAvailable()) {
2317 localStorage.setItem(dbname, dbstring);
2318 callback(null);
2319 } else {
2320 callback(new Error('localStorage is not available'));
2321 }
2322 };
2323
2324 /**
2325 * deleteDatabase() - delete the database from localstorage, will throw an error if it
2326 * can't be deleted
2327 * @param {string} dbname - the filename of the database to delete
2328 * @param {function} callback - the callback to handle the result
2329 * @memberof LokiLocalStorageAdapter
2330 */
2331 LokiLocalStorageAdapter.prototype.deleteDatabase = function deleteDatabase(dbname, callback) {
2332 if (localStorageAvailable()) {
2333 localStorage.removeItem(dbname);
2334 callback(null);
2335 } else {
2336 callback(new Error('localStorage is not available'));
2337 }
2338 };
2339
2340 /**
2341 * Wait for throttledSaves to complete and invoke your callback when drained or duration is met.
2342 *
2343 * @param {function} callback - callback to fire when save queue is drained, it is passed a sucess parameter value
2344 * @param {object=} options - configuration options
2345 * @param {boolean} options.recursiveWait - (default: true) if after queue is drained, another save was kicked off, wait for it
2346 * @param {bool} options.recursiveWaitLimit - (default: false) limit our recursive waiting to a duration
2347 * @param {int} options.recursiveWaitLimitDelay - (default: 2000) cutoff in ms to stop recursively re-draining
2348 * @memberof Loki
2349 */
2350 Loki.prototype.throttledSaveDrain = function(callback, options) {
2351 var self = this;
2352 var now = (new Date()).getTime();
2353
2354 if (!this.throttledSaves) {
2355 callback(true);
2356 }
2357
2358 options = options || {};
2359 if (!options.hasOwnProperty('recursiveWait')) {
2360 options.recursiveWait = true;
2361 }
2362 if (!options.hasOwnProperty('recursiveWaitLimit')) {
2363 options.recursiveWaitLimit = false;
2364 }
2365 if (!options.hasOwnProperty('recursiveWaitLimitDuration')) {
2366 options.recursiveWaitLimitDuration = 2000;
2367 }
2368 if (!options.hasOwnProperty('started')) {
2369 options.started = (new Date()).getTime();
2370 }
2371
2372 // if save is pending
2373 if (this.throttledSaves && this.throttledSavePending) {
2374 // if we want to wait until we are in a state where there are no pending saves at all
2375 if (options.recursiveWait) {
2376 // queue the following meta callback for when it completes
2377 this.throttledCallbacks.push(function() {
2378 // if there is now another save pending...
2379 if (self.throttledSavePending) {
2380 // if we wish to wait only so long and we have exceeded limit of our waiting, callback with false success value
2381 if (options.recursiveWaitLimit && (now - options.started > options.recursiveWaitLimitDuration)) {
2382 callback(false);
2383 return;
2384 }
2385 // it must be ok to wait on next queue drain
2386 self.throttledSaveDrain(callback, options);
2387 return;
2388 }
2389 // no pending saves so callback with true success
2390 else {
2391 callback(true);
2392 return;
2393 }
2394 });
2395 }
2396 // just notify when current queue is depleted
2397 else {
2398 this.throttledCallbacks.push(callback);
2399 return;
2400 }
2401 }
2402 // no save pending, just callback
2403 else {
2404 callback(true);
2405 }
2406 };
2407
2408 /**
2409 * Internal load logic, decoupled from throttling/contention logic
2410 *
2411 * @param {object} options - not currently used (remove or allow overrides?)
2412 * @param {function=} callback - (Optional) user supplied async callback / error handler
2413 */
2414 Loki.prototype.loadDatabaseInternal = function (options, callback) {
2415 var cFun = callback || function (err, data) {
2416 if (err) {
2417 throw err;
2418 }
2419 },
2420 self = this;
2421
2422 // the persistenceAdapter should be present if all is ok, but check to be sure.
2423 if (this.persistenceAdapter !== null) {
2424
2425 this.persistenceAdapter.loadDatabase(this.filename, function loadDatabaseCallback(dbString) {
2426 if (typeof (dbString) === 'string') {
2427 var parseSuccess = false;
2428 try {
2429 self.loadJSON(dbString, options || {});
2430 parseSuccess = true;
2431 } catch (err) {
2432 cFun(err);
2433 }
2434 if (parseSuccess) {
2435 cFun(null);
2436 self.emit('loaded', 'database ' + self.filename + ' loaded');
2437 }
2438 } else {
2439 // falsy result means new database
2440 if (!dbString) {
2441 cFun(null);
2442 self.emit('loaded', 'empty database ' + self.filename + ' loaded');
2443 return;
2444 }
2445
2446 // instanceof error means load faulted
2447 if (dbString instanceof Error) {
2448 cFun(dbString);
2449 return;
2450 }
2451
2452 // if adapter has returned an js object (other than null or error) attempt to load from JSON object
2453 if (typeof (dbString) === "object") {
2454 self.loadJSONObject(dbString, options || {});
2455 cFun(null); // return null on success
2456 self.emit('loaded', 'database ' + self.filename + ' loaded');
2457 return;
2458 }
2459
2460 cFun("unexpected adapter response : " + dbString);
2461 }
2462 });
2463
2464 } else {
2465 cFun(new Error('persistenceAdapter not configured'));
2466 }
2467 };
2468
2469 /**
2470 * Handles manually loading from file system, local storage, or adapter (such as indexeddb)
2471 * This method utilizes loki configuration options (if provided) to determine which
2472 * persistence method to use, or environment detection (if configuration was not provided).
2473 * To avoid contention with any throttledSaves, we will drain the save queue first.
2474 *
2475 * If you are configured with autosave, you do not need to call this method yourself.
2476 *
2477 * @param {object} options - if throttling saves and loads, this controls how we drain save queue before loading
2478 * @param {boolean} options.recursiveWait - (default: true) wait recursively until no saves are queued
2479 * @param {bool} options.recursiveWaitLimit - (default: false) limit our recursive waiting to a duration
2480 * @param {int} options.recursiveWaitLimitDelay - (default: 2000) cutoff in ms to stop recursively re-draining
2481 * @param {function=} callback - (Optional) user supplied async callback / error handler
2482 * @memberof Loki
2483 * @example
2484 * db.loadDatabase({}, function(err) {
2485 * if (err) {
2486 * console.log("error : " + err);
2487 * }
2488 * else {
2489 * console.log("database loaded.");
2490 * }
2491 * });
2492 */
2493 Loki.prototype.loadDatabase = function (options, callback) {
2494 var self=this;
2495
2496 // if throttling disabled, just call internal
2497 if (!this.throttledSaves) {
2498 this.loadDatabaseInternal(options, callback);
2499 return;
2500 }
2501
2502 // try to drain any pending saves in the queue to lock it for loading
2503 this.throttledSaveDrain(function(success) {
2504 if (success) {
2505 // pause/throttle saving until loading is done
2506 self.throttledSavePending = true;
2507
2508 self.loadDatabaseInternal(options, function(err) {
2509 // now that we are finished loading, if no saves were throttled, disable flag
2510 if (self.throttledCallbacks.length === 0) {
2511 self.throttledSavePending = false;
2512 }
2513 // if saves requests came in while loading, kick off new save to kick off resume saves
2514 else {
2515 self.saveDatabase();
2516 }
2517
2518 if (typeof callback === 'function') {
2519 callback(err);
2520 }
2521 });
2522 return;
2523 }
2524 else {
2525 if (typeof callback === 'function') {
2526 callback(new Error("Unable to pause save throttling long enough to read database"));
2527 }
2528 }
2529 }, options);
2530 };
2531
2532 /**
2533 * Internal save logic, decoupled from save throttling logic
2534 */
2535 Loki.prototype.saveDatabaseInternal = function (callback) {
2536 var cFun = callback || function (err) {
2537 if (err) {
2538 throw err;
2539 }
2540 return;
2541 },
2542 self = this;
2543
2544 // the persistenceAdapter should be present if all is ok, but check to be sure.
2545 if (this.persistenceAdapter !== null) {
2546 // check if the adapter is requesting (and supports) a 'reference' mode export
2547 if (this.persistenceAdapter.mode === "reference" && typeof this.persistenceAdapter.exportDatabase === "function") {
2548 // filename may seem redundant but loadDatabase will need to expect this same filename
2549 this.persistenceAdapter.exportDatabase(this.filename, this.copy({removeNonSerializable:true}), function exportDatabaseCallback(err) {
2550 self.autosaveClearFlags();
2551 cFun(err);
2552 });
2553 }
2554 // otherwise just pass the serialized database to adapter
2555 else {
2556 // persistenceAdapter might be asynchronous, so we must clear `dirty` immediately
2557 // or autosave won't work if an update occurs between here and the callback
2558 self.autosaveClearFlags();
2559 this.persistenceAdapter.saveDatabase(this.filename, self.serialize(), function saveDatabasecallback(err) {
2560 cFun(err);
2561 });
2562 }
2563 } else {
2564 cFun(new Error('persistenceAdapter not configured'));
2565 }
2566 };
2567
2568 /**
2569 * Handles manually saving to file system, local storage, or adapter (such as indexeddb)
2570 * This method utilizes loki configuration options (if provided) to determine which
2571 * persistence method to use, or environment detection (if configuration was not provided).
2572 *
2573 * If you are configured with autosave, you do not need to call this method yourself.
2574 *
2575 * @param {function=} callback - (Optional) user supplied async callback / error handler
2576 * @memberof Loki
2577 * @example
2578 * db.saveDatabase(function(err) {
2579 * if (err) {
2580 * console.log("error : " + err);
2581 * }
2582 * else {
2583 * console.log("database saved.");
2584 * }
2585 * });
2586 */
2587 Loki.prototype.saveDatabase = function (callback) {
2588 if (!this.throttledSaves) {
2589 this.saveDatabaseInternal(callback);
2590 return;
2591 }
2592
2593 if (this.throttledSavePending) {
2594 this.throttledCallbacks.push(callback);
2595 return;
2596 }
2597
2598 var localCallbacks = this.throttledCallbacks;
2599 this.throttledCallbacks = [];
2600 localCallbacks.unshift(callback);
2601 this.throttledSavePending = true;
2602
2603 var self = this;
2604 this.saveDatabaseInternal(function(err) {
2605 self.throttledSavePending = false;
2606 localCallbacks.forEach(function(pcb) {
2607 if (typeof pcb === 'function') {
2608 // Queue the callbacks so we first finish this method execution
2609 setTimeout(function() {
2610 pcb(err);
2611 }, 1);
2612 }
2613 });
2614
2615 // since this is called async, future requests may have come in, if so.. kick off next save
2616 if (self.throttledCallbacks.length > 0) {
2617 self.saveDatabase();
2618 }
2619 });
2620 };
2621
2622 // alias
2623 Loki.prototype.save = Loki.prototype.saveDatabase;
2624
2625 /**
2626 * Handles deleting a database from file system, local
2627 * storage, or adapter (indexeddb)
2628 * This method utilizes loki configuration options (if provided) to determine which
2629 * persistence method to use, or environment detection (if configuration was not provided).
2630 *
2631 * @param {function=} callback - (Optional) user supplied async callback / error handler
2632 * @memberof Loki
2633 */
2634 Loki.prototype.deleteDatabase = function (options, callback) {
2635 var cFun = callback || function (err, data) {
2636 if (err) {
2637 throw err;
2638 }
2639 };
2640
2641 // we aren't even using options, so we will support syntax where
2642 // callback is passed as first and only argument
2643 if (typeof options === 'function' && !callback) {
2644 cFun = options;
2645 }
2646
2647 // the persistenceAdapter should be present if all is ok, but check to be sure.
2648 if (this.persistenceAdapter !== null) {
2649 this.persistenceAdapter.deleteDatabase(this.filename, function deleteDatabaseCallback(err) {
2650 cFun(err);
2651 });
2652 } else {
2653 cFun(new Error('persistenceAdapter not configured'));
2654 }
2655 };
2656
2657 /**
2658 * autosaveDirty - check whether any collections are 'dirty' meaning we need to save (entire) database
2659 *
2660 * @returns {boolean} - true if database has changed since last autosave, false if not.
2661 */
2662 Loki.prototype.autosaveDirty = function () {
2663 for (var idx = 0; idx < this.collections.length; idx++) {
2664 if (this.collections[idx].dirty) {
2665 return true;
2666 }
2667 }
2668
2669 return false;
2670 };
2671
2672 /**
2673 * autosaveClearFlags - resets dirty flags on all collections.
2674 * Called from saveDatabase() after db is saved.
2675 *
2676 */
2677 Loki.prototype.autosaveClearFlags = function () {
2678 for (var idx = 0; idx < this.collections.length; idx++) {
2679 this.collections[idx].dirty = false;
2680 }
2681 };
2682
2683 /**
2684 * autosaveEnable - begin a javascript interval to periodically save the database.
2685 *
2686 * @param {object} options - not currently used (remove or allow overrides?)
2687 * @param {function=} callback - (Optional) user supplied async callback
2688 */
2689 Loki.prototype.autosaveEnable = function (options, callback) {
2690 this.autosave = true;
2691
2692 var delay = 5000,
2693 self = this;
2694
2695 if (typeof (this.autosaveInterval) !== 'undefined' && this.autosaveInterval !== null) {
2696 delay = this.autosaveInterval;
2697 }
2698
2699 this.autosaveHandle = setInterval(function autosaveHandleInterval() {
2700 // use of dirty flag will need to be hierarchical since mods are done at collection level with no visibility of 'db'
2701 // so next step will be to implement collection level dirty flags set on insert/update/remove
2702 // along with loki level isdirty() function which iterates all collections to see if any are dirty
2703
2704 if (self.autosaveDirty()) {
2705 self.saveDatabase(callback);
2706 }
2707 }, delay);
2708 };
2709
2710 /**
2711 * autosaveDisable - stop the autosave interval timer.
2712 *
2713 */
2714 Loki.prototype.autosaveDisable = function () {
2715 if (typeof (this.autosaveHandle) !== 'undefined' && this.autosaveHandle !== null) {
2716 clearInterval(this.autosaveHandle);
2717 this.autosaveHandle = null;
2718 }
2719 };
2720
2721
2722 /**
2723 * Resultset class allowing chainable queries. Intended to be instanced internally.
2724 * Collection.find(), Collection.where(), and Collection.chain() instantiate this.
2725 *
2726 * @example
2727 * mycollection.chain()
2728 * .find({ 'doors' : 4 })
2729 * .where(function(obj) { return obj.name === 'Toyota' })
2730 * .data();
2731 *
2732 * @constructor Resultset
2733 * @param {Collection} collection - The collection which this Resultset will query against.
2734 */
2735 function Resultset(collection, options) {
2736 options = options || {};
2737
2738 // retain reference to collection we are querying against
2739 this.collection = collection;
2740 this.filteredrows = [];
2741 this.filterInitialized = false;
2742
2743 return this;
2744 }
2745
2746 /**
2747 * reset() - Reset the resultset to its initial state.
2748 *
2749 * @returns {Resultset} Reference to this resultset, for future chain operations.
2750 */
2751 Resultset.prototype.reset = function () {
2752 if (this.filteredrows.length > 0) {
2753 this.filteredrows = [];
2754 }
2755 this.filterInitialized = false;
2756 return this;
2757 };
2758
2759 /**
2760 * toJSON() - Override of toJSON to avoid circular references
2761 *
2762 */
2763 Resultset.prototype.toJSON = function () {
2764 var copy = this.copy();
2765 copy.collection = null;
2766 return copy;
2767 };
2768
2769 /**
2770 * Allows you to limit the number of documents passed to next chain operation.
2771 * A resultset copy() is made to avoid altering original resultset.
2772 *
2773 * @param {int} qty - The number of documents to return.
2774 * @returns {Resultset} Returns a copy of the resultset, limited by qty, for subsequent chain ops.
2775 * @memberof Resultset
2776 */
2777 Resultset.prototype.limit = function (qty) {
2778 // if this has no filters applied, we need to populate filteredrows first
2779 if (!this.filterInitialized && this.filteredrows.length === 0) {
2780 this.filteredrows = this.collection.prepareFullDocIndex();
2781 }
2782
2783 var rscopy = new Resultset(this.collection);
2784 rscopy.filteredrows = this.filteredrows.slice(0, qty);
2785 rscopy.filterInitialized = true;
2786 return rscopy;
2787 };
2788
2789 /**
2790 * Used for skipping 'pos' number of documents in the resultset.
2791 *
2792 * @param {int} pos - Number of documents to skip; all preceding documents are filtered out.
2793 * @returns {Resultset} Returns a copy of the resultset, containing docs starting at 'pos' for subsequent chain ops.
2794 * @memberof Resultset
2795 */
2796 Resultset.prototype.offset = function (pos) {
2797 // if this has no filters applied, we need to populate filteredrows first
2798 if (!this.filterInitialized && this.filteredrows.length === 0) {
2799 this.filteredrows = this.collection.prepareFullDocIndex();
2800 }
2801
2802 var rscopy = new Resultset(this.collection);
2803 rscopy.filteredrows = this.filteredrows.slice(pos);
2804 rscopy.filterInitialized = true;
2805 return rscopy;
2806 };
2807
2808 /**
2809 * copy() - To support reuse of resultset in branched query situations.
2810 *
2811 * @returns {Resultset} Returns a copy of the resultset (set) but the underlying document references will be the same.
2812 * @memberof Resultset
2813 */
2814 Resultset.prototype.copy = function () {
2815 var result = new Resultset(this.collection);
2816
2817 if (this.filteredrows.length > 0) {
2818 result.filteredrows = this.filteredrows.slice();
2819 }
2820 result.filterInitialized = this.filterInitialized;
2821
2822 return result;
2823 };
2824
2825 /**
2826 * Alias of copy()
2827 * @memberof Resultset
2828 */
2829 Resultset.prototype.branch = Resultset.prototype.copy;
2830
2831 /**
2832 * transform() - executes a named collection transform or raw array of transform steps against the resultset.
2833 *
2834 * @param transform {(string|array)} - name of collection transform or raw transform array
2835 * @param parameters {object=} - (Optional) object property hash of parameters, if the transform requires them.
2836 * @returns {Resultset} either (this) resultset or a clone of of this resultset (depending on steps)
2837 * @memberof Resultset
2838 */
2839 Resultset.prototype.transform = function (transform, parameters) {
2840 var idx,
2841 step,
2842 rs = this;
2843
2844 // if transform is name, then do lookup first
2845 if (typeof transform === 'string') {
2846 if (this.collection.transforms.hasOwnProperty(transform)) {
2847 transform = this.collection.transforms[transform];
2848 }
2849 }
2850
2851 // either they passed in raw transform array or we looked it up, so process
2852 if (typeof transform !== 'object' || !Array.isArray(transform)) {
2853 throw new Error("Invalid transform");
2854 }
2855
2856 if (typeof parameters !== 'undefined') {
2857 transform = Utils.resolveTransformParams(transform, parameters);
2858 }
2859
2860 for (idx = 0; idx < transform.length; idx++) {
2861 step = transform[idx];
2862
2863 switch (step.type) {
2864 case "find":
2865 rs.find(step.value);
2866 break;
2867 case "where":
2868 rs.where(step.value);
2869 break;
2870 case "simplesort":
2871 rs.simplesort(step.property, step.desc);
2872 break;
2873 case "compoundsort":
2874 rs.compoundsort(step.value);
2875 break;
2876 case "sort":
2877 rs.sort(step.value);
2878 break;
2879 case "limit":
2880 rs = rs.limit(step.value);
2881 break; // limit makes copy so update reference
2882 case "offset":
2883 rs = rs.offset(step.value);
2884 break; // offset makes copy so update reference
2885 case "map":
2886 rs = rs.map(step.value, step.dataOptions);
2887 break;
2888 case "eqJoin":
2889 rs = rs.eqJoin(step.joinData, step.leftJoinKey, step.rightJoinKey, step.mapFun, step.dataOptions);
2890 break;
2891 // following cases break chain by returning array data so make any of these last in transform steps
2892 case "mapReduce":
2893 rs = rs.mapReduce(step.mapFunction, step.reduceFunction);
2894 break;
2895 // following cases update documents in current filtered resultset (use carefully)
2896 case "update":
2897 rs.update(step.value);
2898 break;
2899 case "remove":
2900 rs.remove();
2901 break;
2902 default:
2903 break;
2904 }
2905 }
2906
2907 return rs;
2908 };
2909
2910 /**
2911 * User supplied compare function is provided two documents to compare. (chainable)
2912 * @example
2913 * rslt.sort(function(obj1, obj2) {
2914 * if (obj1.name === obj2.name) return 0;
2915 * if (obj1.name > obj2.name) return 1;
2916 * if (obj1.name < obj2.name) return -1;
2917 * });
2918 *
2919 * @param {function} comparefun - A javascript compare function used for sorting.
2920 * @returns {Resultset} Reference to this resultset, sorted, for future chain operations.
2921 * @memberof Resultset
2922 */
2923 Resultset.prototype.sort = function (comparefun) {
2924 // if this has no filters applied, just we need to populate filteredrows first
2925 if (!this.filterInitialized && this.filteredrows.length === 0) {
2926 this.filteredrows = this.collection.prepareFullDocIndex();
2927 }
2928
2929 var wrappedComparer =
2930 (function (userComparer, data) {
2931 return function (a, b) {
2932 return userComparer(data[a], data[b]);
2933 };
2934 })(comparefun, this.collection.data);
2935
2936 this.filteredrows.sort(wrappedComparer);
2937
2938 return this;
2939 };
2940
2941 /**
2942 * Simpler, loose evaluation for user to sort based on a property name. (chainable).
2943 * Sorting based on the same lt/gt helper functions used for binary indices.
2944 *
2945 * @param {string} propname - name of property to sort by.
2946 * @param {bool=} isdesc - (Optional) If true, the property will be sorted in descending order
2947 * @returns {Resultset} Reference to this resultset, sorted, for future chain operations.
2948 * @memberof Resultset
2949 */
2950 Resultset.prototype.simplesort = function (propname, isdesc) {
2951 if (typeof (isdesc) === 'undefined') {
2952 isdesc = false;
2953 }
2954
2955 // if this has no filters applied, just we need to populate filteredrows first
2956 if (!this.filterInitialized && this.filteredrows.length === 0) {
2957 // if we have a binary index and no other filters applied, we can use that instead of sorting (again)
2958 if (this.collection.binaryIndices.hasOwnProperty(propname)) {
2959 // make sure index is up-to-date
2960 this.collection.ensureIndex(propname);
2961 // copy index values into filteredrows
2962 this.filteredrows = this.collection.binaryIndices[propname].values.slice(0);
2963
2964 if (isdesc) {
2965 this.filteredrows.reverse();
2966 }
2967
2968 // we are done, return this (resultset) for further chain ops
2969 return this;
2970 }
2971 // otherwise initialize array for sort below
2972 else {
2973 this.filteredrows = this.collection.prepareFullDocIndex();
2974 }
2975 }
2976
2977 var wrappedComparer =
2978 (function (prop, desc, data) {
2979 var val1, val2, arr;
2980 return function (a, b) {
2981 if (~prop.indexOf('.')) {
2982 arr = prop.split('.');
2983 val1 = arr.reduce(function(obj, i) { return obj && obj[i] || undefined; }, data[a]);
2984 val2 = arr.reduce(function(obj, i) { return obj && obj[i] || undefined; }, data[b]);
2985 } else {
2986 val1 = data[a][prop];
2987 val2 = data[b][prop];
2988 }
2989 return sortHelper(val1, val2, desc);
2990 };
2991 })(propname, isdesc, this.collection.data);
2992
2993 this.filteredrows.sort(wrappedComparer);
2994
2995 return this;
2996 };
2997
2998 /**
2999 * Allows sorting a resultset based on multiple columns.
3000 * @example
3001 * // to sort by age and then name (both ascending)
3002 * rs.compoundsort(['age', 'name']);
3003 * // to sort by age (ascending) and then by name (descending)
3004 * rs.compoundsort(['age', ['name', true]);
3005 *
3006 * @param {array} properties - array of property names or subarray of [propertyname, isdesc] used evaluate sort order
3007 * @returns {Resultset} Reference to this resultset, sorted, for future chain operations.
3008 * @memberof Resultset
3009 */
3010 Resultset.prototype.compoundsort = function (properties) {
3011 if (properties.length === 0) {
3012 throw new Error("Invalid call to compoundsort, need at least one property");
3013 }
3014
3015 var prop;
3016 if (properties.length === 1) {
3017 prop = properties[0];
3018 if (Array.isArray(prop)) {
3019 return this.simplesort(prop[0], prop[1]);
3020 }
3021 return this.simplesort(prop, false);
3022 }
3023
3024 // unify the structure of 'properties' to avoid checking it repeatedly while sorting
3025 for (var i = 0, len = properties.length; i < len; i += 1) {
3026 prop = properties[i];
3027 if (!Array.isArray(prop)) {
3028 properties[i] = [prop, false];
3029 }
3030 }
3031
3032 // if this has no filters applied, just we need to populate filteredrows first
3033 if (!this.filterInitialized && this.filteredrows.length === 0) {
3034 this.filteredrows = this.collection.prepareFullDocIndex();
3035 }
3036
3037 var wrappedComparer =
3038 (function (props, data) {
3039 return function (a, b) {
3040 return compoundeval(props, data[a], data[b]);
3041 };
3042 })(properties, this.collection.data);
3043
3044 this.filteredrows.sort(wrappedComparer);
3045
3046 return this;
3047 };
3048
3049 /**
3050 * findOr() - oversee the operation of OR'ed query expressions.
3051 * OR'ed expression evaluation runs each expression individually against the full collection,
3052 * and finally does a set OR on each expression's results.
3053 * Each evaluation can utilize a binary index to prevent multiple linear array scans.
3054 *
3055 * @param {array} expressionArray - array of expressions
3056 * @returns {Resultset} this resultset for further chain ops.
3057 */
3058 Resultset.prototype.findOr = function (expressionArray) {
3059 var fr = null,
3060 fri = 0,
3061 frlen = 0,
3062 docset = [],
3063 idxset = [],
3064 idx = 0,
3065 origCount = this.count();
3066
3067 // If filter is already initialized, then we query against only those items already in filter.
3068 // This means no index utilization for fields, so hopefully its filtered to a smallish filteredrows.
3069 for (var ei = 0, elen = expressionArray.length; ei < elen; ei++) {
3070 // we need to branch existing query to run each filter separately and combine results
3071 fr = this.branch().find(expressionArray[ei]).filteredrows;
3072 frlen = fr.length;
3073 // if the find operation did not reduce the initial set, then the initial set is the actual result
3074 if (frlen === origCount) {
3075 return this;
3076 }
3077
3078 // add any document 'hits'
3079 for (fri = 0; fri < frlen; fri++) {
3080 idx = fr[fri];
3081 if (idxset[idx] === undefined) {
3082 idxset[idx] = true;
3083 docset.push(idx);
3084 }
3085 }
3086 }
3087
3088 this.filteredrows = docset;
3089 this.filterInitialized = true;
3090
3091 return this;
3092 };
3093 Resultset.prototype.$or = Resultset.prototype.findOr;
3094
3095 /**
3096 * findAnd() - oversee the operation of AND'ed query expressions.
3097 * AND'ed expression evaluation runs each expression progressively against the full collection,
3098 * internally utilizing existing chained resultset functionality.
3099 * Only the first filter can utilize a binary index.
3100 *
3101 * @param {array} expressionArray - array of expressions
3102 * @returns {Resultset} this resultset for further chain ops.
3103 */
3104 Resultset.prototype.findAnd = function (expressionArray) {
3105 // we have already implementing method chaining in this (our Resultset class)
3106 // so lets just progressively apply user supplied and filters
3107 for (var i = 0, len = expressionArray.length; i < len; i++) {
3108 if (this.count() === 0) {
3109 return this;
3110 }
3111 this.find(expressionArray[i]);
3112 }
3113 return this;
3114 };
3115 Resultset.prototype.$and = Resultset.prototype.findAnd;
3116
3117 /**
3118 * Used for querying via a mongo-style query object.
3119 *
3120 * @param {object} query - A mongo-style query object used for filtering current results.
3121 * @param {boolean=} firstOnly - (Optional) Used by collection.findOne()
3122 * @returns {Resultset} this resultset for further chain ops.
3123 * @memberof Resultset
3124 */
3125 Resultset.prototype.find = function (query, firstOnly) {
3126 if (this.collection.data.length === 0) {
3127 this.filteredrows = [];
3128 this.filterInitialized = true;
3129 return this;
3130 }
3131
3132 var queryObject = query || 'getAll',
3133 p,
3134 property,
3135 queryObjectOp,
3136 obj,
3137 operator,
3138 value,
3139 key,
3140 searchByIndex = false,
3141 result = [],
3142 filters = [],
3143 index = null;
3144
3145 // flag if this was invoked via findOne()
3146 firstOnly = firstOnly || false;
3147
3148 if (typeof queryObject === 'object') {
3149 for (p in queryObject) {
3150 obj = {};
3151 obj[p] = queryObject[p];
3152 filters.push(obj);
3153
3154 if (hasOwnProperty.call(queryObject, p)) {
3155 property = p;
3156 queryObjectOp = queryObject[p];
3157 }
3158 }
3159 // if more than one expression in single query object,
3160 // convert implicit $and to explicit $and
3161 if (filters.length > 1) {
3162 return this.find({ '$and': filters }, firstOnly);
3163 }
3164 }
3165
3166 // apply no filters if they want all
3167 if (!property || queryObject === 'getAll') {
3168 if (firstOnly) {
3169 this.filteredrows = (this.collection.data.length > 0)?[0]: [];
3170 this.filterInitialized = true;
3171 }
3172
3173 return this;
3174 }
3175
3176 // injecting $and and $or expression tree evaluation here.
3177 if (property === '$and' || property === '$or') {
3178 this[property](queryObjectOp);
3179
3180 // for chained find with firstonly,
3181 if (firstOnly && this.filteredrows.length > 1) {
3182 this.filteredrows = this.filteredrows.slice(0, 1);
3183 }
3184
3185 return this;
3186 }
3187
3188 // see if query object is in shorthand mode (assuming eq operator)
3189 if (queryObjectOp === null || (typeof queryObjectOp !== 'object' || queryObjectOp instanceof Date)) {
3190 operator = '$eq';
3191 value = queryObjectOp;
3192 } else if (typeof queryObjectOp === 'object') {
3193 for (key in queryObjectOp) {
3194 if (hasOwnProperty.call(queryObjectOp, key)) {
3195 operator = key;
3196 value = queryObjectOp[key];
3197 break;
3198 }
3199 }
3200 } else {
3201 throw new Error('Do not know what you want to do.');
3202 }
3203
3204 // for regex ops, precompile
3205 if (operator === '$regex') {
3206 if (Array.isArray(value)) {
3207 value = new RegExp(value[0], value[1]);
3208 } else if (!(value instanceof RegExp)) {
3209 value = new RegExp(value);
3210 }
3211 }
3212
3213 // if user is deep querying the object such as find('name.first': 'odin')
3214 var usingDotNotation = (property.indexOf('.') !== -1);
3215
3216 // if an index exists for the property being queried against, use it
3217 // for now only enabling where it is the first filter applied and prop is indexed
3218 var doIndexCheck = !usingDotNotation && !this.filterInitialized;
3219
3220 if (doIndexCheck && this.collection.binaryIndices[property] && indexedOps[operator]) {
3221 // this is where our lazy index rebuilding will take place
3222 // basically we will leave all indexes dirty until we need them
3223 // so here we will rebuild only the index tied to this property
3224 // ensureIndex() will only rebuild if flagged as dirty since we are not passing force=true param
3225 if (this.collection.adaptiveBinaryIndices !== true) {
3226 this.collection.ensureIndex(property);
3227 }
3228
3229 searchByIndex = true;
3230 index = this.collection.binaryIndices[property];
3231 }
3232
3233 // the comparison function
3234 var fun = LokiOps[operator];
3235
3236 // "shortcut" for collection data
3237 var t = this.collection.data;
3238 // filter data length
3239 var i = 0,
3240 len = 0;
3241
3242 // Query executed differently depending on :
3243 // - whether the property being queried has an index defined
3244 // - if chained, we handle first pass differently for initial filteredrows[] population
3245 //
3246 // For performance reasons, each case has its own if block to minimize in-loop calculations
3247
3248 var filter, rowIdx = 0;
3249
3250 // If the filteredrows[] is already initialized, use it
3251 if (this.filterInitialized) {
3252 filter = this.filteredrows;
3253 len = filter.length;
3254
3255 // currently supporting dot notation for non-indexed conditions only
3256 if (usingDotNotation) {
3257 property = property.split('.');
3258 for(i=0; i<len; i++) {
3259 rowIdx = filter[i];
3260 if (dotSubScan(t[rowIdx], property, fun, value)) {
3261 result.push(rowIdx);
3262 }
3263 }
3264 } else {
3265 for(i=0; i<len; i++) {
3266 rowIdx = filter[i];
3267 if (fun(t[rowIdx][property], value)) {
3268 result.push(rowIdx);
3269 }
3270 }
3271 }
3272 }
3273 // first chained query so work against data[] but put results in filteredrows
3274 else {
3275 // if not searching by index
3276 if (!searchByIndex) {
3277 len = t.length;
3278
3279 if (usingDotNotation) {
3280 property = property.split('.');
3281 for(i=0; i<len; i++) {
3282 if (dotSubScan(t[i], property, fun, value)) {
3283 result.push(i);
3284 if (firstOnly) {
3285 this.filteredrows = result;
3286 this.filterInitialized = true;
3287 return this;
3288 }
3289 }
3290 }
3291 } else {
3292 for(i=0; i<len; i++) {
3293 if (fun(t[i][property], value)) {
3294 result.push(i);
3295 if (firstOnly) {
3296 this.filteredrows = result;
3297 this.filterInitialized = true;
3298 return this;
3299 }
3300 }
3301 }
3302 }
3303 } else {
3304 // search by index
3305 var segm = this.collection.calculateRange(operator, property, value);
3306
3307 if (operator !== '$in') {
3308 for (i = segm[0]; i <= segm[1]; i++) {
3309 if (indexedOps[operator] !== true) {
3310 // must be a function, implying 2nd phase filtering of results from calculateRange
3311 if (indexedOps[operator](t[index.values[i]][property], value)) {
3312 result.push(index.values[i]);
3313 if (firstOnly) {
3314 this.filteredrows = result;
3315 this.filterInitialized = true;
3316 return this;
3317 }
3318 }
3319 }
3320 else {
3321 result.push(index.values[i]);
3322 if (firstOnly) {
3323 this.filteredrows = result;
3324 this.filterInitialized = true;
3325 return this;
3326 }
3327 }
3328 }
3329 } else {
3330 for (i = 0, len = segm.length; i < len; i++) {
3331 result.push(index.values[segm[i]]);
3332 if (firstOnly) {
3333 this.filteredrows = result;
3334 this.filterInitialized = true;
3335 return this;
3336 }
3337 }
3338 }
3339 }
3340
3341 }
3342
3343 this.filteredrows = result;
3344 this.filterInitialized = true; // next time work against filteredrows[]
3345 return this;
3346 };
3347
3348
3349 /**
3350 * where() - Used for filtering via a javascript filter function.
3351 *
3352 * @param {function} fun - A javascript function used for filtering current results by.
3353 * @returns {Resultset} this resultset for further chain ops.
3354 * @memberof Resultset
3355 */
3356 Resultset.prototype.where = function (fun) {
3357 var viewFunction,
3358 result = [];
3359
3360 if ('function' === typeof fun) {
3361 viewFunction = fun;
3362 } else {
3363 throw new TypeError('Argument is not a stored view or a function');
3364 }
3365 try {
3366 // If the filteredrows[] is already initialized, use it
3367 if (this.filterInitialized) {
3368 var j = this.filteredrows.length;
3369
3370 while (j--) {
3371 if (viewFunction(this.collection.data[this.filteredrows[j]]) === true) {
3372 result.push(this.filteredrows[j]);
3373 }
3374 }
3375
3376 this.filteredrows = result;
3377
3378 return this;
3379 }
3380 // otherwise this is initial chained op, work against data, push into filteredrows[]
3381 else {
3382 var k = this.collection.data.length;
3383
3384 while (k--) {
3385 if (viewFunction(this.collection.data[k]) === true) {
3386 result.push(k);
3387 }
3388 }
3389
3390 this.filteredrows = result;
3391 this.filterInitialized = true;
3392
3393 return this;
3394 }
3395 } catch (err) {
3396 throw err;
3397 }
3398 };
3399
3400 /**
3401 * count() - returns the number of documents in the resultset.
3402 *
3403 * @returns {number} The number of documents in the resultset.
3404 * @memberof Resultset
3405 */
3406 Resultset.prototype.count = function () {
3407 if (this.filterInitialized) {
3408 return this.filteredrows.length;
3409 }
3410 return this.collection.count();
3411 };
3412
3413 /**
3414 * Terminates the chain and returns array of filtered documents
3415 *
3416 * @param {object=} options - allows specifying 'forceClones' and 'forceCloneMethod' options.
3417 * @param {boolean} options.forceClones - Allows forcing the return of cloned objects even when
3418 * the collection is not configured for clone object.
3419 * @param {string} options.forceCloneMethod - Allows overriding the default or collection specified cloning method.
3420 * Possible values include 'parse-stringify', 'jquery-extend-deep', 'shallow', 'shallow-assign'
3421 * @param {bool} options.removeMeta - Will force clones and strip $loki and meta properties from documents
3422 *
3423 * @returns {array} Array of documents in the resultset
3424 * @memberof Resultset
3425 */
3426 Resultset.prototype.data = function (options) {
3427 var result = [],
3428 data = this.collection.data,
3429 obj,
3430 len,
3431 i,
3432 method;
3433
3434 options = options || {};
3435
3436 // if user opts to strip meta, then force clones and use 'shallow' if 'force' options are not present
3437 if (options.removeMeta && !options.forceClones) {
3438 options.forceClones = true;
3439 options.forceCloneMethod = options.forceCloneMethod || 'shallow';
3440 }
3441
3442 // if collection has delta changes active, then force clones and use 'parse-stringify' for effective change tracking of nested objects
3443 if (!this.collection.disableDeltaChangesApi) {
3444 options.forceClones = true;
3445 options.forceCloneMethod = 'parse-stringify';
3446 }
3447
3448 // if this has no filters applied, just return collection.data
3449 if (!this.filterInitialized) {
3450 if (this.filteredrows.length === 0) {
3451 // determine whether we need to clone objects or not
3452 if (this.collection.cloneObjects || options.forceClones) {
3453 len = data.length;
3454 method = options.forceCloneMethod || this.collection.cloneMethod;
3455
3456 for (i = 0; i < len; i++) {
3457 obj = clone(data[i], method);
3458 if (options.removeMeta) {
3459 delete obj.$loki;
3460 delete obj.meta;
3461 }
3462 result.push(obj);
3463 }
3464 return result;
3465 }
3466 // otherwise we are not cloning so return sliced array with same object references
3467 else {
3468 return data.slice();
3469 }
3470 } else {
3471 // filteredrows must have been set manually, so use it
3472 this.filterInitialized = true;
3473 }
3474 }
3475
3476 var fr = this.filteredrows;
3477 len = fr.length;
3478
3479 if (this.collection.cloneObjects || options.forceClones) {
3480 method = options.forceCloneMethod || this.collection.cloneMethod;
3481 for (i = 0; i < len; i++) {
3482 obj = clone(data[fr[i]], method);
3483 if (options.removeMeta) {
3484 delete obj.$loki;
3485 delete obj.meta;
3486 }
3487 result.push(obj);
3488 }
3489 } else {
3490 for (i = 0; i < len; i++) {
3491 result.push(data[fr[i]]);
3492 }
3493 }
3494 return result;
3495 };
3496
3497 /**
3498 * Used to run an update operation on all documents currently in the resultset.
3499 *
3500 * @param {function} updateFunction - User supplied updateFunction(obj) will be executed for each document object.
3501 * @returns {Resultset} this resultset for further chain ops.
3502 * @memberof Resultset
3503 */
3504 Resultset.prototype.update = function (updateFunction) {
3505
3506 if (typeof (updateFunction) !== "function") {
3507 throw new TypeError('Argument is not a function');
3508 }
3509
3510 // if this has no filters applied, we need to populate filteredrows first
3511 if (!this.filterInitialized && this.filteredrows.length === 0) {
3512 this.filteredrows = this.collection.prepareFullDocIndex();
3513 }
3514
3515 var len = this.filteredrows.length,
3516 rcd = this.collection.data;
3517
3518 for (var idx = 0; idx < len; idx++) {
3519 // pass in each document object currently in resultset to user supplied updateFunction
3520 updateFunction(rcd[this.filteredrows[idx]]);
3521
3522 // notify collection we have changed this object so it can update meta and allow DynamicViews to re-evaluate
3523 this.collection.update(rcd[this.filteredrows[idx]]);
3524 }
3525
3526 return this;
3527 };
3528
3529 /**
3530 * Removes all document objects which are currently in resultset from collection (as well as resultset)
3531 *
3532 * @returns {Resultset} this (empty) resultset for further chain ops.
3533 * @memberof Resultset
3534 */
3535 Resultset.prototype.remove = function () {
3536
3537 // if this has no filters applied, we need to populate filteredrows first
3538 if (!this.filterInitialized && this.filteredrows.length === 0) {
3539 this.filteredrows = this.collection.prepareFullDocIndex();
3540 }
3541
3542 this.collection.remove(this.data());
3543
3544 this.filteredrows = [];
3545
3546 return this;
3547 };
3548
3549 /**
3550 * data transformation via user supplied functions
3551 *
3552 * @param {function} mapFunction - this function accepts a single document for you to transform and return
3553 * @param {function} reduceFunction - this function accepts many (array of map outputs) and returns single value
3554 * @returns {value} The output of your reduceFunction
3555 * @memberof Resultset
3556 */
3557 Resultset.prototype.mapReduce = function (mapFunction, reduceFunction) {
3558 try {
3559 return reduceFunction(this.data().map(mapFunction));
3560 } catch (err) {
3561 throw err;
3562 }
3563 };
3564
3565 /**
3566 * eqJoin() - Left joining two sets of data. Join keys can be defined or calculated properties
3567 * eqJoin expects the right join key values to be unique. Otherwise left data will be joined on the last joinData object with that key
3568 * @param {Array|Resultset|Collection} joinData - Data array to join to.
3569 * @param {(string|function)} leftJoinKey - Property name in this result set to join on or a function to produce a value to join on
3570 * @param {(string|function)} rightJoinKey - Property name in the joinData to join on or a function to produce a value to join on
3571 * @param {function=} mapFun - (Optional) A function that receives each matching pair and maps them into output objects - function(left,right){return joinedObject}
3572 * @param {object=} dataOptions - options to data() before input to your map function
3573 * @param {bool} dataOptions.removeMeta - allows removing meta before calling mapFun
3574 * @param {boolean} dataOptions.forceClones - forcing the return of cloned objects to your map object
3575 * @param {string} dataOptions.forceCloneMethod - Allows overriding the default or collection specified cloning method.
3576 * @returns {Resultset} A resultset with data in the format [{left: leftObj, right: rightObj}]
3577 * @memberof Resultset
3578 */
3579 Resultset.prototype.eqJoin = function (joinData, leftJoinKey, rightJoinKey, mapFun, dataOptions) {
3580
3581 var leftData = [],
3582 leftDataLength,
3583 rightData = [],
3584 rightDataLength,
3585 key,
3586 result = [],
3587 leftKeyisFunction = typeof leftJoinKey === 'function',
3588 rightKeyisFunction = typeof rightJoinKey === 'function',
3589 joinMap = {};
3590
3591 //get the left data
3592 leftData = this.data(dataOptions);
3593 leftDataLength = leftData.length;
3594
3595 //get the right data
3596 if (joinData instanceof Collection) {
3597 rightData = joinData.chain().data(dataOptions);
3598 } else if (joinData instanceof Resultset) {
3599 rightData = joinData.data(dataOptions);
3600 } else if (Array.isArray(joinData)) {
3601 rightData = joinData;
3602 } else {
3603 throw new TypeError('joinData needs to be an array or result set');
3604 }
3605 rightDataLength = rightData.length;
3606
3607 //construct a lookup table
3608
3609 for (var i = 0; i < rightDataLength; i++) {
3610 key = rightKeyisFunction ? rightJoinKey(rightData[i]) : rightData[i][rightJoinKey];
3611 joinMap[key] = rightData[i];
3612 }
3613
3614 if (!mapFun) {
3615 mapFun = function (left, right) {
3616 return {
3617 left: left,
3618 right: right
3619 };
3620 };
3621 }
3622
3623 //Run map function over each object in the resultset
3624 for (var j = 0; j < leftDataLength; j++) {
3625 key = leftKeyisFunction ? leftJoinKey(leftData[j]) : leftData[j][leftJoinKey];
3626 result.push(mapFun(leftData[j], joinMap[key] || {}));
3627 }
3628
3629 //return return a new resultset with no filters
3630 this.collection = new Collection('joinData');
3631 this.collection.insert(result);
3632 this.filteredrows = [];
3633 this.filterInitialized = false;
3634
3635 return this;
3636 };
3637
3638 /**
3639 * Applies a map function into a new collection for further chaining.
3640 * @param {function} mapFun - javascript map function
3641 * @param {object=} dataOptions - options to data() before input to your map function
3642 * @param {bool} dataOptions.removeMeta - allows removing meta before calling mapFun
3643 * @param {boolean} dataOptions.forceClones - forcing the return of cloned objects to your map object
3644 * @param {string} dataOptions.forceCloneMethod - Allows overriding the default or collection specified cloning method.
3645 * @memberof Resultset
3646 */
3647 Resultset.prototype.map = function (mapFun, dataOptions) {
3648 var data = this.data(dataOptions).map(mapFun);
3649 //return return a new resultset with no filters
3650 this.collection = new Collection('mappedData');
3651 this.collection.insert(data);
3652 this.filteredrows = [];
3653 this.filterInitialized = false;
3654
3655 return this;
3656 };
3657
3658 /**
3659 * DynamicView class is a versatile 'live' view class which can have filters and sorts applied.
3660 * Collection.addDynamicView(name) instantiates this DynamicView object and notifies it
3661 * whenever documents are add/updated/removed so it can remain up-to-date. (chainable)
3662 *
3663 * @example
3664 * var mydv = mycollection.addDynamicView('test'); // default is non-persistent
3665 * mydv.applyFind({ 'doors' : 4 });
3666 * mydv.applyWhere(function(obj) { return obj.name === 'Toyota'; });
3667 * var results = mydv.data();
3668 *
3669 * @constructor DynamicView
3670 * @implements LokiEventEmitter
3671 * @param {Collection} collection - A reference to the collection to work against
3672 * @param {string} name - The name of this dynamic view
3673 * @param {object=} options - (Optional) Pass in object with 'persistent' and/or 'sortPriority' options.
3674 * @param {boolean} [options.persistent=false] - indicates if view is to main internal results array in 'resultdata'
3675 * @param {string} [options.sortPriority='passive'] - 'passive' (sorts performed on call to data) or 'active' (after updates)
3676 * @param {number} options.minRebuildInterval - minimum rebuild interval (need clarification to docs here)
3677 * @see {@link Collection#addDynamicView} to construct instances of DynamicView
3678 */
3679 function DynamicView(collection, name, options) {
3680 this.collection = collection;
3681 this.name = name;
3682 this.rebuildPending = false;
3683 this.options = options || {};
3684
3685 if (!this.options.hasOwnProperty('persistent')) {
3686 this.options.persistent = false;
3687 }
3688
3689 // 'persistentSortPriority':
3690 // 'passive' will defer the sort phase until they call data(). (most efficient overall)
3691 // 'active' will sort async whenever next idle. (prioritizes read speeds)
3692 if (!this.options.hasOwnProperty('sortPriority')) {
3693 this.options.sortPriority = 'passive';
3694 }
3695
3696 if (!this.options.hasOwnProperty('minRebuildInterval')) {
3697 this.options.minRebuildInterval = 1;
3698 }
3699
3700 this.resultset = new Resultset(collection);
3701 this.resultdata = [];
3702 this.resultsdirty = false;
3703
3704 this.cachedresultset = null;
3705
3706 // keep ordered filter pipeline
3707 this.filterPipeline = [];
3708
3709 // sorting member variables
3710 // we only support one active search, applied using applySort() or applySimpleSort()
3711 this.sortFunction = null;
3712 this.sortCriteria = null;
3713 this.sortDirty = false;
3714
3715 // for now just have 1 event for when we finally rebuilt lazy view
3716 // once we refactor transactions, i will tie in certain transactional events
3717
3718 this.events = {
3719 'rebuild': []
3720 };
3721 }
3722
3723 DynamicView.prototype = new LokiEventEmitter();
3724
3725
3726 /**
3727 * rematerialize() - internally used immediately after deserialization (loading)
3728 * This will clear out and reapply filterPipeline ops, recreating the view.
3729 * Since where filters do not persist correctly, this method allows
3730 * restoring the view to state where user can re-apply those where filters.
3731 *
3732 * @param {Object=} options - (Optional) allows specification of 'removeWhereFilters' option
3733 * @returns {DynamicView} This dynamic view for further chained ops.
3734 * @memberof DynamicView
3735 * @fires DynamicView.rebuild
3736 */
3737 DynamicView.prototype.rematerialize = function (options) {
3738 var fpl,
3739 fpi,
3740 idx;
3741
3742 options = options || {};
3743
3744 this.resultdata = [];
3745 this.resultsdirty = true;
3746 this.resultset = new Resultset(this.collection);
3747
3748 if (this.sortFunction || this.sortCriteria) {
3749 this.sortDirty = true;
3750 }
3751
3752 if (options.hasOwnProperty('removeWhereFilters')) {
3753 // for each view see if it had any where filters applied... since they don't
3754 // serialize those functions lets remove those invalid filters
3755 fpl = this.filterPipeline.length;
3756 fpi = fpl;
3757 while (fpi--) {
3758 if (this.filterPipeline[fpi].type === 'where') {
3759 if (fpi !== this.filterPipeline.length - 1) {
3760 this.filterPipeline[fpi] = this.filterPipeline[this.filterPipeline.length - 1];
3761 }
3762
3763 this.filterPipeline.length--;
3764 }
3765 }
3766 }
3767
3768 // back up old filter pipeline, clear filter pipeline, and reapply pipeline ops
3769 var ofp = this.filterPipeline;
3770 this.filterPipeline = [];
3771
3772 // now re-apply 'find' filterPipeline ops
3773 fpl = ofp.length;
3774 for (idx = 0; idx < fpl; idx++) {
3775 this.applyFind(ofp[idx].val);
3776 }
3777
3778 // during creation of unit tests, i will remove this forced refresh and leave lazy
3779 this.data();
3780
3781 // emit rebuild event in case user wants to be notified
3782 this.emit('rebuild', this);
3783
3784 return this;
3785 };
3786
3787 /**
3788 * branchResultset() - Makes a copy of the internal resultset for branched queries.
3789 * Unlike this dynamic view, the branched resultset will not be 'live' updated,
3790 * so your branched query should be immediately resolved and not held for future evaluation.
3791 *
3792 * @param {(string|array=)} transform - Optional name of collection transform, or an array of transform steps
3793 * @param {object=} parameters - optional parameters (if optional transform requires them)
3794 * @returns {Resultset} A copy of the internal resultset for branched queries.
3795 * @memberof DynamicView
3796 */
3797 DynamicView.prototype.branchResultset = function (transform, parameters) {
3798 var rs = this.resultset.branch();
3799
3800 if (typeof transform === 'undefined') {
3801 return rs;
3802 }
3803
3804 return rs.transform(transform, parameters);
3805 };
3806
3807 /**
3808 * toJSON() - Override of toJSON to avoid circular references
3809 *
3810 */
3811 DynamicView.prototype.toJSON = function () {
3812 var copy = new DynamicView(this.collection, this.name, this.options);
3813
3814 copy.resultset = this.resultset;
3815 copy.resultdata = []; // let's not save data (copy) to minimize size
3816 copy.resultsdirty = true;
3817 copy.filterPipeline = this.filterPipeline;
3818 copy.sortFunction = this.sortFunction;
3819 copy.sortCriteria = this.sortCriteria;
3820 copy.sortDirty = this.sortDirty;
3821
3822 // avoid circular reference, reapply in db.loadJSON()
3823 copy.collection = null;
3824
3825 return copy;
3826 };
3827
3828 /**
3829 * removeFilters() - Used to clear pipeline and reset dynamic view to initial state.
3830 * Existing options should be retained.
3831 * @param {object=} options - configure removeFilter behavior
3832 * @param {boolean=} options.queueSortPhase - (default: false) if true we will async rebuild view (maybe set default to true in future?)
3833 * @memberof DynamicView
3834 */
3835 DynamicView.prototype.removeFilters = function (options) {
3836 options = options || {};
3837
3838 this.rebuildPending = false;
3839 this.resultset.reset();
3840 this.resultdata = [];
3841 this.resultsdirty = true;
3842
3843 this.cachedresultset = null;
3844
3845 // keep ordered filter pipeline
3846 this.filterPipeline = [];
3847
3848 // sorting member variables
3849 // we only support one active search, applied using applySort() or applySimpleSort()
3850 this.sortFunction = null;
3851 this.sortCriteria = null;
3852 this.sortDirty = false;
3853
3854 if (options.queueSortPhase === true) {
3855 this.queueSortPhase();
3856 }
3857 };
3858
3859 /**
3860 * applySort() - Used to apply a sort to the dynamic view
3861 * @example
3862 * dv.applySort(function(obj1, obj2) {
3863 * if (obj1.name === obj2.name) return 0;
3864 * if (obj1.name > obj2.name) return 1;
3865 * if (obj1.name < obj2.name) return -1;
3866 * });
3867 *
3868 * @param {function} comparefun - a javascript compare function used for sorting
3869 * @returns {DynamicView} this DynamicView object, for further chain ops.
3870 * @memberof DynamicView
3871 */
3872 DynamicView.prototype.applySort = function (comparefun) {
3873 this.sortFunction = comparefun;
3874 this.sortCriteria = null;
3875
3876 this.queueSortPhase();
3877
3878 return this;
3879 };
3880
3881 /**
3882 * applySimpleSort() - Used to specify a property used for view translation.
3883 * @example
3884 * dv.applySimpleSort("name");
3885 *
3886 * @param {string} propname - Name of property by which to sort.
3887 * @param {boolean=} isdesc - (Optional) If true, the sort will be in descending order.
3888 * @returns {DynamicView} this DynamicView object, for further chain ops.
3889 * @memberof DynamicView
3890 */
3891 DynamicView.prototype.applySimpleSort = function (propname, isdesc) {
3892 this.sortCriteria = [
3893 [propname, isdesc || false]
3894 ];
3895 this.sortFunction = null;
3896
3897 this.queueSortPhase();
3898
3899 return this;
3900 };
3901
3902 /**
3903 * applySortCriteria() - Allows sorting a resultset based on multiple columns.
3904 * @example
3905 * // to sort by age and then name (both ascending)
3906 * dv.applySortCriteria(['age', 'name']);
3907 * // to sort by age (ascending) and then by name (descending)
3908 * dv.applySortCriteria(['age', ['name', true]);
3909 * // to sort by age (descending) and then by name (descending)
3910 * dv.applySortCriteria(['age', true], ['name', true]);
3911 *
3912 * @param {array} properties - array of property names or subarray of [propertyname, isdesc] used evaluate sort order
3913 * @returns {DynamicView} Reference to this DynamicView, sorted, for future chain operations.
3914 * @memberof DynamicView
3915 */
3916 DynamicView.prototype.applySortCriteria = function (criteria) {
3917 this.sortCriteria = criteria;
3918 this.sortFunction = null;
3919
3920 this.queueSortPhase();
3921
3922 return this;
3923 };
3924
3925 /**
3926 * startTransaction() - marks the beginning of a transaction.
3927 *
3928 * @returns {DynamicView} this DynamicView object, for further chain ops.
3929 */
3930 DynamicView.prototype.startTransaction = function () {
3931 this.cachedresultset = this.resultset.copy();
3932
3933 return this;
3934 };
3935
3936 /**
3937 * commit() - commits a transaction.
3938 *
3939 * @returns {DynamicView} this DynamicView object, for further chain ops.
3940 */
3941 DynamicView.prototype.commit = function () {
3942 this.cachedresultset = null;
3943
3944 return this;
3945 };
3946
3947 /**
3948 * rollback() - rolls back a transaction.
3949 *
3950 * @returns {DynamicView} this DynamicView object, for further chain ops.
3951 */
3952 DynamicView.prototype.rollback = function () {
3953 this.resultset = this.cachedresultset;
3954
3955 if (this.options.persistent) {
3956 // for now just rebuild the persistent dynamic view data in this worst case scenario
3957 // (a persistent view utilizing transactions which get rolled back), we already know the filter so not too bad.
3958 this.resultdata = this.resultset.data();
3959
3960 this.emit('rebuild', this);
3961 }
3962
3963 return this;
3964 };
3965
3966
3967 /**
3968 * Implementation detail.
3969 * _indexOfFilterWithId() - Find the index of a filter in the pipeline, by that filter's ID.
3970 *
3971 * @param {(string|number)} uid - The unique ID of the filter.
3972 * @returns {number}: index of the referenced filter in the pipeline; -1 if not found.
3973 */
3974 DynamicView.prototype._indexOfFilterWithId = function (uid) {
3975 if (typeof uid === 'string' || typeof uid === 'number') {
3976 for (var idx = 0, len = this.filterPipeline.length; idx < len; idx += 1) {
3977 if (uid === this.filterPipeline[idx].uid) {
3978 return idx;
3979 }
3980 }
3981 }
3982 return -1;
3983 };
3984
3985 /**
3986 * Implementation detail.
3987 * _addFilter() - Add the filter object to the end of view's filter pipeline and apply the filter to the resultset.
3988 *
3989 * @param {object} filter - The filter object. Refer to applyFilter() for extra details.
3990 */
3991 DynamicView.prototype._addFilter = function (filter) {
3992 this.filterPipeline.push(filter);
3993 this.resultset[filter.type](filter.val);
3994 };
3995
3996 /**
3997 * reapplyFilters() - Reapply all the filters in the current pipeline.
3998 *
3999 * @returns {DynamicView} this DynamicView object, for further chain ops.
4000 */
4001 DynamicView.prototype.reapplyFilters = function () {
4002 this.resultset.reset();
4003
4004 this.cachedresultset = null;
4005 if (this.options.persistent) {
4006 this.resultdata = [];
4007 this.resultsdirty = true;
4008 }
4009
4010 var filters = this.filterPipeline;
4011 this.filterPipeline = [];
4012
4013 for (var idx = 0, len = filters.length; idx < len; idx += 1) {
4014 this._addFilter(filters[idx]);
4015 }
4016
4017 if (this.sortFunction || this.sortCriteria) {
4018 this.queueSortPhase();
4019 } else {
4020 this.queueRebuildEvent();
4021 }
4022
4023 return this;
4024 };
4025
4026 /**
4027 * applyFilter() - Adds or updates a filter in the DynamicView filter pipeline
4028 *
4029 * @param {object} filter - A filter object to add to the pipeline.
4030 * The object is in the format { 'type': filter_type, 'val', filter_param, 'uid', optional_filter_id }
4031 * @returns {DynamicView} this DynamicView object, for further chain ops.
4032 * @memberof DynamicView
4033 */
4034 DynamicView.prototype.applyFilter = function (filter) {
4035 var idx = this._indexOfFilterWithId(filter.uid);
4036 if (idx >= 0) {
4037 this.filterPipeline[idx] = filter;
4038 return this.reapplyFilters();
4039 }
4040
4041 this.cachedresultset = null;
4042 if (this.options.persistent) {
4043 this.resultdata = [];
4044 this.resultsdirty = true;
4045 }
4046
4047 this._addFilter(filter);
4048
4049 if (this.sortFunction || this.sortCriteria) {
4050 this.queueSortPhase();
4051 } else {
4052 this.queueRebuildEvent();
4053 }
4054
4055 return this;
4056 };
4057
4058 /**
4059 * applyFind() - Adds or updates a mongo-style query option in the DynamicView filter pipeline
4060 *
4061 * @param {object} query - A mongo-style query object to apply to pipeline
4062 * @param {(string|number)=} uid - Optional: The unique ID of this filter, to reference it in the future.
4063 * @returns {DynamicView} this DynamicView object, for further chain ops.
4064 * @memberof DynamicView
4065 */
4066 DynamicView.prototype.applyFind = function (query, uid) {
4067 this.applyFilter({
4068 type: 'find',
4069 val: query,
4070 uid: uid
4071 });
4072 return this;
4073 };
4074
4075 /**
4076 * applyWhere() - Adds or updates a javascript filter function in the DynamicView filter pipeline
4077 *
4078 * @param {function} fun - A javascript filter function to apply to pipeline
4079 * @param {(string|number)=} uid - Optional: The unique ID of this filter, to reference it in the future.
4080 * @returns {DynamicView} this DynamicView object, for further chain ops.
4081 * @memberof DynamicView
4082 */
4083 DynamicView.prototype.applyWhere = function (fun, uid) {
4084 this.applyFilter({
4085 type: 'where',
4086 val: fun,
4087 uid: uid
4088 });
4089 return this;
4090 };
4091
4092 /**
4093 * removeFilter() - Remove the specified filter from the DynamicView filter pipeline
4094 *
4095 * @param {(string|number)} uid - The unique ID of the filter to be removed.
4096 * @returns {DynamicView} this DynamicView object, for further chain ops.
4097 * @memberof DynamicView
4098 */
4099 DynamicView.prototype.removeFilter = function (uid) {
4100 var idx = this._indexOfFilterWithId(uid);
4101 if (idx < 0) {
4102 throw new Error("Dynamic view does not contain a filter with ID: " + uid);
4103 }
4104
4105 this.filterPipeline.splice(idx, 1);
4106 this.reapplyFilters();
4107 return this;
4108 };
4109
4110 /**
4111 * count() - returns the number of documents representing the current DynamicView contents.
4112 *
4113 * @returns {number} The number of documents representing the current DynamicView contents.
4114 * @memberof DynamicView
4115 */
4116 DynamicView.prototype.count = function () {
4117 // in order to be accurate we will pay the minimum cost (and not alter dv state management)
4118 // recurring resultset data resolutions should know internally its already up to date.
4119 // for persistent data this will not update resultdata nor fire rebuild event.
4120 if (this.resultsdirty) {
4121 this.resultdata = this.resultset.data();
4122 }
4123
4124 return this.resultset.count();
4125 };
4126
4127 /**
4128 * data() - resolves and pending filtering and sorting, then returns document array as result.
4129 *
4130 * @param {object=} options - optional parameters to pass to resultset.data() if non-persistent
4131 * @param {boolean} options.forceClones - Allows forcing the return of cloned objects even when
4132 * the collection is not configured for clone object.
4133 * @param {string} options.forceCloneMethod - Allows overriding the default or collection specified cloning method.
4134 * Possible values include 'parse-stringify', 'jquery-extend-deep', 'shallow', 'shallow-assign'
4135 * @param {bool} options.removeMeta - Will force clones and strip $loki and meta properties from documents
4136 * @returns {array} An array of documents representing the current DynamicView contents.
4137 * @memberof DynamicView
4138 */
4139 DynamicView.prototype.data = function (options) {
4140 // using final sort phase as 'catch all' for a few use cases which require full rebuild
4141 if (this.sortDirty || this.resultsdirty) {
4142 this.performSortPhase({
4143 suppressRebuildEvent: true
4144 });
4145 }
4146 return (this.options.persistent) ? (this.resultdata) : (this.resultset.data(options));
4147 };
4148
4149 /**
4150 * queueRebuildEvent() - When the view is not sorted we may still wish to be notified of rebuild events.
4151 * This event will throttle and queue a single rebuild event when batches of updates affect the view.
4152 */
4153 DynamicView.prototype.queueRebuildEvent = function () {
4154 if (this.rebuildPending) {
4155 return;
4156 }
4157 this.rebuildPending = true;
4158
4159 var self = this;
4160 setTimeout(function () {
4161 if (self.rebuildPending) {
4162 self.rebuildPending = false;
4163 self.emit('rebuild', self);
4164 }
4165 }, this.options.minRebuildInterval);
4166 };
4167
4168 /**
4169 * queueSortPhase : If the view is sorted we will throttle sorting to either :
4170 * (1) passive - when the user calls data(), or
4171 * (2) active - once they stop updating and yield js thread control
4172 */
4173 DynamicView.prototype.queueSortPhase = function () {
4174 // already queued? exit without queuing again
4175 if (this.sortDirty) {
4176 return;
4177 }
4178 this.sortDirty = true;
4179
4180 var self = this;
4181 if (this.options.sortPriority === "active") {
4182 // active sorting... once they are done and yield js thread, run async performSortPhase()
4183 setTimeout(function () {
4184 self.performSortPhase();
4185 }, this.options.minRebuildInterval);
4186 } else {
4187 // must be passive sorting... since not calling performSortPhase (until data call), lets use queueRebuildEvent to
4188 // potentially notify user that data has changed.
4189 this.queueRebuildEvent();
4190 }
4191 };
4192
4193 /**
4194 * performSortPhase() - invoked synchronously or asynchronously to perform final sort phase (if needed)
4195 *
4196 */
4197 DynamicView.prototype.performSortPhase = function (options) {
4198 // async call to this may have been pre-empted by synchronous call to data before async could fire
4199 if (!this.sortDirty && !this.resultsdirty) {
4200 return;
4201 }
4202
4203 options = options || {};
4204
4205 if (this.sortDirty) {
4206 if (this.sortFunction) {
4207 this.resultset.sort(this.sortFunction);
4208 } else if (this.sortCriteria) {
4209 this.resultset.compoundsort(this.sortCriteria);
4210 }
4211
4212 this.sortDirty = false;
4213 }
4214
4215 if (this.options.persistent) {
4216 // persistent view, rebuild local resultdata array
4217 this.resultdata = this.resultset.data();
4218 this.resultsdirty = false;
4219 }
4220
4221 if (!options.suppressRebuildEvent) {
4222 this.emit('rebuild', this);
4223 }
4224 };
4225
4226 /**
4227 * evaluateDocument() - internal method for (re)evaluating document inclusion.
4228 * Called by : collection.insert() and collection.update().
4229 *
4230 * @param {int} objIndex - index of document to (re)run through filter pipeline.
4231 * @param {bool} isNew - true if the document was just added to the collection.
4232 */
4233 DynamicView.prototype.evaluateDocument = function (objIndex, isNew) {
4234 // if no filter applied yet, the result 'set' should remain 'everything'
4235 if (!this.resultset.filterInitialized) {
4236 if (this.options.persistent) {
4237 this.resultdata = this.resultset.data();
4238 }
4239 // need to re-sort to sort new document
4240 if (this.sortFunction || this.sortCriteria) {
4241 this.queueSortPhase();
4242 } else {
4243 this.queueRebuildEvent();
4244 }
4245 return;
4246 }
4247
4248 var ofr = this.resultset.filteredrows;
4249 var oldPos = (isNew) ? (-1) : (ofr.indexOf(+objIndex));
4250 var oldlen = ofr.length;
4251
4252 // creating a 1-element resultset to run filter chain ops on to see if that doc passes filters;
4253 // mostly efficient algorithm, slight stack overhead price (this function is called on inserts and updates)
4254 var evalResultset = new Resultset(this.collection);
4255 evalResultset.filteredrows = [objIndex];
4256 evalResultset.filterInitialized = true;
4257 var filter;
4258 for (var idx = 0, len = this.filterPipeline.length; idx < len; idx++) {
4259 filter = this.filterPipeline[idx];
4260 evalResultset[filter.type](filter.val);
4261 }
4262
4263 // not a true position, but -1 if not pass our filter(s), 0 if passed filter(s)
4264 var newPos = (evalResultset.filteredrows.length === 0) ? -1 : 0;
4265
4266 // wasn't in old, shouldn't be now... do nothing
4267 if (oldPos === -1 && newPos === -1) return;
4268
4269 // wasn't in resultset, should be now... add
4270 if (oldPos === -1 && newPos !== -1) {
4271 ofr.push(objIndex);
4272
4273 if (this.options.persistent) {
4274 this.resultdata.push(this.collection.data[objIndex]);
4275 }
4276
4277 // need to re-sort to sort new document
4278 if (this.sortFunction || this.sortCriteria) {
4279 this.queueSortPhase();
4280 } else {
4281 this.queueRebuildEvent();
4282 }
4283
4284 return;
4285 }
4286
4287 // was in resultset, shouldn't be now... delete
4288 if (oldPos !== -1 && newPos === -1) {
4289 if (oldPos < oldlen - 1) {
4290 ofr.splice(oldPos, 1);
4291
4292 if (this.options.persistent) {
4293 this.resultdata.splice(oldPos, 1);
4294 }
4295 } else {
4296 ofr.length = oldlen - 1;
4297
4298 if (this.options.persistent) {
4299 this.resultdata.length = oldlen - 1;
4300 }
4301 }
4302
4303 // in case changes to data altered a sort column
4304 if (this.sortFunction || this.sortCriteria) {
4305 this.queueSortPhase();
4306 } else {
4307 this.queueRebuildEvent();
4308 }
4309
4310 return;
4311 }
4312
4313 // was in resultset, should still be now... (update persistent only?)
4314 if (oldPos !== -1 && newPos !== -1) {
4315 if (this.options.persistent) {
4316 // in case document changed, replace persistent view data with the latest collection.data document
4317 this.resultdata[oldPos] = this.collection.data[objIndex];
4318 }
4319
4320 // in case changes to data altered a sort column
4321 if (this.sortFunction || this.sortCriteria) {
4322 this.queueSortPhase();
4323 } else {
4324 this.queueRebuildEvent();
4325 }
4326
4327 return;
4328 }
4329 };
4330
4331 /**
4332 * removeDocument() - internal function called on collection.delete()
4333 */
4334 DynamicView.prototype.removeDocument = function (objIndex) {
4335 // if no filter applied yet, the result 'set' should remain 'everything'
4336 if (!this.resultset.filterInitialized) {
4337 if (this.options.persistent) {
4338 this.resultdata = this.resultset.data();
4339 }
4340 // in case changes to data altered a sort column
4341 if (this.sortFunction || this.sortCriteria) {
4342 this.queueSortPhase();
4343 } else {
4344 this.queueRebuildEvent();
4345 }
4346 return;
4347 }
4348
4349 var ofr = this.resultset.filteredrows;
4350 var oldPos = ofr.indexOf(+objIndex);
4351 var oldlen = ofr.length;
4352 var idx;
4353
4354 if (oldPos !== -1) {
4355 // if not last row in resultdata, swap last to hole and truncate last row
4356 if (oldPos < oldlen - 1) {
4357 ofr[oldPos] = ofr[oldlen - 1];
4358 ofr.length = oldlen - 1;
4359
4360 if (this.options.persistent) {
4361 this.resultdata[oldPos] = this.resultdata[oldlen - 1];
4362 this.resultdata.length = oldlen - 1;
4363 }
4364 }
4365 // last row, so just truncate last row
4366 else {
4367 ofr.length = oldlen - 1;
4368
4369 if (this.options.persistent) {
4370 this.resultdata.length = oldlen - 1;
4371 }
4372 }
4373
4374 // in case changes to data altered a sort column
4375 if (this.sortFunction || this.sortCriteria) {
4376 this.queueSortPhase();
4377 } else {
4378 this.queueRebuildEvent();
4379 }
4380 }
4381
4382 // since we are using filteredrows to store data array positions
4383 // if they remove a document (whether in our view or not),
4384 // we need to adjust array positions -1 for all document array references after that position
4385 oldlen = ofr.length;
4386 for (idx = 0; idx < oldlen; idx++) {
4387 if (ofr[idx] > objIndex) {
4388 ofr[idx]--;
4389 }
4390 }
4391 };
4392
4393 /**
4394 * mapReduce() - data transformation via user supplied functions
4395 *
4396 * @param {function} mapFunction - this function accepts a single document for you to transform and return
4397 * @param {function} reduceFunction - this function accepts many (array of map outputs) and returns single value
4398 * @returns The output of your reduceFunction
4399 * @memberof DynamicView
4400 */
4401 DynamicView.prototype.mapReduce = function (mapFunction, reduceFunction) {
4402 try {
4403 return reduceFunction(this.data().map(mapFunction));
4404 } catch (err) {
4405 throw err;
4406 }
4407 };
4408
4409
4410 /**
4411 * Collection class that handles documents of same type
4412 * @constructor Collection
4413 * @implements LokiEventEmitter
4414 * @param {string} name - collection name
4415 * @param {(array|object)=} options - (optional) array of property names to be indicized OR a configuration object
4416 * @param {array=} [options.unique=[]] - array of property names to define unique constraints for
4417 * @param {array=} [options.exact=[]] - array of property names to define exact constraints for
4418 * @param {array=} [options.indices=[]] - array property names to define binary indexes for
4419 * @param {boolean} [options.adaptiveBinaryIndices=true] - collection indices will be actively rebuilt rather than lazily
4420 * @param {boolean} [options.asyncListeners=false] - whether listeners are invoked asynchronously
4421 * @param {boolean} [options.disableMeta=false] - set to true to disable meta property on documents
4422 * @param {boolean} [options.disableChangesApi=true] - set to false to enable Changes API
4423 * @param {boolean} [options.disableDeltaChangesApi=true] - set to false to enable Delta Changes API (requires Changes API, forces cloning)
4424 * @param {boolean} [options.autoupdate=false] - use Object.observe to update objects automatically
4425 * @param {boolean} [options.clone=false] - specify whether inserts and queries clone to/from user
4426 * @param {boolean} [options.serializableIndices=true[]] - converts date values on binary indexed properties to epoch time
4427 * @param {string} [options.cloneMethod='parse-stringify'] - 'parse-stringify', 'jquery-extend-deep', 'shallow', 'shallow-assign'
4428 * @param {int=} options.ttl - age of document (in ms.) before document is considered aged/stale.
4429 * @param {int=} options.ttlInterval - time interval for clearing out 'aged' documents; not set by default.
4430 * @see {@link Loki#addCollection} for normal creation of collections
4431 */
4432 function Collection(name, options) {
4433 // the name of the collection
4434
4435 this.name = name;
4436 // the data held by the collection
4437 this.data = [];
4438 this.idIndex = []; // index of id
4439 this.binaryIndices = {}; // user defined indexes
4440 this.constraints = {
4441 unique: {},
4442 exact: {}
4443 };
4444
4445 // unique contraints contain duplicate object references, so they are not persisted.
4446 // we will keep track of properties which have unique contraint applied here, and regenerate on load
4447 this.uniqueNames = [];
4448
4449 // transforms will be used to store frequently used query chains as a series of steps
4450 // which itself can be stored along with the database.
4451 this.transforms = {};
4452
4453 // the object type of the collection
4454 this.objType = name;
4455
4456 // in autosave scenarios we will use collection level dirty flags to determine whether save is needed.
4457 // currently, if any collection is dirty we will autosave the whole database if autosave is configured.
4458 // defaulting to true since this is called from addCollection and adding a collection should trigger save
4459 this.dirty = true;
4460
4461 // private holders for cached data
4462 this.cachedIndex = null;
4463 this.cachedBinaryIndex = null;
4464 this.cachedData = null;
4465 var self = this;
4466
4467 /* OPTIONS */
4468 options = options || {};
4469
4470 // exact match and unique constraints
4471 if (options.hasOwnProperty('unique')) {
4472 if (!Array.isArray(options.unique)) {
4473 options.unique = [options.unique];
4474 }
4475 options.unique.forEach(function (prop) {
4476 self.uniqueNames.push(prop); // used to regenerate on subsequent database loads
4477 self.constraints.unique[prop] = new UniqueIndex(prop);
4478 });
4479 }
4480
4481 if (options.hasOwnProperty('exact')) {
4482 options.exact.forEach(function (prop) {
4483 self.constraints.exact[prop] = new ExactIndex(prop);
4484 });
4485 }
4486
4487 // if set to true we will optimally keep indices 'fresh' during insert/update/remove ops (never dirty/never needs rebuild)
4488 // if you frequently intersperse insert/update/remove ops between find ops this will likely be significantly faster option.
4489 this.adaptiveBinaryIndices = options.hasOwnProperty('adaptiveBinaryIndices') ? options.adaptiveBinaryIndices : true;
4490
4491 // is collection transactional
4492 this.transactional = options.hasOwnProperty('transactional') ? options.transactional : false;
4493
4494 // options to clone objects when inserting them
4495 this.cloneObjects = options.hasOwnProperty('clone') ? options.clone : false;
4496
4497 // default clone method (if enabled) is parse-stringify
4498 this.cloneMethod = options.hasOwnProperty('cloneMethod') ? options.cloneMethod : "parse-stringify";
4499
4500 // option to make event listeners async, default is sync
4501 this.asyncListeners = options.hasOwnProperty('asyncListeners') ? options.asyncListeners : false;
4502
4503 // if set to true we will not maintain a meta property for a document
4504 this.disableMeta = options.hasOwnProperty('disableMeta') ? options.disableMeta : false;
4505
4506 // disable track changes
4507 this.disableChangesApi = options.hasOwnProperty('disableChangesApi') ? options.disableChangesApi : true;
4508
4509 // disable delta update object style on changes
4510 this.disableDeltaChangesApi = options.hasOwnProperty('disableDeltaChangesApi') ? options.disableDeltaChangesApi : true;
4511 if (this.disableChangesApi) { this.disableDeltaChangesApi = true; }
4512
4513 // option to observe objects and update them automatically, ignored if Object.observe is not supported
4514 this.autoupdate = options.hasOwnProperty('autoupdate') ? options.autoupdate : false;
4515
4516 // by default, if you insert a document into a collection with binary indices, if those indexed properties contain
4517 // a DateTime we will convert to epoch time format so that (across serializations) its value position will be the
4518 // same 'after' serialization as it was 'before'.
4519 this.serializableIndices = options.hasOwnProperty('serializableIndices') ? options.serializableIndices : true;
4520
4521 //option to activate a cleaner daemon - clears "aged" documents at set intervals.
4522 this.ttl = {
4523 age: null,
4524 ttlInterval: null,
4525 daemon: null
4526 };
4527 this.setTTL(options.ttl || -1, options.ttlInterval);
4528
4529 // currentMaxId - change manually at your own peril!
4530 this.maxId = 0;
4531
4532 this.DynamicViews = [];
4533
4534 // events
4535 this.events = {
4536 'insert': [],
4537 'update': [],
4538 'pre-insert': [],
4539 'pre-update': [],
4540 'close': [],
4541 'flushbuffer': [],
4542 'error': [],
4543 'delete': [],
4544 'warning': []
4545 };
4546
4547 // changes are tracked by collection and aggregated by the db
4548 this.changes = [];
4549
4550 // initialize the id index
4551 this.ensureId();
4552 var indices = [];
4553 // initialize optional user-supplied indices array ['age', 'lname', 'zip']
4554 if (options && options.indices) {
4555 if (Object.prototype.toString.call(options.indices) === '[object Array]') {
4556 indices = options.indices;
4557 } else if (typeof options.indices === 'string') {
4558 indices = [options.indices];
4559 } else {
4560 throw new TypeError('Indices needs to be a string or an array of strings');
4561 }
4562 }
4563
4564 for (var idx = 0; idx < indices.length; idx++) {
4565 this.ensureIndex(indices[idx]);
4566 }
4567
4568 function observerCallback(changes) {
4569
4570 var changedObjects = typeof Set === 'function' ? new Set() : [];
4571
4572 if (!changedObjects.add)
4573 changedObjects.add = function (object) {
4574 if (this.indexOf(object) === -1)
4575 this.push(object);
4576 return this;
4577 };
4578
4579 changes.forEach(function (change) {
4580 changedObjects.add(change.object);
4581 });
4582
4583 changedObjects.forEach(function (object) {
4584 if (!hasOwnProperty.call(object, '$loki'))
4585 return self.removeAutoUpdateObserver(object);
4586 try {
4587 self.update(object);
4588 } catch (err) {}
4589 });
4590 }
4591
4592 this.observerCallback = observerCallback;
4593
4594 /*
4595 * This method creates a clone of the current status of an object and associates operation and collection name,
4596 * so the parent db can aggregate and generate a changes object for the entire db
4597 */
4598 function createChange(name, op, obj, old) {
4599 self.changes.push({
4600 name: name,
4601 operation: op,
4602 obj: op == 'U' && !self.disableDeltaChangesApi ? getChangeDelta(obj, old) : JSON.parse(JSON.stringify(obj))
4603 });
4604 }
4605
4606 //Compare changed object (which is a forced clone) with existing object and return the delta
4607 function getChangeDelta(obj, old) {
4608 if (old) {
4609 return getObjectDelta(old, obj);
4610 }
4611 else {
4612 return JSON.parse(JSON.stringify(obj));
4613 }
4614 }
4615
4616 this.getChangeDelta = getChangeDelta;
4617
4618 function getObjectDelta(oldObject, newObject) {
4619 var propertyNames = newObject !== null && typeof newObject === 'object' ? Object.keys(newObject) : null;
4620 if (propertyNames && propertyNames.length && ['string', 'boolean', 'number'].indexOf(typeof(newObject)) < 0) {
4621 var delta = {};
4622 for (var i = 0; i < propertyNames.length; i++) {
4623 var propertyName = propertyNames[i];
4624 if (newObject.hasOwnProperty(propertyName)) {
4625 if (!oldObject.hasOwnProperty(propertyName) || self.uniqueNames.indexOf(propertyName) >= 0 || propertyName == '$loki' || propertyName == 'meta') {
4626 delta[propertyName] = newObject[propertyName];
4627 }
4628 else {
4629 var propertyDelta = getObjectDelta(oldObject[propertyName], newObject[propertyName]);
4630 if (typeof propertyDelta !== "undefined" && propertyDelta != {}) {
4631 delta[propertyName] = propertyDelta;
4632 }
4633 }
4634 }
4635 }
4636 return Object.keys(delta).length === 0 ? undefined : delta;
4637 }
4638 else {
4639 return oldObject === newObject ? undefined : newObject;
4640 }
4641 }
4642
4643 this.getObjectDelta = getObjectDelta;
4644
4645 // clear all the changes
4646 function flushChanges() {
4647 self.changes = [];
4648 }
4649
4650 this.getChanges = function () {
4651 return self.changes;
4652 };
4653
4654 this.flushChanges = flushChanges;
4655
4656 /**
4657 * If the changes API is disabled make sure only metadata is added without re-evaluating everytime if the changesApi is enabled
4658 */
4659 function insertMeta(obj) {
4660 var len, idx;
4661
4662 if (self.disableMeta || !obj) {
4663 return;
4664 }
4665
4666 // if batch insert
4667 if (Array.isArray(obj)) {
4668 len = obj.length;
4669
4670 for(idx=0; idx<len; idx++) {
4671 if (!obj[idx].hasOwnProperty('meta')) {
4672 obj[idx].meta = {};
4673 }
4674
4675 obj[idx].meta.created = (new Date()).getTime();
4676 obj[idx].meta.revision = 0;
4677 }
4678
4679 return;
4680 }
4681
4682 // single object
4683 if (!obj.meta) {
4684 obj.meta = {};
4685 }
4686
4687 obj.meta.created = (new Date()).getTime();
4688 obj.meta.revision = 0;
4689 }
4690
4691 function updateMeta(obj) {
4692 if (self.disableMeta || !obj) {
4693 return;
4694 }
4695 obj.meta.updated = (new Date()).getTime();
4696 obj.meta.revision += 1;
4697 }
4698
4699 function createInsertChange(obj) {
4700 createChange(self.name, 'I', obj);
4701 }
4702
4703 function createUpdateChange(obj, old) {
4704 createChange(self.name, 'U', obj, old);
4705 }
4706
4707 function insertMetaWithChange(obj) {
4708 insertMeta(obj);
4709 createInsertChange(obj);
4710 }
4711
4712 function updateMetaWithChange(obj, old) {
4713 updateMeta(obj);
4714 createUpdateChange(obj, old);
4715 }
4716
4717
4718 /* assign correct handler based on ChangesAPI flag */
4719 var insertHandler, updateHandler;
4720
4721 function setHandlers() {
4722 insertHandler = self.disableChangesApi ? insertMeta : insertMetaWithChange;
4723 updateHandler = self.disableChangesApi ? updateMeta : updateMetaWithChange;
4724 }
4725
4726 setHandlers();
4727
4728 this.setChangesApi = function (enabled) {
4729 self.disableChangesApi = !enabled;
4730 if (!enabled) { self.disableDeltaChangesApi = false; }
4731 setHandlers();
4732 };
4733 /**
4734 * built-in events
4735 */
4736 this.on('insert', function insertCallback(obj) {
4737 insertHandler(obj);
4738 });
4739
4740 this.on('update', function updateCallback(obj, old) {
4741 updateHandler(obj, old);
4742 });
4743
4744 this.on('delete', function deleteCallback(obj) {
4745 if (!self.disableChangesApi) {
4746 createChange(self.name, 'R', obj);
4747 }
4748 });
4749
4750 this.on('warning', function (warning) {
4751 self.console.warn(warning);
4752 });
4753 // for de-serialization purposes
4754 flushChanges();
4755 }
4756
4757 Collection.prototype = new LokiEventEmitter();
4758
4759 Collection.prototype.console = {
4760 log: function () {},
4761 warn: function () {},
4762 error: function () {},
4763 };
4764
4765 Collection.prototype.addAutoUpdateObserver = function (object) {
4766 if (!this.autoupdate || typeof Object.observe !== 'function')
4767 return;
4768
4769 Object.observe(object, this.observerCallback, ['add', 'update', 'delete', 'reconfigure', 'setPrototype']);
4770 };
4771
4772 Collection.prototype.removeAutoUpdateObserver = function (object) {
4773 if (!this.autoupdate || typeof Object.observe !== 'function')
4774 return;
4775
4776 Object.unobserve(object, this.observerCallback);
4777 };
4778
4779 /**
4780 * Adds a named collection transform to the collection
4781 * @param {string} name - name to associate with transform
4782 * @param {array} transform - an array of transformation 'step' objects to save into the collection
4783 * @memberof Collection
4784 * @example
4785 * users.addTransform('progeny', [
4786 * {
4787 * type: 'find',
4788 * value: {
4789 * 'age': {'$lte': 40}
4790 * }
4791 * }
4792 * ]);
4793 *
4794 * var results = users.chain('progeny').data();
4795 */
4796 Collection.prototype.addTransform = function (name, transform) {
4797 if (this.transforms.hasOwnProperty(name)) {
4798 throw new Error("a transform by that name already exists");
4799 }
4800
4801 this.transforms[name] = transform;
4802 };
4803
4804 /**
4805 * Retrieves a named transform from the collection.
4806 * @param {string} name - name of the transform to lookup.
4807 * @memberof Collection
4808 */
4809 Collection.prototype.getTransform = function (name) {
4810 return this.transforms[name];
4811 };
4812
4813 /**
4814 * Updates a named collection transform to the collection
4815 * @param {string} name - name to associate with transform
4816 * @param {object} transform - a transformation object to save into collection
4817 * @memberof Collection
4818 */
4819 Collection.prototype.setTransform = function (name, transform) {
4820 this.transforms[name] = transform;
4821 };
4822
4823 /**
4824 * Removes a named collection transform from the collection
4825 * @param {string} name - name of collection transform to remove
4826 * @memberof Collection
4827 */
4828 Collection.prototype.removeTransform = function (name) {
4829 delete this.transforms[name];
4830 };
4831
4832 Collection.prototype.byExample = function (template) {
4833 var k, obj, query;
4834 query = [];
4835 for (k in template) {
4836 if (!template.hasOwnProperty(k)) continue;
4837 query.push((
4838 obj = {},
4839 obj[k] = template[k],
4840 obj
4841 ));
4842 }
4843 return {
4844 '$and': query
4845 };
4846 };
4847
4848 Collection.prototype.findObject = function (template) {
4849 return this.findOne(this.byExample(template));
4850 };
4851
4852 Collection.prototype.findObjects = function (template) {
4853 return this.find(this.byExample(template));
4854 };
4855
4856 /*----------------------------+
4857 | TTL daemon |
4858 +----------------------------*/
4859 Collection.prototype.ttlDaemonFuncGen = function () {
4860 var collection = this;
4861 var age = this.ttl.age;
4862 return function ttlDaemon() {
4863 var now = Date.now();
4864 var toRemove = collection.chain().where(function daemonFilter(member) {
4865 var timestamp = member.meta.updated || member.meta.created;
4866 var diff = now - timestamp;
4867 return age < diff;
4868 });
4869 toRemove.remove();
4870 };
4871 };
4872
4873 /**
4874 * Updates or applies collection TTL settings.
4875 * @param {int} age - age (in ms) to expire document from collection
4876 * @param {int} interval - time (in ms) to clear collection of aged documents.
4877 * @memberof Collection
4878 */
4879 Collection.prototype.setTTL = function (age, interval) {
4880 if (age < 0) {
4881 clearInterval(this.ttl.daemon);
4882 } else {
4883 this.ttl.age = age;
4884 this.ttl.ttlInterval = interval;
4885 this.ttl.daemon = setInterval(this.ttlDaemonFuncGen(), interval);
4886 }
4887 };
4888
4889 /*----------------------------+
4890 | INDEXING |
4891 +----------------------------*/
4892
4893 /**
4894 * create a row filter that covers all documents in the collection
4895 */
4896 Collection.prototype.prepareFullDocIndex = function () {
4897 var len = this.data.length;
4898 var indexes = new Array(len);
4899 for (var i = 0; i < len; i += 1) {
4900 indexes[i] = i;
4901 }
4902 return indexes;
4903 };
4904
4905 /**
4906 * Will allow reconfiguring certain collection options.
4907 * @param {boolean} options.adaptiveBinaryIndices - collection indices will be actively rebuilt rather than lazily
4908 * @memberof Collection
4909 */
4910 Collection.prototype.configureOptions = function (options) {
4911 options = options || {};
4912
4913 if (options.hasOwnProperty('adaptiveBinaryIndices')) {
4914 this.adaptiveBinaryIndices = options.adaptiveBinaryIndices;
4915
4916 // if switching to adaptive binary indices, make sure none are 'dirty'
4917 if (this.adaptiveBinaryIndices) {
4918 this.ensureAllIndexes();
4919 }
4920 }
4921 };
4922
4923 /**
4924 * Ensure binary index on a certain field
4925 * @param {string} property - name of property to create binary index on
4926 * @param {boolean=} force - (Optional) flag indicating whether to construct index immediately
4927 * @memberof Collection
4928 */
4929 Collection.prototype.ensureIndex = function (property, force) {
4930 // optional parameter to force rebuild whether flagged as dirty or not
4931 if (typeof (force) === 'undefined') {
4932 force = false;
4933 }
4934
4935 if (property === null || property === undefined) {
4936 throw new Error('Attempting to set index without an associated property');
4937 }
4938
4939 if (this.binaryIndices[property] && !force) {
4940 if (!this.binaryIndices[property].dirty) return;
4941 }
4942
4943 // if the index is already defined and we are using adaptiveBinaryIndices and we are not forcing a rebuild, return.
4944 if (this.adaptiveBinaryIndices === true && this.binaryIndices.hasOwnProperty(property) && !force) {
4945 return;
4946 }
4947
4948 var index = {
4949 'name': property,
4950 'dirty': true,
4951 'values': this.prepareFullDocIndex()
4952 };
4953 this.binaryIndices[property] = index;
4954
4955 var wrappedComparer =
4956 (function (prop, data) {
4957 var val1, val2, arr;
4958 return function (a, b) {
4959 if (~prop.indexOf('.')) {
4960 arr = prop.split('.');
4961 val1 = arr.reduce(function(obj, i) { return obj && obj[i] || undefined; }, data[a]);
4962 val2 = arr.reduce(function(obj, i) { return obj && obj[i] || undefined; }, data[b]);
4963 } else {
4964 val1 = data[a][prop];
4965 val2 = data[b][prop];
4966 }
4967
4968 if (val1 !== val2) {
4969 if (ltHelper(val1, val2, false)) return -1;
4970 if (gtHelper(val1, val2, false)) return 1;
4971 }
4972 return 0;
4973 };
4974 })(property, this.data);
4975
4976 index.values.sort(wrappedComparer);
4977 index.dirty = false;
4978
4979 this.dirty = true; // for autosave scenarios
4980 };
4981
4982 Collection.prototype.getSequencedIndexValues = function (property) {
4983 var idx, idxvals = this.binaryIndices[property].values;
4984 var result = "";
4985
4986 for (idx = 0; idx < idxvals.length; idx++) {
4987 result += " [" + idx + "] " + this.data[idxvals[idx]][property];
4988 }
4989
4990 return result;
4991 };
4992
4993 Collection.prototype.ensureUniqueIndex = function (field) {
4994 var index = this.constraints.unique[field];
4995 if (!index) {
4996 // keep track of new unique index for regenerate after database (re)load.
4997 if (this.uniqueNames.indexOf(field) == -1) {
4998 this.uniqueNames.push(field);
4999 }
5000 }
5001
5002 // if index already existed, (re)loading it will likely cause collisions, rebuild always
5003 this.constraints.unique[field] = index = new UniqueIndex(field);
5004 this.data.forEach(function (obj) {
5005 index.set(obj);
5006 });
5007 return index;
5008 };
5009
5010 /**
5011 * Ensure all binary indices
5012 */
5013 Collection.prototype.ensureAllIndexes = function (force) {
5014 var key, bIndices = this.binaryIndices;
5015 for (key in bIndices) {
5016 if (hasOwnProperty.call(bIndices, key)) {
5017 this.ensureIndex(key, force);
5018 }
5019 }
5020 };
5021
5022 Collection.prototype.flagBinaryIndexesDirty = function () {
5023 var key, bIndices = this.binaryIndices;
5024 for (key in bIndices) {
5025 if (hasOwnProperty.call(bIndices, key)) {
5026 bIndices[key].dirty = true;
5027 }
5028 }
5029 };
5030
5031 Collection.prototype.flagBinaryIndexDirty = function (index) {
5032 if (this.binaryIndices[index])
5033 this.binaryIndices[index].dirty = true;
5034 };
5035
5036 /**
5037 * Quickly determine number of documents in collection (or query)
5038 * @param {object=} query - (optional) query object to count results of
5039 * @returns {number} number of documents in the collection
5040 * @memberof Collection
5041 */
5042 Collection.prototype.count = function (query) {
5043 if (!query) {
5044 return this.data.length;
5045 }
5046
5047 return this.chain().find(query).filteredrows.length;
5048 };
5049
5050 /**
5051 * Rebuild idIndex
5052 */
5053 Collection.prototype.ensureId = function () {
5054 var len = this.data.length,
5055 i = 0;
5056
5057 this.idIndex = [];
5058 for (i; i < len; i += 1) {
5059 this.idIndex.push(this.data[i].$loki);
5060 }
5061 };
5062
5063 /**
5064 * Rebuild idIndex async with callback - useful for background syncing with a remote server
5065 */
5066 Collection.prototype.ensureIdAsync = function (callback) {
5067 this.async(function () {
5068 this.ensureId();
5069 }, callback);
5070 };
5071
5072 /**
5073 * Add a dynamic view to the collection
5074 * @param {string} name - name of dynamic view to add
5075 * @param {object=} options - options to configure dynamic view with
5076 * @param {boolean} [options.persistent=false] - indicates if view is to main internal results array in 'resultdata'
5077 * @param {string} [options.sortPriority='passive'] - 'passive' (sorts performed on call to data) or 'active' (after updates)
5078 * @param {number} options.minRebuildInterval - minimum rebuild interval (need clarification to docs here)
5079 * @returns {DynamicView} reference to the dynamic view added
5080 * @memberof Collection
5081 * @example
5082 * var pview = users.addDynamicView('progeny');
5083 * pview.applyFind({'age': {'$lte': 40}});
5084 * pview.applySimpleSort('name');
5085 *
5086 * var results = pview.data();
5087 **/
5088
5089 Collection.prototype.addDynamicView = function (name, options) {
5090 var dv = new DynamicView(this, name, options);
5091 this.DynamicViews.push(dv);
5092
5093 return dv;
5094 };
5095
5096 /**
5097 * Remove a dynamic view from the collection
5098 * @param {string} name - name of dynamic view to remove
5099 * @memberof Collection
5100 **/
5101 Collection.prototype.removeDynamicView = function (name) {
5102 for (var idx = 0; idx < this.DynamicViews.length; idx++) {
5103 if (this.DynamicViews[idx].name === name) {
5104 this.DynamicViews.splice(idx, 1);
5105 }
5106 }
5107 };
5108
5109 /**
5110 * Look up dynamic view reference from within the collection
5111 * @param {string} name - name of dynamic view to retrieve reference of
5112 * @returns {DynamicView} A reference to the dynamic view with that name
5113 * @memberof Collection
5114 **/
5115 Collection.prototype.getDynamicView = function (name) {
5116 for (var idx = 0; idx < this.DynamicViews.length; idx++) {
5117 if (this.DynamicViews[idx].name === name) {
5118 return this.DynamicViews[idx];
5119 }
5120 }
5121
5122 return null;
5123 };
5124
5125 /**
5126 * Applies a 'mongo-like' find query object and passes all results to an update function.
5127 * For filter function querying you should migrate to [updateWhere()]{@link Collection#updateWhere}.
5128 *
5129 * @param {object|function} filterObject - 'mongo-like' query object (or deprecated filterFunction mode)
5130 * @param {function} updateFunction - update function to run against filtered documents
5131 * @memberof Collection
5132 */
5133 Collection.prototype.findAndUpdate = function (filterObject, updateFunction) {
5134 if (typeof (filterObject) === "function") {
5135 this.updateWhere(filterObject, updateFunction);
5136 }
5137 else {
5138 this.chain().find(filterObject).update(updateFunction);
5139 }
5140 };
5141
5142 /**
5143 * Applies a 'mongo-like' find query object removes all documents which match that filter.
5144 *
5145 * @param {object} filterObject - 'mongo-like' query object
5146 * @memberof Collection
5147 */
5148 Collection.prototype.findAndRemove = function(filterObject) {
5149 this.chain().find(filterObject).remove();
5150 };
5151
5152 /**
5153 * Adds object(s) to collection, ensure object(s) have meta properties, clone it if necessary, etc.
5154 * @param {(object|array)} doc - the document (or array of documents) to be inserted
5155 * @returns {(object|array)} document or documents inserted
5156 * @memberof Collection
5157 * @example
5158 * users.insert({
5159 * name: 'Odin',
5160 * age: 50,
5161 * address: 'Asgard'
5162 * });
5163 *
5164 * // alternatively, insert array of documents
5165 * users.insert([{ name: 'Thor', age: 35}, { name: 'Loki', age: 30}]);
5166 */
5167 Collection.prototype.insert = function (doc) {
5168 if (!Array.isArray(doc)) {
5169 return this.insertOne(doc);
5170 }
5171
5172 // holder to the clone of the object inserted if collections is set to clone objects
5173 var obj;
5174 var results = [];
5175
5176 this.emit('pre-insert', doc);
5177 for (var i = 0, len = doc.length; i < len; i++) {
5178 obj = this.insertOne(doc[i], true);
5179 if (!obj) {
5180 return undefined;
5181 }
5182 results.push(obj);
5183 }
5184 // at the 'batch' level, if clone option is true then emitted docs are clones
5185 this.emit('insert', results);
5186
5187 // if clone option is set, clone return values
5188 results = this.cloneObjects ? clone(results, this.cloneMethod) : results;
5189
5190 return results.length === 1 ? results[0] : results;
5191 };
5192
5193 /**
5194 * Adds a single object, ensures it has meta properties, clone it if necessary, etc.
5195 * @param {object} doc - the document to be inserted
5196 * @param {boolean} bulkInsert - quiet pre-insert and insert event emits
5197 * @returns {object} document or 'undefined' if there was a problem inserting it
5198 */
5199 Collection.prototype.insertOne = function (doc, bulkInsert) {
5200 var err = null;
5201 var returnObj;
5202
5203 if (typeof doc !== 'object') {
5204 err = new TypeError('Document needs to be an object');
5205 } else if (doc === null) {
5206 err = new TypeError('Object cannot be null');
5207 }
5208
5209 if (err !== null) {
5210 this.emit('error', err);
5211 throw err;
5212 }
5213
5214 // if configured to clone, do so now... otherwise just use same obj reference
5215 var obj = this.cloneObjects ? clone(doc, this.cloneMethod) : doc;
5216
5217 if (!this.disableMeta && typeof obj.meta === 'undefined') {
5218 obj.meta = {
5219 revision: 0,
5220 created: 0
5221 };
5222 }
5223
5224 // both 'pre-insert' and 'insert' events are passed internal data reference even when cloning
5225 // insert needs internal reference because that is where loki itself listens to add meta
5226 if (!bulkInsert) {
5227 this.emit('pre-insert', obj);
5228 }
5229 if (!this.add(obj)) {
5230 return undefined;
5231 }
5232
5233 returnObj = obj;
5234 if (!bulkInsert) {
5235 this.emit('insert', obj);
5236 returnObj = this.cloneObjects ? clone(obj, this.cloneMethod) : obj;
5237 }
5238
5239 this.addAutoUpdateObserver(returnObj);
5240 return returnObj;
5241 };
5242
5243 /**
5244 * Empties the collection.
5245 * @param {object=} options - configure clear behavior
5246 * @param {bool=} options.removeIndices - (default: false)
5247 * @memberof Collection
5248 */
5249 Collection.prototype.clear = function (options) {
5250 var self = this;
5251
5252 options = options || {};
5253
5254 this.data = [];
5255 this.idIndex = [];
5256 this.cachedIndex = null;
5257 this.cachedBinaryIndex = null;
5258 this.cachedData = null;
5259 this.maxId = 0;
5260 this.DynamicViews = [];
5261 this.dirty = true;
5262
5263 // if removing indices entirely
5264 if (options.removeIndices === true) {
5265 this.binaryIndices = {};
5266
5267 this.constraints = {
5268 unique: {},
5269 exact: {}
5270 };
5271 this.uniqueNames = [];
5272 }
5273 // clear indices but leave definitions in place
5274 else {
5275 // clear binary indices
5276 var keys = Object.keys(this.binaryIndices);
5277 keys.forEach(function(biname) {
5278 self.binaryIndices[biname].dirty = false;
5279 self.binaryIndices[biname].values = [];
5280 });
5281
5282 // clear entire unique indices definition
5283 this.constraints = {
5284 unique: {},
5285 exact: {}
5286 };
5287
5288 // add definitions back
5289 this.uniqueNames.forEach(function(uiname) {
5290 self.ensureUniqueIndex(uiname);
5291 });
5292 }
5293 };
5294
5295 /**
5296 * Updates an object and notifies collection that the document has changed.
5297 * @param {object} doc - document to update within the collection
5298 * @memberof Collection
5299 */
5300 Collection.prototype.update = function (doc) {
5301 if (Array.isArray(doc)) {
5302 var k = 0,
5303 len = doc.length;
5304
5305 // if not cloning, disable adaptive binary indices for the duration of the batch update,
5306 // followed by lazy rebuild and re-enabling adaptive indices after batch update.
5307 var adaptiveBatchOverride = !this.cloneObjects &&
5308 this.adaptiveBinaryIndices && Object.keys(this.binaryIndices).length > 0;
5309
5310 if (adaptiveBatchOverride) {
5311 this.adaptiveBinaryIndices = false;
5312 }
5313
5314 for (k; k < len; k += 1) {
5315 this.update(doc[k]);
5316 }
5317
5318 if (adaptiveBatchOverride) {
5319 this.ensureAllIndexes();
5320 this.adaptiveBinaryIndices = true;
5321 }
5322
5323 return;
5324 }
5325
5326 // verify object is a properly formed document
5327 if (!hasOwnProperty.call(doc, '$loki')) {
5328 throw new Error('Trying to update unsynced document. Please save the document first by using insert() or addMany()');
5329 }
5330 try {
5331 this.startTransaction();
5332 var arr = this.get(doc.$loki, true),
5333 oldInternal, // ref to existing obj
5334 newInternal, // ref to new internal obj
5335 position,
5336 self = this;
5337
5338 if (!arr) {
5339 throw new Error('Trying to update a document not in collection.');
5340 }
5341
5342 oldInternal = arr[0]; // -internal- obj ref
5343 position = arr[1]; // position in data array
5344
5345 // if configured to clone, do so now... otherwise just use same obj reference
5346 newInternal = this.cloneObjects || !this.disableDeltaChangesApi ? clone(doc, this.cloneMethod) : doc;
5347
5348 this.emit('pre-update', doc);
5349
5350 Object.keys(this.constraints.unique).forEach(function (key) {
5351 self.constraints.unique[key].update(oldInternal, newInternal);
5352 });
5353
5354 // operate the update
5355 this.data[position] = newInternal;
5356
5357 if (newInternal !== doc) {
5358 this.addAutoUpdateObserver(doc);
5359 }
5360
5361 // now that we can efficiently determine the data[] position of newly added document,
5362 // submit it for all registered DynamicViews to evaluate for inclusion/exclusion
5363 for (var idx = 0; idx < this.DynamicViews.length; idx++) {
5364 this.DynamicViews[idx].evaluateDocument(position, false);
5365 }
5366
5367 var key;
5368 if (this.adaptiveBinaryIndices) {
5369 // for each binary index defined in collection, immediately update rather than flag for lazy rebuild
5370 var bIndices = this.binaryIndices;
5371 for (key in bIndices) {
5372 this.adaptiveBinaryIndexUpdate(position, key);
5373 }
5374 }
5375 else {
5376 this.flagBinaryIndexesDirty();
5377 }
5378
5379 this.idIndex[position] = newInternal.$loki;
5380 //this.flagBinaryIndexesDirty();
5381
5382 this.commit();
5383 this.dirty = true; // for autosave scenarios
5384
5385 this.emit('update', doc, this.cloneObjects || !this.disableDeltaChangesApi ? clone(oldInternal, this.cloneMethod) : null);
5386 return doc;
5387 } catch (err) {
5388 this.rollback();
5389 this.console.error(err.message);
5390 this.emit('error', err);
5391 throw (err); // re-throw error so user does not think it succeeded
5392 }
5393 };
5394
5395 /**
5396 * Add object to collection
5397 */
5398 Collection.prototype.add = function (obj) {
5399 // if parameter isn't object exit with throw
5400 if ('object' !== typeof obj) {
5401 throw new TypeError('Object being added needs to be an object');
5402 }
5403 // if object you are adding already has id column it is either already in the collection
5404 // or the object is carrying its own 'id' property. If it also has a meta property,
5405 // then this is already in collection so throw error, otherwise rename to originalId and continue adding.
5406 if (typeof (obj.$loki) !== 'undefined') {
5407 throw new Error('Document is already in collection, please use update()');
5408 }
5409
5410 /*
5411 * try adding object to collection
5412 */
5413 try {
5414 this.startTransaction();
5415 this.maxId++;
5416
5417 if (isNaN(this.maxId)) {
5418 this.maxId = (this.data[this.data.length - 1].$loki + 1);
5419 }
5420
5421 obj.$loki = this.maxId;
5422
5423 if (!this.disableMeta) {
5424 obj.meta.version = 0;
5425 }
5426
5427 var key, constrUnique = this.constraints.unique;
5428 for (key in constrUnique) {
5429 if (hasOwnProperty.call(constrUnique, key)) {
5430 constrUnique[key].set(obj);
5431 }
5432 }
5433
5434 // add new obj id to idIndex
5435 this.idIndex.push(obj.$loki);
5436
5437 // add the object
5438 this.data.push(obj);
5439
5440 var addedPos = this.data.length - 1;
5441
5442 // now that we can efficiently determine the data[] position of newly added document,
5443 // submit it for all registered DynamicViews to evaluate for inclusion/exclusion
5444 var dvlen = this.DynamicViews.length;
5445 for (var i = 0; i < dvlen; i++) {
5446 this.DynamicViews[i].evaluateDocument(addedPos, true);
5447 }
5448
5449 if (this.adaptiveBinaryIndices) {
5450 // for each binary index defined in collection, immediately update rather than flag for lazy rebuild
5451 var bIndices = this.binaryIndices;
5452 for (key in bIndices) {
5453 this.adaptiveBinaryIndexInsert(addedPos, key);
5454 }
5455 }
5456 else {
5457 this.flagBinaryIndexesDirty();
5458 }
5459
5460 this.commit();
5461 this.dirty = true; // for autosave scenarios
5462
5463 return (this.cloneObjects) ? (clone(obj, this.cloneMethod)) : (obj);
5464 } catch (err) {
5465 this.rollback();
5466 this.console.error(err.message);
5467 this.emit('error', err);
5468 throw (err); // re-throw error so user does not think it succeeded
5469 }
5470 };
5471
5472 /**
5473 * Applies a filter function and passes all results to an update function.
5474 *
5475 * @param {function} filterFunction - filter function whose results will execute update
5476 * @param {function} updateFunction - update function to run against filtered documents
5477 * @memberof Collection
5478 */
5479 Collection.prototype.updateWhere = function(filterFunction, updateFunction) {
5480 var results = this.where(filterFunction),
5481 i = 0,
5482 obj;
5483 try {
5484 for (i; i < results.length; i++) {
5485 obj = updateFunction(results[i]);
5486 this.update(obj);
5487 }
5488
5489 } catch (err) {
5490 this.rollback();
5491 this.console.error(err.message);
5492 }
5493 };
5494
5495 /**
5496 * Remove all documents matching supplied filter function.
5497 * For 'mongo-like' querying you should migrate to [findAndRemove()]{@link Collection#findAndRemove}.
5498 * @param {function|object} query - query object to filter on
5499 * @memberof Collection
5500 */
5501 Collection.prototype.removeWhere = function (query) {
5502 var list;
5503 if (typeof query === 'function') {
5504 list = this.data.filter(query);
5505 this.remove(list);
5506 } else {
5507 this.chain().find(query).remove();
5508 }
5509 };
5510
5511 Collection.prototype.removeDataOnly = function () {
5512 this.remove(this.data.slice());
5513 };
5514
5515 /**
5516 * Remove a document from the collection
5517 * @param {object} doc - document to remove from collection
5518 * @memberof Collection
5519 */
5520 Collection.prototype.remove = function (doc) {
5521 if (typeof doc === 'number') {
5522 doc = this.get(doc);
5523 }
5524
5525 if ('object' !== typeof doc) {
5526 throw new Error('Parameter is not an object');
5527 }
5528 if (Array.isArray(doc)) {
5529 var k = 0,
5530 len = doc.length;
5531 for (k; k < len; k += 1) {
5532 this.remove(doc[k]);
5533 }
5534 return;
5535 }
5536
5537 if (!hasOwnProperty.call(doc, '$loki')) {
5538 throw new Error('Object is not a document stored in the collection');
5539 }
5540
5541 try {
5542 this.startTransaction();
5543 var arr = this.get(doc.$loki, true),
5544 // obj = arr[0],
5545 position = arr[1];
5546 var self = this;
5547 Object.keys(this.constraints.unique).forEach(function (key) {
5548 if (doc[key] !== null && typeof doc[key] !== 'undefined') {
5549 self.constraints.unique[key].remove(doc[key]);
5550 }
5551 });
5552 // now that we can efficiently determine the data[] position of newly added document,
5553 // submit it for all registered DynamicViews to remove
5554 for (var idx = 0; idx < this.DynamicViews.length; idx++) {
5555 this.DynamicViews[idx].removeDocument(position);
5556 }
5557
5558 if (this.adaptiveBinaryIndices) {
5559 // for each binary index defined in collection, immediately update rather than flag for lazy rebuild
5560 var key, bIndices = this.binaryIndices;
5561 for (key in bIndices) {
5562 this.adaptiveBinaryIndexRemove(position, key);
5563 }
5564 }
5565 else {
5566 this.flagBinaryIndexesDirty();
5567 }
5568
5569 this.data.splice(position, 1);
5570 this.removeAutoUpdateObserver(doc);
5571
5572 // remove id from idIndex
5573 this.idIndex.splice(position, 1);
5574
5575 this.commit();
5576 this.dirty = true; // for autosave scenarios
5577 this.emit('delete', arr[0]);
5578 delete doc.$loki;
5579 delete doc.meta;
5580 return doc;
5581
5582 } catch (err) {
5583 this.rollback();
5584 this.console.error(err.message);
5585 this.emit('error', err);
5586 return null;
5587 }
5588 };
5589
5590 /*---------------------+
5591 | Finding methods |
5592 +----------------------*/
5593
5594 /**
5595 * Get by Id - faster than other methods because of the searching algorithm
5596 * @param {int} id - $loki id of document you want to retrieve
5597 * @param {boolean} returnPosition - if 'true' we will return [object, position]
5598 * @returns {(object|array|null)} Object reference if document was found, null if not,
5599 * or an array if 'returnPosition' was passed.
5600 * @memberof Collection
5601 */
5602 Collection.prototype.get = function (id, returnPosition) {
5603 var retpos = returnPosition || false,
5604 data = this.idIndex,
5605 max = data.length - 1,
5606 min = 0,
5607 mid = (min + max) >> 1;
5608
5609 id = typeof id === 'number' ? id : parseInt(id, 10);
5610
5611 if (isNaN(id)) {
5612 throw new TypeError('Passed id is not an integer');
5613 }
5614
5615 while (data[min] < data[max]) {
5616 mid = (min + max) >> 1;
5617
5618 if (data[mid] < id) {
5619 min = mid + 1;
5620 } else {
5621 max = mid;
5622 }
5623 }
5624
5625 if (max === min && data[min] === id) {
5626 if (retpos) {
5627 return [this.data[min], min];
5628 }
5629 return this.data[min];
5630 }
5631 return null;
5632
5633 };
5634
5635 /**
5636 * Perform binary range lookup for the data[dataPosition][binaryIndexName] property value
5637 * Since multiple documents may contain the same value (which the index is sorted on),
5638 * we hone in on range and then linear scan range to find exact index array position.
5639 * @param {int} dataPosition : coll.data array index/position
5640 * @param {string} binaryIndexName : index to search for dataPosition in
5641 */
5642 Collection.prototype.getBinaryIndexPosition = function(dataPosition, binaryIndexName) {
5643 var val = this.data[dataPosition][binaryIndexName];
5644 var index = this.binaryIndices[binaryIndexName].values;
5645
5646 // i think calculateRange can probably be moved to collection
5647 // as it doesn't seem to need resultset. need to verify
5648 //var rs = new Resultset(this, null, null);
5649 var range = this.calculateRange("$eq", binaryIndexName, val);
5650
5651 if (range[0] === 0 && range[1] === -1) {
5652 // uhoh didn't find range
5653 return null;
5654 }
5655
5656 var min = range[0];
5657 var max = range[1];
5658
5659 // narrow down the sub-segment of index values
5660 // where the indexed property value exactly matches our
5661 // value and then linear scan to find exact -index- position
5662 for(var idx = min; idx <= max; idx++) {
5663 if (index[idx] === dataPosition) return idx;
5664 }
5665
5666 // uhoh
5667 return null;
5668 };
5669
5670 /**
5671 * Adaptively insert a selected item to the index.
5672 * @param {int} dataPosition : coll.data array index/position
5673 * @param {string} binaryIndexName : index to search for dataPosition in
5674 */
5675 Collection.prototype.adaptiveBinaryIndexInsert = function(dataPosition, binaryIndexName) {
5676 var index = this.binaryIndices[binaryIndexName].values;
5677 var val = this.data[dataPosition][binaryIndexName];
5678
5679 // If you are inserting a javascript Date value into a binary index, convert to epoch time
5680 if (this.serializableIndices === true && val instanceof Date) {
5681 this.data[dataPosition][binaryIndexName] = val.getTime();
5682 val = this.data[dataPosition][binaryIndexName];
5683 }
5684
5685 var idxPos = (index.length === 0)?0:this.calculateRangeStart(binaryIndexName, val, true);
5686
5687 // insert new data index into our binary index at the proper sorted location for relevant property calculated by idxPos.
5688 // doing this after adjusting dataPositions so no clash with previous item at that position.
5689 this.binaryIndices[binaryIndexName].values.splice(idxPos, 0, dataPosition);
5690 };
5691
5692 /**
5693 * Adaptively update a selected item within an index.
5694 * @param {int} dataPosition : coll.data array index/position
5695 * @param {string} binaryIndexName : index to search for dataPosition in
5696 */
5697 Collection.prototype.adaptiveBinaryIndexUpdate = function(dataPosition, binaryIndexName) {
5698 // linear scan needed to find old position within index unless we optimize for clone scenarios later
5699 // within (my) node 5.6.0, the following for() loop with strict compare is -much- faster than indexOf()
5700 var idxPos,
5701 index = this.binaryIndices[binaryIndexName].values,
5702 len=index.length;
5703
5704 for(idxPos=0; idxPos < len; idxPos++) {
5705 if (index[idxPos] === dataPosition) break;
5706 }
5707
5708 //var idxPos = this.binaryIndices[binaryIndexName].values.indexOf(dataPosition);
5709 this.binaryIndices[binaryIndexName].values.splice(idxPos, 1);
5710
5711 //this.adaptiveBinaryIndexRemove(dataPosition, binaryIndexName, true);
5712 this.adaptiveBinaryIndexInsert(dataPosition, binaryIndexName);
5713 };
5714
5715 /**
5716 * Adaptively remove a selected item from the index.
5717 * @param {int} dataPosition : coll.data array index/position
5718 * @param {string} binaryIndexName : index to search for dataPosition in
5719 */
5720 Collection.prototype.adaptiveBinaryIndexRemove = function(dataPosition, binaryIndexName, removedFromIndexOnly) {
5721 var idxPos = this.getBinaryIndexPosition(dataPosition, binaryIndexName);
5722 var index = this.binaryIndices[binaryIndexName].values;
5723 var len,
5724 idx;
5725
5726 if (idxPos === null) {
5727 // throw new Error('unable to determine binary index position');
5728 return null;
5729 }
5730
5731 // remove document from index
5732 this.binaryIndices[binaryIndexName].values.splice(idxPos, 1);
5733
5734 // if we passed this optional flag parameter, we are calling from adaptiveBinaryIndexUpdate,
5735 // in which case data positions stay the same.
5736 if (removedFromIndexOnly === true) {
5737 return;
5738 }
5739
5740 // since index stores data array positions, if we remove a document
5741 // we need to adjust array positions -1 for all document positions greater than removed position
5742 len = index.length;
5743 for (idx = 0; idx < len; idx++) {
5744 if (index[idx] > dataPosition) {
5745 index[idx]--;
5746 }
5747 }
5748 };
5749
5750 /**
5751 * Internal method used for index maintenance and indexed searching.
5752 * Calculates the beginning of an index range for a given value.
5753 * For index maintainance (adaptive:true), we will return a valid index position to insert to.
5754 * For querying (adaptive:false/undefined), we will :
5755 * return lower bound/index of range of that value (if found)
5756 * return next lower index position if not found (hole)
5757 * If index is empty it is assumed to be handled at higher level, so
5758 * this method assumes there is at least 1 document in index.
5759 *
5760 * @param {string} prop - name of property which has binary index
5761 * @param {any} val - value to find within index
5762 * @param {bool?} adaptive - if true, we will return insert position
5763 */
5764 Collection.prototype.calculateRangeStart = function (prop, val, adaptive) {
5765 var rcd = this.data;
5766 var index = this.binaryIndices[prop].values;
5767 var min = 0;
5768 var max = index.length - 1;
5769 var mid = 0;
5770
5771 if (index.length === 0) {
5772 return -1;
5773 }
5774
5775 var minVal = rcd[index[min]][prop];
5776 var maxVal = rcd[index[max]][prop];
5777
5778 // hone in on start position of value
5779 while (min < max) {
5780 mid = (min + max) >> 1;
5781
5782 if (ltHelper(rcd[index[mid]][prop], val, false)) {
5783 min = mid + 1;
5784 } else {
5785 max = mid;
5786 }
5787 }
5788
5789 var lbound = min;
5790
5791 // found it... return it
5792 if (aeqHelper(val, rcd[index[lbound]][prop])) {
5793 return lbound;
5794 }
5795
5796 // if not in index and our value is less than the found one
5797 if (ltHelper(val, rcd[index[lbound]][prop], false)) {
5798 return adaptive?lbound:lbound-1;
5799 }
5800
5801 // not in index and our value is greater than the found one
5802 return adaptive?lbound+1:lbound;
5803 };
5804
5805 /**
5806 * Internal method used for indexed $between. Given a prop (index name), and a value
5807 * (which may or may not yet exist) this will find the final position of that upper range value.
5808 */
5809 Collection.prototype.calculateRangeEnd = function (prop, val) {
5810 var rcd = this.data;
5811 var index = this.binaryIndices[prop].values;
5812 var min = 0;
5813 var max = index.length - 1;
5814 var mid = 0;
5815
5816 if (index.length === 0) {
5817 return -1;
5818 }
5819
5820 var minVal = rcd[index[min]][prop];
5821 var maxVal = rcd[index[max]][prop];
5822
5823 // hone in on start position of value
5824 while (min < max) {
5825 mid = (min + max) >> 1;
5826
5827 if (ltHelper(val, rcd[index[mid]][prop], false)) {
5828 max = mid;
5829 } else {
5830 min = mid + 1;
5831 }
5832 }
5833
5834 var ubound = max;
5835
5836 // only eq if last element in array is our val
5837 if (aeqHelper(val, rcd[index[ubound]][prop])) {
5838 return ubound;
5839 }
5840
5841 // if not in index and our value is less than the found one
5842 if (gtHelper(val, rcd[index[ubound]][prop], false)) {
5843 return ubound+1;
5844 }
5845
5846 // either hole or first nonmatch
5847 if (aeqHelper(val, rcd[index[ubound-1]][prop])) {
5848 return ubound-1;
5849 }
5850
5851 // hole, so ubound if nearest gt than the val we were looking for
5852 return ubound;
5853 };
5854
5855 /**
5856 * calculateRange() - Binary Search utility method to find range/segment of values matching criteria.
5857 * this is used for collection.find() and first find filter of resultset/dynview
5858 * slightly different than get() binary search in that get() hones in on 1 value,
5859 * but we have to hone in on many (range)
5860 * @param {string} op - operation, such as $eq
5861 * @param {string} prop - name of property to calculate range for
5862 * @param {object} val - value to use for range calculation.
5863 * @returns {array} [start, end] index array positions
5864 */
5865 Collection.prototype.calculateRange = function (op, prop, val) {
5866 var rcd = this.data;
5867 var index = this.binaryIndices[prop].values;
5868 var min = 0;
5869 var max = index.length - 1;
5870 var mid = 0;
5871 var lbound, lval;
5872 var ubound, uval;
5873
5874 // when no documents are in collection, return empty range condition
5875 if (rcd.length === 0) {
5876 return [0, -1];
5877 }
5878
5879 var minVal = rcd[index[min]][prop];
5880 var maxVal = rcd[index[max]][prop];
5881
5882 // if value falls outside of our range return [0, -1] to designate no results
5883 switch (op) {
5884 case '$eq':
5885 case '$aeq':
5886 if (ltHelper(val, minVal, false) || gtHelper(val, maxVal, false)) {
5887 return [0, -1];
5888 }
5889 break;
5890 case '$dteq':
5891 if (ltHelper(val, minVal, false) || gtHelper(val, maxVal, false)) {
5892 return [0, -1];
5893 }
5894 break;
5895 case '$gt':
5896 // none are within range
5897 if (gtHelper(val, maxVal, true)) {
5898 return [0, -1];
5899 }
5900 // all are within range
5901 if (gtHelper(minVal, val, false)) {
5902 return [min, max];
5903 }
5904 break;
5905 case '$gte':
5906 // none are within range
5907 if (gtHelper(val, maxVal, false)) {
5908 return [0, -1];
5909 }
5910 // all are within range
5911 if (gtHelper(minVal, val, true)) {
5912 return [min, max];
5913 }
5914 break;
5915 case '$lt':
5916 // none are within range
5917 if (ltHelper(val, minVal, true)) {
5918 return [0, -1];
5919 }
5920 // all are within range
5921 if (ltHelper(maxVal, val, false)) {
5922 return [min, max];
5923 }
5924 break;
5925 case '$lte':
5926 // none are within range
5927 if (ltHelper(val, minVal, false)) {
5928 return [0, -1];
5929 }
5930 // all are within range
5931 if (ltHelper(maxVal, val, true)) {
5932 return [min, max];
5933 }
5934 break;
5935 case '$between':
5936 // none are within range (low range is greater)
5937 if (gtHelper(val[0], maxVal, false)) {
5938 return [0, -1];
5939 }
5940 // none are within range (high range lower)
5941 if (ltHelper(val[1], minVal, false)) {
5942 return [0, -1];
5943 }
5944
5945 lbound = this.calculateRangeStart(prop, val[0]);
5946 ubound = this.calculateRangeEnd(prop, val[1]);
5947
5948 if (lbound < 0) lbound++;
5949 if (ubound > max) ubound--;
5950
5951 if (!gtHelper(rcd[index[lbound]][prop], val[0], true)) lbound++;
5952 if (!ltHelper(rcd[index[ubound]][prop], val[1], true)) ubound--;
5953
5954 if (ubound < lbound) return [0, -1];
5955
5956 return ([lbound, ubound]);
5957 case '$in':
5958 var idxset = [],
5959 segResult = [];
5960 // query each value '$eq' operator and merge the seqment results.
5961 for (var j = 0, len = val.length; j < len; j++) {
5962 var seg = this.calculateRange('$eq', prop, val[j]);
5963
5964 for (var i = seg[0]; i <= seg[1]; i++) {
5965 if (idxset[i] === undefined) {
5966 idxset[i] = true;
5967 segResult.push(i);
5968 }
5969 }
5970 }
5971 return segResult;
5972 }
5973
5974 // determine lbound where needed
5975 switch (op) {
5976 case '$eq':
5977 case '$aeq':
5978 case '$dteq':
5979 case '$gte':
5980 case '$lt':
5981 lbound = this.calculateRangeStart(prop, val);
5982 lval = rcd[index[lbound]][prop];
5983 break;
5984 default: break;
5985 }
5986
5987 // determine ubound where needed
5988 switch (op) {
5989 case '$eq':
5990 case '$aeq':
5991 case '$dteq':
5992 case '$lte':
5993 case '$gt':
5994 ubound = this.calculateRangeEnd(prop, val);
5995 uval = rcd[index[ubound]][prop];
5996 break;
5997 default: break;
5998 }
5999
6000
6001 switch (op) {
6002 case '$eq':
6003 case '$aeq':
6004 case '$dteq':
6005 // if hole (not found)
6006 //if (ltHelper(lval, val, false) || gtHelper(lval, val, false)) {
6007 // return [0, -1];
6008 //}
6009 if (!aeqHelper(lval, val)) {
6010 return [0, -1];
6011 }
6012
6013 return [lbound, ubound];
6014
6015 //case '$dteq':
6016 // if hole (not found)
6017 // if (lval > val || lval < val) {
6018 // return [0, -1];
6019 // }
6020
6021 // return [lbound, ubound];
6022
6023 case '$gt':
6024 // (an eqHelper would probably be better test)
6025 // if hole (not found) ub position is already greater
6026 if (!aeqHelper(rcd[index[ubound]][prop], val)) {
6027 //if (gtHelper(rcd[index[ubound]][prop], val, false)) {
6028 return [ubound, max];
6029 }
6030 // otherwise (found) so ubound is still equal, get next
6031 return [ubound+1, max];
6032
6033 case '$gte':
6034 // if hole (not found) lb position marks left outside of range
6035 if (!aeqHelper(rcd[index[lbound]][prop], val)) {
6036 //if (ltHelper(rcd[index[lbound]][prop], val, false)) {
6037 return [lbound+1, max];
6038 }
6039 // otherwise (found) so lb is first position where its equal
6040 return [lbound, max];
6041
6042 case '$lt':
6043 // if hole (not found) position already is less than
6044 if (!aeqHelper(rcd[index[lbound]][prop], val)) {
6045 //if (ltHelper(rcd[index[lbound]][prop], val, false)) {
6046 return [min, lbound];
6047 }
6048 // otherwise (found) so lb marks left inside of eq range, get previous
6049 return [min, lbound-1];
6050
6051 case '$lte':
6052 // if hole (not found) ub position marks right outside so get previous
6053 if (!aeqHelper(rcd[index[ubound]][prop], val)) {
6054 //if (gtHelper(rcd[index[ubound]][prop], val, false)) {
6055 return [min, ubound-1];
6056 }
6057 // otherwise (found) so ub is last position where its still equal
6058 return [min, ubound];
6059
6060 default:
6061 return [0, rcd.length - 1];
6062 }
6063 };
6064
6065 /**
6066 * Retrieve doc by Unique index
6067 * @param {string} field - name of uniquely indexed property to use when doing lookup
6068 * @param {value} value - unique value to search for
6069 * @returns {object} document matching the value passed
6070 * @memberof Collection
6071 */
6072 Collection.prototype.by = function (field, value) {
6073 var self;
6074 if (value === undefined) {
6075 self = this;
6076 return function (value) {
6077 return self.by(field, value);
6078 };
6079 }
6080
6081 var result = this.constraints.unique[field].get(value);
6082 if (!this.cloneObjects) {
6083 return result;
6084 } else {
6085 return clone(result, this.cloneMethod);
6086 }
6087 };
6088
6089 /**
6090 * Find one object by index property, by property equal to value
6091 * @param {object} query - query object used to perform search with
6092 * @returns {(object|null)} First matching document, or null if none
6093 * @memberof Collection
6094 */
6095 Collection.prototype.findOne = function (query) {
6096 query = query || {};
6097
6098 // Instantiate Resultset and exec find op passing firstOnly = true param
6099 var result = this.chain().find(query,true).data();
6100
6101 if (Array.isArray(result) && result.length === 0) {
6102 return null;
6103 } else {
6104 if (!this.cloneObjects) {
6105 return result[0];
6106 } else {
6107 return clone(result[0], this.cloneMethod);
6108 }
6109 }
6110 };
6111
6112 /**
6113 * Chain method, used for beginning a series of chained find() and/or view() operations
6114 * on a collection.
6115 *
6116 * @param {array=} transform - Ordered array of transform step objects similar to chain
6117 * @param {object=} parameters - Object containing properties representing parameters to substitute
6118 * @returns {Resultset} (this) resultset, or data array if any map or join functions where called
6119 * @memberof Collection
6120 */
6121 Collection.prototype.chain = function (transform, parameters) {
6122 var rs = new Resultset(this);
6123
6124 if (typeof transform === 'undefined') {
6125 return rs;
6126 }
6127
6128 return rs.transform(transform, parameters);
6129 };
6130
6131 /**
6132 * Find method, api is similar to mongodb.
6133 * for more complex queries use [chain()]{@link Collection#chain} or [where()]{@link Collection#where}.
6134 * @example {@tutorial Query Examples}
6135 * @param {object} query - 'mongo-like' query object
6136 * @returns {array} Array of matching documents
6137 * @memberof Collection
6138 */
6139 Collection.prototype.find = function (query) {
6140 return this.chain().find(query).data();
6141 };
6142
6143 /**
6144 * Find object by unindexed field by property equal to value,
6145 * simply iterates and returns the first element matching the query
6146 */
6147 Collection.prototype.findOneUnindexed = function (prop, value) {
6148 var i = this.data.length,
6149 doc;
6150 while (i--) {
6151 if (this.data[i][prop] === value) {
6152 doc = this.data[i];
6153 return doc;
6154 }
6155 }
6156 return null;
6157 };
6158
6159 /**
6160 * Transaction methods
6161 */
6162
6163 /** start the transation */
6164 Collection.prototype.startTransaction = function () {
6165 if (this.transactional) {
6166 this.cachedData = clone(this.data, this.cloneMethod);
6167 this.cachedIndex = this.idIndex;
6168 this.cachedBinaryIndex = this.binaryIndices;
6169
6170 // propagate startTransaction to dynamic views
6171 for (var idx = 0; idx < this.DynamicViews.length; idx++) {
6172 this.DynamicViews[idx].startTransaction();
6173 }
6174 }
6175 };
6176
6177 /** commit the transation */
6178 Collection.prototype.commit = function () {
6179 if (this.transactional) {
6180 this.cachedData = null;
6181 this.cachedIndex = null;
6182 this.cachedBinaryIndex = null;
6183
6184 // propagate commit to dynamic views
6185 for (var idx = 0; idx < this.DynamicViews.length; idx++) {
6186 this.DynamicViews[idx].commit();
6187 }
6188 }
6189 };
6190
6191 /** roll back the transation */
6192 Collection.prototype.rollback = function () {
6193 if (this.transactional) {
6194 if (this.cachedData !== null && this.cachedIndex !== null) {
6195 this.data = this.cachedData;
6196 this.idIndex = this.cachedIndex;
6197 this.binaryIndices = this.cachedBinaryIndex;
6198 }
6199
6200 // propagate rollback to dynamic views
6201 for (var idx = 0; idx < this.DynamicViews.length; idx++) {
6202 this.DynamicViews[idx].rollback();
6203 }
6204 }
6205 };
6206
6207 // async executor. This is only to enable callbacks at the end of the execution.
6208 Collection.prototype.async = function (fun, callback) {
6209 setTimeout(function () {
6210 if (typeof fun === 'function') {
6211 fun();
6212 callback();
6213 } else {
6214 throw new TypeError('Argument passed for async execution is not a function');
6215 }
6216 }, 0);
6217 };
6218
6219 /**
6220 * Query the collection by supplying a javascript filter function.
6221 * @example
6222 * var results = coll.where(function(obj) {
6223 * return obj.legs === 8;
6224 * });
6225 *
6226 * @param {function} fun - filter function to run against all collection docs
6227 * @returns {array} all documents which pass your filter function
6228 * @memberof Collection
6229 */
6230 Collection.prototype.where = function (fun) {
6231 return this.chain().where(fun).data();
6232 };
6233
6234 /**
6235 * Map Reduce operation
6236 *
6237 * @param {function} mapFunction - function to use as map function
6238 * @param {function} reduceFunction - function to use as reduce function
6239 * @returns {data} The result of your mapReduce operation
6240 * @memberof Collection
6241 */
6242 Collection.prototype.mapReduce = function (mapFunction, reduceFunction) {
6243 try {
6244 return reduceFunction(this.data.map(mapFunction));
6245 } catch (err) {
6246 throw err;
6247 }
6248 };
6249
6250 /**
6251 * Join two collections on specified properties
6252 *
6253 * @param {array|Resultset|Collection} joinData - array of documents to 'join' to this collection
6254 * @param {string} leftJoinProp - property name in collection
6255 * @param {string} rightJoinProp - property name in joinData
6256 * @param {function=} mapFun - (Optional) map function to use
6257 * @param {object=} dataOptions - options to data() before input to your map function
6258 * @param {bool} dataOptions.removeMeta - allows removing meta before calling mapFun
6259 * @param {boolean} dataOptions.forceClones - forcing the return of cloned objects to your map object
6260 * @param {string} dataOptions.forceCloneMethod - Allows overriding the default or collection specified cloning method.
6261 * @returns {Resultset} Result of the mapping operation
6262 * @memberof Collection
6263 */
6264 Collection.prototype.eqJoin = function (joinData, leftJoinProp, rightJoinProp, mapFun, dataOptions) {
6265 // logic in Resultset class
6266 return new Resultset(this).eqJoin(joinData, leftJoinProp, rightJoinProp, mapFun, dataOptions);
6267 };
6268
6269 /* ------ STAGING API -------- */
6270 /**
6271 * stages: a map of uniquely identified 'stages', which hold copies of objects to be
6272 * manipulated without affecting the data in the original collection
6273 */
6274 Collection.prototype.stages = {};
6275
6276 /**
6277 * (Staging API) create a stage and/or retrieve it
6278 * @memberof Collection
6279 */
6280 Collection.prototype.getStage = function (name) {
6281 if (!this.stages[name]) {
6282 this.stages[name] = {};
6283 }
6284 return this.stages[name];
6285 };
6286 /**
6287 * a collection of objects recording the changes applied through a commmitStage
6288 */
6289 Collection.prototype.commitLog = [];
6290
6291 /**
6292 * (Staging API) create a copy of an object and insert it into a stage
6293 * @memberof Collection
6294 */
6295 Collection.prototype.stage = function (stageName, obj) {
6296 var copy = JSON.parse(JSON.stringify(obj));
6297 this.getStage(stageName)[obj.$loki] = copy;
6298 return copy;
6299 };
6300
6301 /**
6302 * (Staging API) re-attach all objects to the original collection, so indexes and views can be rebuilt
6303 * then create a message to be inserted in the commitlog
6304 * @param {string} stageName - name of stage
6305 * @param {string} message
6306 * @memberof Collection
6307 */
6308 Collection.prototype.commitStage = function (stageName, message) {
6309 var stage = this.getStage(stageName),
6310 prop,
6311 timestamp = new Date().getTime();
6312
6313 for (prop in stage) {
6314
6315 this.update(stage[prop]);
6316 this.commitLog.push({
6317 timestamp: timestamp,
6318 message: message,
6319 data: JSON.parse(JSON.stringify(stage[prop]))
6320 });
6321 }
6322 this.stages[stageName] = {};
6323 };
6324
6325 Collection.prototype.no_op = function () {
6326 return;
6327 };
6328
6329 /**
6330 * @memberof Collection
6331 */
6332 Collection.prototype.extract = function (field) {
6333 var i = 0,
6334 len = this.data.length,
6335 isDotNotation = isDeepProperty(field),
6336 result = [];
6337 for (i; i < len; i += 1) {
6338 result.push(deepProperty(this.data[i], field, isDotNotation));
6339 }
6340 return result;
6341 };
6342
6343 /**
6344 * @memberof Collection
6345 */
6346 Collection.prototype.max = function (field) {
6347 return Math.max.apply(null, this.extract(field));
6348 };
6349
6350 /**
6351 * @memberof Collection
6352 */
6353 Collection.prototype.min = function (field) {
6354 return Math.min.apply(null, this.extract(field));
6355 };
6356
6357 /**
6358 * @memberof Collection
6359 */
6360 Collection.prototype.maxRecord = function (field) {
6361 var i = 0,
6362 len = this.data.length,
6363 deep = isDeepProperty(field),
6364 result = {
6365 index: 0,
6366 value: undefined
6367 },
6368 max;
6369
6370 for (i; i < len; i += 1) {
6371 if (max !== undefined) {
6372 if (max < deepProperty(this.data[i], field, deep)) {
6373 max = deepProperty(this.data[i], field, deep);
6374 result.index = this.data[i].$loki;
6375 }
6376 } else {
6377 max = deepProperty(this.data[i], field, deep);
6378 result.index = this.data[i].$loki;
6379 }
6380 }
6381 result.value = max;
6382 return result;
6383 };
6384
6385 /**
6386 * @memberof Collection
6387 */
6388 Collection.prototype.minRecord = function (field) {
6389 var i = 0,
6390 len = this.data.length,
6391 deep = isDeepProperty(field),
6392 result = {
6393 index: 0,
6394 value: undefined
6395 },
6396 min;
6397
6398 for (i; i < len; i += 1) {
6399 if (min !== undefined) {
6400 if (min > deepProperty(this.data[i], field, deep)) {
6401 min = deepProperty(this.data[i], field, deep);
6402 result.index = this.data[i].$loki;
6403 }
6404 } else {
6405 min = deepProperty(this.data[i], field, deep);
6406 result.index = this.data[i].$loki;
6407 }
6408 }
6409 result.value = min;
6410 return result;
6411 };
6412
6413 /**
6414 * @memberof Collection
6415 */
6416 Collection.prototype.extractNumerical = function (field) {
6417 return this.extract(field).map(parseBase10).filter(Number).filter(function (n) {
6418 return !(isNaN(n));
6419 });
6420 };
6421
6422 /**
6423 * Calculates the average numerical value of a property
6424 *
6425 * @param {string} field - name of property in docs to average
6426 * @returns {number} average of property in all docs in the collection
6427 * @memberof Collection
6428 */
6429 Collection.prototype.avg = function (field) {
6430 return average(this.extractNumerical(field));
6431 };
6432
6433 /**
6434 * Calculate standard deviation of a field
6435 * @memberof Collection
6436 * @param {string} field
6437 */
6438 Collection.prototype.stdDev = function (field) {
6439 return standardDeviation(this.extractNumerical(field));
6440 };
6441
6442 /**
6443 * @memberof Collection
6444 * @param {string} field
6445 */
6446 Collection.prototype.mode = function (field) {
6447 var dict = {},
6448 data = this.extract(field);
6449 data.forEach(function (obj) {
6450 if (dict[obj]) {
6451 dict[obj] += 1;
6452 } else {
6453 dict[obj] = 1;
6454 }
6455 });
6456 var max,
6457 prop, mode;
6458 for (prop in dict) {
6459 if (max) {
6460 if (max < dict[prop]) {
6461 mode = prop;
6462 }
6463 } else {
6464 mode = prop;
6465 max = dict[prop];
6466 }
6467 }
6468 return mode;
6469 };
6470
6471 /**
6472 * @memberof Collection
6473 * @param {string} field - property name
6474 */
6475 Collection.prototype.median = function (field) {
6476 var values = this.extractNumerical(field);
6477 values.sort(sub);
6478
6479 var half = Math.floor(values.length / 2);
6480
6481 if (values.length % 2) {
6482 return values[half];
6483 } else {
6484 return (values[half - 1] + values[half]) / 2.0;
6485 }
6486 };
6487
6488 /**
6489 * General utils, including statistical functions
6490 */
6491 function isDeepProperty(field) {
6492 return field.indexOf('.') !== -1;
6493 }
6494
6495 function parseBase10(num) {
6496 return parseFloat(num, 10);
6497 }
6498
6499 function isNotUndefined(obj) {
6500 return obj !== undefined;
6501 }
6502
6503 function add(a, b) {
6504 return a + b;
6505 }
6506
6507 function sub(a, b) {
6508 return a - b;
6509 }
6510
6511 function median(values) {
6512 values.sort(sub);
6513 var half = Math.floor(values.length / 2);
6514 return (values.length % 2) ? values[half] : ((values[half - 1] + values[half]) / 2.0);
6515 }
6516
6517 function average(array) {
6518 return (array.reduce(add, 0)) / array.length;
6519 }
6520
6521 function standardDeviation(values) {
6522 var avg = average(values);
6523 var squareDiffs = values.map(function (value) {
6524 var diff = value - avg;
6525 var sqrDiff = diff * diff;
6526 return sqrDiff;
6527 });
6528
6529 var avgSquareDiff = average(squareDiffs);
6530
6531 var stdDev = Math.sqrt(avgSquareDiff);
6532 return stdDev;
6533 }
6534
6535 function deepProperty(obj, property, isDeep) {
6536 if (isDeep === false) {
6537 // pass without processing
6538 return obj[property];
6539 }
6540 var pieces = property.split('.'),
6541 root = obj;
6542 while (pieces.length > 0) {
6543 root = root[pieces.shift()];
6544 }
6545 return root;
6546 }
6547
6548 function binarySearch(array, item, fun) {
6549 var lo = 0,
6550 hi = array.length,
6551 compared,
6552 mid;
6553 while (lo < hi) {
6554 mid = (lo + hi) >> 1;
6555 compared = fun.apply(null, [item, array[mid]]);
6556 if (compared === 0) {
6557 return {
6558 found: true,
6559 index: mid
6560 };
6561 } else if (compared < 0) {
6562 hi = mid;
6563 } else {
6564 lo = mid + 1;
6565 }
6566 }
6567 return {
6568 found: false,
6569 index: hi
6570 };
6571 }
6572
6573 function BSonSort(fun) {
6574 return function (array, item) {
6575 return binarySearch(array, item, fun);
6576 };
6577 }
6578
6579 function KeyValueStore() {}
6580
6581 KeyValueStore.prototype = {
6582 keys: [],
6583 values: [],
6584 sort: function (a, b) {
6585 return (a < b) ? -1 : ((a > b) ? 1 : 0);
6586 },
6587 setSort: function (fun) {
6588 this.bs = new BSonSort(fun);
6589 },
6590 bs: function () {
6591 return new BSonSort(this.sort);
6592 },
6593 set: function (key, value) {
6594 var pos = this.bs(this.keys, key);
6595 if (pos.found) {
6596 this.values[pos.index] = value;
6597 } else {
6598 this.keys.splice(pos.index, 0, key);
6599 this.values.splice(pos.index, 0, value);
6600 }
6601 },
6602 get: function (key) {
6603 return this.values[binarySearch(this.keys, key, this.sort).index];
6604 }
6605 };
6606
6607 function UniqueIndex(uniqueField) {
6608 this.field = uniqueField;
6609 this.keyMap = {};
6610 this.lokiMap = {};
6611 }
6612 UniqueIndex.prototype.keyMap = {};
6613 UniqueIndex.prototype.lokiMap = {};
6614 UniqueIndex.prototype.set = function (obj) {
6615 var fieldValue = obj[this.field];
6616 if (fieldValue !== null && typeof (fieldValue) !== 'undefined') {
6617 if (this.keyMap[fieldValue]) {
6618 throw new Error('Duplicate key for property ' + this.field + ': ' + fieldValue);
6619 } else {
6620 this.keyMap[fieldValue] = obj;
6621 this.lokiMap[obj.$loki] = fieldValue;
6622 }
6623 }
6624 };
6625 UniqueIndex.prototype.get = function (key) {
6626 return this.keyMap[key];
6627 };
6628
6629 UniqueIndex.prototype.byId = function (id) {
6630 return this.keyMap[this.lokiMap[id]];
6631 };
6632 /**
6633 * Updates a document's unique index given an updated object.
6634 * @param {Object} obj Original document object
6635 * @param {Object} doc New document object (likely the same as obj)
6636 */
6637 UniqueIndex.prototype.update = function (obj, doc) {
6638 if (this.lokiMap[obj.$loki] !== doc[this.field]) {
6639 var old = this.lokiMap[obj.$loki];
6640 this.set(doc);
6641 // make the old key fail bool test, while avoiding the use of delete (mem-leak prone)
6642 this.keyMap[old] = undefined;
6643 } else {
6644 this.keyMap[obj[this.field]] = doc;
6645 }
6646 };
6647 UniqueIndex.prototype.remove = function (key) {
6648 var obj = this.keyMap[key];
6649 if (obj !== null && typeof obj !== 'undefined') {
6650 this.keyMap[key] = undefined;
6651 this.lokiMap[obj.$loki] = undefined;
6652 } else {
6653 throw new Error('Key is not in unique index: ' + this.field);
6654 }
6655 };
6656 UniqueIndex.prototype.clear = function () {
6657 this.keyMap = {};
6658 this.lokiMap = {};
6659 };
6660
6661 function ExactIndex(exactField) {
6662 this.index = {};
6663 this.field = exactField;
6664 }
6665
6666 // add the value you want returned to the key in the index
6667 ExactIndex.prototype = {
6668 set: function add(key, val) {
6669 if (this.index[key]) {
6670 this.index[key].push(val);
6671 } else {
6672 this.index[key] = [val];
6673 }
6674 },
6675
6676 // remove the value from the index, if the value was the last one, remove the key
6677 remove: function remove(key, val) {
6678 var idxSet = this.index[key];
6679 for (var i in idxSet) {
6680 if (idxSet[i] == val) {
6681 idxSet.splice(i, 1);
6682 }
6683 }
6684 if (idxSet.length < 1) {
6685 this.index[key] = undefined;
6686 }
6687 },
6688
6689 // get the values related to the key, could be more than one
6690 get: function get(key) {
6691 return this.index[key];
6692 },
6693
6694 // clear will zap the index
6695 clear: function clear(key) {
6696 this.index = {};
6697 }
6698 };
6699
6700 function SortedIndex(sortedField) {
6701 this.field = sortedField;
6702 }
6703
6704 SortedIndex.prototype = {
6705 keys: [],
6706 values: [],
6707 // set the default sort
6708 sort: function (a, b) {
6709 return (a < b) ? -1 : ((a > b) ? 1 : 0);
6710 },
6711 bs: function () {
6712 return new BSonSort(this.sort);
6713 },
6714 // and allow override of the default sort
6715 setSort: function (fun) {
6716 this.bs = new BSonSort(fun);
6717 },
6718 // add the value you want returned to the key in the index
6719 set: function (key, value) {
6720 var pos = binarySearch(this.keys, key, this.sort);
6721 if (pos.found) {
6722 this.values[pos.index].push(value);
6723 } else {
6724 this.keys.splice(pos.index, 0, key);
6725 this.values.splice(pos.index, 0, [value]);
6726 }
6727 },
6728 // get all values which have a key == the given key
6729 get: function (key) {
6730 var bsr = binarySearch(this.keys, key, this.sort);
6731 if (bsr.found) {
6732 return this.values[bsr.index];
6733 } else {
6734 return [];
6735 }
6736 },
6737 // get all values which have a key < the given key
6738 getLt: function (key) {
6739 var bsr = binarySearch(this.keys, key, this.sort);
6740 var pos = bsr.index;
6741 if (bsr.found) pos--;
6742 return this.getAll(key, 0, pos);
6743 },
6744 // get all values which have a key > the given key
6745 getGt: function (key) {
6746 var bsr = binarySearch(this.keys, key, this.sort);
6747 var pos = bsr.index;
6748 if (bsr.found) pos++;
6749 return this.getAll(key, pos, this.keys.length);
6750 },
6751
6752 // get all vals from start to end
6753 getAll: function (key, start, end) {
6754 var results = [];
6755 for (var i = start; i < end; i++) {
6756 results = results.concat(this.values[i]);
6757 }
6758 return results;
6759 },
6760 // just in case someone wants to do something smart with ranges
6761 getPos: function (key) {
6762 return binarySearch(this.keys, key, this.sort);
6763 },
6764 // remove the value from the index, if the value was the last one, remove the key
6765 remove: function (key, value) {
6766 var pos = binarySearch(this.keys, key, this.sort).index;
6767 var idxSet = this.values[pos];
6768 for (var i in idxSet) {
6769 if (idxSet[i] == value) idxSet.splice(i, 1);
6770 }
6771 if (idxSet.length < 1) {
6772 this.keys.splice(pos, 1);
6773 this.values.splice(pos, 1);
6774 }
6775 },
6776 // clear will zap the index
6777 clear: function () {
6778 this.keys = [];
6779 this.values = [];
6780 }
6781 };
6782
6783
6784 Loki.LokiOps = LokiOps;
6785 Loki.Collection = Collection;
6786 Loki.KeyValueStore = KeyValueStore;
6787 Loki.LokiMemoryAdapter = LokiMemoryAdapter;
6788 Loki.LokiPartitioningAdapter = LokiPartitioningAdapter;
6789 Loki.LokiLocalStorageAdapter = LokiLocalStorageAdapter;
6790 Loki.LokiFsAdapter = LokiFsAdapter;
6791 Loki.persistenceAdapters = {
6792 fs: LokiFsAdapter,
6793 localStorage: LokiLocalStorageAdapter
6794 };
6795 Loki.aeq = aeqHelper;
6796 Loki.lt = ltHelper;
6797 Loki.gt = gtHelper;
6798 return Loki;
6799 }());
6800
6801}));