react-native-mosquito-transport 0.0.18 → 0.0.21

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.
Files changed (30) hide show
  1. package/.jshintignore +4 -0
  2. package/.jshintrc +16 -0
  3. package/README.md +75 -1
  4. package/TODO +10 -1
  5. package/example/ios/MosquitodbExample.xcodeproj/project.pbxproj +6 -5
  6. package/example/ios/MosquitodbExample.xcworkspace/contents.xcworkspacedata +10 -0
  7. package/example/ios/MosquitodbExample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  8. package/example/ios/MosquitodbExample.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +8 -0
  9. package/example/ios/MosquitodbExample.xcworkspace/xcuserdata/anthony.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  10. package/example/ios/MosquitodbExample.xcworkspace/xcuserdata/anthony.xcuserdatad/WorkspaceSettings.xcsettings +14 -0
  11. package/ios/Mosquitodb.swift +14 -1
  12. package/package.json +15 -14
  13. package/src/helpers/engine_api.js +39 -0
  14. package/src/helpers/peripherals.js +73 -127
  15. package/src/helpers/utils.js +48 -19
  16. package/src/helpers/values.js +8 -47
  17. package/src/helpers/variables.js +14 -6
  18. package/src/index.d.ts +103 -43
  19. package/src/index.js +198 -121
  20. package/src/products/auth/accessor.js +97 -36
  21. package/src/products/auth/index.js +151 -82
  22. package/src/products/database/accessor.js +720 -223
  23. package/src/products/database/bson.js +16 -0
  24. package/src/products/database/counter.js +16 -0
  25. package/src/products/database/index.js +303 -190
  26. package/src/products/database/types.js +1 -1
  27. package/src/products/database/validator.js +517 -254
  28. package/src/products/http_callable/index.js +111 -106
  29. package/src/products/storage/index.js +97 -88
  30. package/src/helpers/EngineApi.js +0 -33
@@ -1,289 +1,552 @@
1
- import { IS_DECIMAL_NUMBER, IS_RAW_OBJECT, IS_WHOLE_NUMBER, queryEntries } from "../../helpers/peripherals";
2
- import { READ_OPS, READ_OPS_LIST, RETRIEVAL } from "../../helpers/values";
1
+ import { guardArray, GuardError, guardObject, GuardSignal, niceGuard, Validator } from "guard-object";
2
+ import { sameInstance } from "../../helpers/peripherals";
3
+ import { RETRIEVAL } from "../../helpers/values";
3
4
  import getLodash from 'lodash.get';
4
- import isEqual from 'lodash.isequal';
5
-
6
- const dirn = ['desc', 'asc', 'ascending', 'descending'];
7
-
8
- export const validateReadConfig = (config, excludedNodes = []) => {
9
- const nodeList = [
10
- 'excludeFields',
11
- 'returnOnly',
12
- 'extraction',
13
- 'episode',
14
- 'retrieval',
15
- 'disableAuth',
16
- 'disableMinimizer'
17
- ].filter(v => !excludedNodes.includes(v));
18
-
19
- if (config) {
20
- if (!IS_RAW_OBJECT(config)) throw `Invalid value assigned to 'config', expected a raw object`;
21
- Object.entries(config).forEach(([k, v]) => {
22
- if (!nodeList.includes(k)) throw `unexpected property '${k}' found in config`;
23
-
24
- if (k === 'excludeFields' || k === 'returnOnly') {
25
- if (typeof v !== 'string' && !Array.isArray(v))
26
- throw `invalid value supplied to ${k}, expected either a string or array of string`;
27
- if (Array.isArray(v)) {
28
- v.forEach(e => {
29
- if (typeof e !== 'string')
30
- throw `invalid value supplied to ${k}, expected a string in array but got ${e}`;
31
- });
32
- }
33
- } else if (k === 'extraction') {
34
- ((Array.isArray(v) ? v : [v]).forEach((e, i) => {
35
- const { limit, sort, direction, collection, find, findOne } = e;
36
- if (typeof limit === 'number' && (!IS_WHOLE_NUMBER(limit) || limit <= 0))
37
- throw `invalid value supplied to limit of extraction[${i}], expected a positive whole number but got ${limit}`;
38
-
39
- if (sort && typeof sort !== 'string')
40
- throw `invalid value supplied to sort in extraction[${i}], expected a string value but got ${sort}`;
41
-
42
- if (collection && typeof collection !== 'string')
43
- throw `invalid value supplied to collection in extraction[${i}], expected a string value but got ${collection}`;
44
-
45
- if (direction && direction !== 1 && direction !== -1 && !dirn.includes(direction))
46
- throw `invalid value supplied to direction in extraction[${i}], expected any of ${[1, -1, ...dirn]} but got ${direction}`;
47
- }));
48
- } else if (k === 'episode') {
49
- if (v !== 0 && v !== 1) throw `invalid value supplied to ${k}, expected either 0 or 1 but got ${v}`;
50
- } else if (k === 'retrieval') {
51
- const h = Object.values(RETRIEVAL);
52
- if (!h.includes(v))
53
- throw `invalid value supplied to ${k}, expected any of ${h} but got ${v}`;
54
- } else if (k === 'disableAuth' || k === 'disableMinimizer') {
55
- if (typeof v !== 'boolean')
56
- throw `invalid value supplied to ${k}, expected a boolean value but got ${v}`;
57
- } else throw `unexpected property '${k}' found in config`;
58
- });
59
- }
5
+ import { Binary, BSONRegExp, BSONSymbol, Code, DBRef, Decimal128, Double, Int32, Long, MaxKey, MinKey, ObjectId, Timestamp, UUID } from 'bson';
6
+ import { bboxPolygon, booleanIntersects, booleanWithin, circle, distance, polygon } from "@turf/turf";
7
+
8
+ const DirectionList = [1, -1, 'asc', 'desc', 'ascending', 'descending'];
9
+ const FilterFootPrint = t => {
10
+ validateFilter(t);
11
+ return true;
12
+ };
13
+ const ReturnAndExcludeFootprint = t => t === undefined ||
14
+ !(Array.isArray(t) ? t : [t]).filter(v => !Validator.TRIMMED_NON_EMPTY_STRING(v)).length;
15
+
16
+ const ConfigFind = t => t && FilterFootPrint(assignExtractionFind({}, t));
17
+
18
+ const FindConfig = {
19
+ extraction: t => t === undefined ||
20
+ (Array.isArray(t) ? t : [t]).filter(m =>
21
+ guardObject({
22
+ collection: isValidCollectionName,
23
+ sort: (t, p) => t === undefined || (Validator.TRIMMED_NON_EMPTY_STRING(t) && p.find),
24
+ direction: (t, p) => t === undefined || (p.sort && p.find && DirectionList.includes(t)),
25
+ limit: (t, p) => t === undefined || (Validator.POSITIVE_INTEGER(t) && p.find),
26
+ find: (t, p) => (t === undefined && p.findOne) || (!p.findOne && ConfigFind(t)),
27
+ findOne: (t, p) => (t === undefined && p.find) || (!p.find && ConfigFind(t)),
28
+ returnOnly: ReturnAndExcludeFootprint,
29
+ excludeFields: ReturnAndExcludeFootprint
30
+ }).validate(m)
31
+ ).length,
32
+ returnOnly: ReturnAndExcludeFootprint,
33
+ excludeFields: ReturnAndExcludeFootprint,
34
+
35
+ episode: t => [undefined, 0, 1].includes(t),
36
+ retrieval: t => t === undefined || Object.values(RETRIEVAL).includes(t),
37
+ disableAuth: t => t === undefined || typeof t === 'boolean',
38
+ disableMinimizer: t => t === undefined || typeof t === 'boolean'
39
+ };
40
+
41
+ export const validateFindConfig = (config) => config === undefined ||
42
+ guardObject(FindConfig).validate(config);
43
+
44
+ export const validateListenFindConfig = (config) => config === undefined ||
45
+ guardObject({
46
+ extraction: FindConfig.extraction,
47
+ returnOnly: FindConfig.returnOnly,
48
+ excludeFields: FindConfig.excludeFields,
49
+ disableAuth: FindConfig.disableAuth,
50
+ episode: t => [undefined, 0, 1].includes(t)
51
+ }).validate(config);
52
+
53
+ export const validateFindObject = command =>
54
+ guardObject({
55
+ // path: GuardSignal.TRIMMED_NON_EMPTY_STRING,
56
+ find: (t, p) => (t === undefined && p.findOne) || (!p.findOne && FilterFootPrint(t)),
57
+ findOne: (t, p) => (t === undefined && p.find) || (!p.find && FilterFootPrint(t)),
58
+ sort: t => t === undefined || Validator.TRIMMED_NON_EMPTY_STRING(t),
59
+ direction: (t, p) => t === undefined || (p.sort && DirectionList.includes(t)),
60
+ limit: t => t === undefined || Validator.POSITIVE_INTEGER(t),
61
+ random: (t, p) => t === undefined || (!p.sort && t === true),
62
+ }).validate({ ...command });
63
+
64
+ export const assignExtractionFind = (data, find) => {
65
+ if (!find) return find;
66
+
67
+ if (niceGuard({ $dynamicValue: GuardSignal.NON_EMPTY_STRING }, find)) {
68
+ return getLodash(data, find.$dynamicValue) || null;
69
+ } else if (Validator.OBJECT(find)) {
70
+ return Object.fromEntries(
71
+ Object.entries(find).map(([k, v]) =>
72
+ Validator.JSON(v) ? [k, assignExtractionFind(data, v)] : [k, v]
73
+ )
74
+ );
75
+ } else if (Array.isArray(find)) {
76
+ return find.map(v => assignExtractionFind(data, v));
77
+ } else return find;
78
+ };
79
+
80
+ export const validateCollectionName = collectionName => {
81
+ // Check if the collection name is empty
82
+ if (!collectionName || typeof collectionName !== 'string')
83
+ throw `collection name must be a non-empty string but got ${collectionName}`;
84
+
85
+ // Collection name cannot start with 'system.' (reserved)
86
+ if (collectionName.startsWith('system.'))
87
+ throw `collection name cannot start with 'system.' but got ${collectionName}`;
88
+
89
+ // Collection name cannot contain the '$' character
90
+ if (collectionName.includes('$'))
91
+ throw `collection name cannot contain the '$' character but got ${collectionName}`;
60
92
  }
61
93
 
62
- export const validateWriteValue = (value, filter, type) => {
63
- const isObject = IS_RAW_OBJECT(value);
94
+ function isValidDatabaseName(dbName) {
95
+ // Check if the database name is empty
96
+ if (!dbName || typeof dbName !== 'string') {
97
+ return false;
98
+ }
64
99
 
65
- if (type === 'setOne' || type === 'setMany') {
66
- if (type === 'setOne' && !isObject) {
67
- throw `expected raw object in ${type}() operation but got ${value}`;
68
- } else if (type === 'setMany' && !Array.isArray(value))
69
- throw `expected an array of document in ${type}() operation but got ${value}`;
100
+ // Database name must be less than 64 characters
101
+ if (Buffer.byteLength(dbName, 'utf8') >= 64) {
102
+ return false;
103
+ }
70
104
 
71
- const duplicateID = {};
105
+ // Database name cannot contain invalid characters: / \ " . $ space
106
+ const invalidDbChars = /[\/\\."$ ]/;
107
+ if (invalidDbChars.test(dbName)) {
108
+ return false;
109
+ }
72
110
 
73
- (Array.isArray(value) ? value : [value]).forEach(e => {
74
- if (!IS_RAW_OBJECT(e)) throw `expected raw object in ${type}() operation but got ${e}`;
75
- if (duplicateID[e._id]) throw `duplicate document _id:${e._id} found in ${type}() operation`;
76
- if (!e._id) throw `No _id found in ${type}() operation`;
77
- duplicateID[e._id] = true;
78
- });
79
- return;
111
+ return true;
112
+ }
113
+
114
+ function isValidCollectionName(collectionName) {
115
+ try {
116
+ validateCollectionName(collectionName);
117
+ return true;
118
+ } catch (_) {
119
+ return false;
80
120
  }
121
+ };
81
122
 
82
- if (type !== 'deleteOne' && type !== 'deleteMany')
83
- if (!isObject) throw `expected raw object in ${type}() operation but got ${value}`;
123
+ export const validateFilter = (filter) => confirmFilterDoc({}, filter);
84
124
 
85
- validateFilter(filter);
125
+ export const confirmFilterDoc = (data, filter) => {
126
+ if (!Validator.OBJECT(filter)) throw `expected an object as filter value but got ${filter}`;
86
127
 
87
- queryEntries(value, []).forEach(([segment]) => {
88
- if (segment.filter(v => v === '_foreign_doc').length)
89
- throw `"_foreign_doc" is a reserved word, don't use it as a field in a document`;
128
+ const logicalList = ['$and', '$or', '$nor'];
129
+ const logics = [[], [], []]; // [$and, $or, $nor]
90
130
 
91
- // TODO: validate rest
131
+ Object.entries(filter).forEach(([key, value]) => {
132
+ if (logicalList.includes(key)) {
133
+ if (!Array.isArray(value)) throw `"${key}" must be an array`;
134
+ if (!value.length) throw `"${key}" must be a nonempty array`;
135
+ logics[logicalList.indexOf(key)].push(...value.map(v => evaluateFilter(data, v)));
136
+ } else logics[0].push(evaluateFilter(data, { [key]: value }));
92
137
  });
93
- }
138
+ const [AND, OR, NOR] = logics;
94
139
 
95
- export const validateFilter = (filter = {}) => evaluateFilter({}, filter);
140
+ return !AND.some(v => !v) &&
141
+ (!OR.length || OR.some(v => v)) &&
142
+ (!NOR.length || NOR.some(v => !v));
143
+ };
96
144
 
97
- export const validateCollectionPath = (path) => {
98
- if (typeof path !== 'string' || path.includes('.') || !path.trim())
99
- throw `invalid collection path "${path}", expected non-empty string and mustn't contain "."`;
100
- }
145
+ const plumeDoc = doc => [doc, ...Array.isArray(doc) ? doc : []];
101
146
 
102
- export const confirmFilterDoc = (data, filter) => {
103
- // [$and, $or]
104
- const logics = [[], []];
147
+ export const defaultBSON = (value, instance) => {
148
+ try {
149
+ return instance.constructor(value);
150
+ } catch (_) {
151
+ return value;
152
+ }
153
+ };
154
+
155
+ export const downcastBSON = d => {
156
+ if (d instanceof BSONRegExp)
157
+ return new RegExp(d.pattern, d.options);
158
+ if (
159
+ [
160
+ Long,
161
+ Double,
162
+ Int32,
163
+ Decimal128
164
+ ].some(v => d instanceof v)
165
+ ) return d * 1;
166
+ return d;
167
+ };
168
+
169
+ const isBasicBSON = d =>
170
+ [
171
+ Code,
172
+ ObjectId,
173
+ Binary,
174
+ MaxKey,
175
+ MinKey,
176
+ UUID,
177
+ Timestamp,
178
+ BSONSymbol
179
+ ].some(v => d instanceof v);
180
+
181
+ export const CompareBson = {
182
+ equal: (doc, q, explicit) => {
183
+ doc = downcastBSON(doc);
184
+ q = downcastBSON(q);
185
+
186
+ if (
187
+ isBasicBSON(q) ||
188
+ isBasicBSON(doc)
189
+ ) {
190
+ return sameInstance(doc, q) &&
191
+ JSON.stringify(doc) === JSON.stringify(q);
192
+ }
105
193
 
106
- Object.entries(filter).forEach(([key, value]) => {
107
- if (key === '$and') {
108
- if (!Array.isArray(value)) throw `$and must be an array`;
109
- value.forEach(v => logics[0].push(evaluateFilter(data, v)));
110
- } else if (key === '$or') {
111
- if (!Array.isArray(value)) throw `$and must be an array`;
112
- value.forEach(v => logics[1].push(evaluateFilter(data, v)));
113
- } else logics[0].push(evaluateFilter(data, { [`${key}`]: value }));
114
- });
194
+ if (q instanceof RegExp) {
195
+ return sameInstance(doc, q) ?
196
+ (doc.source === q.source && doc.flags === q.flags) :
197
+ (explicit && typeof doc === 'string' && q.test(doc));
198
+ }
199
+ return JSON.stringify(doc) === JSON.stringify(q)
200
+ },
201
+ greater: (doc, q) => {
202
+ doc = downcastBSON(doc);
203
+ q = downcastBSON(q);
204
+
205
+ if (doc instanceof Timestamp || q instanceof Timestamp) {
206
+ return sameInstance(doc, q) && doc.greaterThan(q);
207
+ }
115
208
 
116
- return !logics[0].filter(v => !v).length && (!logics[1].length || !!logics[1].filter(v => v).length);
117
- }
209
+ return typeof doc === typeof q && ![q, doc].some(v => Array.isArray(v) || Validator.OBJECT(v)) && doc > q;
210
+ },
211
+ lesser: (doc, q) => {
212
+ doc = downcastBSON(doc);
213
+ q = downcastBSON(q);
118
214
 
119
- const dataTypesValue = [
120
- 'double',
121
- 'string',
122
- 'object',
123
- 'array',
124
- 'decimal',
125
- // 'long',
126
- 'int',
127
- 'bool',
128
- 'date',
129
- 'null',
130
- 'number'
215
+ if (doc instanceof Timestamp || q instanceof Timestamp) {
216
+ return sameInstance(doc, q) && doc.lessThan(q);
217
+ }
218
+
219
+ return typeof doc === typeof q && ![q, doc].some(v => Array.isArray(v) || Validator.OBJECT(v)) && doc < q;
220
+ },
221
+ };
222
+
223
+ const BsonTypeMap = {
224
+ double: [1, d => d instanceof Double],
225
+ string: [2, d => typeof d === 'string'],
226
+ object: [3, d => Validator.OBJECT(d)],
227
+ array: [4, d => Array.isArray(d)],
228
+ binData: [5, d => d instanceof Binary],
229
+ objectId: [7, d => d instanceof ObjectId],
230
+ bool: [8, d => typeof d === 'boolean'],
231
+ date: [9, d => d instanceof Date],
232
+ null: [10, d => d === null],
233
+ regex: [11, d => d instanceof RegExp || d instanceof BSONRegExp],
234
+ dbPointer: [12, d => d instanceof DBRef],
235
+ javascript: [13, d => d instanceof Code],
236
+ symbol: [14, d => d instanceof BSONSymbol],
237
+ int: [16, d => d instanceof Int32],
238
+ timestamp: [17, d => d instanceof Timestamp],
239
+ long: [18, d => d instanceof Long],
240
+ decimal: [19, d => d instanceof Decimal128],
241
+ minKey: [-1, d => d instanceof MinKey],
242
+ maxKey: [127, d => d instanceof MaxKey],
243
+ number: [undefined, d => d instanceof Double ||
244
+ d instanceof Int32 ||
245
+ d instanceof Long ||
246
+ d instanceof Decimal128 ||
247
+ Validator.NUMBER(d)]
248
+ };
249
+
250
+ const COORDINATE_GUARD = [
251
+ GuardSignal.COORDINATE.LONGITUDE_INT,
252
+ GuardSignal.COORDINATE.LATITUDE_INT
131
253
  ];
132
254
 
133
- const { $IN, $NIN, $GT, $GTE, $LT, $LTE, $EQ, $EXISTS, $REGEX, $NE, $SIZE, $TEXT, $TYPE } = READ_OPS;
134
-
135
- // TODO: fix exact field value doc, deep nesting and other query
136
-
137
- const evaluateFilter = (data = {}, filter = {}) => {
138
- if (!IS_RAW_OBJECT(data)) throw `data must be a raw object`;
139
- if (!IS_RAW_OBJECT(filter)) throw `expected a raw object but got ${filter}`;
140
-
141
- const dataObj = { ...data },
142
- logics = [];
143
-
144
- queryEntries(filter, []).forEach(([segment, value]) => {
145
- let commandSplit = segment.map((e, i) => e.startsWith('$') ? ({ $: e, i }) : null).filter(v => v);
146
-
147
- if (commandSplit.length) {
148
- const { $, i: dex } = commandSplit[0],
149
- pathValue = dex ? getLodash(dataObj, segment.filter((_, i) => i < dex).join('.')) : null;
150
-
151
- if (!READ_OPS_LIST.includes($))
152
- throw `"${$}" operation is currently not supported`;
153
-
154
- if ($ !== $TEXT && (dex !== segment.length - 1 || !dex))
155
- throw `"${$} must be at the last position as an operator"`;
156
-
157
- if ($ === $IN) {
158
- if (!Array.isArray(value)) throw `The value assigned to "${$}" operator must be an array`;
159
-
160
- if (pathValue !== undefined) {
161
- logics.push(
162
- !!(Array.isArray(pathValue) ? pathValue : [pathValue])
163
- .filter(v => !!value.filter(t => checkTestEquality(t, v)).length).length
164
- );
165
- } else logics.push(false);
166
- } else if ($ === $NIN) {
167
- if (!Array.isArray(value)) throw `The value assigned to "${$}" operator must be an array`;
168
-
169
- if (pathValue !== undefined) {
170
- logics.push(
171
- !(Array.isArray(pathValue) ? pathValue : [pathValue])
172
- .filter(v => !!value.filter(t => checkTestEquality(t, v)).length).length
173
- );
174
- } else logics.push(true);
175
- } else if ($ === $GT) {
176
- if (pathValue !== undefined) {
177
- logics.push(
178
- !!(Array.isArray(pathValue) ? pathValue : [pathValue])
179
- .filter(v => v > value).length
180
- );
181
- } else logics.push(false);
182
- } else if ($ === $GTE) {
183
- if (pathValue !== undefined) {
184
- logics.push(
185
- !!(Array.isArray(pathValue) ? pathValue : [pathValue])
186
- .filter(v => v >= value).length
187
- );
188
- } else logics.push(false);
189
- } else if ($ === $LT) {
190
- if (pathValue !== undefined) {
191
- logics.push(
192
- !!(Array.isArray(pathValue) ? pathValue : [pathValue])
193
- .filter(v => v < value).length
194
- );
195
- } else logics.push(false);
196
- } else if ($ === $LTE) {
197
- if (pathValue !== undefined) {
198
- logics.push(
199
- !!(Array.isArray(pathValue) ? pathValue : [pathValue])
200
- .filter(v => v <= value).length
201
- );
202
- } else logics.push(false);
203
- } else if ($ === $TEXT) {
204
- if (commandSplit.slice(-1)[0].$ === '$search') {
205
- const { $caseSensitive, $search, $field } = filter.$text;
206
-
207
- if (typeof value !== 'string' || typeof $search !== 'string')
208
- throw `$search must have a string value`;
209
- if (!$field) throw '"$field" is required inside "$text" operator when "disableCache=false"';
210
- const fieldArr = Array.isArray($field) ? $field : [$field];
211
-
212
- fieldArr.forEach(v => {
213
- if (typeof v !== 'string' || !v.trim())
214
- throw `invalid item inside "$field", expected a non-empty string but got "${v}"`;
215
- });
216
-
217
- const searchTxt = fieldArr.map(v => getLodash(dataObj, v || '')).map(v =>
218
- `${typeof v === 'string' ? v :
219
- Array.isArray(v) ? v.map(v => typeof v === 'string' ? v : '').join(' ').trim() : ''}`.trim()
220
- ).join(' ').trim();
221
-
222
- logics.push(
223
- $caseSensitive ? searchTxt.includes(value.trim()) :
224
- searchTxt.toLowerCase().includes(value.toLowerCase().trim())
225
- );
226
- }
227
- } else if ($ === $EQ) {
255
+ const validateGeoNear = q =>
256
+ guardObject({
257
+ $geometry: {
258
+ type: 'Point',
259
+ coordinates: COORDINATE_GUARD
260
+ },
261
+ $minDistance: (t, p) => Validator.POSITIVE_NUMBER(t) && p.$maxDistance > t,
262
+ $maxDistance: (t, p) => Validator.POSITIVE_NUMBER(t) && p.$minDistance < t
263
+ }).validate(q);
264
+
265
+ const FilterUtils = {
266
+ $eq: (doc, q) => plumeDoc(doc).some(v =>
267
+ CompareBson.equal(v, q)
268
+ ),
269
+
270
+ $ne: (doc, q) => !FilterUtils.$eq(doc, q),
271
+
272
+ $gt: (doc, q) => plumeDoc(doc).some(v =>
273
+ CompareBson.greater(v, q)
274
+ ),
275
+
276
+ $gte: (doc, q) => plumeDoc(doc).some(v =>
277
+ CompareBson.greater(v, q) || CompareBson.equal(v, q)
278
+ ),
279
+
280
+ $lt: (doc, q) => plumeDoc(doc).some(v =>
281
+ CompareBson.lesser(v, q)
282
+ ),
283
+
284
+ $lte: (doc, q) => plumeDoc(doc).some(v =>
285
+ CompareBson.lesser(v, q) || CompareBson.equal(v, q)
286
+ ),
287
+
288
+ $in: (doc, q) => {
289
+ if (!Array.isArray(q)) throw '$in needs an array';
290
+ return plumeDoc(doc).some(v =>
291
+ q.some(k => CompareBson.equal(v, k, true))
292
+ );
293
+ },
294
+
295
+ $nin: (doc, q) => !FilterUtils.$in(doc, q),
296
+
297
+ $all: (doc, q) => {
298
+ if (!Array.isArray(q)) throw '$all needs an array';
299
+ return plumeDoc(doc).filter(v =>
300
+ q.some(k => CompareBson.equal(v, k, true))
301
+ ).length >= q.length;
302
+ },
303
+
304
+ $size: (doc, q) => {
305
+ if (!Validator.POSITIVE_INTEGER(q))
306
+ throw `Failed to parse $size. Expected a positive integer in: $size: ${q}`;
307
+ return Array.isArray(doc) && doc.length === q;
308
+ },
309
+
310
+ $type: (doc, q) => {
311
+ if (q === undefined) return false;
312
+ return plumeDoc(doc).some(docx => {
313
+ if (q in BsonTypeMap) {
314
+ return BsonTypeMap[q][1](docx);
315
+ }
316
+ const c = Object.entries(BsonTypeMap).find(([_, v]) => v[0] === q);
317
+ if (c) return c[1][1](docx);
318
+ if (typeof q === 'number') throw `Invalid numerical type code: ${q}`;
319
+ throw `Unknown type name alias: ${q}`;
320
+ });
321
+ },
322
+
323
+ $regex: (doc, q) => {
324
+ doc = downcastBSON(doc);
325
+ q = downcastBSON(q);
326
+
327
+ return plumeDoc(doc).some(docx => {
328
+ if (q instanceof RegExp) {
329
+ return typeof docx === 'string' ? q.test(docx) :
330
+ (docx instanceof RegExp &&
331
+ docx.source === q.source &&
332
+ docx.flags === q.flags);
333
+ }
228
334
 
229
- } else if ($ === $EXISTS) {
335
+ if (typeof q === 'string') {
336
+ return typeof docx === 'string' &&
337
+ !!docx.match(q);
338
+ }
230
339
 
231
- } else if ($ === $REGEX) {
340
+ throw '$regex has to be a string or a regex';
341
+ });
342
+ },
343
+
344
+ $exists: (doc, q) => {
345
+ return q ? doc !== undefined : doc === undefined;
346
+ },
347
+
348
+ $ne: (doc, q) => !FilterUtils.$eq(doc, q),
349
+
350
+ $text: (parent, q) => {
351
+ guardObject({
352
+ $search: GuardSignal.STRING,
353
+ $field: t => Validator.STRING(t) || (t.length && niceGuard(guardArray(GuardSignal.STRING), t)),
354
+ $caseSensitive: t => t === undefined || Validator.BOOLEAN(t)
355
+ }).validate(q);
356
+ let { $field, $search, $caseSensitive } = q;
357
+
358
+ $field = (Array.isArray($field) ? $field : [$field]).map(v => {
359
+ const f = getLodash({ ...parent }, v);
360
+ return typeof f === 'string' ? f : '';
361
+ }).join(' ');
362
+
363
+ if (!$caseSensitive) {
364
+ $field = $field.toLowerCase();
365
+ $search = $search.toLowerCase();
366
+ }
232
367
 
233
- } else if ($ === $NE) {
368
+ return $field.includes($search);
369
+ },
234
370
 
235
- } else if ($ === $SIZE) {
236
- if (!IS_WHOLE_NUMBER(value) || v < 0) throw '$size must be a positive whole number';
237
- logics.push(Array.isArray(pathValue) && pathValue.length === value);
238
- } else if ($ === $TYPE) {
239
- if (!dataTypesValue.includes(value))
240
- throw `invalid value supplied to ${$}, mosquioto-transport only recognise "${dataTypesValue}" data types`;
371
+ $geoIntersects: (doc, q) => {
372
+ if (
373
+ !niceGuard({
374
+ $geometry: {
375
+ type: 'Point',
376
+ coordinates: COORDINATE_GUARD
377
+ }
378
+ }, q) &&
379
+ !niceGuard({
380
+ $geometry: {
381
+ type: 'LineString',
382
+ coordinates: [COORDINATE_GUARD, COORDINATE_GUARD]
383
+ }
384
+ }, q) &&
385
+ !niceGuard({
386
+ $geometry: {
387
+ type: 'Polygon',
388
+ coordinates: guardArray(guardArray(COORDINATE_GUARD))
389
+ }
390
+ }, q) &&
391
+ !niceGuard({
392
+ $geometry: {
393
+ type: 'MultiPoint',
394
+ coordinates: guardArray(COORDINATE_GUARD)
395
+ }
396
+ }, q) &&
397
+ !niceGuard({
398
+ $geometry: {
399
+ type: 'MultiLineString',
400
+ coordinates: guardArray([COORDINATE_GUARD, COORDINATE_GUARD])
401
+ }
402
+ }, q) &&
403
+ !niceGuard({
404
+ $geometry: {
405
+ type: 'MultiPolygon',
406
+ coordinates: guardArray(guardArray(guardArray(COORDINATE_GUARD)))
407
+ }
408
+ }, q)
409
+ ) throw `unknown operator: ${q}`;
241
410
 
242
- const cock = (v) => {
243
- if (typeof v === 'number') {
244
- if (isNaN(v)) {
245
- return ((value === 'decimal' || value === 'double') && IS_DECIMAL_NUMBER(v)) || value === 'int' || value === 'number';
411
+ try {
412
+ return booleanIntersects(doc, q.$geometry);
413
+ } catch (_) {
414
+ return false;
415
+ }
416
+ },
417
+ $geoWithin: (doc, q) => {
418
+ const { $box, $geometry, $center, $centerSphere, $polygon } = { ...q };
419
+ try {
420
+ if ($geometry) {
421
+ if (
422
+ niceGuard({
423
+ $geometry: {
424
+ type: 'Polygon',
425
+ coordinates: guardArray(guardArray(COORDINATE_GUARD))
246
426
  }
247
- } else if (typeof v === 'boolean') {
248
- return value === 'bool';
249
- } else if (typeof v === 'string') {
250
- return value === 'string';
251
- } else if (v === null) {
252
- return value === 'null';
253
- } else if (v instanceof RegExp) {
254
- return value === 'regex';
255
- } else if (v instanceof Date) {
256
- return value === 'date';
257
- } else if (IS_RAW_OBJECT(v)) {
258
- return value === 'object';
259
- }
260
- return false;
427
+ }, q) ||
428
+ niceGuard({
429
+ $geometry: {
430
+ type: 'MultiPolygon',
431
+ coordinates: guardArray(guardArray(guardArray(COORDINATE_GUARD)))
432
+ }
433
+ }, q)
434
+ ) {
435
+ return booleanWithin(doc, $geometry);
261
436
  }
437
+ } else if ($box) {
438
+ guardObject({ $box: Array(2).fill(COORDINATE_GUARD) }).validate(q);
439
+
440
+ const [bottomLeft, topRight] = $box;
441
+ const boundingBox = bboxPolygon([bottomLeft[0], bottomLeft[1], topRight[0], topRight[1]]);
442
+ return booleanWithin(doc, boundingBox);
443
+ } else if ($center) {
444
+ guardObject({ $center: [COORDINATE_GUARD, GuardSignal.POSITIVE_NUMBER] }).validate(q);
445
+
446
+ const [center, radius] = $center;
447
+ return booleanWithin(doc, circle(center, radius, { units: 'kilometers' }));
448
+ } else if ($centerSphere) {
449
+ guardObject({ $centerSphere: [COORDINATE_GUARD, GuardSignal.POSITIVE_NUMBER] }).validate(q);
450
+
451
+ const [center, radius] = $centerSphere;
452
+ // Convert radians to km
453
+ return booleanWithin(doc, circle(center, radius * 6371, { units: 'kilometers' }));
454
+ } else if ($polygon) {
455
+ guardObject({ $polygon: guardArray(COORDINATE_GUARD) }).validate(q);
456
+ return booleanWithin(doc, polygon([$polygon]));
457
+ }
458
+ } catch (e) {
459
+ if (!(e instanceof GuardError)) return false;
460
+ }
461
+ throw `unknown operator: ${JSON.stringify(q)}`;
462
+ },
463
+ $near: (doc, q) => {
464
+ validateGeoNear(q);
465
+ try {
466
+ const { $geometry, $maxDistance, $minDistance } = q;
467
+ const distanceOffset = distance($geometry, doc);
468
+
469
+ if ($minDistance && distanceOffset < $minDistance) {
470
+ return false;
471
+ }
262
472
 
263
- logics.push(
264
- (Array.isArray(pathValue) && value === 'array') ||
265
- !!(Array.isArray(pathValue) ? pathValue : [pathValue])
266
- .filter(v => cock(v)).length
267
- );
473
+ if ($maxDistance && distanceOffset > $maxDistance) {
474
+ return false;
475
+ }
476
+
477
+ return true;
478
+ } catch (error) {
479
+ return false;
480
+ }
481
+ },
482
+ $nearSphere: (doc, q) => {
483
+ validateGeoNear(q);
484
+ try {
485
+ const { $geometry, $maxDistance, $minDistance } = q.$nearSphere;
486
+ const distanceOffset = distance($geometry, doc, { units: 'degrees' });
487
+
488
+ if ($minDistance && distanceOffset < $minDistance) {
489
+ return false;
490
+ }
491
+
492
+ if ($maxDistance && distanceOffset > $maxDistance) {
493
+ return false;
268
494
  }
269
- } else {
270
- const pathValue = getLodash(dataObj, segment.join('.'));
271
495
 
272
- if (pathValue !== undefined) {
496
+ return true;
497
+ } catch (_) {
498
+ return false;
499
+ }
500
+ }
501
+ };
502
+
503
+ const evaluateFilter = (data, filter = {}, parentData, level = 0) => {
504
+ if (!Validator.OBJECT(filter)) throw `filter must be a raw object but got ${filter}`;
505
+ if (!level) parentData = data;
506
+
507
+ const logics = [];
508
+
509
+ Object.entries(filter).map(([key, value]) => {
510
+ if (key.startsWith('$') && (key !== '$text' || !Validator.OBJECT(value)) && !level)
511
+ throw `unknown top level operator: ${key}`;
512
+
513
+ let thisData;
514
+ try {
515
+ thisData = data && getLodash(data, key);
516
+ } catch (_) { }
517
+
518
+ if (key === '$text' && !level) {
519
+ logics.push(FilterUtils.$text(parentData, value));
520
+ } else if (Validator.OBJECT(value) && !level) {
521
+ const valueEntrie = Object.entries(value);
522
+
523
+ if (valueEntrie.some(([k]) => k.startsWith('$'))) {
524
+ valueEntrie.forEach(([query, queryValue]) => {
525
+ if (query in FilterUtils) {
526
+ if (query === '$text' && level) throw '$text must be a top level operator';
527
+ logics.push(FilterUtils[query](thisData, queryValue));
528
+ } else if (query === '$not') {
529
+ if (Validator.OBJECT(queryValue)) {
530
+ logics.push(!evaluateFilter(thisData, queryValue, parentData, level + 1));
531
+ } else logics.push(
532
+ !plumeDoc(thisData).some(v => CompareBson.equal(v, queryValue, true))
533
+ );
534
+ } else throw `unknown operator: ${query}`;
535
+ });
536
+ } else if (valueEntrie.length) {
537
+ logics.push(evaluateFilter(thisData, value, parentData, level + 1));
538
+ } else {
273
539
  logics.push(
274
- !!(Array.isArray(pathValue) ? pathValue : [pathValue])
275
- .filter(v => !!checkTestEquality(value, v)).length
540
+ Validator.OBJECT(thisData) &&
541
+ !Object.keys(thisData).length
276
542
  );
277
- } else logics.push(false);
543
+ }
544
+ } else {
545
+ logics.push(
546
+ plumeDoc(thisData).some(v => CompareBson.equal(v, value, true))
547
+ );
278
548
  }
279
549
  });
280
550
 
281
- return !logics.filter(v => !v).length;
282
- }
283
-
284
- const checkTestEquality = (test, o) => {
285
- if (test instanceof RegExp) {
286
- if (typeof o === 'string') return test.test(o);
287
- else return false;
288
- } else return isEqual(test, o);
289
- }
551
+ return !logics.some(v => !v);
552
+ };