Skip to content

Commit

Permalink
feat(LiveQuery): Support $and, $nor, $containedBy, $geoWithin (#7113)
Browse files Browse the repository at this point in the history
* feat(LiveQuery): Support $and, $nor, $containedBy, $geoWithin, $geoIntersects

* Update CHANGELOG.md

* Update CHANGELOG.md
  • Loading branch information
dplewis authored Jan 11, 2021
1 parent 8851810 commit 93781b2
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
139 changes: 139 additions & 0 deletions spec/QueryTools.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down Expand Up @@ -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);
});
});
34 changes: 34 additions & 0 deletions src/LiveQuery/QueryTools.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 93781b2

Please sign in to comment.