Skip to content

Commit bf2e501

Browse files
committed
"Near" fix for where clauses
See loopbackio#993 for original code Tests for "Near" fix for where clause See loopbackio#993 for original code Fix `nearFilter` key array
1 parent 66c54a9 commit bf2e501

File tree

2 files changed

+143
-51
lines changed

2 files changed

+143
-51
lines changed

lib/geo.js

Lines changed: 88 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -9,80 +9,119 @@ var assert = require('assert');
99

1010
/*!
1111
* Get a near filter from a given where object. For connector use only.
12+
* NB: This only supports one near parameter per where object. eg, this:
13+
* {where: {or: [{location: {near: "29,-90"}},{location: {near: "50,-72"}}]}}
14+
* would throw an error.
1215
*/
1316

1417
exports.nearFilter = function nearFilter(where) {
15-
var result = false;
16-
17-
if (where && typeof where === 'object') {
18-
Object.keys(where).forEach(function(key) {
19-
var ex = where[key];
20-
21-
if (ex && ex.near) {
22-
result = {
23-
near: ex.near,
24-
maxDistance: ex.maxDistance,
25-
unit: ex.unit,
26-
key: key,
27-
};
18+
function nearSearch(clause, parentKeys) {
19+
if (typeof clause !== 'object') {
20+
return false;
21+
}
22+
if (!parentKeys) {
23+
parentKeys = [];
24+
}
25+
26+
Object.keys(clause).forEach(function(clauseKey) {
27+
if (Array.isArray(clause[clauseKey])) {
28+
clause[clauseKey].forEach(function(el) {
29+
var ret = nearSearch(el, parentKeys.concat(clauseKey));
30+
if (ret) return ret;
31+
});
32+
} else {
33+
if (clause[clauseKey].hasOwnProperty('near')) {
34+
var result = clause[clauseKey];
35+
nearResults.push({
36+
near: result.near,
37+
maxDistance: result.maxDistance,
38+
unit: result.unit,
39+
key: parentKeys.concat(clauseKey),
40+
});
41+
}
2842
}
2943
});
3044
}
45+
var nearResults = [];
46+
nearSearch(where);
3147

32-
return result;
48+
if (nearResults.length === 0) {
49+
return false;
50+
}
51+
return nearResults;
3352
};
3453

3554
/*!
36-
* Filter a set of objects using the given `nearFilter`.
55+
* Filter a set of objects using the given `nearFilter`. Can support multiple
56+
* locations, but will include results from all of them.
57+
*
58+
* WARNING: "or" operator with GeoPoint does not work as expected, eg:
59+
* {where: {or: [{location: {near: (29,-90)}},{name:'Sean'}]}}
60+
* Will actually work as if you had used "and". This is because geo filtering
61+
* takes place outside of the SQL query, so the result set of "name = Sean" is
62+
* returned by the database, and then the location filtering happens in the app
63+
* logic. So the "near" operator is always an "and" of the SQL filters, and "or"
64+
* of other GeoPoint filters.
65+
*
66+
* Additionally, since this step occurs after the SQL result set is returned,
67+
* if using GeoPoints with pagination the result set may be smaller than the
68+
* page size. The page size is enforced at the DB level, and then we may
69+
* remove results at the Geo-app level. If we "limit: 25", but 4 of those results
70+
* do not have a matching geopoint field, the request will only return 21 results.
71+
* This may make it erroneously look like a given page is the end of the result set.
3772
*/
3873

3974
exports.filter = function(arr, filter) {
40-
var origin = filter.near;
41-
var max = filter.maxDistance > 0 ? filter.maxDistance : false;
42-
var unit = filter.unit;
43-
var key = filter.key;
44-
45-
// create distance index
4675
var distances = {};
4776
var result = [];
4877

49-
arr.forEach(function(obj) {
50-
var loc = obj[key];
78+
filters.forEach(function(filter) {
79+
var origin = filter.near;
80+
var max = filter.maxDistance > 0 ? filter.maxDistance : false;
81+
var unit = filter.unit;
82+
var key = filter.key;
5183

52-
// filter out objects without locations
53-
if (!loc) return;
84+
// create distance index
85+
arr.forEach(function(obj) {
86+
var loc = obj[key];
5487

55-
if (!(loc instanceof GeoPoint)) {
56-
loc = GeoPoint(loc);
57-
}
88+
// filter out objects without locations
89+
if (!loc) return;
90+
91+
if (!(loc instanceof GeoPoint)) {
92+
loc = GeoPoint(loc);
93+
}
5894

59-
if (typeof loc.lat !== 'number') return;
60-
if (typeof loc.lng !== 'number') return;
95+
if (typeof loc.lat !== 'number') return;
96+
if (typeof loc.lng !== 'number') return;
6197

62-
var d = GeoPoint.distanceBetween(origin, loc, {type: unit});
98+
var d = GeoPoint.distanceBetween(origin, loc, { type: unit });
6399

64-
if (max && d > max) {
65-
// dont add
66-
} else {
67-
distances[obj.id] = d;
68-
result.push(obj);
69-
}
70-
});
100+
if (max && d > max) {
101+
// dont add
102+
} else {
103+
distances[obj.id] = d;
104+
result.push(obj);
105+
}
106+
});
71107

72-
return result.sort(function(objA, objB) {
73-
var a = objA[key];
74-
var b = objB[key];
108+
result.sort(function(objA, objB) {
109+
var a = objA[key];
110+
var b = objB[key];
75111

76-
if (a && b) {
77-
var da = distances[objA.id];
78-
var db = distances[objB.id];
112+
if (a && b) {
113+
var da = distances[objA.id];
114+
var db = distances[objB.id];
79115

80-
if (db === da) return 0;
81-
return da > db ? 1 : -1;
82-
} else {
83-
return 0;
84-
}
116+
if (db === da) return 0;
117+
return da > db ? 1 : -1;
118+
} else {
119+
return 0;
120+
}
121+
});
85122
});
123+
124+
return result;
86125
};
87126

88127
exports.GeoPoint = GeoPoint;

test/basic-querying.test.js

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ describe('basic-querying', function() {
2222
role: {type: String, index: true},
2323
order: {type: Number, index: true, sort: true},
2424
vip: {type: Boolean},
25+
addressLoc: { type: 'GeoPoint' },
2526
});
2627

2728
db.automigrate(done);
@@ -520,6 +521,48 @@ describe('basic-querying', function() {
520521
});
521522
});
522523

524+
it('should support nested GeoPoint near queries', function(done) {
525+
User.find({
526+
where: { and: [{ addressLoc: { near: '29.9,-90.07' }}, { vip: true }] },
527+
}, function(err, users) {
528+
if (err) return done(err);
529+
users.should.have.property('length', 2);
530+
users[0].addressLoc.should.not.equal(null);
531+
done();
532+
});
533+
});
534+
535+
it('should support very nested GeoPoint near queries', function(done) {
536+
User.find({
537+
where: { and: [
538+
{ and: [{ addressLoc: { near: '29.9,-90.07' }}, { order: 2 }] },
539+
{ vip: true },
540+
] },
541+
}, function(err, users) {
542+
if (err) return done(err);
543+
users.should.have.property('length', 1);
544+
users[0].addressLoc.should.not.equal(null);
545+
done();
546+
});
547+
});
548+
549+
it('should support multiple GeoPoint near queries', function(done) {
550+
User.find({
551+
where: { and: [
552+
{ or: [
553+
{ addressLoc: { near: '29.9,-90.04', maxDistance: 300 }},
554+
{ addressLoc: { near: '22.97, -88.03', maxDistance: 300 }},
555+
] },
556+
{ vip: true },
557+
] },
558+
}, function(err, users) {
559+
if (err) return done(err);
560+
users.should.have.property('length', 2);
561+
users[0].addressLoc.should.not.equal(null);
562+
done();
563+
});
564+
});
565+
523566
it('should only include fields as specified', function(done) {
524567
var remaining = 0;
525568

@@ -557,7 +600,9 @@ describe('basic-querying', function() {
557600
}
558601

559602
sample({name: true}).expect(['name']);
560-
sample({name: false}).expect(['id', 'seq', 'email', 'role', 'order', 'birthday', 'vip']);
603+
sample({ name: false }).expect([
604+
'id', 'seq', 'email', 'role', 'order', 'birthday', 'vip', 'addressLoc'
605+
]);
561606
sample({name: false, id: true}).expect(['id']);
562607
sample({id: true}).expect(['id']);
563608
sample('id').expect(['id']);
@@ -869,6 +914,7 @@ function seed(done) {
869914
birthday: new Date('1980-12-08'),
870915
order: 2,
871916
vip: true,
917+
addressLoc: { lat: 29.97, lng: -90.03 },
872918
},
873919
{
874920
seq: 1,
@@ -878,8 +924,15 @@ function seed(done) {
878924
birthday: new Date('1942-06-18'),
879925
order: 1,
880926
vip: true,
927+
addressLoc: { lat: 22.97, lng: -88.03 },
928+
},
929+
{
930+
seq: 2,
931+
name: 'George Harrison',
932+
order: 5,
933+
vip: false,
934+
addressLoc: { lat: 22.7, lng: -89.03 },
881935
},
882-
{seq: 2, name: 'George Harrison', order: 5, vip: false},
883936
{seq: 3, name: 'Ringo Starr', order: 6, vip: false},
884937
{seq: 4, name: 'Pete Best', order: 4},
885938
{seq: 5, name: 'Stuart Sutcliffe', order: 3, vip: true},

0 commit comments

Comments
 (0)