diff --git a/spec/ParseQuery.Aggregate.spec.js b/spec/ParseQuery.Aggregate.spec.js index c698395085..3668d58969 100644 --- a/spec/ParseQuery.Aggregate.spec.js +++ b/spec/ParseQuery.Aggregate.spec.js @@ -744,4 +744,47 @@ describe('Parse.Query Aggregate testing', () => { fail(err); }); }); + + it_exclude_dbs(['postgres'])('aggregate allow multiple of same stage', (done) => { + const pointer1 = new TestObject({ value: 1}); + const pointer2 = new TestObject({ value: 2}); + const pointer3 = new TestObject({ value: 3}); + + const obj1 = new TestObject({ pointer: pointer1, name: 'Hello' }); + const obj2 = new TestObject({ pointer: pointer2, name: 'Hello' }); + const obj3 = new TestObject({ pointer: pointer3, name: 'World' }); + + const options = Object.assign({}, masterKeyOptions, { + body: [{ + match: { name: "Hello" }, + }, { + // Transform className$objectId to objectId and store in new field tempPointer + project: { + tempPointer: { $substr: [ "$_p_pointer", 11, -1 ] }, // Remove TestObject$ + }, + }, { + // Left Join, replace objectId stored in tempPointer with an actual object + lookup: { + from: "test_TestObject", + localField: "tempPointer", + foreignField: "_id", + as: "tempPointer" + }, + }, { + // lookup returns an array, Deconstructs an array field to objects + unwind: { + path: "$tempPointer", + }, + }, { + match : { "tempPointer.value" : 2 }, + }] + }); + Parse.Object.saveAll([pointer1, pointer2, pointer3, obj1, obj2, obj3]).then(() => { + return rp.get(Parse.serverURL + '/aggregate/TestObject', options); + }).then((resp) => { + expect(resp.results.length).toEqual(1); + expect(resp.results[0].tempPointer.value).toEqual(2); + done(); + }); + }); }); diff --git a/src/Routers/AggregateRouter.js b/src/Routers/AggregateRouter.js index 8f4e859b6d..c99436f380 100644 --- a/src/Routers/AggregateRouter.js +++ b/src/Routers/AggregateRouter.js @@ -4,60 +4,56 @@ import * as middleware from '../middlewares'; import Parse from 'parse/node'; import UsersRouter from './UsersRouter'; -const ALLOWED_KEYS = [ - 'where', - 'distinct', - 'project', +const BASE_KEYS = ['where', 'distinct']; + +const PIPELINE_KEYS = [ + 'addFields', + 'bucket', + 'bucketAuto', + 'collStats', + 'count', + 'currentOp', + 'facet', + 'geoNear', + 'graphLookup', + 'group', + 'indexStats', + 'limit', + 'listLocalSessions', + 'listSessions', + 'lookup', 'match', + 'out', + 'project', 'redact', - 'limit', - 'skip', - 'unwind', - 'group', + 'replaceRoot', 'sample', + 'skip', 'sort', - 'geoNear', - 'lookup', - 'out', - 'indexStats', - 'facet', - 'bucket', - 'bucketAuto', 'sortByCount', - 'addFields', - 'replaceRoot', - 'count', - 'graphLookup', + 'unwind', ]; +const ALLOWED_KEYS = [...BASE_KEYS, ...PIPELINE_KEYS]; + export class AggregateRouter extends ClassesRouter { handleFind(req) { const body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query)); const options = {}; - const pipeline = []; + let pipeline = []; - for (const key in body) { - if (ALLOWED_KEYS.indexOf(key) === -1) { - throw new Parse.Error(Parse.Error.INVALID_QUERY, `Invalid parameter for query: ${key}`); + if (Array.isArray(body)) { + pipeline = body.map((stage) => { + const stageName = Object.keys(stage)[0]; + return this.transformStage(stageName, stage); + }); + } else { + const stages = []; + for (const stageName in body) { + stages.push(this.transformStage(stageName, body)); } - if (key === 'group') { - if (body[key].hasOwnProperty('_id')) { - throw new Parse.Error( - Parse.Error.INVALID_QUERY, - `Invalid parameter for query: group. Please use objectId instead of _id` - ); - } - if (!body[key].hasOwnProperty('objectId')) { - throw new Parse.Error( - Parse.Error.INVALID_QUERY, - `Invalid parameter for query: group. objectId is required` - ); - } - body[key]._id = body[key].objectId; - delete body[key].objectId; - } - pipeline.push({ [`$${key}`]: body[key] }); + pipeline = stages; } if (body.distinct) { options.distinct = String(body.distinct); @@ -76,6 +72,32 @@ export class AggregateRouter extends ClassesRouter { }); } + transformStage(stageName, stage) { + if (ALLOWED_KEYS.indexOf(stageName) === -1) { + throw new Parse.Error( + Parse.Error.INVALID_QUERY, + `Invalid parameter for query: ${stageName}` + ); + } + if (stageName === 'group') { + if (stage[stageName].hasOwnProperty('_id')) { + throw new Parse.Error( + Parse.Error.INVALID_QUERY, + `Invalid parameter for query: group. Please use objectId instead of _id` + ); + } + if (!stage[stageName].hasOwnProperty('objectId')) { + throw new Parse.Error( + Parse.Error.INVALID_QUERY, + `Invalid parameter for query: group. objectId is required` + ); + } + stage[stageName]._id = stage[stageName].objectId; + delete stage[stageName].objectId; + } + return { [`$${stageName}`]: stage[stageName] }; + } + mountRoutes() { this.route('GET','/aggregate/:className', middleware.promiseEnforceMasterKeyAccess, req => { return this.handleFind(req); }); }