Skip to content

Commit 93781b2

Browse files
authored
feat(LiveQuery): Support $and, $nor, $containedBy, $geoWithin (#7113)
* feat(LiveQuery): Support $and, $nor, $containedBy, $geoWithin, $geoIntersects * Update CHANGELOG.md * Update CHANGELOG.md
1 parent 8851810 commit 93781b2

File tree

3 files changed

+174
-0
lines changed

3 files changed

+174
-0
lines changed

Diff for: CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ ___
99
- 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)
1010
- FIX: request.context for afterFind triggers. [#7078](https://github.com/parse-community/parse-server/pull/7078). Thanks to [dblythy](https://github.com/dblythy)
1111
- 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)
12+
- 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)
1213

1314
### 4.5.0
1415
[Full Changelog](https://github.com/parse-community/parse-server/compare/4.4.0...4.5.0)

Diff for: spec/QueryTools.spec.js

+139
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,50 @@ describe('matchesQuery', function () {
313313
expect(matchesQuery(player, orQuery)).toBe(true);
314314
});
315315

316+
it('matches an $and query', () => {
317+
const player = {
318+
id: new Id('Player', 'P1'),
319+
name: 'Player 1',
320+
score: 12,
321+
};
322+
323+
const q = new Parse.Query('Player');
324+
q.equalTo('name', 'Player 1');
325+
const q2 = new Parse.Query('Player');
326+
q2.equalTo('score', 12);
327+
const q3 = new Parse.Query('Player');
328+
q3.equalTo('score', 100);
329+
const andQuery1 = Parse.Query.and(q, q2);
330+
const andQuery2 = Parse.Query.and(q, q3);
331+
expect(matchesQuery(player, q)).toBe(true);
332+
expect(matchesQuery(player, q2)).toBe(true);
333+
expect(matchesQuery(player, andQuery1)).toBe(true);
334+
expect(matchesQuery(player, andQuery2)).toBe(false);
335+
});
336+
337+
it('matches an $nor query', () => {
338+
const player = {
339+
id: new Id('Player', 'P1'),
340+
name: 'Player 1',
341+
score: 12,
342+
};
343+
344+
const q = new Parse.Query('Player');
345+
q.equalTo('name', 'Player 1');
346+
const q2 = new Parse.Query('Player');
347+
q2.equalTo('name', 'Player 2');
348+
const q3 = new Parse.Query('Player');
349+
q3.equalTo('name', 'Player 3');
350+
351+
const norQuery1 = Parse.Query.nor(q, q2);
352+
const norQuery2 = Parse.Query.nor(q2, q3);
353+
expect(matchesQuery(player, q)).toBe(true);
354+
expect(matchesQuery(player, q2)).toBe(false);
355+
expect(matchesQuery(player, q3)).toBe(false);
356+
expect(matchesQuery(player, norQuery1)).toBe(false);
357+
expect(matchesQuery(player, norQuery2)).toBe(true);
358+
});
359+
316360
it('matches $regex queries', function () {
317361
const player = {
318362
id: new Id('Player', 'P1'),
@@ -632,4 +676,99 @@ describe('matchesQuery', function () {
632676
q.greaterThanOrEqualTo('dateJSON', now);
633677
expect(matchesQuery(Object.assign({}, obj), q)).toBe(true);
634678
});
679+
680+
it('should support containedBy query', () => {
681+
const obj1 = {
682+
id: new Id('Numbers', 'N1'),
683+
numbers: [0, 1, 2],
684+
};
685+
const obj2 = {
686+
id: new Id('Numbers', 'N2'),
687+
numbers: [2, 0],
688+
};
689+
const obj3 = {
690+
id: new Id('Numbers', 'N3'),
691+
numbers: [1, 2, 3, 4],
692+
};
693+
694+
const q = new Parse.Query('Numbers');
695+
q.containedBy('numbers', [1, 2, 3, 4, 5]);
696+
expect(matchesQuery(obj1, q)).toBe(false);
697+
expect(matchesQuery(obj2, q)).toBe(false);
698+
expect(matchesQuery(obj3, q)).toBe(true);
699+
});
700+
701+
it('should support withinPolygon query', () => {
702+
const sacramento = {
703+
id: new Id('Location', 'L1'),
704+
location: new Parse.GeoPoint(38.52, -121.5),
705+
name: 'Sacramento',
706+
};
707+
const honolulu = {
708+
id: new Id('Location', 'L2'),
709+
location: new Parse.GeoPoint(21.35, -157.93),
710+
name: 'Honolulu',
711+
};
712+
const sf = {
713+
id: new Id('Location', 'L3'),
714+
location: new Parse.GeoPoint(37.75, -122.68),
715+
name: 'San Francisco',
716+
};
717+
718+
const points = [
719+
new Parse.GeoPoint(37.85, -122.33),
720+
new Parse.GeoPoint(37.85, -122.9),
721+
new Parse.GeoPoint(37.68, -122.9),
722+
new Parse.GeoPoint(37.68, -122.33),
723+
];
724+
const q = new Parse.Query('Location');
725+
q.withinPolygon('location', points);
726+
727+
expect(matchesQuery(sacramento, q)).toBe(false);
728+
expect(matchesQuery(honolulu, q)).toBe(false);
729+
expect(matchesQuery(sf, q)).toBe(true);
730+
});
731+
732+
it('should support polygonContains query', () => {
733+
const p1 = [
734+
[0, 0],
735+
[0, 1],
736+
[1, 1],
737+
[1, 0],
738+
];
739+
const p2 = [
740+
[0, 0],
741+
[0, 2],
742+
[2, 2],
743+
[2, 0],
744+
];
745+
const p3 = [
746+
[10, 10],
747+
[10, 15],
748+
[15, 15],
749+
[15, 10],
750+
[10, 10],
751+
];
752+
753+
const obj1 = {
754+
id: new Id('Bounds', 'B1'),
755+
polygon: new Parse.Polygon(p1),
756+
};
757+
const obj2 = {
758+
id: new Id('Bounds', 'B2'),
759+
polygon: new Parse.Polygon(p2),
760+
};
761+
const obj3 = {
762+
id: new Id('Bounds', 'B3'),
763+
polygon: new Parse.Polygon(p3),
764+
};
765+
766+
const point = new Parse.GeoPoint(0.5, 0.5);
767+
const q = new Parse.Query('Bounds');
768+
q.polygonContains('polygon', point);
769+
770+
expect(matchesQuery(obj1, q)).toBe(true);
771+
expect(matchesQuery(obj2, q)).toBe(true);
772+
expect(matchesQuery(obj3, q)).toBe(false);
773+
});
635774
});

Diff for: src/LiveQuery/QueryTools.js

+34
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,22 @@ function matchesKeyConstraints(object, key, constraints) {
165165
}
166166
return false;
167167
}
168+
if (key === '$and') {
169+
for (i = 0; i < constraints.length; i++) {
170+
if (!matchesQuery(object, constraints[i])) {
171+
return false;
172+
}
173+
}
174+
return true;
175+
}
176+
if (key === '$nor') {
177+
for (i = 0; i < constraints.length; i++) {
178+
if (matchesQuery(object, constraints[i])) {
179+
return false;
180+
}
181+
}
182+
return true;
183+
}
168184
if (key === '$relatedTo') {
169185
// Bail! We can't handle relational queries locally
170186
return false;
@@ -306,6 +322,24 @@ function matchesKeyConstraints(object, key, constraints) {
306322
object[key].longitude > southWest.longitude &&
307323
object[key].longitude < northEast.longitude
308324
);
325+
case '$containedBy': {
326+
for (const value of object[key]) {
327+
if (!contains(compareTo, value)) {
328+
return false;
329+
}
330+
}
331+
return true;
332+
}
333+
case '$geoWithin': {
334+
const points = compareTo.$polygon.map(geoPoint => [geoPoint.latitude, geoPoint.longitude]);
335+
const polygon = new Parse.Polygon(points);
336+
return polygon.containsPoint(object[key]);
337+
}
338+
case '$geoIntersects': {
339+
const polygon = new Parse.Polygon(object[key].coordinates);
340+
const point = new Parse.GeoPoint(compareTo.$point);
341+
return polygon.containsPoint(point);
342+
}
309343
case '$options':
310344
// Not a query type, but a way to add options to $regex. Ignore and
311345
// avoid the default

0 commit comments

Comments
 (0)