From 93781b2195d8074032d2c4dfd77a56acf17dc9e1 Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Mon, 11 Jan 2021 01:35:32 -0600 Subject: [PATCH] feat(LiveQuery): Support $and, $nor, $containedBy, $geoWithin (#7113) * feat(LiveQuery): Support $and, $nor, $containedBy, $geoWithin, $geoIntersects * Update CHANGELOG.md * Update CHANGELOG.md --- CHANGELOG.md | 1 + spec/QueryTools.spec.js | 139 ++++++++++++++++++++++++++++++++++++ src/LiveQuery/QueryTools.js | 34 +++++++++ 3 files changed, 174 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d928e41ffb..0980a9c215 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ___ - IMPROVE: Optimize queries on classes with pointer permissions. [#7061](https://github.com/parse-community/parse-server/pull/7061). Thanks to [Pedro Diaz](https://github.com/pdiaz) - FIX: request.context for afterFind triggers. [#7078](https://github.com/parse-community/parse-server/pull/7078). Thanks to [dblythy](https://github.com/dblythy) - NEW: Added convenience method Parse.Cloud.sendEmail(...) to send email via email adapter in Cloud Code. [#7089](https://github.com/parse-community/parse-server/pull/7089). Thanks to [dblythy](https://github.com/dblythy) +- NEW: LiveQuery support for $and, $nor, $containedBy, $geoWithin, $geoIntersects queries [#7113](https://github.com/parse-community/parse-server/pull/7113). Thanks to [dplewis](https://github.com/dplewis) ### 4.5.0 [Full Changelog](https://github.com/parse-community/parse-server/compare/4.4.0...4.5.0) diff --git a/spec/QueryTools.spec.js b/spec/QueryTools.spec.js index 24e3f64848..de4772a61c 100644 --- a/spec/QueryTools.spec.js +++ b/spec/QueryTools.spec.js @@ -313,6 +313,50 @@ describe('matchesQuery', function () { expect(matchesQuery(player, orQuery)).toBe(true); }); + it('matches an $and query', () => { + const player = { + id: new Id('Player', 'P1'), + name: 'Player 1', + score: 12, + }; + + const q = new Parse.Query('Player'); + q.equalTo('name', 'Player 1'); + const q2 = new Parse.Query('Player'); + q2.equalTo('score', 12); + const q3 = new Parse.Query('Player'); + q3.equalTo('score', 100); + const andQuery1 = Parse.Query.and(q, q2); + const andQuery2 = Parse.Query.and(q, q3); + expect(matchesQuery(player, q)).toBe(true); + expect(matchesQuery(player, q2)).toBe(true); + expect(matchesQuery(player, andQuery1)).toBe(true); + expect(matchesQuery(player, andQuery2)).toBe(false); + }); + + it('matches an $nor query', () => { + const player = { + id: new Id('Player', 'P1'), + name: 'Player 1', + score: 12, + }; + + const q = new Parse.Query('Player'); + q.equalTo('name', 'Player 1'); + const q2 = new Parse.Query('Player'); + q2.equalTo('name', 'Player 2'); + const q3 = new Parse.Query('Player'); + q3.equalTo('name', 'Player 3'); + + const norQuery1 = Parse.Query.nor(q, q2); + const norQuery2 = Parse.Query.nor(q2, q3); + expect(matchesQuery(player, q)).toBe(true); + expect(matchesQuery(player, q2)).toBe(false); + expect(matchesQuery(player, q3)).toBe(false); + expect(matchesQuery(player, norQuery1)).toBe(false); + expect(matchesQuery(player, norQuery2)).toBe(true); + }); + it('matches $regex queries', function () { const player = { id: new Id('Player', 'P1'), @@ -632,4 +676,99 @@ describe('matchesQuery', function () { q.greaterThanOrEqualTo('dateJSON', now); expect(matchesQuery(Object.assign({}, obj), q)).toBe(true); }); + + it('should support containedBy query', () => { + const obj1 = { + id: new Id('Numbers', 'N1'), + numbers: [0, 1, 2], + }; + const obj2 = { + id: new Id('Numbers', 'N2'), + numbers: [2, 0], + }; + const obj3 = { + id: new Id('Numbers', 'N3'), + numbers: [1, 2, 3, 4], + }; + + const q = new Parse.Query('Numbers'); + q.containedBy('numbers', [1, 2, 3, 4, 5]); + expect(matchesQuery(obj1, q)).toBe(false); + expect(matchesQuery(obj2, q)).toBe(false); + expect(matchesQuery(obj3, q)).toBe(true); + }); + + it('should support withinPolygon query', () => { + const sacramento = { + id: new Id('Location', 'L1'), + location: new Parse.GeoPoint(38.52, -121.5), + name: 'Sacramento', + }; + const honolulu = { + id: new Id('Location', 'L2'), + location: new Parse.GeoPoint(21.35, -157.93), + name: 'Honolulu', + }; + const sf = { + id: new Id('Location', 'L3'), + location: new Parse.GeoPoint(37.75, -122.68), + name: 'San Francisco', + }; + + const points = [ + new Parse.GeoPoint(37.85, -122.33), + new Parse.GeoPoint(37.85, -122.9), + new Parse.GeoPoint(37.68, -122.9), + new Parse.GeoPoint(37.68, -122.33), + ]; + const q = new Parse.Query('Location'); + q.withinPolygon('location', points); + + expect(matchesQuery(sacramento, q)).toBe(false); + expect(matchesQuery(honolulu, q)).toBe(false); + expect(matchesQuery(sf, q)).toBe(true); + }); + + it('should support polygonContains query', () => { + const p1 = [ + [0, 0], + [0, 1], + [1, 1], + [1, 0], + ]; + const p2 = [ + [0, 0], + [0, 2], + [2, 2], + [2, 0], + ]; + const p3 = [ + [10, 10], + [10, 15], + [15, 15], + [15, 10], + [10, 10], + ]; + + const obj1 = { + id: new Id('Bounds', 'B1'), + polygon: new Parse.Polygon(p1), + }; + const obj2 = { + id: new Id('Bounds', 'B2'), + polygon: new Parse.Polygon(p2), + }; + const obj3 = { + id: new Id('Bounds', 'B3'), + polygon: new Parse.Polygon(p3), + }; + + const point = new Parse.GeoPoint(0.5, 0.5); + const q = new Parse.Query('Bounds'); + q.polygonContains('polygon', point); + + expect(matchesQuery(obj1, q)).toBe(true); + expect(matchesQuery(obj2, q)).toBe(true); + expect(matchesQuery(obj3, q)).toBe(false); + }); }); diff --git a/src/LiveQuery/QueryTools.js b/src/LiveQuery/QueryTools.js index 03be6cfe41..735788218b 100644 --- a/src/LiveQuery/QueryTools.js +++ b/src/LiveQuery/QueryTools.js @@ -165,6 +165,22 @@ function matchesKeyConstraints(object, key, constraints) { } return false; } + if (key === '$and') { + for (i = 0; i < constraints.length; i++) { + if (!matchesQuery(object, constraints[i])) { + return false; + } + } + return true; + } + if (key === '$nor') { + for (i = 0; i < constraints.length; i++) { + if (matchesQuery(object, constraints[i])) { + return false; + } + } + return true; + } if (key === '$relatedTo') { // Bail! We can't handle relational queries locally return false; @@ -306,6 +322,24 @@ function matchesKeyConstraints(object, key, constraints) { object[key].longitude > southWest.longitude && object[key].longitude < northEast.longitude ); + case '$containedBy': { + for (const value of object[key]) { + if (!contains(compareTo, value)) { + return false; + } + } + return true; + } + case '$geoWithin': { + const points = compareTo.$polygon.map(geoPoint => [geoPoint.latitude, geoPoint.longitude]); + const polygon = new Parse.Polygon(points); + return polygon.containsPoint(object[key]); + } + case '$geoIntersects': { + const polygon = new Parse.Polygon(object[key].coordinates); + const point = new Parse.GeoPoint(compareTo.$point); + return polygon.containsPoint(point); + } case '$options': // Not a query type, but a way to add options to $regex. Ignore and // avoid the default