Skip to content

Commit a712abb

Browse files
dplewisflovilmart
authored andcommitted
Support for Aggregate Queries (#485)
* Support for Aggregate Queries * bump to parse-server to 2.7.0 * travis.yml 6.11.4 and docs * jest tests * improve jest test * fix CoreManager tests * rebase master
1 parent a209282 commit a712abb

File tree

6 files changed

+413
-57
lines changed

6 files changed

+413
-57
lines changed

integration/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"dependencies": {
44
"express": "^4.13.4",
55
"mocha": "^2.4.5",
6-
"parse-server": "^2.6.0"
6+
"parse-server": "^2.7.0"
77
},
88
"scripts": {
99
"test": "mocha --reporter dot -t 5000"
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
'use strict';
2+
3+
const assert = require('assert');
4+
const clear = require('./clear');
5+
const mocha = require('mocha');
6+
const Parse = require('../../node');
7+
8+
const TestObject = Parse.Object.extend('TestObject');
9+
10+
describe('Parse Aggregate Query', () => {
11+
before((done) => {
12+
Parse.initialize('integration', null, 'notsosecret');
13+
Parse.CoreManager.set('SERVER_URL', 'http://localhost:1337/parse');
14+
Parse.Storage._clear();
15+
clear().then(() => {
16+
const obj1 = new TestObject({score: 10, name: 'foo'});
17+
const obj2 = new TestObject({score: 10, name: 'foo'});
18+
const obj3 = new TestObject({score: 10, name: 'bar'});
19+
const obj4 = new TestObject({score: 20, name: 'dpl'});
20+
return Parse.Object.saveAll([obj1, obj2, obj3, obj4]);
21+
}).then(() => {
22+
return Parse.User.logOut();
23+
})
24+
.then(() => { done() }, () => { done() });
25+
});
26+
27+
it('aggregate pipeline object query', (done) => {
28+
const pipeline = {
29+
group: { objectId: '$name' }
30+
};
31+
const query = new Parse.Query(TestObject);
32+
query.aggregate(pipeline).then((results) => {
33+
assert.equal(results.length, 3);
34+
done();
35+
});
36+
});
37+
38+
it('aggregate pipeline array query', (done) => {
39+
const pipeline = [
40+
{ group: { objectId: '$name' } }
41+
];
42+
const query = new Parse.Query(TestObject);
43+
query.aggregate(pipeline).then((results) => {
44+
assert.equal(results.length, 3);
45+
done();
46+
});
47+
});
48+
49+
it('aggregate pipeline invalid query', (done) => {
50+
const pipeline = 1234;
51+
const query = new Parse.Query(TestObject);
52+
try {
53+
query.aggregate(pipeline).then(() => {});
54+
} catch (e) {
55+
done();
56+
}
57+
});
58+
59+
it('distinct query', (done) => {
60+
const query = new Parse.Query(TestObject);
61+
query.distinct('score').then((results) => {
62+
assert.equal(results.length, 2);
63+
assert.equal(results[0], 10);
64+
assert.equal(results[1], 20);
65+
done();
66+
});
67+
});
68+
69+
it('distinct equalTo query', (done) => {
70+
const query = new Parse.Query(TestObject);
71+
query.equalTo('name', 'foo')
72+
query.distinct('score').then((results) => {
73+
assert.equal(results.length, 1);
74+
assert.equal(results[0], 10);
75+
done();
76+
});
77+
});
78+
});

src/CoreManager.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ type PushController = {
7171
};
7272
type QueryController = {
7373
find: (className: string, params: QueryJSON, options: RequestOptions) => ParsePromise;
74+
aggregate: (className: string, params: any, options: RequestOptions) => ParsePromise;
7475
};
7576
type RESTController = {
7677
request: (method: string, path: string, data: mixed) => ParsePromise;
@@ -268,7 +269,7 @@ module.exports = {
268269
},
269270

270271
setQueryController(controller: QueryController) {
271-
requireMethods('QueryController', ['find'], controller);
272+
requireMethods('QueryController', ['find', 'aggregate'], controller);
272273
config['QueryController'] = controller;
273274
},
274275

src/ParseQuery.js

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,91 @@ class ParseQuery {
484484
})._thenRunCallbacks(options);
485485
}
486486

487+
/**
488+
* Executes a distinct query and returns unique values
489+
*
490+
* @param {String} key A field to find distinct values
491+
* @param {Object} options A Backbone-style options object. Valid options
492+
* are:<ul>
493+
* <li>success: Function to call when the count completes successfully.
494+
* <li>error: Function to call when the find fails.
495+
* <li>sessionToken: A valid session token, used for making a request on
496+
* behalf of a specific user.
497+
* </ul>
498+
*
499+
* @return {Parse.Promise} A promise that is resolved with the query completes.
500+
*/
501+
distinct(key: string, options?: FullOptions): ParsePromise {
502+
options = options || {};
503+
504+
const distinctOptions = {
505+
useMasterKey: true
506+
};
507+
if (options.hasOwnProperty('sessionToken')) {
508+
distinctOptions.sessionToken = options.sessionToken;
509+
}
510+
const controller = CoreManager.getQueryController();
511+
const params = {
512+
distinct: key,
513+
where: this._where
514+
};
515+
516+
return controller.aggregate(
517+
this.className,
518+
params,
519+
distinctOptions
520+
).then((results) => {
521+
return results.results;
522+
})._thenRunCallbacks(options);
523+
}
524+
525+
/**
526+
* Executes an aggregate query and returns aggregate results
527+
*
528+
* @param {Mixed} pipeline Array or Object of stages to process query
529+
* @param {Object} options A Backbone-style options object. Valid options
530+
* are:<ul>
531+
* <li>success: Function to call when the count completes successfully.
532+
* <li>error: Function to call when the find fails.
533+
* <li>sessionToken: A valid session token, used for making a request on
534+
* behalf of a specific user.
535+
* </ul>
536+
*
537+
* @return {Parse.Promise} A promise that is resolved with the query completes.
538+
*/
539+
aggregate(pipeline: mixed, options?: FullOptions): ParsePromise {
540+
options = options || {};
541+
542+
const aggregateOptions = {
543+
useMasterKey: true
544+
};
545+
if (options.hasOwnProperty('sessionToken')) {
546+
aggregateOptions.sessionToken = options.sessionToken;
547+
}
548+
const controller = CoreManager.getQueryController();
549+
let stages = {};
550+
551+
if (Array.isArray(pipeline)) {
552+
pipeline.forEach((stage) => {
553+
for (let op in stage) {
554+
stages[op] = stage[op];
555+
}
556+
});
557+
} else if (pipeline && typeof pipeline === 'object') {
558+
stages = pipeline;
559+
} else {
560+
throw new Error('Invalid pipeline must be Array or Object');
561+
}
562+
563+
return controller.aggregate(
564+
this.className,
565+
stages,
566+
aggregateOptions
567+
).then((results) => {
568+
return results.results;
569+
})._thenRunCallbacks(options);
570+
}
571+
487572
/**
488573
* Retrieves at most one Parse.Object that satisfies this query.
489574
*
@@ -1196,6 +1281,17 @@ var DefaultController = {
11961281
params,
11971282
options
11981283
);
1284+
},
1285+
1286+
aggregate(className: string, params: any, options: RequestOptions): ParsePromise {
1287+
const RESTController = CoreManager.getRESTController();
1288+
1289+
return RESTController.request(
1290+
'GET',
1291+
'aggregate/' + className,
1292+
params,
1293+
options
1294+
);
11991295
}
12001296
};
12011297

src/__tests__/CoreManager-test.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,13 +220,15 @@ describe('CoreManager', () => {
220220
);
221221

222222
expect(CoreManager.setQueryController.bind(null, {
223-
find: function() {}
223+
find: function() {},
224+
aggregate: function() {}
224225
})).not.toThrow();
225226
});
226227

227228
it('can set and get QueryController', () => {
228229
var controller = {
229-
find: function() {}
230+
find: function() {},
231+
aggregate: function() {}
230232
};
231233

232234
CoreManager.setQueryController(controller);

0 commit comments

Comments
 (0)