From 88474f3d1d975b91580c757f880a74046b85bbca Mon Sep 17 00:00:00 2001 From: Manuel Trezza Date: Mon, 11 Jun 2018 18:39:16 +0200 Subject: [PATCH 1/6] add support for geoWithin.centerSphere queries via withJSON --- spec/ParseQuery.spec.js | 27 +++++++ src/Adapters/Storage/Mongo/MongoTransform.js | 78 +++++++++++++------- 2 files changed, 80 insertions(+), 25 deletions(-) diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index 8aee36a5b3..792b0fcb7a 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -3985,4 +3985,31 @@ describe('Parse.Query testing', () => { }) }); + it('toJSON works with geoWithin.centerSphere', (done) => { + const inbound = new Parse.GeoPoint(1.5, 1.5); + const onbound = new Parse.GeoPoint(10, 10); + const outbound = new Parse.GeoPoint(20, 20); + const obj1 = new Parse.Object('TestObject', {location: inbound}); + const obj2 = new Parse.Object('TestObject', {location: onbound}); + const obj3 = new Parse.Object('TestObject', {location: outbound}); + Parse.Object.saveAll([obj1, obj2, obj3]).then(() => { + const center = new Parse.GeoPoint(0, 0); + const distanceInKilometers = 1569 + 1; // 1569km is the approximate distance between {0, 0} and {10, 10}. + const q = new Parse.Query(TestObject); + const jsonQ = q.toJSON(); + jsonQ.where.location = { + '$geoWithin': { + '$centerSphere': [ + center, + distanceInKilometers / 6371.0 + ] + } + }; + q.withJSON(jsonQ); + return q.find(); + }).then(results => { + equal(results.length, 2); + done(); + }); + }); }); diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 78578865f9..75f43e35cc 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -905,44 +905,72 @@ function transformConstraint(constraint, field) { case '$geoWithin': { const polygon = constraint[key]['$polygon']; - let points; - if (typeof polygon === 'object' && polygon.__type === 'Polygon') { - if (!polygon.coordinates || polygon.coordinates.length < 3) { + const centerSphere = constraint[key]['$centerSphere']; + if (polygon !== undefined) { + let points; + if (typeof polygon === 'object' && polygon.__type === 'Polygon') { + if (!polygon.coordinates || polygon.coordinates.length < 3) { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + 'bad $geoWithin value; Polygon.coordinates should contain at least 3 lon/lat pairs' + ); + } + points = polygon.coordinates; + } else if (polygon instanceof Array) { + if (polygon.length < 3) { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + 'bad $geoWithin value; $polygon should contain at least 3 GeoPoints' + ); + } + points = polygon; + } else { throw new Parse.Error( Parse.Error.INVALID_JSON, - 'bad $geoWithin value; Polygon.coordinates should contain at least 3 lon/lat pairs' + 'bad $geoWithin value; $polygon should be Polygon object or Array of Parse.GeoPoint\'s' ); } - points = polygon.coordinates; - } else if (polygon instanceof Array) { - if (polygon.length < 3) { + points = points.map((point) => { + if (point instanceof Array && point.length === 2) { + Parse.GeoPoint._validate(point[1], point[0]); + return point; + } + if (!GeoPointCoder.isValidJSON(point)) { + throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad $geoWithin value'); + } else { + Parse.GeoPoint._validate(point.latitude, point.longitude); + } + return [point.longitude, point.latitude]; + }); + answer[key] = { + '$polygon': points + }; + } else if (centerSphere !== undefined) { + if (!(centerSphere instanceof Array) || centerSphere.length < 2) { throw new Parse.Error( Parse.Error.INVALID_JSON, - 'bad $geoWithin value; $polygon should contain at least 3 GeoPoints' + 'bad $geoWithin value; $centerSphere should be an array of Parse.Geopoint and distance' ); } - points = polygon; - } else { - throw new Parse.Error( - Parse.Error.INVALID_JSON, - 'bad $geoWithin value; $polygon should be Polygon object or Array of Parse.GeoPoint\'s' - ); - } - points = points.map((point) => { + const point = centerSphere[0]; if (point instanceof Array && point.length === 2) { Parse.GeoPoint._validate(point[1], point[0]); - return point; - } - if (!GeoPointCoder.isValidJSON(point)) { - throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad $geoWithin value'); + } else if (!GeoPointCoder.isValidJSON(point)) { + throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad $geoWithin value; $centerSphere geo point invalid'); } else { Parse.GeoPoint._validate(point.latitude, point.longitude); } - return [point.longitude, point.latitude]; - }); - answer[key] = { - '$polygon': points - }; + const distance = centerSphere[1] + if(isNaN(distance) || distance < 0) { + throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad $geoWithin value; $centerSphere distance invalid') + } + answer[key] = { + '$centerSphere': [ + [point.longitude, point.latitude], + distance + ] + }; + } break; } case '$geoIntersects': { From b69a44785f4ed7e5c2f00d47673346ce60e5b63f Mon Sep 17 00:00:00 2001 From: Manuel Trezza Date: Mon, 11 Jun 2018 22:12:08 +0200 Subject: [PATCH 2/6] added test for passing array of lat, lng instead of Parse.GeoPoint --- spec/ParseQuery.spec.js | 21 ++++++++++++++++++-- src/Adapters/Storage/Mongo/MongoTransform.js | 14 ++++++------- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index 792b0fcb7a..d498246361 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -3992,9 +3992,9 @@ describe('Parse.Query testing', () => { const obj1 = new Parse.Object('TestObject', {location: inbound}); const obj2 = new Parse.Object('TestObject', {location: onbound}); const obj3 = new Parse.Object('TestObject', {location: outbound}); + const center = new Parse.GeoPoint(0, 0); + const distanceInKilometers = 1569 + 1; // 1569km is the approximate distance between {0, 0} and {10, 10}. Parse.Object.saveAll([obj1, obj2, obj3]).then(() => { - const center = new Parse.GeoPoint(0, 0); - const distanceInKilometers = 1569 + 1; // 1569km is the approximate distance between {0, 0} and {10, 10}. const q = new Parse.Query(TestObject); const jsonQ = q.toJSON(); jsonQ.where.location = { @@ -4009,6 +4009,23 @@ describe('Parse.Query testing', () => { return q.find(); }).then(results => { equal(results.length, 2); + const q = new Parse.Query(TestObject); + const jsonQ = q.toJSON(); + jsonQ.where.location = { + '$geoWithin': { + '$centerSphere': [ + [0, 0], + distanceInKilometers / 6371.0 + ] + } + }; + q.withJSON(jsonQ); + return q.find(); + }).then(results => { + equal(results.length, 2); + done(); + }).catch(error => { + fail(error); done(); }); }); diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 75f43e35cc..f3f888ace8 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -947,22 +947,22 @@ function transformConstraint(constraint, field) { }; } else if (centerSphere !== undefined) { if (!(centerSphere instanceof Array) || centerSphere.length < 2) { - throw new Parse.Error( - Parse.Error.INVALID_JSON, - 'bad $geoWithin value; $centerSphere should be an array of Parse.Geopoint and distance' - ); + throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad $geoWithin value; $centerSphere should be an array of Parse.GeoPoint and distance'); } - const point = centerSphere[0]; + // Get point, convert to geo point if necessary and validate + let point = centerSphere[0]; if (point instanceof Array && point.length === 2) { Parse.GeoPoint._validate(point[1], point[0]); + point = new Parse.GeoPoint(point[1], point[0]); } else if (!GeoPointCoder.isValidJSON(point)) { throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad $geoWithin value; $centerSphere geo point invalid'); } else { Parse.GeoPoint._validate(point.latitude, point.longitude); } - const distance = centerSphere[1] + // Get distance and validate + const distance = centerSphere[1]; if(isNaN(distance) || distance < 0) { - throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad $geoWithin value; $centerSphere distance invalid') + throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad $geoWithin value; $centerSphere distance invalid'); } answer[key] = { '$centerSphere': [ From bfbdf5910cd589776057ab9342d9da8d762aa427 Mon Sep 17 00:00:00 2001 From: Manuel Trezza Date: Mon, 11 Jun 2018 23:48:30 +0200 Subject: [PATCH 3/6] added postgres support --- .../Postgres/PostgresStorageAdapter.js | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index ce950f41fc..0771c1ca54 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -535,6 +535,29 @@ const buildWhereClause = ({ schema, query, index }): WhereClause => { index += 2; } + if (fieldValue.$geoWithin && fieldValue.$geoWithin.$centerSphere) { + const centerSphere = fieldValue.$geoWithin.$centerSphere; + if (!(centerSphere instanceof Array) || centerSphere.length < 2) { + throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad $geoWithin value; $centerSphere should be an array of Parse.GeoPoint and distance'); + } + // Get point and validate + let point = centerSphere[0]; + if (point instanceof Array && point.length === 2) { + point = new Parse.GeoPoint(point[1], point[0]); + } else { + Parse.GeoPoint._validate(point.latitude, point.longitude); + } + // Get distance and validate + const distance = centerSphere[1]; + if(isNaN(distance) || distance < 0) { + throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad $geoWithin value; $centerSphere distance invalid'); + } + const distanceInKM = distance * 6371 * 1000; + patterns.push(`ST_distance_sphere($${index}:name::geometry, POINT($${index + 1}, $${index + 2})::geometry) <= $${index + 3}`); + values.push(fieldName, point.longitude, point.latitude, distanceInKM); + index += 4; + } + if (fieldValue.$geoWithin && fieldValue.$geoWithin.$polygon) { const polygon = fieldValue.$geoWithin.$polygon; let points; From 14f9d6d74f2b3bba687a33cf1fc4762b2d8b9f85 Mon Sep 17 00:00:00 2001 From: Manuel Trezza Date: Tue, 12 Jun 2018 00:45:38 +0200 Subject: [PATCH 4/6] added more tests --- spec/ParseQuery.spec.js | 44 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index d498246361..293c34191f 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -3985,7 +3985,7 @@ describe('Parse.Query testing', () => { }) }); - it('toJSON works with geoWithin.centerSphere', (done) => { + it('withJSON supports geoWithin.centerSphere', (done) => { const inbound = new Parse.GeoPoint(1.5, 1.5); const onbound = new Parse.GeoPoint(10, 10); const outbound = new Parse.GeoPoint(20, 20); @@ -4029,4 +4029,46 @@ describe('Parse.Query testing', () => { done(); }); }); + + it('withJSON with geoWithin.centerSphere fails without parameters', (done) => { + const q = new Parse.Query(TestObject); + const jsonQ = q.toJSON(); + jsonQ.where.location = { + '$geoWithin': { + '$centerSphere': [ + ] + } + }; + q.withJSON(jsonQ); + q.find(expectError(Parse.Error.INVALID_JSON, done)); + }); + + it('withJSON with geoWithin.centerSphere fails without distance', (done) => { + const q = new Parse.Query(TestObject); + const jsonQ = q.toJSON(); + jsonQ.where.location = { + '$geoWithin': { + '$centerSphere': [ + [0, 0] + ] + } + }; + q.withJSON(jsonQ); + q.find(expectError(Parse.Error.INVALID_JSON, done)); + }); + + it('withJSON with geoWithin.centerSphere fails with invalid geo point', (done) => { + const q = new Parse.Query(TestObject); + const jsonQ = q.toJSON(); + jsonQ.where.location = { + '$geoWithin': { + '$centerSphere': [ + [0], + 1 + ] + } + }; + q.withJSON(jsonQ); + q.find(expectError(Parse.Error.INVALID_JSON, done)); + }); }); From 1f69f16af8b5e759d1f8a553704552bca10fba2d Mon Sep 17 00:00:00 2001 From: Manuel Trezza Date: Tue, 12 Jun 2018 01:50:44 +0200 Subject: [PATCH 5/6] improved tests and validation --- spec/ParseQuery.spec.js | 4 ++-- spec/helper.js | 10 ++++++---- src/Adapters/Storage/Mongo/MongoTransform.js | 4 +--- .../Storage/Postgres/PostgresStorageAdapter.js | 16 +++++++++++++--- 4 files changed, 22 insertions(+), 12 deletions(-) diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index 293c34191f..f38bf33179 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -4063,12 +4063,12 @@ describe('Parse.Query testing', () => { jsonQ.where.location = { '$geoWithin': { '$centerSphere': [ - [0], + [-190,-190], 1 ] } }; q.withJSON(jsonQ); - q.find(expectError(Parse.Error.INVALID_JSON, done)); + q.find(expectError(undefined, done)); }); }); diff --git a/spec/helper.js b/spec/helper.js index d4186f66ee..b7dd8138af 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -294,11 +294,13 @@ function expectError(errorCode, callback) { error: function(obj, e) { // Some methods provide 2 parameters. e = e || obj; - if (!e) { - fail('expected a specific error but got a blank error'); - return; + if (errorCode !== undefined) { + if (!e) { + fail('expected a specific error but got a blank error'); + return; + } + expect(e.code).toEqual(errorCode, e.message); } - expect(e.code).toEqual(errorCode, e.message); if (callback) { callback(e); } diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index f3f888ace8..453bddef70 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -952,13 +952,11 @@ function transformConstraint(constraint, field) { // Get point, convert to geo point if necessary and validate let point = centerSphere[0]; if (point instanceof Array && point.length === 2) { - Parse.GeoPoint._validate(point[1], point[0]); point = new Parse.GeoPoint(point[1], point[0]); } else if (!GeoPointCoder.isValidJSON(point)) { throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad $geoWithin value; $centerSphere geo point invalid'); - } else { - Parse.GeoPoint._validate(point.latitude, point.longitude); } + Parse.GeoPoint._validate(point.latitude, point.longitude); // Get distance and validate const distance = centerSphere[1]; if(isNaN(distance) || distance < 0) { diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index 0771c1ca54..2df5abb66d 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -540,13 +540,14 @@ const buildWhereClause = ({ schema, query, index }): WhereClause => { if (!(centerSphere instanceof Array) || centerSphere.length < 2) { throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad $geoWithin value; $centerSphere should be an array of Parse.GeoPoint and distance'); } - // Get point and validate + // Get point, convert to geo point if necessary and validate let point = centerSphere[0]; if (point instanceof Array && point.length === 2) { point = new Parse.GeoPoint(point[1], point[0]); - } else { - Parse.GeoPoint._validate(point.latitude, point.longitude); + } else if (!GeoPointCoder.isValidJSON(point)) { + throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad $geoWithin value; $centerSphere geo point invalid'); } + Parse.GeoPoint._validate(point.latitude, point.longitude); // Get distance and validate const distance = centerSphere[1]; if(isNaN(distance) || distance < 0) { @@ -2009,4 +2010,13 @@ function literalizeRegexPart(s: string) { ); } +var GeoPointCoder = { + isValidJSON(value) { + return (typeof value === 'object' && + value !== null && + value.__type === 'GeoPoint' + ); + } +}; + export default PostgresStorageAdapter; From c0f877091890f4984ee33cc9eafb169a6467bc25 Mon Sep 17 00:00:00 2001 From: Manuel Trezza Date: Tue, 12 Jun 2018 02:35:10 +0200 Subject: [PATCH 6/6] added more tests --- spec/ParseQuery.spec.js | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index f38bf33179..533c23d980 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -4043,13 +4043,14 @@ describe('Parse.Query testing', () => { q.find(expectError(Parse.Error.INVALID_JSON, done)); }); - it('withJSON with geoWithin.centerSphere fails without distance', (done) => { + it('withJSON with geoWithin.centerSphere fails with invalid distance', (done) => { const q = new Parse.Query(TestObject); const jsonQ = q.toJSON(); jsonQ.where.location = { '$geoWithin': { '$centerSphere': [ - [0, 0] + [0, 0], + 'invalid_distance' ] } }; @@ -4057,7 +4058,7 @@ describe('Parse.Query testing', () => { q.find(expectError(Parse.Error.INVALID_JSON, done)); }); - it('withJSON with geoWithin.centerSphere fails with invalid geo point', (done) => { + it('withJSON with geoWithin.centerSphere fails with invalid coordinate', (done) => { const q = new Parse.Query(TestObject); const jsonQ = q.toJSON(); jsonQ.where.location = { @@ -4071,4 +4072,19 @@ describe('Parse.Query testing', () => { q.withJSON(jsonQ); q.find(expectError(undefined, done)); }); + + it('withJSON with geoWithin.centerSphere fails with invalid geo point', (done) => { + const q = new Parse.Query(TestObject); + const jsonQ = q.toJSON(); + jsonQ.where.location = { + '$geoWithin': { + '$centerSphere': [ + {'longitude': 0, 'dummytude': 0}, + 1 + ] + } + }; + q.withJSON(jsonQ); + q.find(expectError(undefined, done)); + }); });