Files
kami-parse-server/src/LiveQuery/QueryTools.js
2016-12-07 18:17:05 -05:00

300 lines
8.1 KiB
JavaScript

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;