UNPKG

alchemymvc

Version:
1,605 lines (1,318 loc) 32.3 kB
/** * NoSQL Datasource * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.1.0 * @version 1.1.0 */ var NoSQL = Function.inherits('Alchemy.Datasource', 'Nosql'); /** * All comparison functions * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.0.0 * @version 1.0.0 */ NoSQL.setStatic('comparisons', {}); /** * All logical operator functions * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.0.0 * @version 1.0.0 */ NoSQL.setStatic('logical_operators', {}); /** * Add comparison function * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.0.0 * @version 1.0.0 */ NoSQL.setStatic(function addComparison(fnc) { this.comparisons[fnc.name] = fnc; }); /** * Add logical operator function * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.0.0 * @version 1.0.0 */ NoSQL.setStatic(function addLogicalOperator(fnc) { this.logical_operators[fnc.name] = fnc; }); /** * Lower than comparison * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.0.0 * @version 1.0.0 */ NoSQL.addComparison(function $lt(a, b) { return NoSQL.areComparable(a, b) && a < b; }); /** * Lower than or equal comparison * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.0.0 * @version 1.0.0 */ NoSQL.addComparison(function $lte(a, b) { return NoSQL.areComparable(a, b) && a <= b; }); /** * Greater than comparison * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.0.0 * @version 1.0.0 */ NoSQL.addComparison(function $gt(a, b) { return NoSQL.areComparable(a, b) && a > b; }); /** * Greater than or equal comparison * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.0.0 * @version 1.0.0 */ NoSQL.addComparison(function $gte(a, b) { return NoSQL.areComparable(a, b) && a >= b; }); /** * Not equal comparison * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.0.0 * @version 1.0.0 */ NoSQL.addComparison(function $ne(a, b) { if (a === undefined) { return true; } return !Object.alike(a, b); }); /** * In comparison * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.0.0 * @version 1.0.0 */ NoSQL.addComparison(function $in(a, b) { var i; if (!Array.isArray(b)) { throw new Error("$in operator called with a non-array"); } for (i = 0; i < b.length; i++) { if (Object.alike(a, b[i])) { return true; } } return false; }); /** * Not in comparison * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.0.0 * @version 1.0.0 */ NoSQL.addComparison(function $nin(a, b) { return !this.$in(a, b); }); /** * Regex comparison * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.0.0 * @version 1.0.0 */ NoSQL.addComparison(function $regex(a, b) { if (!RegExp.isRegExp(b)) { throw new Error('$regex operator called with non regular expression'); } if (typeof a != 'string') { return false; } return b.test(a); }); /** * Exists comparison * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.0.0 * @version 1.0.0 */ NoSQL.addComparison(function $exists(value, exists) { // The value needs to be truthy or an empty string // (This is how mongo does it, so 0 or false returns falsy) exists = !!(exists || exists === ''); if (value === undefined) { return !exists; } else { return exists; } }); /** * Array size comparison * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.0.0 * @version 1.0.0 */ NoSQL.addComparison(function $size(obj, value) { if (!Array.isArray(obj)) { return false; } if (value % 1 !== 0) { throw new Error('$size operator called without an integer'); } return obj.length == value; }); /** * Array elemMatch comparison * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.0.0 * @version 1.0.0 */ NoSQL.addComparison(function $elemMatch(obj, value) { var i; if (!Array.isArray(obj)) { return result; } while (i--) { if (NoSQL.match(obj[i], value)) { return true; } } return false; }); /** * Match any of the subqueries * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.0.0 * @version 1.0.0 */ NoSQL.addLogicalOperator(function $or(obj, query) { var i; if (!Array.isArray(query)) { throw new Error('$or operator used without an array'); } for (i = 0; i < query.length; i++) { if (NoSQL.match(obj, query[i])) { return true; } } return false; }); /** * Match all of the subqueries * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.0.0 * @version 1.0.0 */ NoSQL.addLogicalOperator(function $and(obj, query) { var i; if (!Array.isArray(query)) { throw new Error('$and operator used without an array'); } for (i = 0; i < query.length; i++) { if (!NoSQL.match(obj, query[i])) { return false; } } return true; }); /** * Inverted match of the query * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.0.0 * @version 1.0.0 */ NoSQL.addLogicalOperator(function $not(obj, query) { return !NoSQL.match(obj, query); }); /** * Use a function to match * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.0.0 * @version 1.0.0 */ NoSQL.addLogicalOperator(function $where(obj, fnc) { var result; if (typeof fnc != 'function') { throw new Error('$where operator used without a function'); } result = fnc.call(obj); if (typeof result != 'boolean') { throw new Error('$where function must return boolean'); } return result; }); /** * Match an object against a specific {key: value} part of a query. * If the treatObjAsValue flag is set, * don't try to match every part separately, but the array as a whole. * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.0.0 * @version 1.0.6 * * @param {Object} obj * @param {Object} query * * @return {boolean} */ NoSQL.setStatic(function matchQueryPart(obj, query_key, query_value, treat_obj_as_value) { var dollar_first_chars, first_chars, obj_value, keys, i; obj_value = NoSQL.getDotValue(obj, query_key); // @TODO: On the client side, in indexdb, dates are dried. // It's stupid to undry the entire object just to check a date, // so just undry a piece of it if (Blast.isBrowser && obj_value && typeof obj_value == 'object' && obj_value.dry) { obj_value = Blast.Bound.JSON.undry(obj_value); } // Check if the value is an array if we don't force a treatment as value if (Array.isArray(obj_value) && !treat_obj_as_value) { // If the queryValue is an array, try to perform an exact match if (Array.isArray(query_value)) { return matchQueryPart(obj, query_key, query_value, true); } // Check if we are using an array-specific comparison function if (query_value !== null && typeof query_value === 'object' && !RegExp.isRegExp(query_value)) { keys = Object.keys(query_value); for (i = 0; i < keys.length; i++) { if (keys[i] == '$size' || keys[i] == '$elemMatch') { return matchQueryPart(obj, query_key, query_value, true); } } } // If not, treat it as an array of { obj, query } where there needs to be at least one match for (i = 0; i < obj_value.length; i++) { if (matchQueryPart({k: obj_value[i]}, 'k', query_value)) { return true; } } return false; } // query_value is an actual object. Determine whether it // contains comparison operators or only normal fields. // Mixed objects are not allowed if (query_value !== null && typeof query_value === 'object' && !RegExp.isRegExp(query_value) && !Array.isArray(query_value)) { keys = Object.keys(query_value); first_chars = keys.map(function eachItem(item) { return item[0]; }); dollar_first_chars = first_chars.filter(function eachChar(c) { return c === '$'; }); if (dollar_first_chars.length !== 0 && dollar_first_chars.length !== first_chars.length) { throw new Error('You cannot mix operators and normal fields'); } // query_value is an object of this form: // {$comparison_operator_1: value_1, ...} if (dollar_first_chars.length > 0) { for (i = 0; i < keys.length; i++) { if (!NoSQL.comparisons[keys[i]]) { throw new Error('Unknown comparison function ' + keys[i]); } if (!NoSQL.comparisons[keys[i]](obj_value, query_value[keys[i]])) { return false; } } return true; } } // Using regular expressions with basic querying if (RegExp.isRegExp(query_value)) { return NoSQL.comparisons.$regex(obj_value, query_value); } // query_value is either anative value or a normal object // Basic matching is possible if (!Object.alike(obj_value, query_value)) { return false; } return true; }); /** * Tell if a given document matches a query * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.0.0 * @version 1.0.0 * * @param {Object} obj * @param {Object} query * * @return {boolean} */ NoSQL.setStatic(function match(obj, query) { var query_value, query_keys, query_key, i; // Primitive queyr against a primitive type if (Object.isPrimitive(obj) || Object.isPrimitive(query)) { return this.matchQueryPart({need_a_key: obj}, 'need_a_key', query); } // Normal query query_keys = Object.keys(query); for (i = 0; i < query_keys.length; i++) { query_key = query_keys[i]; query_value = query[query_key]; if (query_key[0] == '$') { if (!this.logical_operators[query_key]) { throw new Error('Unknown logical operator ' + query_key); } if (!this.logical_operators[query_key](obj, query_value)) { return false; } } else { if (!this.matchQueryPart(obj, query_key, query_value)) { return false; } } } return true; }); /** * Parse a record path * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.0.0 * @version 1.0.0 * * @param {string} path * * @return {Object} */ NoSQL.setStatic(function parseRecordPath(path) { var result = {}, first; // Cast to an array of path pieces if (!Array.isArray(path)) { path = path.split('.'); } // Get the first character of the (possible) alias first = path[0][0]; // Check if it's a valid uppercase character if (first === first.toUpperCase() && first !== first.toLowerCase()) { result.alias = path.shift(); } result.path = path; result.field = path[0]; return result; }); /** * Get a value from object with dot notation * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.0.0 * @version 1.0.0 * * @return {Object} */ NoSQL.setStatic(function getDotValue(obj, path) { var pieces, first, objs, i; if (Array.isArray(path)) { pieces = path; } else { pieces = path.split('.'); } if (!obj) { return undefined; } if (pieces.length == 0) { return obj; } first = pieces[0]; if (pieces.length == 1) { return obj[first]; } if (Array.isArray(obj[first])) { i = parseInt(pieces[1], 10); if (typeof i === 'number' && !isNaN(i)) { return getDotValue(obj[first][i], pieces.slice(2)); } // Return the array of values objs = []; for (i = 0; i < obj[first].length; i++) { objs.push(getDotValue(obj[first][i], pieces.slice(1))); } return objs; } return getDotValue(obj[first], pieces.slice(1)); }); /** * Are 2 values comparable * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.0.0 * @version 1.0.0 * * @return {boolean} */ NoSQL.setStatic(function areComparable(a, b) { var at = typeof a, bt = typeof b; if (at != bt) { return false; } if (at != 'string' && at != 'number' && !Date.isDate(a) && bt != 'string' && bt != 'number' && !Date.isDate(b)) { return false; } return true; }); /** * Compile criteria into a MongoDB query object * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.1.0 * @version 1.4.0 * * @param {Criteria} criteria The criteria to convert * @param {Object} context The optional context (for Trail retrieval) * * @return {Object} */ NoSQL.setStatic(function convertCriteriaToConditions(criteria, context) { let config = { for_database: false, }; return convertCriteriaToConditionsWithConfig(criteria, config, context); }); /** * Compile criteria into a MongoDB query object with the given configuration * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.1.0 * @version 1.4.0 * * @param {Criteria} criteria The criteria to convert * @param {Object} config Configuration flags * @param {Object} context The optional context * * @return {Object} */ function convertCriteriaToConditionsWithConfig(criteria, config, context) { if (context) { if (!Object.isPlainObject(context) || !context.$0) { context = {$0: context}; } } if (!config) { config = { for_database : false, }; } return convertCriteriaGroupToConditions(criteria, criteria.group, config, context); } /** * Compile criteria into a MongoDB query object * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.1.0 * @version 1.4.0 * * @param {Criteria} criteria The criteria to convert * @param {Group} group The current group * @param {Object} config Configuration flags * @param {Object} context The optional context * * @return {Object} */ function convertCriteriaGroupToConditions(criteria, group, config, context) { let is_for_database = config?.for_database, assoc_model, aggregate, result = [], entry, assoc, temp, i; let getAggregate = () => { if (!aggregate) { aggregate = { pipeline: [], lookups: {} }; } } for (i = 0; i < group.items.length; i++) { entry = group.items[i]; // If the "association" is actually the current model, just remove it if (entry.association && entry.association == criteria.model.model_name) { entry.association = null; } if (entry.association) { getAggregate(); // Get the association info assoc = criteria.model.getAssociation(entry.association); if (result.length) { aggregate.pipeline.push({ $match: { $and: result } }); result = []; } if (!aggregate.lookups[assoc.alias]) { aggregate.lookups[assoc.alias] = true; assoc_model = alchemy.getModel(assoc.modelName); if (assoc.type == 'BelongsTo') { // Add a check so that we only get records that have a parent temp = {}; temp[assoc.options.localKey] = {$ne: null}; aggregate.pipeline.push({ $match: temp }); aggregate.pipeline.push({ $lookup: { from : assoc_model.table, localField : assoc.options.localKey, foreignField : assoc.options.foreignKey, as : assoc.alias } }); } else if (assoc.type == 'HasMany') { aggregate.pipeline.push({ $lookup: { from : assoc_model.table, localField : assoc.options.localKey, foreignField : assoc.options.foreignKey, as : assoc.alias } }); } else { throw new Error('No support for ' + assoc.type + ' association yet'); } if (assoc.options.singular) { aggregate.pipeline.push({ $unwind: '$' + assoc.alias }); } } } if (entry.group_type) { let compiled_group = convertCriteriaGroupToConditions(entry.criteria, entry, config, context), obj = {}; if (compiled_group.$and) { compiled_group = compiled_group.$and; } else if (compiled_group.pipeline) { // @TODO: this won't work compiled_group = compiled_group.pipeline; } else { compiled_group = Array.cast(compiled_group); } if (compiled_group) { obj['$' + entry.group_type] = compiled_group; result.push(obj); } } else { let item, not, obj = {}; let field_entry = {}, name = entry.target_path, queries_property = name.indexOf('.') > -1; // Do we need to look into an object itself? if (entry.db_property) { // Make sure the query doesn't already specifically query this property if (name == entry.field.path) { name += '.' + entry.db_property; queries_property = true; } } if (entry.association) { name = entry.association + '.' + name; } for (let i = 0; i < entry.items.length; i++) { item = entry.items[i]; if (context && item.value && typeof item.value == 'object') { item = { ...item, value : Classes.Develry.Placeholder.deepResolve(item.value, context), }; } // If the value is a RegExp, we might have to stringify the value if (shouldStringify(entry, item) && RegExp.isRegExp(item.value)) { let stringified_field = name + '_stringified'; getAggregate(); aggregate.pipeline.push({ $addFields: { [stringified_field]: { $convert: { input: '$' + name, to: 'string', onError: '', onNull: '' } } } }); name = stringified_field; } if (item.type == 'ne') { obj.$ne = item.value; } else if (item.type == 'not') { if (typeof item.value == 'undefined') { not = true; continue; } else { obj.$not = item.value; } } else if (item.type == 'equals') { obj = item.value; } else if (item.type == 'contains') { let do_regexp_search = true; if (entry.field) { if (entry.field.is_array || entry.field.options?.type == 'HasAndBelongsToMany') { do_regexp_search = false; obj = item.value; } } if (do_regexp_search) { obj = RegExp.interpret(item.value); } } else if (item.type == 'in') { // @TODO: This shouldn't be needed, // Mongo actually allows this, but NeDB does NOT if (Array.isArray(item.value) && item.value.length == 1 && Array.isArray(item.value[0])) { item.value = item.value[0]; } obj = {$in: item.value}; } else if (item.type == 'gt' || item.type == 'gte' || item.type == 'lt' || item.type == 'lte') { obj['$' + item.type] = item.value; } else if (item.type == 'exists') { if (item.value || item.value == null) { obj.$exists = true; } else { obj.$exists = false; } } else if (item.type == 'isNull') { obj = null; if (item.value === false) { obj = {$ne: null}; } } else if (item.type == 'isEmpty') { if (is_for_database) { let exists = false, comparator = '$eq'; if (item.value === false) { exists = true; comparator = '$ne'; } let $or = [ {$exists: exists} ]; if (!entry.field) { throw new Error('Could not find field for path "' + entry.target_path + '" in model ' + entry.model?.name); } if (entry.field.is_array) { $or.push({[comparator]: []}); } else if (entry.field instanceof Classes.Alchemy.Field.String) { $or.push({[comparator]: ''}); } $or.push({[comparator]: null}); obj.$or = $or; } else { obj = {$isEmpty: item.value ?? true}; } } else { throw new Error('Unknown criteria expression: "' + item.type + '"'); } let multiple_fields, prefixed_name = name; // Temporary fix to actually query translatable field contents // (entry.field can be undefined if trying to query a path) if (entry.field?.is_translatable) { let prefix = criteria.options.locale, specific_prefix = !!prefix; if (specific_prefix && criteria?.model?.translate === false) { specific_prefix = false; } // If a prefix is specified, only query that translation if (specific_prefix) { prefixed_name = name + '.' + prefix; } else { multiple_fields = []; // No prefixes specified, so look through all translations for (let key in Prefix.all()) { multiple_fields.push(name + '.' + key); } } } if (obj && obj.$or) { let $or = [], i; for (i = 0; i < obj.$or.length; i++) { if (multiple_fields) { for (let name of multiple_fields) { $or.push({ [name] : obj.$or[i] }); } } else { $or.push({ [prefixed_name] : obj.$or[i] }); } } if (not) { let $and = [], $not, key, i; for (i = 0; i < $or.length; i++) { $not = {}; for (key in $or[i]) { $not[key] = {$not: $or[i][key]}; } $and.push($not); } field_entry.$and = $and; } else { field_entry.$or = $or; } } else { if (not) { not = false; if (Object.isPrimitive(obj)) { obj = {$ne: obj}; } else { obj = {$not: obj}; } } // Temporary fix to actually query translatable field contents // (entry.field can be undefined if trying to query a path) if (multiple_fields) { let $or = []; for (let name of multiple_fields) { let temp = {}; temp[name] = obj; $or.push(temp); } field_entry.$or = $or; } else { field_entry[prefixed_name] = obj; } } result.push(field_entry); } } } if (aggregate) { if (result.length) { aggregate.pipeline.push({ $match: { $and: result } }); } return aggregate; } if (!result.length) { result = {}; } else if (result.length === 1) { result = result[0]; } else { result = {$and: result}; } return result; }; /** * Compile criteria into a MongoDB-compatible query object * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.1.0 * @version 1.4.0 * * @param {Criteria} criteria * @param {Group} group * * @return {Object} */ NoSQL.setMethod(function compileCriteria(criteria, group) { return convertCriteriaToConditionsWithConfig(criteria, {for_database: true}, group); }); /** * Is the given item about a string field? * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.2.0 * @version 1.2.0 * * @param {Object} entry * @param {Object} item * * @return {boolean} */ function shouldStringify(entry, item) { if (!item || !item.value) { return false; } try { let field = entry.model.getField(entry.target_path); // The field value should only be stringified if it isn't a string already return !(field instanceof Classes.Alchemy.Field.String); } catch (err) {} return false; } /** * Compile an AQL string into a MongoDB-compatible query object * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.4.0 * @version 1.4.0 * * @param {string} query * * @return {Object} */ NoSQL.setStatic(function convertAQLToConditions(query) { let tokens = tokenizeAQL(query); let conditions = convertAQLTokens(tokens, (name, operator, value, not) => { switch (operator) { case 'is': case 'eq': case '==': case '=': operator = '$eq'; break; case 'ne': case '!=': operator = '$ne'; break; case 'gt': case '>': operator = '$gt'; break; case 'gte': case '>=': operator = '$gte'; break; case 'lt': case '<': operator = '$lt'; break; case 'lte': case '<=': operator = '$lte'; break; case 'empty': operator = '$isEmpty'; value = {value: !not}; break; } switch (value.type) { case 'name': value = Trail.fromDot(value.value).ifNull(null); break; case 'number': value = Number(value.value); break; case 'boolean': value = value.value == 'true'; break; case 'string': value = String.parseQuoted(value.value); break; default: value = value.value; break; } let result = { [name]: { [operator]: value, } }; return result; }); return conditions; }); /** * Parse the AQL query into tokens * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.4.0 * @version 1.4.0 * * @param {string} query * * @return {Array} */ function tokenizeAQL(query) { const START_STATE = 1, NAME_STATE = 2, OPERATOR_STATE = 3, VALUE_STATE = 4; let expressions = [], tokens = Function.tokenize(query, true), current_name, current_operator, current_value, current_not, lower, token, type, value, i; let current_state = START_STATE; let states = [current_state]; const replaceState = state => { states[states.length - 1] = state; current_state = state; }; const pushState = state => { current_state = state; states.push(current_state); return current_state; }; const popState = () => { current_state = states.pop(); return current_state; }; const createGroup = () => { if (hasCurrent()) { closeValue(); } expressions.push(true); pushState(START_STATE); }; const closeGroup = () => { if (hasCurrent()) { closeValue(); } popState(); expressions.push(false); }; const setValue = (token) => { if (token === true) { current_value = true; } else { let new_value = { type : token.type, value: token.value, not : !!current_not, }; if (current_value) { if (current_value.type == 'name') { current_value.value = current_value.value + new_value.value; } else { reject('Unexpected value'); } } else { current_value = new_value; } } }; const hasCurrent = () => !!(current_name || current_operator || current_value); const closeValue = (next_logic_type) => { if (!hasCurrent()) { reject('Incomplete statement'); } let entry = [ current_name, current_operator, current_value, current_not, next_logic_type, ]; expressions.push(entry); current_name = null; current_operator = null; current_value = null; current_not = null; replaceState(START_STATE); } const reject = (message) => { throw new Error(message); }; for (i = 0; i < tokens.length; i++) { token = tokens[i]; type = token.type; // All whitespace can be ignored if (type == 'whitespace') { continue; } value = token.value; if (value == '(') { createGroup(); continue; } if (value == ')') { closeGroup(); continue; } lower = value.toLowerCase(); if (current_state == START_STATE) { if (type != 'name') { reject('Expected name'); } if (lower == 'not' || lower == 'and' || lower == 'or') { expressions.push(lower); } else { current_name = value; pushState(NAME_STATE); } } else if (current_state == NAME_STATE) { if (type == 'punct') { if (value == '.') { current_name += '.'; } else { current_operator = value; replaceState(OPERATOR_STATE); } } else if (type == 'name') { if (lower == 'not') { current_not = !current_not; console.log('NOT! Current operator is', current_operator); replaceState(OPERATOR_STATE); } else if (lower == 'is') { current_operator = '=='; replaceState(OPERATOR_STATE); } else { current_name += value; } } } else if (current_state == OPERATOR_STATE) { // Is there already an operator? if (current_operator) { if (lower == 'not') { current_not = !current_not; } else if (type == 'name' && lower == 'empty') { current_operator = 'empty'; setValue(true); replaceState(VALUE_STATE); } else { setValue(token); replaceState(VALUE_STATE); } } else if (type == 'name') { current_operator = lower; } else { reject('Expected operator'); } } else if (current_state == VALUE_STATE) { if (type == 'name') { if (lower == 'not' && !current_value) { current_not = !current_not; } else if (lower == 'or' || lower == 'and') { if (!current_value) { reject('Expected a value'); } closeValue(lower); continue; } } if (type == 'punct') { if (value == '.') { setValue(token); } else { reject('Unexpected punctuation'); } } else if (type == 'name') { setValue(token); } else if (current_value) { reject('Unexpected string'); } else { setValue(token); } } } if (hasCurrent()) { closeValue(); } return expressions; } /** * Convert AQL tokens into a conditions object * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.4.0 * @version 1.4.0 * * @param {string} query * * @return {Object} */ function convertAQLTokens(expressions, expression_converter) { let current_group, current_logic, next_is_not, expression, i; // Make sure there currently exists a group with the given logic type const ensureGroup = (logic) => { if (!current_group) { current_group = { logic, expressions: [], }; } else { if (!current_group.logic) { current_group.logic = logic; } else if (current_group.logic != logic) { pushGroup(logic); } } current_logic = current_group.logic; return current_group; }; // Create the root group let root = ensureGroup(); // Start a new group of the given logic type. const pushGroup = (logic) => { let group = { logic, expressions: [], not: next_is_not }; if (logic == 'or') { group.expressions.push(simplifyGroups(current_group)); current_group = current_group.parent; } group.parent = current_group; if (!current_group) { root = group; } else { current_group.child = group; } current_group = group; }; // Close the current group (& restore the parent group) const closeGroup = () => { let group = current_group; current_group = group.parent; } const simplifyGroups = (group) => { let result = {}; let logic; if (group.logic == 'and' || !group.logic) { logic = '$and'; } else if (group.logic == 'or') { logic = '$or'; } else { throw new Error('Unknown logic type'); } result[logic] = group.expressions; if (group.child) { result[logic].push(simplifyGroups(group.child)); } if (group.not) { result = { $nor: result }; } return result; }; for (i = 0; i < expressions.length; i++) { expression = expressions[i]; if (expression === true) { pushGroup(); } else if (expression === false) { closeGroup(); } else if (expression === 'and' || expression === 'or') { ensureGroup(expression); } else if (expression === 'not') { next_is_not = true; continue; } else { current_logic = expression[4]; // And gets precedence if (current_logic == 'and') { ensureGroup(current_logic); } expression = expression_converter(...expression); current_group.expressions.push(expression); if (current_logic == 'or') { ensureGroup(current_logic); } } next_is_not = false; } return simplifyGroups(root); } /** * Get the MongoDB options from this criteria * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.1.0 * @version 1.1.0 * * @param {Criteria} criteria * * @return {Object} */ NoSQL.setMethod(function compileCriteriaOptions(criteria) { var result = {}, fields = criteria.getFieldsToSelect(); if (fields.length) { result.projection = fields; } else { result.projection = null; } if (criteria.options.sort) { result.sort = criteria.options.sort; } if (criteria.options.skip) { result.skip = criteria.options.skip; } if (criteria.options.limit) { result.limit = criteria.options.limit; } return result; }); /** * Handle items from the datasource * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.1.0 * @version 1.1.0 * * @param {Model} model * @param {Array} rows * * @return {Array} */ NoSQL.setMethod(function organizeResultItems(model, rows) { var associations = model.associations || {}, result = [], main, row, set, key, i; for (i = 0; i < rows.length; i++) { row = rows[i]; main = {}; set = {}; for (key in row) { if (associations[key] != null) { set[key] = row[key]; } else { main[key] = row[key]; } } set[model.name] = main; result.push(set); } return result; });