Skip to content

Commit e6cc820

Browse files
dplewisflovilmart
authored andcommitted
Add Polygon Type To Schema / PolygonContain to Query (#3944)
* Added type polygon to schema * refactoring and more tests * fix tests * update test and transform * add support for polygonContains * fix transform.mongoObjectToParseObject test * add indexes for polygon * index test * postgres test fix * remove invalid loop test * add invalid loop test * nit
1 parent 0571c6f commit e6cc820

File tree

9 files changed

+470
-5
lines changed

9 files changed

+470
-5
lines changed

spec/MongoTransform.spec.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,13 +145,25 @@ describe('parseObjectToMongoObjectForCreate', () => {
145145
});
146146

147147
it('geopoint', (done) => {
148-
var input = {location: [180, -180]};
148+
var input = {location: [45, -45]};
149149
var output = transform.mongoObjectToParseObject(null, input, {
150150
fields: { location: { type: 'GeoPoint' }},
151151
});
152152
expect(typeof output.location).toEqual('object');
153153
expect(output.location).toEqual(
154-
{__type: 'GeoPoint', longitude: 180, latitude: -180}
154+
{__type: 'GeoPoint', longitude: 45, latitude: -45}
155+
);
156+
done();
157+
});
158+
159+
it('polygon', (done) => {
160+
var input = {location: { type: 'Polygon', coordinates: [[[45, -45],[45, -45]]]}};
161+
var output = transform.mongoObjectToParseObject(null, input, {
162+
fields: { location: { type: 'Polygon' }},
163+
});
164+
expect(typeof output.location).toEqual('object');
165+
expect(output.location).toEqual(
166+
{__type: 'Polygon', coordinates: [[45, -45],[45, -45]]}
155167
);
156168
done();
157169
});

spec/ParseObject.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ describe('Parse.Object testing', () => {
344344

345345
it("invalid __type", function(done) {
346346
var item = new Parse.Object("Item");
347-
var types = ['Pointer', 'File', 'Date', 'GeoPoint', 'Bytes'];
347+
var types = ['Pointer', 'File', 'Date', 'GeoPoint', 'Bytes', 'Polygon'];
348348
var tests = types.map(type => {
349349
var test = new Parse.Object("Item");
350350
test.set('foo', {

spec/ParsePolygon.spec.js

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
const TestObject = Parse.Object.extend('TestObject');
2+
const MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAdapter');
3+
const mongoURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase';
4+
const rp = require('request-promise');
5+
const defaultHeaders = {
6+
'X-Parse-Application-Id': 'test',
7+
'X-Parse-Rest-API-Key': 'rest'
8+
}
9+
10+
describe('Parse.Polygon testing', () => {
11+
it('polygon save open path', (done) => {
12+
const coords = [[0,0],[0,1],[1,1],[1,0]];
13+
const closed = [[0,0],[0,1],[1,1],[1,0],[0,0]];
14+
const obj = new TestObject();
15+
obj.set('polygon', {__type: 'Polygon', coordinates: coords});
16+
return obj.save().then(() => {
17+
const query = new Parse.Query(TestObject);
18+
return query.get(obj.id);
19+
}).then((result) => {
20+
const polygon = result.get('polygon');
21+
equal(polygon.__type, 'Polygon');
22+
equal(polygon.coordinates, closed);
23+
done();
24+
}, done.fail);
25+
});
26+
27+
it('polygon save closed path', (done) => {
28+
const coords = [[0,0],[0,1],[1,1],[1,0],[0,0]];
29+
const obj = new TestObject();
30+
obj.set('polygon', {__type: 'Polygon', coordinates: coords});
31+
return obj.save().then(() => {
32+
const query = new Parse.Query(TestObject);
33+
return query.get(obj.id);
34+
}).then((result) => {
35+
const polygon = result.get('polygon');
36+
equal(polygon.__type, 'Polygon');
37+
equal(polygon.coordinates, coords);
38+
done();
39+
}, done.fail);
40+
});
41+
42+
it('polygon equalTo (open/closed) path', (done) => {
43+
const openPoints = [[0,0],[0,1],[1,1],[1,0]];
44+
const closedPoints = [[0,0],[0,1],[1,1],[1,0],[0,0]];
45+
const openPolygon = {__type: 'Polygon', coordinates: openPoints};
46+
const closedPolygon = {__type: 'Polygon', coordinates: closedPoints};
47+
const obj = new TestObject();
48+
obj.set('polygon', openPolygon);
49+
return obj.save().then(() => {
50+
const query = new Parse.Query(TestObject);
51+
query.equalTo('polygon', openPolygon);
52+
return query.find();
53+
}).then((results) => {
54+
const polygon = results[0].get('polygon');
55+
equal(polygon.__type, 'Polygon');
56+
equal(polygon.coordinates, closedPoints);
57+
const query = new Parse.Query(TestObject);
58+
query.equalTo('polygon', closedPolygon);
59+
return query.find();
60+
}).then((results) => {
61+
const polygon = results[0].get('polygon');
62+
equal(polygon.__type, 'Polygon');
63+
equal(polygon.coordinates, closedPoints);
64+
done();
65+
}, done.fail);
66+
});
67+
68+
it('polygon update', (done) => {
69+
const oldCoords = [[0,0],[0,1],[1,1],[1,0]];
70+
const oldPolygon = {__type: 'Polygon', coordinates: oldCoords};
71+
const newCoords = [[2,2],[2,3],[3,3],[3,2]];
72+
const newPolygon = {__type: 'Polygon', coordinates: newCoords};
73+
const obj = new TestObject();
74+
obj.set('polygon', oldPolygon);
75+
return obj.save().then(() => {
76+
obj.set('polygon', newPolygon);
77+
return obj.save();
78+
}).then(() => {
79+
const query = new Parse.Query(TestObject);
80+
return query.get(obj.id);
81+
}).then((result) => {
82+
const polygon = result.get('polygon');
83+
newCoords.push(newCoords[0]);
84+
equal(polygon.__type, 'Polygon');
85+
equal(polygon.coordinates, newCoords);
86+
done();
87+
}, done.fail);
88+
});
89+
90+
it('polygon invalid value', (done) => {
91+
const coords = [['foo','bar'],[0,1],[1,0],[1,1],[0,0]];
92+
const obj = new TestObject();
93+
obj.set('polygon', {__type: 'Polygon', coordinates: coords});
94+
return obj.save().then(() => {
95+
const query = new Parse.Query(TestObject);
96+
return query.get(obj.id);
97+
}).then(done.fail, done);
98+
});
99+
100+
it('polygon three points minimum', (done) => {
101+
const coords = [[0,0]];
102+
const obj = new TestObject();
103+
obj.set('polygon', {__type: 'Polygon', coordinates: coords});
104+
obj.save().then(done.fail, done);
105+
});
106+
107+
it('polygon three different points minimum', (done) => {
108+
const coords = [[0,0],[0,1],[0,0]];
109+
const obj = new TestObject();
110+
obj.set('polygon', {__type: 'Polygon', coordinates: coords});
111+
obj.save().then(done.fail, done);
112+
});
113+
114+
it('polygon counterclockwise', (done) => {
115+
const coords = [[1,1],[0,1],[0,0],[1,0]];
116+
const closed = [[1,1],[0,1],[0,0],[1,0],[1,1]];
117+
const obj = new TestObject();
118+
obj.set('polygon', {__type: 'Polygon', coordinates: coords});
119+
obj.save().then(() => {
120+
const query = new Parse.Query(TestObject);
121+
return query.get(obj.id);
122+
}).then((result) => {
123+
const polygon = result.get('polygon');
124+
equal(polygon.__type, 'Polygon');
125+
equal(polygon.coordinates, closed);
126+
done();
127+
}, done.fail);
128+
});
129+
130+
it('polygonContain query', (done) => {
131+
const points1 = [[0,0],[0,1],[1,1],[1,0]];
132+
const points2 = [[0,0],[0,2],[2,2],[2,0]];
133+
const points3 = [[10,10],[10,15],[15,15],[15,10],[10,10]];
134+
const polygon1 = {__type: 'Polygon', coordinates: points1};
135+
const polygon2 = {__type: 'Polygon', coordinates: points2};
136+
const polygon3 = {__type: 'Polygon', coordinates: points3};
137+
const obj1 = new TestObject({location: polygon1});
138+
const obj2 = new TestObject({location: polygon2});
139+
const obj3 = new TestObject({location: polygon3});
140+
Parse.Object.saveAll([obj1, obj2, obj3]).then(() => {
141+
const where = {
142+
location: {
143+
$geoIntersects: {
144+
$point: { __type: 'GeoPoint', latitude: 0.5, longitude: 0.5 }
145+
}
146+
}
147+
};
148+
return rp.post({
149+
url: Parse.serverURL + '/classes/TestObject',
150+
json: { where, '_method': 'GET' },
151+
headers: {
152+
'X-Parse-Application-Id': Parse.applicationId,
153+
'X-Parse-Javascript-Key': Parse.javaScriptKey
154+
}
155+
});
156+
}).then((resp) => {
157+
expect(resp.results.length).toBe(2);
158+
done();
159+
}, done.fail);
160+
});
161+
162+
it('polygonContain invalid input', (done) => {
163+
const points = [[0,0],[0,1],[1,1],[1,0]];
164+
const polygon = {__type: 'Polygon', coordinates: points};
165+
const obj = new TestObject({location: polygon});
166+
obj.save().then(() => {
167+
const where = {
168+
location: {
169+
$geoIntersects: {
170+
$point: { __type: 'GeoPoint', latitude: 181, longitude: 181 }
171+
}
172+
}
173+
};
174+
return rp.post({
175+
url: Parse.serverURL + '/classes/TestObject',
176+
json: { where, '_method': 'GET' },
177+
headers: {
178+
'X-Parse-Application-Id': Parse.applicationId,
179+
'X-Parse-Javascript-Key': Parse.javaScriptKey
180+
}
181+
});
182+
}).then(done.fail, done);
183+
});
184+
185+
it('polygonContain invalid geoPoint', (done) => {
186+
const points = [[0,0],[0,1],[1,1],[1,0]];
187+
const polygon = {__type: 'Polygon', coordinates: points};
188+
const obj = new TestObject({location: polygon});
189+
obj.save().then(() => {
190+
const where = {
191+
location: {
192+
$geoIntersects: {
193+
$point: []
194+
}
195+
}
196+
};
197+
return rp.post({
198+
url: Parse.serverURL + '/classes/TestObject',
199+
json: { where, '_method': 'GET' },
200+
headers: {
201+
'X-Parse-Application-Id': Parse.applicationId,
202+
'X-Parse-Javascript-Key': Parse.javaScriptKey
203+
}
204+
});
205+
}).then(done.fail, done);
206+
});
207+
});
208+
209+
describe_only_db('mongo')('Parse.Polygon testing', () => {
210+
it('support 2d and 2dsphere', (done) => {
211+
const coords = [[0,0],[0,1],[1,1],[1,0],[0,0]];
212+
const polygon = {__type: 'Polygon', coordinates: coords};
213+
const location = {__type: 'GeoPoint', latitude:10, longitude:10};
214+
const databaseAdapter = new MongoStorageAdapter({ uri: mongoURI });
215+
return reconfigureServer({
216+
appId: 'test',
217+
restAPIKey: 'rest',
218+
publicServerURL: 'http://localhost:8378/1',
219+
databaseAdapter
220+
}).then(() => {
221+
return databaseAdapter.createIndex('TestObject', {location: '2d'});
222+
}).then(() => {
223+
return databaseAdapter.createIndex('TestObject', {polygon: '2dsphere'});
224+
}).then(() => {
225+
return rp.post({
226+
url: 'http://localhost:8378/1/classes/TestObject',
227+
json: {
228+
'_method': 'POST',
229+
location,
230+
polygon,
231+
polygon2: polygon
232+
},
233+
headers: defaultHeaders
234+
});
235+
}).then((resp) => {
236+
return rp.post({
237+
url: `http://localhost:8378/1/classes/TestObject/${resp.objectId}`,
238+
json: {'_method': 'GET'},
239+
headers: defaultHeaders
240+
});
241+
}).then((resp) => {
242+
equal(resp.location, location);
243+
equal(resp.polygon, polygon);
244+
equal(resp.polygon2, polygon);
245+
return databaseAdapter.getIndexes('TestObject');
246+
}).then((indexes) => {
247+
equal(indexes.length, 4);
248+
equal(indexes[0].key, {_id: 1});
249+
equal(indexes[1].key, {location: '2d'});
250+
equal(indexes[2].key, {polygon: '2dsphere'});
251+
equal(indexes[3].key, {polygon2: '2dsphere'});
252+
done();
253+
}, done.fail);
254+
});
255+
256+
it('polygon loop is not valid', (done) => {
257+
const coords = [[0,0],[0,1],[1,0],[1,1]];
258+
const obj = new TestObject();
259+
obj.set('polygon', {__type: 'Polygon', coordinates: coords});
260+
obj.save().then(done.fail, done);
261+
});
262+
});

spec/Schema.spec.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,7 @@ describe('SchemaController', () => {
524524
aPointer: {type: 'Pointer', targetClass: 'ThisClassDoesNotExistYet'},
525525
aRelation: {type: 'Relation', targetClass: 'NewClass'},
526526
aBytes: {type: 'Bytes'},
527+
aPolygon: {type: 'Polygon'},
527528
}))
528529
.then(actualSchema => {
529530
const expectedSchema = {
@@ -544,6 +545,7 @@ describe('SchemaController', () => {
544545
aPointer: { type: 'Pointer', targetClass: 'ThisClassDoesNotExistYet' },
545546
aRelation: { type: 'Relation', targetClass: 'NewClass' },
546547
aBytes: {type: 'Bytes'},
548+
aPolygon: {type: 'Polygon'},
547549
},
548550
classLevelPermissions: {
549551
find: { '*': true },

src/Adapters/Storage/Mongo/MongoSchemaCollection.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ function mongoFieldToParseSchemaField(type) {
2525
case 'geopoint': return {type: 'GeoPoint'};
2626
case 'file': return {type: 'File'};
2727
case 'bytes': return {type: 'Bytes'};
28+
case 'polygon': return {type: 'Polygon'};
2829
}
2930
}
3031

@@ -98,6 +99,7 @@ function parseFieldTypeToMongoFieldType({ type, targetClass }) {
9899
case 'GeoPoint': return 'geopoint';
99100
case 'File': return 'file';
100101
case 'Bytes': return 'bytes';
102+
case 'Polygon': return 'polygon';
101103
}
102104
}
103105

src/Adapters/Storage/Mongo/MongoStorageAdapter.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,8 @@ export class MongoStorageAdapter {
182182

183183
addFieldIfNotExists(className, fieldName, type) {
184184
return this._schemaCollection()
185-
.then(schemaCollection => schemaCollection.addFieldIfNotExists(className, fieldName, type));
185+
.then(schemaCollection => schemaCollection.addFieldIfNotExists(className, fieldName, type))
186+
.then(() => this.createIndexesIfNeeded(className, fieldName, type));
186187
}
187188

188189
// Drops a collection. Resolves with true if it was a Parse Schema (eg. _User, Custom, etc.)
@@ -429,6 +430,21 @@ export class MongoStorageAdapter {
429430
return this._adaptiveCollection(className)
430431
.then(collection => collection._mongoCollection.createIndex(index));
431432
}
433+
434+
createIndexesIfNeeded(className, fieldName, type) {
435+
if (type && type.type === 'Polygon') {
436+
const index = {
437+
[fieldName]: '2dsphere'
438+
};
439+
return this.createIndex(className, index);
440+
}
441+
return Promise.resolve();
442+
}
443+
444+
getIndexes(className) {
445+
return this._adaptiveCollection(className)
446+
.then(collection => collection._mongoCollection.indexes());
447+
}
432448
}
433449

434450
export default MongoStorageAdapter;

0 commit comments

Comments
 (0)