-
Notifications
You must be signed in to change notification settings - Fork 1
/
sdb.js
1293 lines (1049 loc) · 35.3 KB
/
sdb.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Copyright 2020 Andrew Hodel
// License MIT
'use strict';
var fs = require('fs');
var crypto = require('crypto');
var sdb = function(path=false) {
if (path) {
if (fs.existsSync(path)) {
// read from path
var local_sdb = JSON.parse(fs.readFileSync(path));
for (var key in local_sdb) {
this[key] = local_sdb[key];
}
} else {
// create that file
this.docs = [];
this.indexes = {};
fs.writeFileSync(path, JSON.stringify(this).toString('utf-8'));
}
} else {
this.docs = [];
this.indexes = {};
}
this.canUse = 1;
}
sdb.prototype.insert = function(doc, already_blocking=false) {
// doc is an object that is the document
// ensure that no field names exist with _ as the first character
for (var field in doc) {
if (field[0] == '_') {
return 'Documents cannot contain fields that start with an _, like '+field;
}
}
if (already_blocking === false) {
// wait for access to the db
while (this.canUse == 0) {
// wait
}
this.canUse = 0;
}
// add an _id to the document
doc._id = crypto.createHash('sha1').update(Math.random().toString() + (new Date()).valueOf().toString()).digest('hex');
var error = null;
// test all required_fields in any indexes and make sure this document has them all
for (var field in this.indexes) {
if (this.indexes[field].required_field) {
// this is a required field
// check that the document has this field
if (typeof(doc[field]) == 'undefined') {
error = 'This document is missing the field "'+field+'" that is required by an index.';
break;
}
}
}
// test if any indexes exist for any of these fields
var add_to_indexes = [];
for (var field in doc) {
if (error != null) {
break;
}
if (typeof(this.indexes[field]) == 'object') {
// an index exists for this field
if (this.indexes[field].unique) {
// this is a unique index, test that this value is unique
for (var c=0; c<this.indexes[field].values.length; c++) {
if (this.indexes[field].values[c].value == doc[field]) {
error = 'The field "'+field+'" in this document is indexed as unique and the value already exists in the index.';
break;
}
}
}
// add this field and value to the indexes
add_to_indexes.push({field: field, value: doc[field]});
}
}
if (error == null) {
// add doc to docs
this.docs.push(doc);
if (add_to_indexes.length > 0) {
// add this document to all of the indexed fields in add_to_indexes
// the position is this.docs.length-1 because it was just added to this.docs
for (var c=0; c<add_to_indexes.length; c++) {
for (var field in this.indexes) {
if (add_to_indexes[c].field == field) {
// matching index
var found_matching_value = false;
for (var r=0; r<this.indexes[field].values.length; r++) {
if (this.indexes[field].values[r].value == add_to_indexes[c].value) {
// this is an existing value in the index
this.indexes[field].values[r].positions.push(this.docs.length-1);
found_matching_value = true;
break;
}
}
if (!found_matching_value) {
// this is a new value in the index
this.indexes[field].values.push({value: add_to_indexes[c].value, positions: [this.docs.length-1]});
}
}
}
}
}
}
if (already_blocking === false) {
// release the atomic hold
this.canUse = 1;
}
if (error == null) {
// return the document
return doc;
} else {
return error;
}
};
// must be a prototype child to access the this object of the module
var index_find = function(sdb_object, docs, positions, query) {
// returns [docs, positions, query]
// index search
for (var key in query) {
var key_found_in_index = false;
// convert regex operator searches to native javascript RegExp
if (typeof(query[key]) == 'object') {
for (var op in query[key]) {
if (op == '$regex') {
// this is a regex search, meaning that
// string should equate to a regex
// like '/asdf/i'
// if the first character is / then the regex is a string in regex format, like /asdf/i
// there can be a total of 5 flags after the last / in the regex, like /asdf/gimuy
// find the position of the last / in the string
var lastSlash = query[key][op].lastIndexOf('/');
// now generate the regex with the flags
var s = query[key][op].slice(1, lastSlash);
var flags = query[key][op].slice(lastSlash+1);
// convert it to a native javascript RegExp
query[key] = new RegExp(s, flags);
}
}
}
var do_index_search = false;
if (typeof(sdb_object.indexes[key]) == 'object') {
// an index exists for this key
do_index_search = true;
}
if (do_index_search) {
/*
if (query[key] instanceof RegExp) {
console.log('doing a regex search through indexes');
} else {
console.log('doing a search through indexes');
}
*/
// do an operator search
var op_search = false;
if (query[key] instanceof Object) {
// this is an operator search
op_search = true;
}
// check each index value and look for a match
for (var c=0; c<sdb_object.indexes[key].values.length; c++) {
if (op_search === true) {
// there's no operator index searches currently
//console.log('index operator search unsupported', query[key]);
} else if (sdb_object.indexes[key].values[c].value == query[key] || (query[key] instanceof RegExp && sdb_object.indexes[key].values[c].value.search(query[key]) > -1)) {
//console.log(key, sdb_object.indexes[key].values[c].value, query[key]);
key_found_in_index = true;
// this is a string or regex search
// found a matching value, add all these documents to docs
for (var n=0; n<sdb_object.indexes[key].values[c].positions.length; n++) {
// ensure the position isn't already added
var existing_position = false;
for (var l=0; l<positions.length; l++) {
if (positions[l][0] == sdb_object.indexes[key].values[c].positions[n]) {
// position is already found
existing_position = true;
// increase the relevance of the document
docs[positions[l][1]]._relevance++;
break;
}
}
if (existing_position) {
continue;
}
var t_doc = sdb_object.docs[sdb_object.indexes[key].values[c].positions[n]];
// add the relevance, the number of matched fields
try {
t_doc._relevance = 1;
} catch (err) {
console.log(err);
console.log('t_doc', t_doc);
console.log('sdb_object.docs.length', sdb_object.docs.length);
console.log('sdb_object.indexes[key].values[c]', sdb_object.indexes[key].values[c]);
console.log('sdb_object.indexes[key].values[c].positions[n]', sdb_object.indexes[key].values[c].positions[n]);
process.exit();
}
docs.push(t_doc);
positions.push([sdb_object.indexes[key].values[c].positions[n], docs.length-1]);
}
}
}
if (op_search === false && key_found_in_index === true) {
// the key was not an op search
// and the index found a result
delete query[key];
}
}
}
return [docs, positions, query];
}
var deep_find_in_doc = function(query, doc) {
// returns match, relevance_mod, has_fulltext, all_query_fields_match
var match = 0;
var relevance_mod = 0;
var has_fulltext = false;
var all_query_fields_match = false;
if (Object.keys(query).length === 0) {
// should return as a match every time
// there was no query
return [1, 1, false, true];
}
var query_len = Object.keys(query).length;
var matched_field_count = 0;
for (var key in query) {
if (query[key] !== null && typeof(query[key]) === 'object') {
if (query[key]['$undef'] !== undefined) {
// the field does not exist in the document
// make sure there is no $undef operator for this field
if (doc[key] === undefined) {
match++;
matched_field_count++;
//console.log('$undef match', key);
continue;
}
}
}
for (var doc_key in doc) {
if (doc_key == key) {
if (doc[doc_key] == query[key] || (query[key] instanceof RegExp && doc[doc_key].search(query[key]) > -1)) {
//console.log('field search in ' + key);
// this is an exact string match or a regex match
match++;;
matched_field_count++;
} else if (query[key] instanceof Object) {
// test if the search key is an operator
//console.log('operator search', query[key]);
var op_field_match = false;
// do an operator search
for (var op in query[key]) {
if (op == '$gt') {
// test if the doc's field's value is greater than the search value
if (Number(doc[doc_key]) > Number(query[key][op])) {
op_field_match = true;
match++;
}
} else if (op == '$gte') {
// test if the doc's field's value is greater than or equal to the search value
if (Number(doc[doc_key]) >= Number(query[key][op])) {
op_field_match = true;
match++;
}
} else if (op == '$lt') {
// test if the doc's field's value is less than the search value
if (Number(doc[doc_key]) < Number(query[key][op])) {
op_field_match = true;
match++;
}
} else if (op == '$lte') {
// test if the doc's field's value is less than or equal to the search value
if (Number(doc[doc_key]) <= Number(query[key][op])) {
op_field_match = true;
match++;
}
} else if (op == '$ne') {
// test if the doc's field's value is not equal to the search value
// works for numbers and strings
if (doc[doc_key] != query[key][op]) {
op_field_match = true;
match++;
}
} else if (op == '$mod') {
// test if the doc's field's value modulus the search value equals 0
if (Number(doc[doc_key]) % Number(query[key][op]) === 0) {
op_field_match = true;
match++;
}
} else if (op == '$fulltext') {
// perform a fulltext search and return how relevant each document is
// first split up each of the words in the search query using the space character
var spaced = query[key][op].split(' ');
// remove simple words from spaced, these are of no use in a full text search
var simple = ['i', 'you', 'the', 'this', 'is', 'of', 'a', 'we', 'us', 'it', 'them', 'they'];
for (var r=spaced.length-1; r>=0; r--) {
for (var n=0; n<simple.length; n++) {
if (spaced[r].toLowerCase() == simple[n] || spaced[r].length == 1) {
spaced.splice(r, 1);
break;
}
}
}
// now loop through the field and test how many times each word was found
var words = doc[doc_key].split(' ');
for (var r=0; r<words.length; r++) {
for (var n=0; n<spaced.length; n++) {
if (words[r].toLowerCase() == spaced[n].toLowerCase()) {
// increase the relevance by one divided by the total searched words found
relevance_mod += (1/words.length);
has_fulltext = true;
op_field_match = true;
}
}
}
}
}
if (op_field_match === true) {
matched_field_count++;
}
}
}
}
}
// this allows searching with {$op: {field: 1, field1: 1}}
// or use the code above to search like {field: {$lt: 10, $gt: 1}}
/*
for (var key in query) {
if (query[key] instanceof Object) {
var op_field_match = false;
// do an operator search
for (var op in query[key]) {
if (key == '$gt') {
// test if the doc's field's value is greater than the search value
if (Number(doc[op]) > Number(query[key][op])) {
op_field_match = true;
match++;
}
} else if (key == '$gte') {
// test if the doc's field's value is greater than or equal to the search value
if (Number(doc[op]) >= Number(query[key][op])) {
op_field_match = true;
match++;
}
} else if (key == '$lt') {
// test if the doc's field's value is less than the search value
if (Number(doc[op]) < Number(query[key][op])) {
op_field_match = true;
match++;
}
} else if (key == '$lte') {
// test if the doc's field's value is less than or equal to the search value
if (Number(doc[op]) <= Number(query[key][op])) {
op_field_match = true;
match++;
}
} else if (key == '$undef') {
// test if the field is not defined
if (doc[op] === undefined) {
op_field_match = true;
match++;
}
} else if (op == '$fulltext') {
// perform a fulltext search and return how relevant each document is
// first split up each of the words in the search query using the space character
var spaced = query[key][op].split(' ');
// remove simple words from spaced, these are of no use in a full text search
var simple = ['i', 'you', 'the', 'this', 'is', 'of', 'a', 'we', 'us', 'it', 'them', 'they'];
for (var r=spaced.length-1; r>=0; r--) {
for (var n=0; n<simple.length; n++) {
if (spaced[r].toLowerCase() == simple[n] || spaced[r].length == 1) {
spaced.splice(r, 1);
break;
}
}
}
// now loop through the field and test how many times each word was found
var words = doc[key].split(' ');
for (var r=0; r<words.length; r++) {
for (var n=0; n<spaced.length; n++) {
if (words[r].toLowerCase() == spaced[n].toLowerCase()) {
// increase the relevance by one divided by the total searched words found
relevance_mod += (1/words.length);
has_fulltext = true;
op_field_match = true;
}
}
}
}
}
if (op_field_match === true) {
matched_field_count++;
}
} else {
// exact match test
if (doc[key] == query[key] || (query[key] instanceof RegExp && doc[key].search(query[key]) > -1)) {
// this is an exact match or a regex match
match++;
matched_field_count++;
}
}
}
// end operator/field inversion
*/
if (matched_field_count == query_len) {
all_query_fields_match = true;
}
return [match, relevance_mod, has_fulltext, all_query_fields_match];
}
sdb.prototype.find = function(query, require_all_keys=true) {
// query is an object of what to search by
// require_all_keys is true by default and requires all query keys to be matched
// wait for access to the db
while (this.canUse == 0) {
// wait
}
this.canUse = 0;
var keys_length = Object.keys(query).length;
if (keys_length == 0) {
// return the whole db as a deep copy
var ret_val = JSON.parse(JSON.stringify(this.docs));
// release the atomic hold
this.canUse = 1;
return ret_val;
}
var docs = [];
var positions = [];
// docs, positions, query are returned
var index_return_array = index_find(this, docs, positions, query);
docs = index_return_array[0];
positions = index_return_array[1];
query = index_return_array[2];
// if there are any remaining keys, do a deep search using them
// meaning search by the fields that were not indexed
if (Object.keys(query).length > 0) {
var has_fulltext = false;
// search through all docs in the sdb database for this query
for (var c=0; c<this.docs.length; c++) {
// returns match, relevance_mod, has_fulltext, all_query_fields_match
var deep_find_doc_result = deep_find_in_doc(query, this.docs[c]);
var match = deep_find_doc_result[0];
var relevance_mod = deep_find_doc_result[1];
has_fulltext = deep_find_doc_result[2];
var all_query_fields_match = deep_find_doc_result[3];
if ((relevance_mod > 0 || match > 0) && (require_all_keys === all_query_fields_match)) {
// this document matches the require_all_keys argument passed to find()
// AND
// relevance_mod > 0 is a $fulltext search match
// match > 0 is the number of fields matched
// check if this document has already been found
var existing_position = false;
for (var l=0; l<positions.length; l++) {
if (positions[l][0] == c) {
// this document is already added
existing_position = true;
// increase the relevance of the document
if (relevance_mod == 0) {
// this is a non $fulltext match
docs[positions[l][1]]._relevance++;
} else {
// this is a $fulltext match, the relevance is a float value
docs[positions[l][1]]._relevance += relevance_mod;
}
break;
}
}
if (existing_position) {
continue;
}
// add the document
var t_doc = this.docs[c];
// add the relevance
t_doc._relevance = relevance_mod;
docs.push(t_doc);
positions.push([c, docs.length-1]);
} else {
// this doc was not matched in a deep search
if (require_all_keys === true) {
// remove it from the response docs, if it was found in an index
// because the rest of the query was not found in the deep search
// and all of it was required
var l = 0;
while (l < docs.length) {
// this is each doc found in the index search
if (docs[l]._id === this.docs[c]._id) {
docs.splice(l, 1);
break;
}
l++;
}
}
}
}
}
// the documents have to be returned as a deep copy
// to avoid being accidently modified
var ret_val = JSON.parse(JSON.stringify(docs));
// release the atomic hold
this.canUse = 1;
return ret_val;
};
//array.sort(naturalSort)
function naturalSort(a, b) {
var re = /(^([+\-]?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?(?=\D|\s|$))|^0x[\da-fA-F]+$|\d+)/g,
sre = /^\s+|\s+$/g, // trim pre-post whitespace
snre = /\s+/g, // normalize all whitespace to single ' ' character
dre = /(^([\w ]+,?[\w ]+)?[\w ]+,?[\w ]+\d+:\d+(:\d+)?[\w ]?|^\d{1,4}[\/\-]\d{1,4}[\/\-]\d{1,4}|^\w+, \w+ \d+, \d{4})/,
hre = /^0x[0-9a-f]+$/i,
ore = /^0/,
i = function(s) {
return (naturalSort.insensitive && ('' + s).toLowerCase() || '' + s).replace(sre, '');
},
// convert all to strings strip whitespace
x = i(a),
y = i(b),
// chunk/tokenize
xN = x.replace(re, '\0$1\0').replace(/\0$/, '').replace(/^\0/, '').split('\0'),
yN = y.replace(re, '\0$1\0').replace(/\0$/, '').replace(/^\0/, '').split('\0'),
// numeric, hex or date detection
xD = parseInt(x.match(hre), 16) || (xN.length !== 1 && Date.parse(x)),
yD = parseInt(y.match(hre), 16) || xD && y.match(dre) && Date.parse(y) || null,
normChunk = function(s, l) {
// normalize spaces; find floats not starting with '0', string or 0 if not defined (Clint Priest)
return (!s.match(ore) || l == 1) && parseFloat(s) || s.replace(snre, ' ').replace(sre, '') || 0;
},
oFxNcL, oFyNcL;
// first try and sort Hex codes or Dates
if (yD) {
if (xD < yD) {
return -1;
} else if (xD > yD) {
return 1;
}
}
// natural sorting through split numeric strings and default strings
for (var cLoc = 0, xNl = xN.length, yNl = yN.length, numS = Math.max(xNl, yNl); cLoc < numS; cLoc++) {
oFxNcL = normChunk(xN[cLoc] || '', xNl);
oFyNcL = normChunk(yN[cLoc] || '', yNl);
// handle numeric vs string comparison - number < string - (Kyle Adams)
if (isNaN(oFxNcL) !== isNaN(oFyNcL)) {
return isNaN(oFxNcL) ? 1 : -1;
}
// if unicode use locale comparison
if (/[^\x00-\x80]/.test(oFxNcL + oFyNcL) && oFxNcL.localeCompare) {
var comp = oFxNcL.localeCompare(oFyNcL);
return comp / Math.abs(comp);
}
if (oFxNcL < oFyNcL) {
return -1;
} else if (oFxNcL > oFyNcL) {
return 1;
}
}
}
sdb.prototype.sort = function(sort, docs) {
while (this.canUse == 0) {
// wait
}
this.canUse = 0;
var ret_docs = JSON.parse(JSON.stringify(docs));
var ret_sorted_docs = [];
var sorted_values = [];
var append_docs = [];
// sort ret_docs by a field value
// {field: sortType}
// highest_first - Z10-A0
// lowest_first - A0-Z10
for (var c=0; c<ret_docs.length; c++) {
if (typeof(ret_docs[c][Object.keys(sort)[0]]) != 'undefined') {
// this field has a value for this document
// add it to the list to of ids and values to be sorted
sorted_values.push(ret_docs[c][Object.keys(sort)[0]]);
} else {
// add it to the list of documents to append to ret_sorted_docs
append_docs.push(ret_docs[c]);
}
}
// sort the values
sorted_values.sort(naturalSort);
// reverse the values if the user specified highest_first
if (sort[Object.keys(sort)[0]] == 'highest_first') {
sorted_values.reverse();
}
// now loop through the sorted_values and find each corresponding ret_doc
// to place it in ret_sorted_docs
for (var c=0; c<sorted_values.length; c++) {
for (var d=0; d<ret_docs.length; d++) {
if (ret_docs[d][Object.keys(sort)[0]] == sorted_values[c]) {
ret_sorted_docs.push(ret_docs[d]);
ret_docs.splice(d, 1);
break;
}
}
}
// append the docs in append_docs
for (var c=0; c<append_docs.length; c++) {
ret_sorted_docs.push(append_docs[c]);
}
this.canUse = 1;
return ret_sorted_docs;
};
sdb.prototype.limit = function(len, docs) {
while (this.canUse == 0) {
// wait
}
this.canUse = 0;
var ret_docs = JSON.parse(JSON.stringify(docs));
// leave only the first len values in docs
for (var c=docs.length-1; c>=0; c--) {
if (c >= len) {
ret_docs.splice(c, 1);
} else {
break;
}
}
this.canUse = 1;
return ret_docs;
};
sdb.prototype.skip = function(len, docs) {
while (this.canUse == 0) {
// wait
}
this.canUse = 0;
var ret_docs = [];
// skip the first len documents
for (var c=0; c<docs.length; c++) {
if (c < len) {
// skip
continue;
} else {
ret_docs.push(docs[c]);
}
}
this.canUse = 1;
return ret_docs;
};
var modifier_update_doc = function(update, existing_doc={}) {
var updated_doc = {};
// generate an array containing every field that will be updated using the modifiers
// to copy all of the other fields in the existing document to updated_doc
var modded_fields = [];
// loop through each modifier
for (var mod in update) {
// mod will be the modifier to use and update[mod] will be an object containing fields and values to use the modifier on
for (var field in update[mod]) {
var field_already_modded = false;
for (var l=0; l<modded_fields.length; l++) {
if (modded_fields[l] == field) {
field_already_modded = true;
break;
}
}
if (!field_already_modded) {
modded_fields.push(field);
}
}
}
// with all the modded_fields
// loop through the existing document and add any non-modded fields to updated_doc
for (var afield in existing_doc) {
var afield_exists = false;
for (var o=0; o<modded_fields.length; o++) {
if (modded_fields[o] == afield) {
afield_exists = true;
break;
}
}
if (!afield_exists) {
// actually add the non modded field to updated_doc
updated_doc[afield] = existing_doc[afield];
}
}
// go back through the modifiers and process the updates for each field
for (var mod in update) {
// mod will be the modifier to use and update[mod] will be an object containing fields and values to use the modifier on
for (var field in update[mod]) {
// apply the modifier to the value in the existing doc and copy the field to updated_doc
switch (mod) {
case '$set':
updated_doc[field] = update[mod][field];
break;
case '$remove':
// don't add it to the updated_doc
break;
case '$add':
if (typeof(existing_doc[field]) == 'undefined') {
updated_doc[field] = Number(update[mod][field]);
} else {
updated_doc[field] = Number(existing_doc[field])+Number(update[mod][field]);
}
break;
case '$subtract':
if (typeof(existing_doc[field]) == 'undefined') {
updated_doc[field] = -Number(update[mod][field]);
} else {
updated_doc[field] = Number(existing_doc[field])-Number(update[mod][field]);
}
break;
case '$multiply':
if (typeof(existing_doc[field]) == 'undefined') {
updated_doc[field] = 0;
} else {
updated_doc[field] = Number(existing_doc[field])*Number(update[mod][field]);
}
break;
case '$divide':
if (typeof(existing_doc[field]) == 'undefined') {
updated_doc[field] = 0;
} else {
updated_doc[field] = Number(existing_doc[field])/Number(update[mod][field]);
}
break;
}
}
}
return updated_doc;
}
sdb.prototype.update = function(query, update, options=null) {
if (options == null) {
options = {};
options.multi = false;
options.upsert = false;
}
var error = null;
// ensure that no field names exist with _ as the first character
for (var field in update) {
if (field[0] == '_') {
return 'Documents cannot contain fields that start with an _, like '+field;
}
}
while (this.canUse == 0) {
// wait
}
this.canUse = 0;
var keys_length = Object.keys(query).length;
// set modifier status
var is_modifier = 0;
for (var key in update) {
if (key == '$set' || key == '$remove' || key == '$add' || key == '$subtract' || key == '$multiply' || key == '$divide') {
is_modifier = 1;
break;
}
}
var updated_docs = [];
// search through the keys and find matching documents
var num_updated_docs = 0;
for (var c=0; c<this.docs.length; c++) {
// returns match, relevance_mod, has_fulltext, all_query_fields_match
var deep_find_doc_result = deep_find_in_doc(query, this.docs[c]);
var match = deep_find_doc_result[0];
var relevance_mod = deep_find_doc_result[1];
var all_query_fields_match = deep_find_doc_result[3];
if ((relevance_mod > 0 || match > 0) && all_query_fields_match === true) {
// all_query_fields_match means safe to modify/delete
// relevance_mod > 0 is a $fulltext search match
// match > 0 is the number of matching fields
var updated_doc;
if (is_modifier) {
// this is a modifier update
// it will be $set, $remove, $add, $subtract, $multiply, $divide
updated_doc = modifier_update_doc(update, this.docs[c]);
} else {
// this is a whole document update
updated_doc = update;
}
// save the _id
updated_doc._id = this.docs[c]._id;
// need to update the indexes here
for (var field in this.indexes) {
// first check if this index is a required_field and ensure it exists in the updated_doc
if (this.indexes[field].required_field) {
if (typeof(updated_doc[field]) == 'undefined') {
error = 'The update does not include the field "'+field+'" that is required by an index.';
break;
}
}
// then check if this updated_doc actually has this field
if (typeof(updated_doc[field]) != 'undefined') {
// this updated_doc has this index field
// test if the index is unique
if (this.indexes[field].unique) {
// this is a unique index, test if the updated_doc's field's value exists in the index
for (var n=0; n<this.indexes[field].values.length; n++) {
if (this.indexes[field].values[n].value == updated_doc[field] && this.indexes[field].values[n].positions[0] != c) {
// this value for this field already exists in this unique index
// and it is not the already existing document
error = 'The unique index for "'+field+'" already has the value "'+updated_doc[field]+'" so the update failed.';
break;
}
}
}
}
if (error != null) {
// there was an error, no need to continue going through the fields in this.indexes
break;
}
}
if (error != null) {
// there was an error, break here to avoid updating the document
break;
}
// there was no error with the proposed insertion of the indexes
// loop through every index field and every value within
// and remove any occurences with a position of that of the original document
for (var field in this.indexes) {
for (var n=this.indexes[field].values.length-1; n>=0; n--) {
for (var p=this.indexes[field].values[n].positions.length-1; p>=0; p--) {
if (this.indexes[field].values[n].positions[p] == c) {
// the original (non updated) document
// had a position here, remove it
this.indexes[field].values[n].positions.splice(p, 1);
//break;
}
}
if (this.indexes[field].values[n].positions.length == 0) {
// there are no positions left for this value, go ahead and remove the value
this.indexes[field].values.splice(n, 1);
}
}
// add the position of each value for this field from the document
if (typeof(updated_doc[field]) != 'undefined') {