UNPKG

mapql

Version:

A MongoDB inspired ES6 Map() query langauge.

469 lines (438 loc) 17.1 kB
/*! * A MongoDB inspired ES6 Map() query language. - Copyright (c) 2017 Louis T. (https://lou.ist/) * Licensed under the MIT license https://raw.githubusercontent.com/LouisT/MapQL/master/LICENSE */ 'use strict'; const queryOperators = require('./operators/Query'), logicalOperators = require('./operators/Logical'), updateOperators = require('./operators/Update'), MapQLDocument = require('./Document'), Cursor = require('./Cursor'), Helpers = require('./Helpers'), GenerateID = new (require('./GenerateID'))(), isEqual = require('is-equal'); class MapQL extends Map { constructor (_map) { super(_map); } /* * Allow MapQL to generate an incremented key if key is omitted. */ set (key = Helpers._null, value = Helpers._null) { return Map.prototype.set.call(this, (value === Helpers._null ? GenerateID.next() : key), (value !== Helpers._null ? value : key)); } /* * Check if MapQL has a specific key, if strict is false return * true if the keys are only similar. */ has (key, strict = true) { if (!strict) { return [...this.keys()].some((_key) => { return isEqual(key, _key); }); } return Map.prototype.has.call(this, key); } /* * Get a key if it exists, if strict is false return value if the * keys are only similar. */ get (key, strict = true) { if (!strict) { for (let [_key, value] of [...this.entries()]) { if (isEqual(key, _key)) { return value; } } return Helpers._null; } return Map.prototype.get.call(this, key); } /* * Convert the query/update object to an Object with an Array * of queries or update modifiers. */ compile (queries = {}, update = false) { let results = { operator: false, list: [] }; for (let key of Object.keys(queries)) { let isLO = this.isLogicalOperator(key); if (Helpers.is(queries[key], 'Object')) { for (let mode of Object.keys(queries[key])) { results.list.push([key, mode, queries[key][mode]]); } // If the query is an array, treat it as a logical operator. } else if (isLO && Array.isArray(queries[key])) { for (let subobj of queries[key]) { // Recursively compile sub-queries for logical operators. results.list.push(this.compile(subobj)); } // Store the logical operator for this query; used in _validate(). results.operator = key; } else { let isUQ = (update ? this.isUpdateOperator(key) : this.isQueryOperator(key)); results.list.push([ update ? (isUQ ? key : '$set') : (isUQ ? Helpers._null : key), (isUQ || update) ? key : '$eq', queries[key] ]); } } return results; } /* * Validate a possible Document. */ isDocument (obj) { return MapQLDocument.isDocument(obj); } /* * Get the valid query, logical, and update operators; with and without static to * avoid this.constructor.<name> calls within the MapQL library itself. */ static get queryOperators () { return queryOperators; } get queryOperators () { return queryOperators; } static get logicalOperators () { return logicalOperators; } get logicalOperators () { return logicalOperators; } static get updateOperators () { return updateOperators; } get updateOperators () { return updateOperators; } /* * Check if a string is a query operator. */ isQueryOperator (qs = Helpers._null) { return this.queryOperators.hasOwnProperty(qs) === true; } /* * Get the query selector to test against. */ getQueryOperator (qs = '$_default') { return this.queryOperators[qs] ? this.queryOperators[qs] : this.queryOperators['$_default']; } /* * Check if a string is a logic operator. */ isLogicalOperator (lo = Helpers._null) { return this.logicalOperators.hasOwnProperty(lo) === true; } /* * Get the logic operator by name. */ getLogicalOperator (lo) { return this.logicalOperators[lo] ? this.logicalOperators[lo] : { fn: [].every }; } /* * Check if a string is an update operator. */ isUpdateOperator (uo = Helpers._null) { return this.updateOperators.hasOwnProperty(uo) === true; } /* * Get the update operator by name. */ getUpdateOperator (uo = '$_default') { return this.updateOperators[uo] ? this.updateOperators[uo] : this.updateOperators['$_default']; } /* * Recursively test the query operator(s) against an entry, checking against any * logic operators provided. */ _validate (entry = [], queries = {}) { return this.getLogicalOperator(queries.operator).fn.call(queries.list, (_query) => { if (this.isLogicalOperator(queries.operator)) { return this._validate(entry, _query); } else { return this.getQueryOperator(_query[1]).fn.apply(this, [ Helpers.dotNotation(_query[0], entry[1], { autoCreate: false }), // Entry value _query[2], // Test value _query[0], // Test key entry // Entry [<Key>, <Value>] ]); } }); } /* * Check all entries against every provided query selector. */ find (queries = {}, projections = {}, one = false, bykey = false) { let cursor = new Cursor(); if (Helpers.is(queries, '!Object')) { let value; if ((value = this.get(queries, false)) !== Helpers._null) { cursor.add(new MapQLDocument(queries, value).bykey(true)); if (one || bykey) { return cursor; } } queries = { '$eq' : queries }; } let _queries = this.compile(queries); if (!!_queries.list.length) { for (let entry of this.entries()) { if (this._validate(!bykey ? entry : [entry[0], entry[0]], _queries)) { cursor.add(new MapQLDocument(entry[0], entry[1]).bykey(bykey)); if (one) { return cursor; } } } return cursor; } else { return new Cursor().add(MapQLDocument.convert(one ? [[...this.entries()][0]] : [...this.entries()])); } } /* * Check all entries against every provided query selector; return one. */ findOne (queries = {}, projections = {}) { return this.find(queries, projections, true); } /* * Check all entry keys against every provided query selector. */ findByKey (queries = {}, projections = {}, one = false) { return this.find(queries, projections, one, true); } /* * Check all entries against every provided query selector; Promise based. */ findPromise (queries = {}, projections = {}, one = false) { return new Promise((resolve, reject) => { try { let results = this.find(queries, projections, one); return !!results.length ? resolve(results) : reject(new Error('No entries found.')); } catch (error) { reject(error); } }); } /* * Update entries using update modifiers if they match * the provided query operators. Returns the query Cursor, * after updates are applied to the Documents. */ update (queries, modifiers, options = {}) { let opts = Object.assign({ multi: false, projections: {} }, options), cursor = this[Helpers.is(queries, 'String') ? 'findByKey' : 'find'](queries, opts.projections, !opts.multi); if (!cursor.empty()) { let update = this.compile(modifiers, true); if (!!update.list.length) { for (let entry of cursor) { update.list.forEach((_update) => { this.getUpdateOperator(_update[0]).fn.apply(this, [_update[1], _update[2], entry, this]); }); } } } return cursor; } /* * Delete entries if they match the provided query operators. * If queries is an Array or String of key(s), treat as array * and remove each key. Returns an Array of deleted IDs. If * `multi` is true remove all matches. */ remove (queries, multi = false) { let removed = []; if (Helpers.is(queries, '!Object')) { for (let key of (Array.isArray(queries) ? queries : [queries])) { if (this.has(key) && this.delete(key)) { removed.push(key); } } } else { let _queries = this.compile(queries); if (!!_queries.list.length) { for (let entry of this.entries()) { if (this._validate(entry, _queries)) { if (this.delete(entry[0])) { if (!multi) { return [entry[0]]; } else { removed.push(entry[0]); } } } } } } return removed; } /* * Export current Document's to JSON key/value. * * Please see README about current import/export downfalls. */ export (options = {}) { let opts = Object.assign({ stringify: true, promise: false, pretty: false, }, options); try { let _export = (value) => { if (Helpers.is(value, 'Set')) { return [...value].map((k) => [_export(k), Helpers.typeToInt(Helpers.getType(k))]); } else if (Helpers.is(value, ['MapQL', 'Map'], false, true)) { return [...value].map(([k,v]) => [_export(k), _export(v), Helpers.typeToInt(Helpers.getType(k)), Helpers.typeToInt(Helpers.getType(v))]); } else if (Helpers.is(value, 'Array')) { return value.map((value) => { return [_export(value), Helpers.typeToInt(Helpers.getType(value))]; }); } else if (Helpers.is(value, 'Object')) { for (let key of Object.keys(value)) { value[key] = convertValueByType(value[key], Helpers.getType(value[key]), _export); } } else if (isTypedArray(value)) { return Array.from(value); } return convertValueByType(value, Helpers.getType(value)); }, exported = _export(Helpers.deepClone(this, MapQL)); return ((res) => { return (opts.provalueise ? Promise.resolve(res) : res); })(opts.stringify ? JSON.stringify(exported, null, (opts.pretty ? 4 : 0)) : exported); } catch (error) { return (opts.promise ? Promise.reject(error) : error); } } /* * Import JSON key/value objects as entries; usually from export(). * * Please see README about current import/export downfalls. * * Note: If a string is passed, attempt to parse with JSON.parse(), * otherwise assume to be a valid Object. */ import (json, options = {}) { let opts = Object.assign({ promise: false }, options); try { (Helpers.is(json, 'String') ? JSON.parse(json) : json).map((entry) => { this.set(fromType(entry[0], entry[2] || ''), fromType(entry[1], entry[3] || '')); }); } catch (error) { if (opts.promise) { return Promise.reject(error); } else { throw error; } } return (opts.promise ? Promise.resolve(this) : this); } /* * Allow the class to have a custom object string tag. */ get [Symbol.toStringTag]() { return (this.constructor.name || 'MapQL'); } } /* * Check if is typed array or Buffer (Uint8Array). */ function isTypedArray (value) { try { if (ArrayBuffer.isView(value) && !(value instanceof DataView)) { return true; } } catch (error) { } return false; } /* * Convert specific data types to specific values based on type for export(). */ function convertValueByType (value, type, _export = false) { let _return = ((_exp) => { return (v, t) => { return _exp ? [_exp(v), t] : v } })(_export); let typeint = Helpers.typeToInt(type); switch (type) { case 'Date': return _return(value.getTime(), typeint); case 'Number': return _return(isNaN(value) ? value.toString() : Number(value), typeint) case 'Symbol': return _return(String(value).slice(7, -1), typeint); default: if (_export) { return _return(value, typeint); } else { return _return(Helpers.is(value, ['!Null', '!Boolean', '!Object']) ? value.toString() : value, typeint); } } }; /* * Convert strings to required data type, used in import(). */ function fromType (entry, type) { let inttype = Helpers.intToType(type); switch (inttype) { case 'MapQL': case 'Map': return (new MapQL()).import(entry); // Convert all 'Map()' entries to MapQL. case 'Set': return new Set(entry.map((val) => { return fromType(val[0], val[1]); })); case 'Array': return entry.map((val) => { return fromType(val[0], val[1]); }); case 'Object': return ((obj) => { for (let key of Object.keys(obj)) { obj[key] = fromType(obj[key][0], obj[key][1]); } return obj; })(entry); case 'Function': // XXX: Function() is a form of eval()! return new Function(`return ${entry};`)(); case 'RegExp': return RegExp.apply(null, entry.match(/\/(.*?)\/([gimuy])?$/).slice(1)); case 'Date': return new Date(entry); case 'Uint8Array': try { return new Uint8Array(entry); } catch (error) { try { return Buffer.from(entry); } catch (error) { return Array.from(entry); } } case 'Buffer': try { return Buffer.from(entry); } catch (error) { try { return new Uint8Array(entry); } catch (error) { return Array.from(entry); } } default: // Execute the function/constructor with the entry value. If type is not a // function or constructor, just return the value. Try without `new`, if // that fails try again with `new`. This attempts to import unknown types. let _fn = (Helpers.__GLOBAL[inttype] ? (new Function(`return ${inttype}`))() : (e) => { return e }); try { return _fn(entry); } catch (e) { try { return new _fn(entry); } catch (error) { console.trace(error); } } } } /* * Export the module for use! */ module.exports = MapQL;