Skip to content

Commit a4a6d0b

Browse files
dplewisflovilmart
authored andcommitted
Aggregate allow multiple of same stage (#4835)
* Aggregate Allow Multiple Stages * remove testing files * nit * spread them
1 parent 74ea6a8 commit a4a6d0b

File tree

2 files changed

+105
-40
lines changed

2 files changed

+105
-40
lines changed

spec/ParseQuery.Aggregate.spec.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -744,4 +744,47 @@ describe('Parse.Query Aggregate testing', () => {
744744
fail(err);
745745
});
746746
});
747+
748+
it_exclude_dbs(['postgres'])('aggregate allow multiple of same stage', (done) => {
749+
const pointer1 = new TestObject({ value: 1});
750+
const pointer2 = new TestObject({ value: 2});
751+
const pointer3 = new TestObject({ value: 3});
752+
753+
const obj1 = new TestObject({ pointer: pointer1, name: 'Hello' });
754+
const obj2 = new TestObject({ pointer: pointer2, name: 'Hello' });
755+
const obj3 = new TestObject({ pointer: pointer3, name: 'World' });
756+
757+
const options = Object.assign({}, masterKeyOptions, {
758+
body: [{
759+
match: { name: "Hello" },
760+
}, {
761+
// Transform className$objectId to objectId and store in new field tempPointer
762+
project: {
763+
tempPointer: { $substr: [ "$_p_pointer", 11, -1 ] }, // Remove TestObject$
764+
},
765+
}, {
766+
// Left Join, replace objectId stored in tempPointer with an actual object
767+
lookup: {
768+
from: "test_TestObject",
769+
localField: "tempPointer",
770+
foreignField: "_id",
771+
as: "tempPointer"
772+
},
773+
}, {
774+
// lookup returns an array, Deconstructs an array field to objects
775+
unwind: {
776+
path: "$tempPointer",
777+
},
778+
}, {
779+
match : { "tempPointer.value" : 2 },
780+
}]
781+
});
782+
Parse.Object.saveAll([pointer1, pointer2, pointer3, obj1, obj2, obj3]).then(() => {
783+
return rp.get(Parse.serverURL + '/aggregate/TestObject', options);
784+
}).then((resp) => {
785+
expect(resp.results.length).toEqual(1);
786+
expect(resp.results[0].tempPointer.value).toEqual(2);
787+
done();
788+
});
789+
});
747790
});

src/Routers/AggregateRouter.js

Lines changed: 62 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,60 +4,56 @@ import * as middleware from '../middlewares';
44
import Parse from 'parse/node';
55
import UsersRouter from './UsersRouter';
66

7-
const ALLOWED_KEYS = [
8-
'where',
9-
'distinct',
10-
'project',
7+
const BASE_KEYS = ['where', 'distinct'];
8+
9+
const PIPELINE_KEYS = [
10+
'addFields',
11+
'bucket',
12+
'bucketAuto',
13+
'collStats',
14+
'count',
15+
'currentOp',
16+
'facet',
17+
'geoNear',
18+
'graphLookup',
19+
'group',
20+
'indexStats',
21+
'limit',
22+
'listLocalSessions',
23+
'listSessions',
24+
'lookup',
1125
'match',
26+
'out',
27+
'project',
1228
'redact',
13-
'limit',
14-
'skip',
15-
'unwind',
16-
'group',
29+
'replaceRoot',
1730
'sample',
31+
'skip',
1832
'sort',
19-
'geoNear',
20-
'lookup',
21-
'out',
22-
'indexStats',
23-
'facet',
24-
'bucket',
25-
'bucketAuto',
2633
'sortByCount',
27-
'addFields',
28-
'replaceRoot',
29-
'count',
30-
'graphLookup',
34+
'unwind',
3135
];
3236

37+
const ALLOWED_KEYS = [...BASE_KEYS, ...PIPELINE_KEYS];
38+
3339
export class AggregateRouter extends ClassesRouter {
3440

3541
handleFind(req) {
3642
const body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query));
3743
const options = {};
38-
const pipeline = [];
44+
let pipeline = [];
3945

40-
for (const key in body) {
41-
if (ALLOWED_KEYS.indexOf(key) === -1) {
42-
throw new Parse.Error(Parse.Error.INVALID_QUERY, `Invalid parameter for query: ${key}`);
46+
if (Array.isArray(body)) {
47+
pipeline = body.map((stage) => {
48+
const stageName = Object.keys(stage)[0];
49+
return this.transformStage(stageName, stage);
50+
});
51+
} else {
52+
const stages = [];
53+
for (const stageName in body) {
54+
stages.push(this.transformStage(stageName, body));
4355
}
44-
if (key === 'group') {
45-
if (body[key].hasOwnProperty('_id')) {
46-
throw new Parse.Error(
47-
Parse.Error.INVALID_QUERY,
48-
`Invalid parameter for query: group. Please use objectId instead of _id`
49-
);
50-
}
51-
if (!body[key].hasOwnProperty('objectId')) {
52-
throw new Parse.Error(
53-
Parse.Error.INVALID_QUERY,
54-
`Invalid parameter for query: group. objectId is required`
55-
);
56-
}
57-
body[key]._id = body[key].objectId;
58-
delete body[key].objectId;
59-
}
60-
pipeline.push({ [`$${key}`]: body[key] });
56+
pipeline = stages;
6157
}
6258
if (body.distinct) {
6359
options.distinct = String(body.distinct);
@@ -76,6 +72,32 @@ export class AggregateRouter extends ClassesRouter {
7672
});
7773
}
7874

75+
transformStage(stageName, stage) {
76+
if (ALLOWED_KEYS.indexOf(stageName) === -1) {
77+
throw new Parse.Error(
78+
Parse.Error.INVALID_QUERY,
79+
`Invalid parameter for query: ${stageName}`
80+
);
81+
}
82+
if (stageName === 'group') {
83+
if (stage[stageName].hasOwnProperty('_id')) {
84+
throw new Parse.Error(
85+
Parse.Error.INVALID_QUERY,
86+
`Invalid parameter for query: group. Please use objectId instead of _id`
87+
);
88+
}
89+
if (!stage[stageName].hasOwnProperty('objectId')) {
90+
throw new Parse.Error(
91+
Parse.Error.INVALID_QUERY,
92+
`Invalid parameter for query: group. objectId is required`
93+
);
94+
}
95+
stage[stageName]._id = stage[stageName].objectId;
96+
delete stage[stageName].objectId;
97+
}
98+
return { [`$${stageName}`]: stage[stageName] };
99+
}
100+
79101
mountRoutes() {
80102
this.route('GET','/aggregate/:className', middleware.promiseEnforceMasterKeyAccess, req => { return this.handleFind(req); });
81103
}

0 commit comments

Comments
 (0)