@electerm/nedb 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/model.js ADDED
@@ -0,0 +1,835 @@
1
+ /**
2
+ * Handle models (i.e. docs)
3
+ * Serialization/deserialization
4
+ * Copying
5
+ * Querying, update
6
+ */
7
+
8
+ var util = require('util')
9
+ , _ = require('./underscore')
10
+ , modifierFunctions = {}
11
+ , lastStepModifierFunctions = {}
12
+ , comparisonFunctions = {}
13
+ , logicalOperators = {}
14
+ , arrayComparisonFunctions = {}
15
+ ;
16
+
17
+
18
+ /**
19
+ * Check a key, throw an error if the key is non valid
20
+ * @param {String} k key
21
+ * @param {Model} v value, needed to treat the Date edge case
22
+ * Non-treatable edge cases here: if part of the object if of the form { $$date: number } or { $$deleted: true }
23
+ * Its serialized-then-deserialized version it will transformed into a Date object
24
+ * But you really need to want it to trigger such behaviour, even when warned not to use '$' at the beginning of the field names...
25
+ */
26
+ function checkKey (k, v) {
27
+ if (typeof k === 'number') {
28
+ k = k.toString();
29
+ }
30
+
31
+ if (k[0] === '$' && !(k === '$$date' && typeof v === 'number') && !(k === '$$deleted' && v === true) && !(k === '$$indexCreated') && !(k === '$$indexRemoved')) {
32
+ throw new Error('Field names cannot begin with the $ character');
33
+ }
34
+
35
+ if (k.indexOf('.') !== -1) {
36
+ throw new Error('Field names cannot contain a .');
37
+ }
38
+ }
39
+
40
+
41
+ /**
42
+ * Check a DB object and throw an error if it's not valid
43
+ * Works by applying the above checkKey function to all fields recursively
44
+ */
45
+ function checkObject (obj) {
46
+ if (util.isArray(obj)) {
47
+ obj.forEach(function (o) {
48
+ checkObject(o);
49
+ });
50
+ }
51
+
52
+ if (typeof obj === 'object' && obj !== null) {
53
+ Object.keys(obj).forEach(function (k) {
54
+ checkKey(k, obj[k]);
55
+ checkObject(obj[k]);
56
+ });
57
+ }
58
+ }
59
+
60
+
61
+ /**
62
+ * Serialize an object to be persisted to a one-line string
63
+ * For serialization/deserialization, we use the native JSON parser and not eval or Function
64
+ * That gives us less freedom but data entered in the database may come from users
65
+ * so eval and the like are not safe
66
+ * Accepted primitive types: Number, String, Boolean, Date, null
67
+ * Accepted secondary types: Objects, Arrays
68
+ */
69
+ function serialize (obj) {
70
+ var res;
71
+
72
+ res = JSON.stringify(obj, function (k, v) {
73
+ checkKey(k, v);
74
+
75
+ if (v === undefined) { return undefined; }
76
+ if (v === null) { return null; }
77
+
78
+ // Hackish way of checking if object is Date (this way it works between execution contexts in node-webkit).
79
+ // We can't use value directly because for dates it is already string in this function (date.toJSON was already called), so we use this
80
+ if (typeof this[k].getTime === 'function') { return { $$date: this[k].getTime() }; }
81
+
82
+ return v;
83
+ });
84
+
85
+ return res;
86
+ }
87
+
88
+
89
+ /**
90
+ * From a one-line representation of an object generate by the serialize function
91
+ * Return the object itself
92
+ */
93
+ function deserialize (rawData) {
94
+ return JSON.parse(rawData, function (k, v) {
95
+ if (k === '$$date') { return new Date(v); }
96
+ if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' || v === null) { return v; }
97
+ if (v && v.$$date) { return v.$$date; }
98
+
99
+ return v;
100
+ });
101
+ }
102
+
103
+
104
+ /**
105
+ * Deep copy a DB object
106
+ * The optional strictKeys flag (defaulting to false) indicates whether to copy everything or only fields
107
+ * where the keys are valid, i.e. don't begin with $ and don't contain a .
108
+ */
109
+ function deepCopy (obj, strictKeys) {
110
+ var res;
111
+
112
+ if ( typeof obj === 'boolean' ||
113
+ typeof obj === 'number' ||
114
+ typeof obj === 'string' ||
115
+ obj === null ||
116
+ (util.isDate(obj)) ) {
117
+ return obj;
118
+ }
119
+
120
+ if (util.isArray(obj)) {
121
+ res = [];
122
+ obj.forEach(function (o) { res.push(deepCopy(o, strictKeys)); });
123
+ return res;
124
+ }
125
+
126
+ if (typeof obj === 'object') {
127
+ res = {};
128
+ Object.keys(obj).forEach(function (k) {
129
+ if (!strictKeys || (k[0] !== '$' && k.indexOf('.') === -1)) {
130
+ res[k] = deepCopy(obj[k], strictKeys);
131
+ }
132
+ });
133
+ return res;
134
+ }
135
+
136
+ return undefined; // For now everything else is undefined. We should probably throw an error instead
137
+ }
138
+
139
+
140
+ /**
141
+ * Tells if an object is a primitive type or a "real" object
142
+ * Arrays are considered primitive
143
+ */
144
+ function isPrimitiveType (obj) {
145
+ return ( typeof obj === 'boolean' ||
146
+ typeof obj === 'number' ||
147
+ typeof obj === 'string' ||
148
+ obj === null ||
149
+ util.isDate(obj) ||
150
+ util.isArray(obj));
151
+ }
152
+
153
+
154
+ /**
155
+ * Utility functions for comparing things
156
+ * Assumes type checking was already done (a and b already have the same type)
157
+ * compareNSB works for numbers, strings and booleans
158
+ */
159
+ function compareNSB (a, b) {
160
+ if (a < b) { return -1; }
161
+ if (a > b) { return 1; }
162
+ return 0;
163
+ }
164
+
165
+ function compareArrays (a, b) {
166
+ var i, comp;
167
+
168
+ for (i = 0; i < Math.min(a.length, b.length); i += 1) {
169
+ comp = compareThings(a[i], b[i]);
170
+
171
+ if (comp !== 0) { return comp; }
172
+ }
173
+
174
+ // Common section was identical, longest one wins
175
+ return compareNSB(a.length, b.length);
176
+ }
177
+
178
+
179
+ /**
180
+ * Compare { things U undefined }
181
+ * Things are defined as any native types (string, number, boolean, null, date) and objects
182
+ * We need to compare with undefined as it will be used in indexes
183
+ * In the case of objects and arrays, we deep-compare
184
+ * If two objects dont have the same type, the (arbitrary) type hierarchy is: undefined, null, number, strings, boolean, dates, arrays, objects
185
+ * Return -1 if a < b, 1 if a > b and 0 if a = b (note that equality here is NOT the same as defined in areThingsEqual!)
186
+ *
187
+ * @param {Function} _compareStrings String comparing function, returning -1, 0 or 1, overriding default string comparison (useful for languages with accented letters)
188
+ */
189
+ function compareThings (a, b, _compareStrings) {
190
+ var aKeys, bKeys, comp, i
191
+ , compareStrings = _compareStrings || compareNSB;
192
+
193
+ // undefined
194
+ if (a === undefined) { return b === undefined ? 0 : -1; }
195
+ if (b === undefined) { return a === undefined ? 0 : 1; }
196
+
197
+ // null
198
+ if (a === null) { return b === null ? 0 : -1; }
199
+ if (b === null) { return a === null ? 0 : 1; }
200
+
201
+ // Numbers
202
+ if (typeof a === 'number') { return typeof b === 'number' ? compareNSB(a, b) : -1; }
203
+ if (typeof b === 'number') { return typeof a === 'number' ? compareNSB(a, b) : 1; }
204
+
205
+ // Strings
206
+ if (typeof a === 'string') { return typeof b === 'string' ? compareStrings(a, b) : -1; }
207
+ if (typeof b === 'string') { return typeof a === 'string' ? compareStrings(a, b) : 1; }
208
+
209
+ // Booleans
210
+ if (typeof a === 'boolean') { return typeof b === 'boolean' ? compareNSB(a, b) : -1; }
211
+ if (typeof b === 'boolean') { return typeof a === 'boolean' ? compareNSB(a, b) : 1; }
212
+
213
+ // Dates
214
+ if (util.isDate(a)) { return util.isDate(b) ? compareNSB(a.getTime(), b.getTime()) : -1; }
215
+ if (util.isDate(b)) { return util.isDate(a) ? compareNSB(a.getTime(), b.getTime()) : 1; }
216
+
217
+ // Arrays (first element is most significant and so on)
218
+ if (util.isArray(a)) { return util.isArray(b) ? compareArrays(a, b) : -1; }
219
+ if (util.isArray(b)) { return util.isArray(a) ? compareArrays(a, b) : 1; }
220
+
221
+ // Objects
222
+ aKeys = Object.keys(a).sort();
223
+ bKeys = Object.keys(b).sort();
224
+
225
+ for (i = 0; i < Math.min(aKeys.length, bKeys.length); i += 1) {
226
+ comp = compareThings(a[aKeys[i]], b[bKeys[i]]);
227
+
228
+ if (comp !== 0) { return comp; }
229
+ }
230
+
231
+ return compareNSB(aKeys.length, bKeys.length);
232
+ }
233
+
234
+
235
+
236
+ // ==============================================================
237
+ // Updating documents
238
+ // ==============================================================
239
+
240
+ /**
241
+ * The signature of modifier functions is as follows
242
+ * Their structure is always the same: recursively follow the dot notation while creating
243
+ * the nested documents if needed, then apply the "last step modifier"
244
+ * @param {Object} obj The model to modify
245
+ * @param {String} field Can contain dots, in that case that means we will set a subfield recursively
246
+ * @param {Model} value
247
+ */
248
+
249
+ /**
250
+ * Set a field to a new value
251
+ */
252
+ lastStepModifierFunctions.$set = function (obj, field, value) {
253
+ obj[field] = value;
254
+ };
255
+
256
+
257
+ /**
258
+ * Unset a field
259
+ */
260
+ lastStepModifierFunctions.$unset = function (obj, field, value) {
261
+ delete obj[field];
262
+ };
263
+
264
+
265
+ /**
266
+ * Push an element to the end of an array field
267
+ * Optional modifier $each instead of value to push several values
268
+ * Optional modifier $slice to slice the resulting array, see https://docs.mongodb.org/manual/reference/operator/update/slice/
269
+ * Différeence with MongoDB: if $slice is specified and not $each, we act as if value is an empty array
270
+ */
271
+ lastStepModifierFunctions.$push = function (obj, field, value) {
272
+ // Create the array if it doesn't exist
273
+ if (!obj.hasOwnProperty(field)) { obj[field] = []; }
274
+
275
+ if (!util.isArray(obj[field])) { throw new Error("Can't $push an element on non-array values"); }
276
+
277
+ if (value !== null && typeof value === 'object' && value.$slice && value.$each === undefined) {
278
+ value.$each = [];
279
+ }
280
+
281
+ if (value !== null && typeof value === 'object' && value.$each) {
282
+ if (Object.keys(value).length >= 3 || (Object.keys(value).length === 2 && value.$slice === undefined)) { throw new Error("Can only use $slice in cunjunction with $each when $push to array"); }
283
+ if (!util.isArray(value.$each)) { throw new Error("$each requires an array value"); }
284
+
285
+ value.$each.forEach(function (v) {
286
+ obj[field].push(v);
287
+ });
288
+
289
+ if (value.$slice === undefined || typeof value.$slice !== 'number') { return; }
290
+
291
+ if (value.$slice === 0) {
292
+ obj[field] = [];
293
+ } else {
294
+ var start, end, n = obj[field].length;
295
+ if (value.$slice < 0) {
296
+ start = Math.max(0, n + value.$slice);
297
+ end = n;
298
+ } else if (value.$slice > 0) {
299
+ start = 0;
300
+ end = Math.min(n, value.$slice);
301
+ }
302
+ obj[field] = obj[field].slice(start, end);
303
+ }
304
+ } else {
305
+ obj[field].push(value);
306
+ }
307
+ };
308
+
309
+
310
+ /**
311
+ * Add an element to an array field only if it is not already in it
312
+ * No modification if the element is already in the array
313
+ * Note that it doesn't check whether the original array contains duplicates
314
+ */
315
+ lastStepModifierFunctions.$addToSet = function (obj, field, value) {
316
+ var addToSet = true;
317
+
318
+ // Create the array if it doesn't exist
319
+ if (!obj.hasOwnProperty(field)) { obj[field] = []; }
320
+
321
+ if (!util.isArray(obj[field])) { throw new Error("Can't $addToSet an element on non-array values"); }
322
+
323
+ if (value !== null && typeof value === 'object' && value.$each) {
324
+ if (Object.keys(value).length > 1) { throw new Error("Can't use another field in conjunction with $each"); }
325
+ if (!util.isArray(value.$each)) { throw new Error("$each requires an array value"); }
326
+
327
+ value.$each.forEach(function (v) {
328
+ lastStepModifierFunctions.$addToSet(obj, field, v);
329
+ });
330
+ } else {
331
+ obj[field].forEach(function (v) {
332
+ if (compareThings(v, value) === 0) { addToSet = false; }
333
+ });
334
+ if (addToSet) { obj[field].push(value); }
335
+ }
336
+ };
337
+
338
+
339
+ /**
340
+ * Remove the first or last element of an array
341
+ */
342
+ lastStepModifierFunctions.$pop = function (obj, field, value) {
343
+ if (!util.isArray(obj[field])) { throw new Error("Can't $pop an element from non-array values"); }
344
+ if (typeof value !== 'number') { throw new Error(value + " isn't an integer, can't use it with $pop"); }
345
+ if (value === 0) { return; }
346
+
347
+ if (value > 0) {
348
+ obj[field] = obj[field].slice(0, obj[field].length - 1);
349
+ } else {
350
+ obj[field] = obj[field].slice(1);
351
+ }
352
+ };
353
+
354
+
355
+ /**
356
+ * Removes all instances of a value from an existing array
357
+ */
358
+ lastStepModifierFunctions.$pull = function (obj, field, value) {
359
+ var arr, i;
360
+
361
+ if (!util.isArray(obj[field])) { throw new Error("Can't $pull an element from non-array values"); }
362
+
363
+ arr = obj[field];
364
+ for (i = arr.length - 1; i >= 0; i -= 1) {
365
+ if (match(arr[i], value)) {
366
+ arr.splice(i, 1);
367
+ }
368
+ }
369
+ };
370
+
371
+
372
+ /**
373
+ * Increment a numeric field's value
374
+ */
375
+ lastStepModifierFunctions.$inc = function (obj, field, value) {
376
+ if (typeof value !== 'number') { throw new Error(value + " must be a number"); }
377
+
378
+ if (typeof obj[field] !== 'number') {
379
+ if (!_.has(obj, field)) {
380
+ obj[field] = value;
381
+ } else {
382
+ throw new Error("Don't use the $inc modifier on non-number fields");
383
+ }
384
+ } else {
385
+ obj[field] += value;
386
+ }
387
+ };
388
+
389
+ /**
390
+ * Updates the value of the field, only if specified field is greater than the current value of the field
391
+ */
392
+ lastStepModifierFunctions.$max = function (obj, field, value) {
393
+ if (typeof obj[field] === 'undefined') {
394
+ obj[field] = value;
395
+ } else if (value > obj[field]) {
396
+ obj[field] = value;
397
+ }
398
+ };
399
+
400
+ /**
401
+ * Updates the value of the field, only if specified field is smaller than the current value of the field
402
+ */
403
+ lastStepModifierFunctions.$min = function (obj, field, value) {
404
+ if (typeof obj[field] === 'undefined') { 
405
+ obj[field] = value;
406
+ } else if (value < obj[field]) {
407
+ obj[field] = value;
408
+ }
409
+ };
410
+
411
+ // Given its name, create the complete modifier function
412
+ function createModifierFunction (modifier) {
413
+ return function (obj, field, value) {
414
+ var fieldParts = typeof field === 'string' ? field.split('.') : field;
415
+
416
+ if (fieldParts.length === 1) {
417
+ lastStepModifierFunctions[modifier](obj, field, value);
418
+ } else {
419
+ if (obj[fieldParts[0]] === undefined) {
420
+ if (modifier === '$unset') { return; } // Bad looking specific fix, needs to be generalized modifiers that behave like $unset are implemented
421
+ obj[fieldParts[0]] = {};
422
+ }
423
+ modifierFunctions[modifier](obj[fieldParts[0]], fieldParts.slice(1), value);
424
+ }
425
+ };
426
+ }
427
+
428
+ // Actually create all modifier functions
429
+ Object.keys(lastStepModifierFunctions).forEach(function (modifier) {
430
+ modifierFunctions[modifier] = createModifierFunction(modifier);
431
+ });
432
+
433
+
434
+ /**
435
+ * Modify a DB object according to an update query
436
+ */
437
+ function modify (obj, updateQuery) {
438
+ var keys = Object.keys(updateQuery)
439
+ , firstChars = _.map(keys, function (item) { return item[0]; })
440
+ , dollarFirstChars = _.filter(firstChars, function (c) { return c === '$'; })
441
+ , newDoc, modifiers
442
+ ;
443
+
444
+ if (keys.indexOf('_id') !== -1 && updateQuery._id !== obj._id) { throw new Error("You cannot change a document's _id"); }
445
+
446
+ if (dollarFirstChars.length !== 0 && dollarFirstChars.length !== firstChars.length) {
447
+ throw new Error("You cannot mix modifiers and normal fields");
448
+ }
449
+
450
+ if (dollarFirstChars.length === 0) {
451
+ // Simply replace the object with the update query contents
452
+ newDoc = deepCopy(updateQuery);
453
+ newDoc._id = obj._id;
454
+ } else {
455
+ // Apply modifiers
456
+ modifiers = _.uniq(keys);
457
+ newDoc = deepCopy(obj);
458
+ modifiers.forEach(function (m) {
459
+ var keys;
460
+
461
+ if (!modifierFunctions[m]) { throw new Error("Unknown modifier " + m); }
462
+
463
+ // Can't rely on Object.keys throwing on non objects since ES6
464
+ // Not 100% satisfying as non objects can be interpreted as objects but no false negatives so we can live with it
465
+ if (typeof updateQuery[m] !== 'object') {
466
+ throw new Error("Modifier " + m + "'s argument must be an object");
467
+ }
468
+
469
+ keys = Object.keys(updateQuery[m]);
470
+ keys.forEach(function (k) {
471
+ modifierFunctions[m](newDoc, k, updateQuery[m][k]);
472
+ });
473
+ });
474
+ }
475
+
476
+ // Check result is valid and return it
477
+ checkObject(newDoc);
478
+
479
+ if (obj._id !== newDoc._id) { throw new Error("You can't change a document's _id"); }
480
+ return newDoc;
481
+ };
482
+
483
+
484
+ // ==============================================================
485
+ // Finding documents
486
+ // ==============================================================
487
+
488
+ /**
489
+ * Get a value from object with dot notation
490
+ * @param {Object} obj
491
+ * @param {String} field
492
+ */
493
+ function getDotValue (obj, field) {
494
+ var fieldParts = typeof field === 'string' ? field.split('.') : field
495
+ , i, objs;
496
+
497
+ if (!obj) { return undefined; } // field cannot be empty so that means we should return undefined so that nothing can match
498
+
499
+ if (fieldParts.length === 0) { return obj; }
500
+
501
+ if (fieldParts.length === 1) { return obj[fieldParts[0]]; }
502
+
503
+ if (util.isArray(obj[fieldParts[0]])) {
504
+ // If the next field is an integer, return only this item of the array
505
+ i = parseInt(fieldParts[1], 10);
506
+ if (typeof i === 'number' && !isNaN(i)) {
507
+ return getDotValue(obj[fieldParts[0]][i], fieldParts.slice(2))
508
+ }
509
+
510
+ // Return the array of values
511
+ objs = new Array();
512
+ for (i = 0; i < obj[fieldParts[0]].length; i += 1) {
513
+ objs.push(getDotValue(obj[fieldParts[0]][i], fieldParts.slice(1)));
514
+ }
515
+ return objs;
516
+ } else {
517
+ return getDotValue(obj[fieldParts[0]], fieldParts.slice(1));
518
+ }
519
+ }
520
+
521
+
522
+ /**
523
+ * Check whether 'things' are equal
524
+ * Things are defined as any native types (string, number, boolean, null, date) and objects
525
+ * In the case of object, we check deep equality
526
+ * Returns true if they are, false otherwise
527
+ */
528
+ function areThingsEqual (a, b) {
529
+ var aKeys , bKeys , i;
530
+
531
+ // Strings, booleans, numbers, null
532
+ if (a === null || typeof a === 'string' || typeof a === 'boolean' || typeof a === 'number' ||
533
+ b === null || typeof b === 'string' || typeof b === 'boolean' || typeof b === 'number') { return a === b; }
534
+
535
+ // Dates
536
+ if (util.isDate(a) || util.isDate(b)) { return util.isDate(a) && util.isDate(b) && a.getTime() === b.getTime(); }
537
+
538
+ // Arrays (no match since arrays are used as a $in)
539
+ // undefined (no match since they mean field doesn't exist and can't be serialized)
540
+ if ((!(util.isArray(a) && util.isArray(b)) && (util.isArray(a) || util.isArray(b))) || a === undefined || b === undefined) { return false; }
541
+
542
+ // General objects (check for deep equality)
543
+ // a and b should be objects at this point
544
+ try {
545
+ aKeys = Object.keys(a);
546
+ bKeys = Object.keys(b);
547
+ } catch (e) {
548
+ return false;
549
+ }
550
+
551
+ if (aKeys.length !== bKeys.length) { return false; }
552
+ for (i = 0; i < aKeys.length; i += 1) {
553
+ if (bKeys.indexOf(aKeys[i]) === -1) { return false; }
554
+ if (!areThingsEqual(a[aKeys[i]], b[aKeys[i]])) { return false; }
555
+ }
556
+ return true;
557
+ }
558
+
559
+
560
+ /**
561
+ * Check that two values are comparable
562
+ */
563
+ function areComparable (a, b) {
564
+ if (typeof a !== 'string' && typeof a !== 'number' && !util.isDate(a) &&
565
+ typeof b !== 'string' && typeof b !== 'number' && !util.isDate(b)) {
566
+ return false;
567
+ }
568
+
569
+ if (typeof a !== typeof b) { return false; }
570
+
571
+ return true;
572
+ }
573
+
574
+
575
+ /**
576
+ * Arithmetic and comparison operators
577
+ * @param {Native value} a Value in the object
578
+ * @param {Native value} b Value in the query
579
+ */
580
+ comparisonFunctions.$lt = function (a, b) {
581
+ return areComparable(a, b) && a < b;
582
+ };
583
+
584
+ comparisonFunctions.$lte = function (a, b) {
585
+ return areComparable(a, b) && a <= b;
586
+ };
587
+
588
+ comparisonFunctions.$gt = function (a, b) {
589
+ return areComparable(a, b) && a > b;
590
+ };
591
+
592
+ comparisonFunctions.$gte = function (a, b) {
593
+ return areComparable(a, b) && a >= b;
594
+ };
595
+
596
+ comparisonFunctions.$ne = function (a, b) {
597
+ if (a === undefined) { return true; }
598
+ return !areThingsEqual(a, b);
599
+ };
600
+
601
+ comparisonFunctions.$in = function (a, b) {
602
+ var i;
603
+
604
+ if (!util.isArray(b)) { throw new Error("$in operator called with a non-array"); }
605
+
606
+ for (i = 0; i < b.length; i += 1) {
607
+ if (areThingsEqual(a, b[i])) { return true; }
608
+ }
609
+
610
+ return false;
611
+ };
612
+
613
+ comparisonFunctions.$nin = function (a, b) {
614
+ if (!util.isArray(b)) { throw new Error("$nin operator called with a non-array"); }
615
+
616
+ return !comparisonFunctions.$in(a, b);
617
+ };
618
+
619
+ comparisonFunctions.$regex = function (a, b) {
620
+ if (!util.isRegExp(b)) { throw new Error("$regex operator called with non regular expression"); }
621
+
622
+ if (typeof a !== 'string') {
623
+ return false
624
+ } else {
625
+ return b.test(a);
626
+ }
627
+ };
628
+
629
+ comparisonFunctions.$exists = function (value, exists) {
630
+ if (exists || exists === '') { // This will be true for all values of exists except false, null, undefined and 0
631
+ exists = true; // That's strange behaviour (we should only use true/false) but that's the way Mongo does it...
632
+ } else {
633
+ exists = false;
634
+ }
635
+
636
+ if (value === undefined) {
637
+ return !exists
638
+ } else {
639
+ return exists;
640
+ }
641
+ };
642
+
643
+ // Specific to arrays
644
+ comparisonFunctions.$size = function (obj, value) {
645
+ if (!util.isArray(obj)) { return false; }
646
+ if (value % 1 !== 0) { throw new Error("$size operator called without an integer"); }
647
+
648
+ return (obj.length == value);
649
+ };
650
+ comparisonFunctions.$elemMatch = function (obj, value) {
651
+ if (!util.isArray(obj)) { return false; }
652
+ var i = obj.length;
653
+ var result = false; // Initialize result
654
+ while (i--) {
655
+ if (match(obj[i], value)) { // If match for array element, return true
656
+ result = true;
657
+ break;
658
+ }
659
+ }
660
+ return result;
661
+ };
662
+ arrayComparisonFunctions.$size = true;
663
+ arrayComparisonFunctions.$elemMatch = true;
664
+
665
+
666
+ /**
667
+ * Match any of the subqueries
668
+ * @param {Model} obj
669
+ * @param {Array of Queries} query
670
+ */
671
+ logicalOperators.$or = function (obj, query) {
672
+ var i;
673
+
674
+ if (!util.isArray(query)) { throw new Error("$or operator used without an array"); }
675
+
676
+ for (i = 0; i < query.length; i += 1) {
677
+ if (match(obj, query[i])) { return true; }
678
+ }
679
+
680
+ return false;
681
+ };
682
+
683
+
684
+ /**
685
+ * Match all of the subqueries
686
+ * @param {Model} obj
687
+ * @param {Array of Queries} query
688
+ */
689
+ logicalOperators.$and = function (obj, query) {
690
+ var i;
691
+
692
+ if (!util.isArray(query)) { throw new Error("$and operator used without an array"); }
693
+
694
+ for (i = 0; i < query.length; i += 1) {
695
+ if (!match(obj, query[i])) { return false; }
696
+ }
697
+
698
+ return true;
699
+ };
700
+
701
+
702
+ /**
703
+ * Inverted match of the query
704
+ * @param {Model} obj
705
+ * @param {Query} query
706
+ */
707
+ logicalOperators.$not = function (obj, query) {
708
+ return !match(obj, query);
709
+ };
710
+
711
+
712
+ /**
713
+ * Use a function to match
714
+ * @param {Model} obj
715
+ * @param {Query} query
716
+ */
717
+ logicalOperators.$where = function (obj, fn) {
718
+ var result;
719
+
720
+ if (!_.isFunction(fn)) { throw new Error("$where operator used without a function"); }
721
+
722
+ result = fn.call(obj);
723
+ if (!_.isBoolean(result)) { throw new Error("$where function must return boolean"); }
724
+
725
+ return result;
726
+ };
727
+
728
+
729
+ /**
730
+ * Tell if a given document matches a query
731
+ * @param {Object} obj Document to check
732
+ * @param {Object} query
733
+ */
734
+ function match (obj, query) {
735
+ var queryKeys, queryKey, queryValue, i;
736
+
737
+ // Primitive query against a primitive type
738
+ // This is a bit of a hack since we construct an object with an arbitrary key only to dereference it later
739
+ // But I don't have time for a cleaner implementation now
740
+ if (isPrimitiveType(obj) || isPrimitiveType(query)) {
741
+ return matchQueryPart({ needAKey: obj }, 'needAKey', query);
742
+ }
743
+
744
+ // Normal query
745
+ queryKeys = Object.keys(query);
746
+ for (i = 0; i < queryKeys.length; i += 1) {
747
+ queryKey = queryKeys[i];
748
+ queryValue = query[queryKey];
749
+
750
+ if (queryKey[0] === '$') {
751
+ if (!logicalOperators[queryKey]) { throw new Error("Unknown logical operator " + queryKey); }
752
+ if (!logicalOperators[queryKey](obj, queryValue)) { return false; }
753
+ } else {
754
+ if (!matchQueryPart(obj, queryKey, queryValue)) { return false; }
755
+ }
756
+ }
757
+
758
+ return true;
759
+ };
760
+
761
+
762
+ /**
763
+ * Match an object against a specific { key: value } part of a query
764
+ * if the treatObjAsValue flag is set, don't try to match every part separately, but the array as a whole
765
+ */
766
+ function matchQueryPart (obj, queryKey, queryValue, treatObjAsValue) {
767
+ var objValue = getDotValue(obj, queryKey)
768
+ , i, keys, firstChars, dollarFirstChars;
769
+
770
+ // Check if the value is an array if we don't force a treatment as value
771
+ if (util.isArray(objValue) && !treatObjAsValue) {
772
+ // If the queryValue is an array, try to perform an exact match
773
+ if (util.isArray(queryValue)) {
774
+ return matchQueryPart(obj, queryKey, queryValue, true);
775
+ }
776
+
777
+ // Check if we are using an array-specific comparison function
778
+ if (queryValue !== null && typeof queryValue === 'object' && !util.isRegExp(queryValue)) {
779
+ keys = Object.keys(queryValue);
780
+ for (i = 0; i < keys.length; i += 1) {
781
+ if (arrayComparisonFunctions[keys[i]]) { return matchQueryPart(obj, queryKey, queryValue, true); }
782
+ }
783
+ }
784
+
785
+ // If not, treat it as an array of { obj, query } where there needs to be at least one match
786
+ for (i = 0; i < objValue.length; i += 1) {
787
+ if (matchQueryPart({ k: objValue[i] }, 'k', queryValue)) { return true; } // k here could be any string
788
+ }
789
+ return false;
790
+ }
791
+
792
+ // queryValue is an actual object. Determine whether it contains comparison operators
793
+ // or only normal fields. Mixed objects are not allowed
794
+ if (queryValue !== null && typeof queryValue === 'object' && !util.isRegExp(queryValue) && !util.isArray(queryValue)) {
795
+ keys = Object.keys(queryValue);
796
+ firstChars = _.map(keys, function (item) { return item[0]; });
797
+ dollarFirstChars = _.filter(firstChars, function (c) { return c === '$'; });
798
+
799
+ if (dollarFirstChars.length !== 0 && dollarFirstChars.length !== firstChars.length) {
800
+ throw new Error("You cannot mix operators and normal fields");
801
+ }
802
+
803
+ // queryValue is an object of this form: { $comparisonOperator1: value1, ... }
804
+ if (dollarFirstChars.length > 0) {
805
+ for (i = 0; i < keys.length; i += 1) {
806
+ if (!comparisonFunctions[keys[i]]) { throw new Error("Unknown comparison function " + keys[i]); }
807
+
808
+ if (!comparisonFunctions[keys[i]](objValue, queryValue[keys[i]])) { return false; }
809
+ }
810
+ return true;
811
+ }
812
+ }
813
+
814
+ // Using regular expressions with basic querying
815
+ if (util.isRegExp(queryValue)) { return comparisonFunctions.$regex(objValue, queryValue); }
816
+
817
+ // queryValue is either a native value or a normal object
818
+ // Basic matching is possible
819
+ if (!areThingsEqual(objValue, queryValue)) { return false; }
820
+
821
+ return true;
822
+ }
823
+
824
+
825
+ // Interface
826
+ module.exports.serialize = serialize;
827
+ module.exports.deserialize = deserialize;
828
+ module.exports.deepCopy = deepCopy;
829
+ module.exports.checkObject = checkObject;
830
+ module.exports.isPrimitiveType = isPrimitiveType;
831
+ module.exports.modify = modify;
832
+ module.exports.getDotValue = getDotValue;
833
+ module.exports.match = match;
834
+ module.exports.areThingsEqual = areThingsEqual;
835
+ module.exports.compareThings = compareThings;