UNPKG

17.1 kBJavaScriptView Raw
1/*!
2 * A MongoDB inspired ES6 Map() query language. - Copyright (c) 2017 Louis T. (https://lou.ist/)
3 * Licensed under the MIT license https://raw.githubusercontent.com/LouisT/MapQL/master/LICENSE
4 */
5'use strict';
6const queryOperators = require('./operators/Query'),
7 logicalOperators = require('./operators/Logical'),
8 updateOperators = require('./operators/Update'),
9 MapQLDocument = require('./Document'),
10 Cursor = require('./Cursor'),
11 Helpers = require('./Helpers'),
12 GenerateID = new (require('./GenerateID'))(),
13 isEqual = require('is-equal');
14
15class MapQL extends Map {
16 constructor (_map) {
17 super(_map);
18 }
19
20 /*
21 * Allow MapQL to generate an incremented key if key is omitted.
22 */
23 set (key = Helpers._null, value = Helpers._null) {
24 return Map.prototype.set.call(this, (value === Helpers._null ? GenerateID.next() : key), (value !== Helpers._null ? value : key));
25 }
26
27 /*
28 * Check if MapQL has a specific key, if strict is false return
29 * true if the keys are only similar.
30 */
31 has (key, strict = true) {
32 if (!strict) {
33 return [...this.keys()].some((_key) => {
34 return isEqual(key, _key);
35 });
36 }
37 return Map.prototype.has.call(this, key);
38 }
39
40 /*
41 * Get a key if it exists, if strict is false return value if the
42 * keys are only similar.
43 */
44 get (key, strict = true) {
45 if (!strict) {
46 for (let [_key, value] of [...this.entries()]) {
47 if (isEqual(key, _key)) {
48 return value;
49 }
50 }
51 return Helpers._null;
52 }
53 return Map.prototype.get.call(this, key);
54 }
55
56 /*
57 * Convert the query/update object to an Object with an Array
58 * of queries or update modifiers.
59 */
60 compile (queries = {}, update = false) {
61 let results = {
62 operator: false,
63 list: []
64 };
65 for (let key of Object.keys(queries)) {
66 let isLO = this.isLogicalOperator(key);
67 if (Helpers.is(queries[key], 'Object')) {
68 for (let mode of Object.keys(queries[key])) {
69 results.list.push([key, mode, queries[key][mode]]);
70 }
71 // If the query is an array, treat it as a logical operator.
72 } else if (isLO && Array.isArray(queries[key])) {
73 for (let subobj of queries[key]) {
74 // Recursively compile sub-queries for logical operators.
75 results.list.push(this.compile(subobj));
76 }
77 // Store the logical operator for this query; used in _validate().
78 results.operator = key;
79 } else {
80 let isUQ = (update ? this.isUpdateOperator(key) : this.isQueryOperator(key));
81 results.list.push([
82 update ? (isUQ ? key : '$set') : (isUQ ? Helpers._null : key),
83 (isUQ || update) ? key : '$eq',
84 queries[key]
85 ]);
86 }
87 }
88 return results;
89 }
90
91 /*
92 * Validate a possible Document.
93 */
94 isDocument (obj) {
95 return MapQLDocument.isDocument(obj);
96 }
97
98 /*
99 * Get the valid query, logical, and update operators; with and without static to
100 * avoid this.constructor.<name> calls within the MapQL library itself.
101 */
102 static get queryOperators () {
103 return queryOperators;
104 }
105 get queryOperators () {
106 return queryOperators;
107 }
108 static get logicalOperators () {
109 return logicalOperators;
110 }
111 get logicalOperators () {
112 return logicalOperators;
113 }
114 static get updateOperators () {
115 return updateOperators;
116 }
117 get updateOperators () {
118 return updateOperators;
119 }
120
121 /*
122 * Check if a string is a query operator.
123 */
124 isQueryOperator (qs = Helpers._null) {
125 return this.queryOperators.hasOwnProperty(qs) === true;
126 }
127
128 /*
129 * Get the query selector to test against.
130 */
131 getQueryOperator (qs = '$_default') {
132 return this.queryOperators[qs] ? this.queryOperators[qs] : this.queryOperators['$_default'];
133 }
134
135 /*
136 * Check if a string is a logic operator.
137 */
138 isLogicalOperator (lo = Helpers._null) {
139 return this.logicalOperators.hasOwnProperty(lo) === true;
140 }
141
142 /*
143 * Get the logic operator by name.
144 */
145 getLogicalOperator (lo) {
146 return this.logicalOperators[lo] ? this.logicalOperators[lo] : { fn: [].every };
147 }
148
149 /*
150 * Check if a string is an update operator.
151 */
152 isUpdateOperator (uo = Helpers._null) {
153 return this.updateOperators.hasOwnProperty(uo) === true;
154 }
155
156 /*
157 * Get the update operator by name.
158 */
159 getUpdateOperator (uo = '$_default') {
160 return this.updateOperators[uo] ? this.updateOperators[uo] : this.updateOperators['$_default'];
161 }
162
163 /*
164 * Recursively test the query operator(s) against an entry, checking against any
165 * logic operators provided.
166 */
167 _validate (entry = [], queries = {}) {
168 return this.getLogicalOperator(queries.operator).fn.call(queries.list, (_query) => {
169 if (this.isLogicalOperator(queries.operator)) {
170 return this._validate(entry, _query);
171 } else {
172 return this.getQueryOperator(_query[1]).fn.apply(this, [
173 Helpers.dotNotation(_query[0], entry[1], { autoCreate: false }), // Entry value
174 _query[2], // Test value
175 _query[0], // Test key
176 entry // Entry [<Key>, <Value>]
177 ]);
178 }
179 });
180 }
181
182 /*
183 * Check all entries against every provided query selector.
184 */
185 find (queries = {}, projections = {}, one = false, bykey = false) {
186 let cursor = new Cursor();
187 if (Helpers.is(queries, '!Object')) {
188 let value;
189 if ((value = this.get(queries, false)) !== Helpers._null) {
190 cursor.add(new MapQLDocument(queries, value).bykey(true));
191 if (one || bykey) {
192 return cursor;
193 }
194 }
195 queries = { '$eq' : queries };
196 }
197 let _queries = this.compile(queries);
198 if (!!_queries.list.length) {
199 for (let entry of this.entries()) {
200 if (this._validate(!bykey ? entry : [entry[0], entry[0]], _queries)) {
201 cursor.add(new MapQLDocument(entry[0], entry[1]).bykey(bykey));
202 if (one) {
203 return cursor;
204 }
205 }
206 }
207 return cursor;
208 } else {
209 return new Cursor().add(MapQLDocument.convert(one ? [[...this.entries()][0]] : [...this.entries()]));
210 }
211 }
212
213 /*
214 * Check all entries against every provided query selector; return one.
215 */
216 findOne (queries = {}, projections = {}) {
217 return this.find(queries, projections, true);
218 }
219
220 /*
221 * Check all entry keys against every provided query selector.
222 */
223 findByKey (queries = {}, projections = {}, one = false) {
224 return this.find(queries, projections, one, true);
225 }
226
227 /*
228 * Check all entries against every provided query selector; Promise based.
229 */
230 findPromise (queries = {}, projections = {}, one = false) {
231 return new Promise((resolve, reject) => {
232 try {
233 let results = this.find(queries, projections, one);
234 return !!results.length ? resolve(results) : reject(new Error('No entries found.'));
235 } catch (error) {
236 reject(error);
237 }
238 });
239 }
240
241 /*
242 * Update entries using update modifiers if they match
243 * the provided query operators. Returns the query Cursor,
244 * after updates are applied to the Documents.
245 */
246 update (queries, modifiers, options = {}) {
247 let opts = Object.assign({
248 multi: false,
249 projections: {}
250 }, options),
251 cursor = this[Helpers.is(queries, 'String') ? 'findByKey' : 'find'](queries, opts.projections, !opts.multi);
252 if (!cursor.empty()) {
253 let update = this.compile(modifiers, true);
254 if (!!update.list.length) {
255 for (let entry of cursor) {
256 update.list.forEach((_update) => {
257 this.getUpdateOperator(_update[0]).fn.apply(this, [_update[1], _update[2], entry, this]);
258 });
259 }
260 }
261 }
262 return cursor;
263 }
264
265 /*
266 * Delete entries if they match the provided query operators.
267 * If queries is an Array or String of key(s), treat as array
268 * and remove each key. Returns an Array of deleted IDs. If
269 * `multi` is true remove all matches.
270 */
271 remove (queries, multi = false) {
272 let removed = [];
273 if (Helpers.is(queries, '!Object')) {
274 for (let key of (Array.isArray(queries) ? queries : [queries])) {
275 if (this.has(key) && this.delete(key)) {
276 removed.push(key);
277 }
278 }
279 } else {
280 let _queries = this.compile(queries);
281 if (!!_queries.list.length) {
282 for (let entry of this.entries()) {
283 if (this._validate(entry, _queries)) {
284 if (this.delete(entry[0])) {
285 if (!multi) {
286 return [entry[0]];
287 } else {
288 removed.push(entry[0]);
289 }
290 }
291 }
292 }
293 }
294 }
295 return removed;
296 }
297
298 /*
299 * Export current Document's to JSON key/value.
300 *
301 * Please see README about current import/export downfalls.
302 */
303 export (options = {}) {
304 let opts = Object.assign({
305 stringify: true,
306 promise: false,
307 pretty: false,
308 }, options);
309 try {
310 let _export = (value) => {
311 if (Helpers.is(value, 'Set')) {
312 return [...value].map((k) => [_export(k), Helpers.typeToInt(Helpers.getType(k))]);
313 } else if (Helpers.is(value, ['MapQL', 'Map'], false, true)) {
314 return [...value].map(([k,v]) => [_export(k), _export(v), Helpers.typeToInt(Helpers.getType(k)), Helpers.typeToInt(Helpers.getType(v))]);
315 } else if (Helpers.is(value, 'Array')) {
316 return value.map((value) => { return [_export(value), Helpers.typeToInt(Helpers.getType(value))]; });
317 } else if (Helpers.is(value, 'Object')) {
318 for (let key of Object.keys(value)) {
319 value[key] = convertValueByType(value[key], Helpers.getType(value[key]), _export);
320 }
321 } else if (isTypedArray(value)) {
322 return Array.from(value);
323 }
324 return convertValueByType(value, Helpers.getType(value));
325 },
326 exported = _export(Helpers.deepClone(this, MapQL));
327
328 return ((res) => {
329 return (opts.provalueise ? Promise.resolve(res) : res);
330 })(opts.stringify ? JSON.stringify(exported, null, (opts.pretty ? 4 : 0)) : exported);
331 } catch (error) {
332 return (opts.promise ? Promise.reject(error) : error);
333 }
334 }
335
336 /*
337 * Import JSON key/value objects as entries; usually from export().
338 *
339 * Please see README about current import/export downfalls.
340 *
341 * Note: If a string is passed, attempt to parse with JSON.parse(),
342 * otherwise assume to be a valid Object.
343 */
344 import (json, options = {}) {
345 let opts = Object.assign({
346 promise: false
347 }, options);
348 try {
349 (Helpers.is(json, 'String') ? JSON.parse(json) : json).map((entry) => {
350 this.set(fromType(entry[0], entry[2] || ''), fromType(entry[1], entry[3] || ''));
351 });
352 } catch (error) {
353 if (opts.promise) {
354 return Promise.reject(error);
355 } else {
356 throw error;
357 }
358 }
359 return (opts.promise ? Promise.resolve(this) : this);
360 }
361
362
363 /*
364 * Allow the class to have a custom object string tag.
365 */
366 get [Symbol.toStringTag]() {
367 return (this.constructor.name || 'MapQL');
368 }
369}
370
371/*
372 * Check if is typed array or Buffer (Uint8Array).
373 */
374function isTypedArray (value) {
375 try {
376 if (ArrayBuffer.isView(value) && !(value instanceof DataView)) {
377 return true;
378 }
379 } catch (error) { }
380 return false;
381}
382
383/*
384 * Convert specific data types to specific values based on type for export().
385 */
386function convertValueByType (value, type, _export = false) {
387 let _return = ((_exp) => {
388 return (v, t) => {
389 return _exp ? [_exp(v), t] : v
390 }
391 })(_export);
392
393 let typeint = Helpers.typeToInt(type);
394 switch (type) {
395 case 'Date':
396 return _return(value.getTime(), typeint);
397 case 'Number':
398 return _return(isNaN(value) ? value.toString() : Number(value), typeint)
399 case 'Symbol':
400 return _return(String(value).slice(7, -1), typeint);
401 default:
402 if (_export) {
403 return _return(value, typeint);
404 } else {
405 return _return(Helpers.is(value, ['!Null', '!Boolean', '!Object']) ? value.toString() : value, typeint);
406 }
407 }
408};
409
410/*
411 * Convert strings to required data type, used in import().
412 */
413function fromType (entry, type) {
414 let inttype = Helpers.intToType(type);
415 switch (inttype) {
416 case 'MapQL': case 'Map':
417 return (new MapQL()).import(entry); // Convert all 'Map()' entries to MapQL.
418 case 'Set':
419 return new Set(entry.map((val) => {
420 return fromType(val[0], val[1]);
421 }));
422 case 'Array':
423 return entry.map((val) => {
424 return fromType(val[0], val[1]);
425 });
426 case 'Object':
427 return ((obj) => {
428 for (let key of Object.keys(obj)) {
429 obj[key] = fromType(obj[key][0], obj[key][1]);
430 }
431 return obj;
432 })(entry);
433 case 'Function':
434 // XXX: Function() is a form of eval()!
435 return new Function(`return ${entry};`)();
436 case 'RegExp':
437 return RegExp.apply(null, entry.match(/\/(.*?)\/([gimuy])?$/).slice(1));
438 case 'Date':
439 return new Date(entry);
440 case 'Uint8Array':
441 try {
442 return new Uint8Array(entry);
443 } catch (error) {
444 try {
445 return Buffer.from(entry);
446 } catch (error) { return Array.from(entry); }
447 }
448 case 'Buffer':
449 try {
450 return Buffer.from(entry);
451 } catch (error) {
452 try {
453 return new Uint8Array(entry);
454 } catch (error) { return Array.from(entry); }
455 }
456 default:
457 // Execute the function/constructor with the entry value. If type is not a
458 // function or constructor, just return the value. Try without `new`, if
459 // that fails try again with `new`. This attempts to import unknown types.
460 let _fn = (Helpers.__GLOBAL[inttype] ? (new Function(`return ${inttype}`))() : (e) => { return e });
461 try { return _fn(entry); } catch (e) { try { return new _fn(entry); } catch (error) { console.trace(error); } }
462 }
463}
464
465/*
466 * Export the module for use!
467 */
468module.exports = MapQL;