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 | ;
|
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 | }));
|