diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index 8aee36a5b3..533c23d980 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -3985,4 +3985,106 @@ describe('Parse.Query testing', () => { }) }); + 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); + 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 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); + 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(); + }); + }); + + 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 with invalid distance', (done) => { + const q = new Parse.Query(TestObject); + const jsonQ = q.toJSON(); + jsonQ.where.location = { + '$geoWithin': { + '$centerSphere': [ + [0, 0], + 'invalid_distance' + ] + } + }; + q.withJSON(jsonQ); + q.find(expectError(Parse.Error.INVALID_JSON, done)); + }); + + it('withJSON with geoWithin.centerSphere fails with invalid coordinate', (done) => { + const q = new Parse.Query(TestObject); + const jsonQ = q.toJSON(); + jsonQ.where.location = { + '$geoWithin': { + '$centerSphere': [ + [-190,-190], + 1 + ] + } + }; + 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)); + }); }); 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 78578865f9..453bddef70 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -905,44 +905,70 @@ 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) { - throw new Parse.Error( - Parse.Error.INVALID_JSON, - 'bad $geoWithin value; $polygon should contain at least 3 GeoPoints' - ); + 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; $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) => { + // 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]); - return point; + 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'); } - if (!GeoPointCoder.isValidJSON(point)) { - throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad $geoWithin value'); - } 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) { + throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad $geoWithin value; $centerSphere distance invalid'); } - return [point.longitude, point.latitude]; - }); - answer[key] = { - '$polygon': points - }; + answer[key] = { + '$centerSphere': [ + [point.longitude, point.latitude], + distance + ] + }; + } break; } case '$geoIntersects': { diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index ce950f41fc..2df5abb66d 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -535,6 +535,30 @@ 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, 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 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) { + 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; @@ -1986,4 +2010,13 @@ function literalizeRegexPart(s: string) { ); } +var GeoPointCoder = { + isValidJSON(value) { + return (typeof value === 'object' && + value !== null && + value.__type === 'GeoPoint' + ); + } +}; + export default PostgresStorageAdapter;