sorla-db 1.2.0 → 1.4.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/README.md +138 -0
- package/dist/sorla.min.js +1 -1
- package/index.d.ts +125 -5
- package/package.json +1 -1
- package/src/dbs/database.js +48 -0
- package/src/documents/cursor.js +61 -9
- package/src/events.js +69 -0
- package/src/query_language/manipulation.js +329 -34
- package/src/query_language/operators.js +66 -1
- package/src/query_language/query.js +6 -2
- package/src/query_language/update.js +167 -0
- package/src/schema/schema.js +306 -0
- package/src/sorla.js +31 -0
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
const documents = require('../documents/document.js');
|
|
2
2
|
const operatorsCheck = require('./operators.js');
|
|
3
|
+
const emitter = require('../events.js');
|
|
4
|
+
const { applyUpdate } = require('./update.js');
|
|
5
|
+
const { compileSchema, ValidationError } = require('../schema/schema.js');
|
|
3
6
|
|
|
4
|
-
class manipulation {
|
|
7
|
+
class manipulation extends emitter {
|
|
5
8
|
constructor(validationSchema = null) {
|
|
9
|
+
super();
|
|
6
10
|
this.validationSchema = validationSchema;
|
|
7
11
|
this._index = {};
|
|
8
12
|
this._prohibitedCharacters = ['$', '.'];
|
|
@@ -21,34 +25,72 @@ class manipulation {
|
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
/**
|
|
24
|
-
*
|
|
28
|
+
* Sets the validation schema of the collection (JSON Schema subset or a function).
|
|
29
|
+
* Documents inserted, updated or replaced afterwards must satisfy it; existing ones are not checked.
|
|
25
30
|
* @author Anderson Arruda < anderson@sysborg.com.br >
|
|
26
|
-
* @version 1.
|
|
27
|
-
* @param validationSchema
|
|
31
|
+
* @version 1.4.0
|
|
32
|
+
* @param object|function|null validationSchema
|
|
28
33
|
* @return void
|
|
29
34
|
**/
|
|
30
35
|
set validationSchema(validationSchema)
|
|
31
36
|
{
|
|
32
|
-
if(
|
|
33
|
-
return;
|
|
34
|
-
|
|
35
|
-
if(['object', 'function'].indexOf(typeof schemaValidation) == -1)
|
|
37
|
+
if(validationSchema === null || typeof validationSchema === 'undefined')
|
|
36
38
|
{
|
|
37
|
-
|
|
39
|
+
this._schema = null;
|
|
40
|
+
return;
|
|
38
41
|
}
|
|
39
42
|
|
|
40
|
-
|
|
43
|
+
if(['object', 'function'].indexOf(typeof validationSchema) === -1 || Array.isArray(validationSchema))
|
|
44
|
+
throw new Error('The validation schema must be an object (JSON Schema) or a function');
|
|
45
|
+
|
|
46
|
+
this._schema = compileSchema(validationSchema);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The schema given to the collection, or null
|
|
51
|
+
* @return object|function|null
|
|
52
|
+
*/
|
|
53
|
+
get validationSchema()
|
|
54
|
+
{
|
|
55
|
+
return this._schema ? this._schema.source : null;
|
|
41
56
|
}
|
|
42
57
|
|
|
43
58
|
/**
|
|
44
|
-
*
|
|
59
|
+
* @param object|function|null schema
|
|
60
|
+
* @return this
|
|
61
|
+
*/
|
|
62
|
+
setSchema(schema)
|
|
63
|
+
{
|
|
64
|
+
this.validationSchema = schema;
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
getSchema()
|
|
69
|
+
{
|
|
70
|
+
return this.validationSchema;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Checks a document against the schema without storing it (e.g. to validate a form)
|
|
75
|
+
* @param object document
|
|
76
|
+
* @return object {valid, errors: [{path, rule, message}]}
|
|
77
|
+
*/
|
|
78
|
+
validate(document)
|
|
79
|
+
{
|
|
80
|
+
const errors = this._schema ? this._schema.validate(document) : [];
|
|
81
|
+
return { valid: errors.length === 0, errors };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Throws a ValidationError when the data does not satisfy the schema
|
|
45
86
|
* @param object data
|
|
46
|
-
* @return
|
|
87
|
+
* @return void
|
|
47
88
|
*/
|
|
48
89
|
_validate(data)
|
|
49
90
|
{
|
|
50
|
-
|
|
51
|
-
|
|
91
|
+
const errors = this._schema ? this._schema.validate(data) : [];
|
|
92
|
+
if(errors.length)
|
|
93
|
+
throw new ValidationError(errors);
|
|
52
94
|
}
|
|
53
95
|
|
|
54
96
|
/**
|
|
@@ -124,6 +166,33 @@ class manipulation {
|
|
|
124
166
|
* @return mixed the _id of the inserted document
|
|
125
167
|
**/
|
|
126
168
|
insertOne(document, callback)
|
|
169
|
+
{
|
|
170
|
+
const id = this._insertDocument(document);
|
|
171
|
+
this._emitChange('insert', { ids: [id], documents: [this._copy(this._documents.at(-1)._data)] });
|
|
172
|
+
|
|
173
|
+
if(typeof callback === 'function')
|
|
174
|
+
callback(id);
|
|
175
|
+
|
|
176
|
+
return id;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Validates and stores one document, without events
|
|
181
|
+
* @param object document
|
|
182
|
+
* @return mixed the _id
|
|
183
|
+
*/
|
|
184
|
+
_insertDocument(document)
|
|
185
|
+
{
|
|
186
|
+
return this._store(this._prepare(document));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Copy of a document ready to be stored: _id, schema defaults, keys and schema checked
|
|
191
|
+
* @param object document
|
|
192
|
+
* @param Set reserved ids taken by the same batch
|
|
193
|
+
* @return object
|
|
194
|
+
*/
|
|
195
|
+
_prepare(document, reserved = null)
|
|
127
196
|
{
|
|
128
197
|
if(!this._isDocument(document))
|
|
129
198
|
throw new Error('Invalid document type');
|
|
@@ -131,27 +200,38 @@ class manipulation {
|
|
|
131
200
|
if(!this._validateKeys(document))
|
|
132
201
|
throw new Error('Invalid document keys');
|
|
133
202
|
|
|
203
|
+
const taken = id => this._ids().has(id) || (reserved !== null && reserved.has(id));
|
|
134
204
|
const data = this._copy(document);
|
|
135
205
|
if(typeof data._id === 'undefined')
|
|
136
206
|
{
|
|
137
207
|
do {
|
|
138
208
|
data._id = this._generateKey();
|
|
139
|
-
} while(
|
|
209
|
+
} while(taken(data._id));
|
|
140
210
|
}
|
|
141
|
-
else if(
|
|
211
|
+
else if(taken(data._id))
|
|
142
212
|
{
|
|
143
213
|
throw new Error(`Duplicate _id "${data._id}"`);
|
|
144
214
|
}
|
|
145
215
|
|
|
146
|
-
if(
|
|
147
|
-
|
|
216
|
+
if(this._schema)
|
|
217
|
+
{
|
|
218
|
+
this._schema.applyDefaults(data, value => this._copy(value));
|
|
219
|
+
if(!this._validateKeys(data))
|
|
220
|
+
throw new Error('Invalid document keys');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
this._validate(data);
|
|
224
|
+
return data;
|
|
225
|
+
}
|
|
148
226
|
|
|
227
|
+
/**
|
|
228
|
+
* @param object data prepared document
|
|
229
|
+
* @return mixed the _id
|
|
230
|
+
*/
|
|
231
|
+
_store(data)
|
|
232
|
+
{
|
|
149
233
|
this._documents.push(new documents(data));
|
|
150
234
|
this._ids().add(data._id);
|
|
151
|
-
|
|
152
|
-
if(typeof callback === 'function')
|
|
153
|
-
callback(data._id);
|
|
154
|
-
|
|
155
235
|
return data._id;
|
|
156
236
|
}
|
|
157
237
|
|
|
@@ -168,27 +248,242 @@ class manipulation {
|
|
|
168
248
|
if(!Array.isArray(documents) || !documents.every(document => this._isDocument(document)))
|
|
169
249
|
throw new Error('Documents must be an array of objects');
|
|
170
250
|
|
|
171
|
-
|
|
172
|
-
|
|
251
|
+
// prepare (and validate) the whole batch before storing anything
|
|
252
|
+
const reserved = new Set();
|
|
253
|
+
const prepared = documents.map(document => {
|
|
254
|
+
const data = this._prepare(document, reserved);
|
|
255
|
+
reserved.add(data._id);
|
|
256
|
+
return data;
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const start = this._documents.length;
|
|
260
|
+
const ids = prepared.map(data => this._store(data));
|
|
261
|
+
this._emitChange('insert', { ids, documents: this._documents.slice(start).map(doc => this._copy(doc._data)) });
|
|
262
|
+
|
|
263
|
+
if(typeof callback === 'function')
|
|
264
|
+
callback(ids);
|
|
265
|
+
|
|
266
|
+
return ids;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Helpers the update operators need
|
|
271
|
+
* @return object
|
|
272
|
+
*/
|
|
273
|
+
_updateHelpers()
|
|
274
|
+
{
|
|
275
|
+
return {
|
|
276
|
+
copy: value => this._copy(value),
|
|
277
|
+
validateKeys: value => this._validateKeys(value),
|
|
278
|
+
operators: this._operators
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Document for an upsert: the equality conditions of the filter, then the update
|
|
284
|
+
* @param object filter
|
|
285
|
+
* @return object
|
|
286
|
+
*/
|
|
287
|
+
_upsertBase(filter)
|
|
288
|
+
{
|
|
289
|
+
const base = {};
|
|
290
|
+
for(const [key, value] of Object.entries(filter))
|
|
173
291
|
{
|
|
174
|
-
|
|
175
|
-
|
|
292
|
+
// only top level equality conditions describe the new document
|
|
293
|
+
if(key.startsWith('$') || key.includes('.') || this._operators.isRegExp(value))
|
|
294
|
+
continue;
|
|
295
|
+
|
|
296
|
+
if(this._operators.isOperatorObject(value))
|
|
297
|
+
{
|
|
298
|
+
if('$eq' in value)
|
|
299
|
+
base[key] = value.$eq;
|
|
176
300
|
|
|
177
|
-
if(typeof document._id === 'undefined')
|
|
178
301
|
continue;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
base[key] = value;
|
|
305
|
+
}
|
|
306
|
+
return base;
|
|
307
|
+
}
|
|
179
308
|
|
|
180
|
-
|
|
181
|
-
|
|
309
|
+
/**
|
|
310
|
+
* Applies an update to the matching documents
|
|
311
|
+
* @param object filter
|
|
312
|
+
* @param object update
|
|
313
|
+
* @param object options {upsert}
|
|
314
|
+
* @param boolean many
|
|
315
|
+
* @return object {matchedCount, modifiedCount, upsertedId}
|
|
316
|
+
*/
|
|
317
|
+
_update(filter, update, options, many)
|
|
318
|
+
{
|
|
319
|
+
this._requireFilter(filter);
|
|
320
|
+
const helpers = this._updateHelpers();
|
|
321
|
+
const changes = [];
|
|
182
322
|
|
|
183
|
-
|
|
323
|
+
// compute every new version first: one invalid update leaves all documents untouched
|
|
324
|
+
for(let i = 0; i < this._documents.length; i++)
|
|
325
|
+
{
|
|
326
|
+
const doc = this._documents[i];
|
|
327
|
+
if(!this._operators.matches(doc, filter))
|
|
328
|
+
continue;
|
|
329
|
+
|
|
330
|
+
const next = applyUpdate(doc._data, update, helpers);
|
|
331
|
+
this._validate(next);
|
|
332
|
+
changes.push({ index: i, previous: doc._data, next });
|
|
333
|
+
if(!many) break;
|
|
184
334
|
}
|
|
185
335
|
|
|
186
|
-
|
|
336
|
+
if(!changes.length && options.upsert)
|
|
337
|
+
{
|
|
338
|
+
const data = applyUpdate(this._upsertBase(filter), update, helpers);
|
|
339
|
+
const id = this._insertDocument(data);
|
|
340
|
+
this._emitChange('insert', { ids: [id], documents: [this._copy(this._documents.at(-1)._data)] });
|
|
341
|
+
return { matchedCount: 0, modifiedCount: 0, upsertedId: id };
|
|
342
|
+
}
|
|
187
343
|
|
|
188
|
-
|
|
189
|
-
|
|
344
|
+
const modified = changes.filter(change => JSON.stringify(change.previous) !== JSON.stringify(change.next));
|
|
345
|
+
for(const change of modified)
|
|
346
|
+
this._documents[change.index] = new documents(change.next);
|
|
190
347
|
|
|
191
|
-
|
|
348
|
+
if(modified.length)
|
|
349
|
+
{
|
|
350
|
+
this._emitChange('update', {
|
|
351
|
+
ids: modified.map(change => change.next._id),
|
|
352
|
+
documents: modified.map(change => this._copy(change.next)),
|
|
353
|
+
previous: modified.map(change => this._copy(change.previous))
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return { matchedCount: changes.length, modifiedCount: modified.length, upsertedId: null };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Updates the first matching document: updateOne({name: 'Ana'}, {$set: {age: 30}, $inc: {visits: 1}})
|
|
362
|
+
* Operators: $set, $unset, $inc, $push (with $each), $pull. The _id cannot change.
|
|
363
|
+
* @param object filter
|
|
364
|
+
* @param object update
|
|
365
|
+
* @param object options {upsert: insert when nothing matches}
|
|
366
|
+
* @return object {matchedCount, modifiedCount, upsertedId}
|
|
367
|
+
*/
|
|
368
|
+
updateOne(filter, update, options = {})
|
|
369
|
+
{
|
|
370
|
+
return this._update(filter, update, options, false);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Updates every matching document, see updateOne()
|
|
375
|
+
* @param object filter
|
|
376
|
+
* @param object update
|
|
377
|
+
* @param object options {upsert}
|
|
378
|
+
* @return object {matchedCount, modifiedCount, upsertedId}
|
|
379
|
+
*/
|
|
380
|
+
updateMany(filter, update, options = {})
|
|
381
|
+
{
|
|
382
|
+
return this._update(filter, update, options, true);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Replaces the first matching document, keeping its _id
|
|
387
|
+
* @param object filter
|
|
388
|
+
* @param object replacement a document without update operators
|
|
389
|
+
* @param object options {upsert}
|
|
390
|
+
* @return object {matchedCount, modifiedCount, upsertedId}
|
|
391
|
+
*/
|
|
392
|
+
replaceOne(filter, replacement, options = {})
|
|
393
|
+
{
|
|
394
|
+
this._requireFilter(filter);
|
|
395
|
+
if(!this._isDocument(replacement))
|
|
396
|
+
throw new Error('The replacement must be a document');
|
|
397
|
+
|
|
398
|
+
if(Object.keys(replacement).some(key => key.startsWith('$')))
|
|
399
|
+
throw new Error('The replacement cannot contain update operators, use updateOne()');
|
|
400
|
+
|
|
401
|
+
if(!this._validateKeys(replacement))
|
|
402
|
+
throw new Error('Invalid document keys');
|
|
403
|
+
|
|
404
|
+
const index = this._documents.findIndex(doc => this._operators.matches(doc, filter));
|
|
405
|
+
if(index === -1)
|
|
406
|
+
{
|
|
407
|
+
if(!options.upsert)
|
|
408
|
+
return { matchedCount: 0, modifiedCount: 0, upsertedId: null };
|
|
409
|
+
|
|
410
|
+
const id = this._insertDocument(replacement);
|
|
411
|
+
this._emitChange('insert', { ids: [id], documents: [this._copy(this._documents.at(-1)._data)] });
|
|
412
|
+
return { matchedCount: 0, modifiedCount: 0, upsertedId: id };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const previous = this._documents[index]._data;
|
|
416
|
+
if(typeof replacement._id !== 'undefined' && replacement._id !== previous._id)
|
|
417
|
+
throw new Error('The _id field cannot be changed');
|
|
418
|
+
|
|
419
|
+
const next = { ...this._copy(replacement), _id: previous._id };
|
|
420
|
+
this._validate(next);
|
|
421
|
+
const modified = JSON.stringify(previous) !== JSON.stringify(next);
|
|
422
|
+
if(modified)
|
|
423
|
+
{
|
|
424
|
+
this._documents[index] = new documents(next);
|
|
425
|
+
this._emitChange('replace', { ids: [next._id], documents: [this._copy(next)], previous: [this._copy(previous)] });
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return { matchedCount: 1, modifiedCount: modified ? 1 : 0, upsertedId: null };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Removes matching documents, keeping the documents array in place
|
|
433
|
+
* @param object filter
|
|
434
|
+
* @param boolean many
|
|
435
|
+
* @return object {deletedCount}
|
|
436
|
+
*/
|
|
437
|
+
_delete(filter, many)
|
|
438
|
+
{
|
|
439
|
+
this._requireFilter(filter);
|
|
440
|
+
const removed = [];
|
|
441
|
+
let write = 0;
|
|
442
|
+
for(let read = 0; read < this._documents.length; read++)
|
|
443
|
+
{
|
|
444
|
+
const doc = this._documents[read];
|
|
445
|
+
if((many || !removed.length) && this._operators.matches(doc, filter))
|
|
446
|
+
removed.push(doc._data);
|
|
447
|
+
else
|
|
448
|
+
this._documents[write++] = doc;
|
|
449
|
+
}
|
|
450
|
+
this._documents.length = write;
|
|
451
|
+
|
|
452
|
+
if(removed.length)
|
|
453
|
+
{
|
|
454
|
+
for(const data of removed)
|
|
455
|
+
this._idSet?.delete(data._id);
|
|
456
|
+
|
|
457
|
+
this._emitChange('delete', { ids: removed.map(data => data._id), documents: removed });
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return { deletedCount: removed.length };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Deletes the first matching document
|
|
465
|
+
* @param object filter
|
|
466
|
+
* @return object {deletedCount}
|
|
467
|
+
*/
|
|
468
|
+
deleteOne(filter)
|
|
469
|
+
{
|
|
470
|
+
return this._delete(filter, false);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Deletes every matching document; deleteMany({}) empties the collection
|
|
475
|
+
* @param object filter
|
|
476
|
+
* @return object {deletedCount}
|
|
477
|
+
*/
|
|
478
|
+
deleteMany(filter)
|
|
479
|
+
{
|
|
480
|
+
return this._delete(filter, true);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
_requireFilter(filter)
|
|
484
|
+
{
|
|
485
|
+
if(!this._isDocument(filter))
|
|
486
|
+
throw new Error('The filter must be a query object, use {} to match every document');
|
|
192
487
|
}
|
|
193
488
|
|
|
194
489
|
//create index
|
|
@@ -4,7 +4,7 @@ class operators {
|
|
|
4
4
|
*/
|
|
5
5
|
constructor() {
|
|
6
6
|
this._logicalOperators = ['$or', '$and', '$not', '$nor'];
|
|
7
|
-
this._comparisonOperators = ['$lt', '$lte', '$gt', '$gte', '$eq', '$ne', '$in', '$nin'];
|
|
7
|
+
this._comparisonOperators = ['$lt', '$lte', '$gt', '$gte', '$eq', '$ne', '$in', '$nin', '$exists', '$regex', '$options'];
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -94,9 +94,15 @@ class operators {
|
|
|
94
94
|
* @return boolean
|
|
95
95
|
*/
|
|
96
96
|
matchesCondition(value, condition) {
|
|
97
|
+
if(this.isRegExp(condition))
|
|
98
|
+
return this.matchesRegex(value, condition);
|
|
99
|
+
|
|
97
100
|
if(!this.isOperatorObject(condition))
|
|
98
101
|
return this.$eq(value, condition);
|
|
99
102
|
|
|
103
|
+
if('$options' in condition && !('$regex' in condition))
|
|
104
|
+
throw new Error('The $options operator must be used with $regex');
|
|
105
|
+
|
|
100
106
|
for(const operator of Object.keys(condition)) {
|
|
101
107
|
if(operator === '$not') {
|
|
102
108
|
if(this.matchesCondition(value, condition[operator]))
|
|
@@ -105,6 +111,16 @@ class operators {
|
|
|
105
111
|
continue;
|
|
106
112
|
}
|
|
107
113
|
|
|
114
|
+
if(operator === '$options')
|
|
115
|
+
continue;
|
|
116
|
+
|
|
117
|
+
if(operator === '$regex') {
|
|
118
|
+
if(!this.matchesRegex(value, this.toRegExp(condition.$regex, condition.$options)))
|
|
119
|
+
return false;
|
|
120
|
+
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
108
124
|
if(!this.isOperator(operator))
|
|
109
125
|
throw new Error(`Unknown operator ${operator}`);
|
|
110
126
|
|
|
@@ -115,6 +131,45 @@ class operators {
|
|
|
115
131
|
return true;
|
|
116
132
|
}
|
|
117
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Whether a value is a RegExp (also from another realm: iframes, workers, vm contexts)
|
|
136
|
+
* @param mixed value
|
|
137
|
+
* @return boolean
|
|
138
|
+
*/
|
|
139
|
+
isRegExp(value) {
|
|
140
|
+
return Object.prototype.toString.call(value) === '[object RegExp]';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Builds the RegExp of a $regex condition
|
|
145
|
+
* @param string|RegExp pattern
|
|
146
|
+
* @param string options i (case insensitive), m (multiline), s (dot matches new lines)
|
|
147
|
+
* @return RegExp
|
|
148
|
+
*/
|
|
149
|
+
toRegExp(pattern, options = '') {
|
|
150
|
+
if(typeof options !== 'string' || /[^ims]/.test(options))
|
|
151
|
+
throw new Error('The $options operator accepts only the flags i, m and s');
|
|
152
|
+
|
|
153
|
+
if(this.isRegExp(pattern))
|
|
154
|
+
return new RegExp(pattern.source, [...new Set((pattern.flags + options).replace(/[gy]/g, ''))].join(''));
|
|
155
|
+
|
|
156
|
+
if(typeof pattern !== 'string')
|
|
157
|
+
throw new Error('The $regex operator must be a string or a RegExp');
|
|
158
|
+
|
|
159
|
+
return new RegExp(pattern, options);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Strings match when the pattern finds them, arrays when one of their strings does
|
|
164
|
+
* @param mixed value
|
|
165
|
+
* @param RegExp regex
|
|
166
|
+
* @return boolean
|
|
167
|
+
*/
|
|
168
|
+
matchesRegex(value, regex) {
|
|
169
|
+
const test = item => typeof item === 'string' && (regex.lastIndex = 0, regex.test(item));
|
|
170
|
+
return Array.isArray(value) ? value.some(test) : test(value);
|
|
171
|
+
}
|
|
172
|
+
|
|
118
173
|
/**
|
|
119
174
|
* Whether a condition is an object made of operators, like {$gt: 5}
|
|
120
175
|
* @param mixed condition
|
|
@@ -128,6 +183,16 @@ class operators {
|
|
|
128
183
|
return keys.length > 0 && keys.every(key => key.startsWith('$'));
|
|
129
184
|
}
|
|
130
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Exists operator: {field: {$exists: true}} matches documents that have the field (even null)
|
|
188
|
+
* @param mixed doc_value
|
|
189
|
+
* @param boolean exists
|
|
190
|
+
* @return boolean
|
|
191
|
+
*/
|
|
192
|
+
get $exists() {
|
|
193
|
+
return (doc_value, exists) => (doc_value !== undefined) === !!exists;
|
|
194
|
+
}
|
|
195
|
+
|
|
131
196
|
/**
|
|
132
197
|
* Not in operator
|
|
133
198
|
* @param mixed doc_value
|
|
@@ -14,9 +14,13 @@ class query extends manipulation
|
|
|
14
14
|
* Count documents
|
|
15
15
|
* @return int
|
|
16
16
|
*/
|
|
17
|
-
count()
|
|
17
|
+
count(query)
|
|
18
18
|
{
|
|
19
|
-
|
|
19
|
+
if(typeof query === 'undefined')
|
|
20
|
+
return this._documents.length;
|
|
21
|
+
|
|
22
|
+
// counts without copying the documents
|
|
23
|
+
return this._documents.reduce((total, doc) => total + (this._operators.matches(doc, query) ? 1 : 0), 0);
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
/**
|