var equalObjects = require('./equalObjects'); var Id = require('./Id'); var Parse = require('parse/node'); /** * Query Hashes are deterministic hashes for Parse Queries. * Any two queries that have the same set of constraints will produce the same * hash. This lets us reliably group components by the queries they depend upon, * and quickly determine if a query has changed. */ /** * Convert $or queries into an array of where conditions */ function flattenOrQueries(where) { if (!where.hasOwnProperty('$or')) { return where; } var accum = []; for (var i = 0; i < where.$or.length; i++) { accum = accum.concat(where.$or[i]); } return accum; } /** * Deterministically turns an object into a string. Disregards ordering */ function stringify(object): string { if (typeof object !== 'object' || object === null) { if (typeof object === 'string') { return '"' + object.replace(/\|/g, '%|') + '"'; } return object + ''; } if (Array.isArray(object)) { var copy = object.map(stringify); copy.sort(); return '[' + copy.join(',') + ']'; } var sections = []; var keys = Object.keys(object); keys.sort(); for (var k = 0; k < keys.length; k++) { sections.push(stringify(keys[k]) + ':' + stringify(object[keys[k]])); } return '{' + sections.join(',') + '}'; } /** * Generate a hash from a query, with unique fields for columns, values, order, * skip, and limit. */ function queryHash(query) { if (query instanceof Parse.Query) { query = { className: query.className, where: query._where } } var where = flattenOrQueries(query.where || {}); var columns = []; var values = []; var i; if (Array.isArray(where)) { var uniqueColumns = {}; for (i = 0; i < where.length; i++) { var subValues = {}; var keys = Object.keys(where[i]); keys.sort(); for (var j = 0; j < keys.length; j++) { subValues[keys[j]] = where[i][keys[j]]; uniqueColumns[keys[j]] = true; } values.push(subValues); } columns = Object.keys(uniqueColumns); columns.sort(); } else { columns = Object.keys(where); columns.sort(); for (i = 0; i < columns.length; i++) { values.push(where[columns[i]]); } } var sections = [columns.join(','), stringify(values)]; return query.className + ':' + sections.join('|'); } /** * matchesQuery -- Determines if an object would be returned by a Parse Query * It's a lightweight, where-clause only implementation of a full query engine. * Since we find queries that match objects, rather than objects that match * queries, we can avoid building a full-blown query tool. */ function matchesQuery(object: any, query: any): boolean { if (query instanceof Parse.Query) { var className = (object.id instanceof Id) ? object.id.className : object.className; if (className !== query.className) { return false; } return matchesQuery(object, query._where); } for (var field in query) { if (!matchesKeyConstraints(object, field, query[field])) { return false; } } return true; } function equalObjectsGeneric(obj, compareTo, eqlFn) { if (Array.isArray(obj)) { for (var i = 0; i < obj.length; i++) { if (eqlFn(obj[i], compareTo)) { return true; } } return false; } return eqlFn(obj, compareTo); } /** * Determines whether an object matches a single key's constraints */ function matchesKeyConstraints(object, key, constraints) { if (constraints === null) { return false; } var i; if (key === '$or') { for (i = 0; i < constraints.length; i++) { if (matchesQuery(object, constraints[i])) { return true; } } return false; } if (key === '$relatedTo') { // Bail! We can't handle relational queries locally return false; } // Equality (or Array contains) cases if (typeof constraints !== 'object') { if (Array.isArray(object[key])) { return object[key].indexOf(constraints) > -1; } return object[key] === constraints; } var compareTo; if (constraints.__type) { if (constraints.__type === 'Pointer') { return equalObjectsGeneric(object[key], constraints, function(obj, ptr) { return ( typeof obj !== 'undefined' && ptr.className === obj.className && ptr.objectId === obj.objectId ); }); } return equalObjectsGeneric(object[key], Parse._decode(key, constraints), equalObjects); } // More complex cases for (var condition in constraints) { compareTo = constraints[condition]; if (compareTo.__type) { compareTo = Parse._decode(key, compareTo); } switch (condition) { case '$lt': if (object[key] >= compareTo) { return false; } break; case '$lte': if (object[key] > compareTo) { return false; } break; case '$gt': if (object[key] <= compareTo) { return false; } break; case '$gte': if (object[key] < compareTo) { return false; } break; case '$ne': if (equalObjects(object[key], compareTo)) { return false; } break; case '$in': if (compareTo.indexOf(object[key]) < 0) { return false; } break; case '$nin': if (compareTo.indexOf(object[key]) > -1) { return false; } break; case '$all': for (i = 0; i < compareTo.length; i++) { if (object[key].indexOf(compareTo[i]) < 0) { return false; } } break; case '$exists': { const propertyExists = typeof object[key] !== 'undefined'; const existenceIsRequired = constraints['$exists']; if (typeof constraints['$exists'] !== 'boolean') { // The SDK will never submit a non-boolean for $exists, but if someone // tries to submit a non-boolean for $exits outside the SDKs, just ignore it. break; } if ((!propertyExists && existenceIsRequired) || (propertyExists && !existenceIsRequired)) { return false; } break; } case '$regex': if (typeof compareTo === 'object') { return compareTo.test(object[key]); } // JS doesn't support perl-style escaping var expString = ''; var escapeEnd = -2; var escapeStart = compareTo.indexOf('\\Q'); while (escapeStart > -1) { // Add the unescaped portion expString += compareTo.substring(escapeEnd + 2, escapeStart); escapeEnd = compareTo.indexOf('\\E', escapeStart); if (escapeEnd > -1) { expString += compareTo.substring(escapeStart + 2, escapeEnd) .replace(/\\\\\\\\E/g, '\\E').replace(/\W/g, '\\$&'); } escapeStart = compareTo.indexOf('\\Q', escapeEnd); } expString += compareTo.substring(Math.max(escapeStart, escapeEnd + 2)); var exp = new RegExp(expString, constraints.$options || ''); if (!exp.test(object[key])) { return false; } break; case '$nearSphere': var distance = compareTo.radiansTo(object[key]); var max = constraints.$maxDistance || Infinity; return distance <= max; case '$within': var southWest = compareTo.$box[0]; var northEast = compareTo.$box[1]; if (southWest.latitude > northEast.latitude || southWest.longitude > northEast.longitude) { // Invalid box, crosses the date line return false; } return ( object[key].latitude > southWest.latitude && object[key].latitude < northEast.latitude && object[key].longitude > southWest.longitude && object[key].longitude < northEast.longitude ); case '$options': // Not a query type, but a way to add options to $regex. Ignore and // avoid the default break; case '$maxDistance': // Not a query type, but a way to add a cap to $nearSphere. Ignore and // avoid the default break; case '$select': return false; case '$dontSelect': return false; default: return false; } } return true; } var QueryTools = { queryHash: queryHash, matchesQuery: matchesQuery }; module.exports = QueryTools;