-
-
Notifications
You must be signed in to change notification settings - Fork 4.8k
Support for Aggregate Queries #4207
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
9ef32c7
a138d86
a04ede9
8b5e858
ea3ec59
6b7b2ca
a8d1b24
9c866c7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,343 @@ | ||
| 'use strict'; | ||
| const Parse = require('parse/node'); | ||
| const rp = require('request-promise'); | ||
|
|
||
| const masterKeyHeaders = { | ||
| 'X-Parse-Application-Id': 'test', | ||
| 'X-Parse-Rest-API-Key': 'test', | ||
| 'X-Parse-Master-Key': 'test' | ||
| } | ||
|
|
||
| const masterKeyOptions = { | ||
| headers: masterKeyHeaders, | ||
| json: true | ||
| } | ||
|
|
||
| const loadTestData = () => { | ||
| const data1 = {score: 10, name: 'foo', sender: {group: 'A'}, size: ['S', 'M']}; | ||
| const data2 = {score: 10, name: 'foo', sender: {group: 'A'}, size: ['M', 'L']}; | ||
| const data3 = {score: 10, name: 'bar', sender: {group: 'B'}, size: ['S']}; | ||
| const data4 = {score: 20, name: 'dpl', sender: {group: 'B'}, size: ['S']}; | ||
| const obj1 = new TestObject(data1); | ||
| const obj2 = new TestObject(data2); | ||
| const obj3 = new TestObject(data3); | ||
| const obj4 = new TestObject(data4); | ||
| return Parse.Object.saveAll([obj1, obj2, obj3, obj4]); | ||
| } | ||
|
|
||
| describe('Parse.Query Aggregate testing', () => { | ||
| beforeEach((done) => { | ||
| loadTestData().then(done, done); | ||
| }); | ||
|
|
||
| it('should only query aggregate with master key', (done) => { | ||
| Parse._request('GET', `aggregate/someClass`, {}) | ||
| .then(() => {}, (error) => { | ||
| expect(error.message).toEqual('unauthorized: master key is required'); | ||
| done(); | ||
| }); | ||
| }); | ||
|
|
||
| it('invalid query', (done) => { | ||
| const options = Object.assign({}, masterKeyOptions, { | ||
| body: { | ||
| unknown: {}, | ||
| } | ||
| }); | ||
| rp.get(Parse.serverURL + '/aggregate/TestObject', options) | ||
| .catch((error) => { | ||
| expect(error.error.code).toEqual(Parse.Error.INVALID_QUERY); | ||
| done(); | ||
| }); | ||
| }); | ||
|
|
||
| it('group by field', (done) => { | ||
| const options = Object.assign({}, masterKeyOptions, { | ||
| body: { | ||
| group: { _id: '$name' }, | ||
| } | ||
| }); | ||
| rp.get(Parse.serverURL + '/aggregate/TestObject', options) | ||
| .then((resp) => { | ||
| expect(resp.results.length).toBe(3); | ||
| done(); | ||
| }).catch(done.fail); | ||
| }); | ||
|
|
||
| it('group sum query', (done) => { | ||
| const options = Object.assign({}, masterKeyOptions, { | ||
| body: { | ||
| group: { _id: null, total: { $sum: '$score' } }, | ||
| } | ||
| }); | ||
| rp.get(Parse.serverURL + '/aggregate/TestObject', options) | ||
| .then((resp) => { | ||
| expect(resp.results[0].total).toBe(50); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just curious what happens if one of the
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just tried. If unset/null it gets ignored |
||
| done(); | ||
| }).catch(done.fail); | ||
| }); | ||
|
|
||
| it('group count query', (done) => { | ||
| const options = Object.assign({}, masterKeyOptions, { | ||
| body: { | ||
| group: { _id: null, total: { $sum: 1 } }, | ||
| } | ||
| }); | ||
| rp.get(Parse.serverURL + '/aggregate/TestObject', options) | ||
| .then((resp) => { | ||
| expect(resp.results[0].total).toBe(4); | ||
| done(); | ||
| }).catch(done.fail); | ||
| }); | ||
|
|
||
| it('group min query', (done) => { | ||
| const options = Object.assign({}, masterKeyOptions, { | ||
| body: { | ||
| group: { _id: null, minScore: { $min: '$score' } }, | ||
| } | ||
| }); | ||
| rp.get(Parse.serverURL + '/aggregate/TestObject', options) | ||
| .then((resp) => { | ||
| expect(resp.results[0].minScore).toBe(10); | ||
| done(); | ||
| }).catch(done.fail); | ||
| }); | ||
|
|
||
| it('group max query', (done) => { | ||
| const options = Object.assign({}, masterKeyOptions, { | ||
| body: { | ||
| group: { _id: null, maxScore: { $max: '$score' } }, | ||
| } | ||
| }); | ||
| rp.get(Parse.serverURL + '/aggregate/TestObject', options) | ||
| .then((resp) => { | ||
| expect(resp.results[0].maxScore).toBe(20); | ||
| done(); | ||
| }).catch(done.fail); | ||
| }); | ||
|
|
||
| it('group avg query', (done) => { | ||
| const options = Object.assign({}, masterKeyOptions, { | ||
| body: { | ||
| group: { _id: null, avgScore: { $avg: '$score' } }, | ||
| } | ||
| }); | ||
| rp.get(Parse.serverURL + '/aggregate/TestObject', options) | ||
| .then((resp) => { | ||
| expect(resp.results[0].avgScore).toBe(12.5); | ||
| done(); | ||
| }).catch(done.fail); | ||
| }); | ||
|
|
||
| it('limit query', (done) => { | ||
| const options = Object.assign({}, masterKeyOptions, { | ||
| body: { | ||
| limit: 2, | ||
| } | ||
| }); | ||
| rp.get(Parse.serverURL + '/aggregate/TestObject', options) | ||
| .then((resp) => { | ||
| expect(resp.results.length).toBe(2); | ||
| done(); | ||
| }).catch(done.fail); | ||
| }); | ||
|
|
||
| it('sort ascending query', (done) => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As the user builds their aggregate queries they have the option to limit, sort, skip. This test is also there for Postgres support
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I realized that right after I noted this, you're good here. |
||
| const options = Object.assign({}, masterKeyOptions, { | ||
| body: { | ||
| sort: { name: 1 }, | ||
| } | ||
| }); | ||
| rp.get(Parse.serverURL + '/aggregate/TestObject', options) | ||
| .then((resp) => { | ||
| expect(resp.results.length).toBe(4); | ||
| expect(resp.results[0].name).toBe('bar'); | ||
| expect(resp.results[1].name).toBe('dpl'); | ||
| expect(resp.results[2].name).toBe('foo'); | ||
| expect(resp.results[3].name).toBe('foo'); | ||
| done(); | ||
| }).catch(done.fail); | ||
| }); | ||
|
|
||
| it('sort decending query', (done) => { | ||
| const options = Object.assign({}, masterKeyOptions, { | ||
| body: { | ||
| sort: { name: -1 }, | ||
| } | ||
| }); | ||
| rp.get(Parse.serverURL + '/aggregate/TestObject', options) | ||
| .then((resp) => { | ||
| expect(resp.results.length).toBe(4); | ||
| expect(resp.results[0].name).toBe('foo'); | ||
| expect(resp.results[1].name).toBe('foo'); | ||
| expect(resp.results[2].name).toBe('dpl'); | ||
| expect(resp.results[3].name).toBe('bar'); | ||
| done(); | ||
| }).catch(done.fail); | ||
| }); | ||
|
|
||
| it('skip query', (done) => { | ||
| const options = Object.assign({}, masterKeyOptions, { | ||
| body: { | ||
| skip: 2, | ||
| } | ||
| }); | ||
| rp.get(Parse.serverURL + '/aggregate/TestObject', options) | ||
| .then((resp) => { | ||
| expect(resp.results.length).toBe(2); | ||
| done(); | ||
| }).catch(done.fail); | ||
| }); | ||
|
|
||
| it('match query', (done) => { | ||
| const options = Object.assign({}, masterKeyOptions, { | ||
| body: { | ||
| match: { score: { $gt: 15 }}, | ||
| } | ||
| }); | ||
| rp.get(Parse.serverURL + '/aggregate/TestObject', options) | ||
| .then((resp) => { | ||
| expect(resp.results.length).toBe(1); | ||
| expect(resp.results[0].score).toBe(20); | ||
| done(); | ||
| }).catch(done.fail); | ||
| }); | ||
|
|
||
| it('project query', (done) => { | ||
| const options = Object.assign({}, masterKeyOptions, { | ||
| body: { | ||
| project: { name: 1 }, | ||
| } | ||
| }); | ||
| rp.get(Parse.serverURL + '/aggregate/TestObject', options) | ||
| .then((resp) => { | ||
| resp.results.forEach((result) => { | ||
| expect(result.name !== undefined).toBe(true); | ||
| expect(result.sender).toBe(undefined); | ||
| expect(result.size).toBe(undefined); | ||
| expect(result.score).toBe(undefined); | ||
| }); | ||
| done(); | ||
| }).catch(done.fail); | ||
| }); | ||
|
|
||
| it('class does not exist return empty', (done) => { | ||
| const options = Object.assign({}, masterKeyOptions, { | ||
| body: { | ||
| group: { _id: null, total: { $sum: '$score' } }, | ||
| } | ||
| }); | ||
| rp.get(Parse.serverURL + '/aggregate/UnknownClass', options) | ||
| .then((resp) => { | ||
| expect(resp.results.length).toBe(0); | ||
| done(); | ||
| }).catch(done.fail); | ||
| }); | ||
|
|
||
| it('field does not exist return empty', (done) => { | ||
| const options = Object.assign({}, masterKeyOptions, { | ||
| body: { | ||
| group: { _id: null, total: { $sum: '$unknownfield' } }, | ||
| } | ||
| }); | ||
| rp.get(Parse.serverURL + '/aggregate/UnknownClass', options) | ||
| .then((resp) => { | ||
| expect(resp.results.length).toBe(0); | ||
| done(); | ||
| }).catch(done.fail); | ||
| }); | ||
|
|
||
| it('distinct query', (done) => { | ||
| const options = Object.assign({}, masterKeyOptions, { | ||
| body: { distinct: 'score' } | ||
| }); | ||
| rp.get(Parse.serverURL + '/aggregate/TestObject', options) | ||
| .then((resp) => { | ||
| expect(resp.results.length).toBe(2); | ||
| expect(resp.results.includes(10)).toBe(true); | ||
| expect(resp.results.includes(20)).toBe(true); | ||
| done(); | ||
| }).catch(done.fail); | ||
| }); | ||
|
|
||
| it('distint query with where', (done) => { | ||
|
||
| const options = Object.assign({}, masterKeyOptions, { | ||
| body: { | ||
| distinct: 'score', | ||
| where: { | ||
| name: 'bar' | ||
| } | ||
| } | ||
| }); | ||
| rp.get(Parse.serverURL + '/aggregate/TestObject', options) | ||
| .then((resp) => { | ||
| expect(resp.results[0]).toBe(10); | ||
| done(); | ||
| }).catch(done.fail); | ||
| }); | ||
|
|
||
| it('distint query with where string', (done) => { | ||
|
||
| const options = Object.assign({}, masterKeyOptions, { | ||
| body: { | ||
| distinct: 'score', | ||
| where: JSON.stringify({name:'bar'}), | ||
| } | ||
| }); | ||
| rp.get(Parse.serverURL + '/aggregate/TestObject', options) | ||
| .then((resp) => { | ||
| expect(resp.results[0]).toBe(10); | ||
| done(); | ||
| }).catch(done.fail); | ||
| }); | ||
|
|
||
| it('distinct nested', (done) => { | ||
| const options = Object.assign({}, masterKeyOptions, { | ||
| body: { distinct: 'sender.group' } | ||
| }); | ||
| rp.get(Parse.serverURL + '/aggregate/TestObject', options) | ||
| .then((resp) => { | ||
| expect(resp.results.length).toBe(2); | ||
| expect(resp.results.includes('A')).toBe(true); | ||
| expect(resp.results.includes('B')).toBe(true); | ||
| done(); | ||
| }).catch(done.fail); | ||
| }); | ||
|
|
||
| it('distinct class does not exist return empty', (done) => { | ||
| const options = Object.assign({}, masterKeyOptions, { | ||
| body: { distinct: 'unknown' } | ||
| }); | ||
| rp.get(Parse.serverURL + '/aggregate/UnknownClass', options) | ||
| .then((resp) => { | ||
| expect(resp.results.length).toBe(0); | ||
| done(); | ||
| }).catch(done.fail); | ||
| }); | ||
|
|
||
| it('distinct field does not exist return empty', (done) => { | ||
| const options = Object.assign({}, masterKeyOptions, { | ||
| body: { distinct: 'unknown' } | ||
| }); | ||
| const obj = new TestObject(); | ||
| obj.save().then(() => { | ||
| return rp.get(Parse.serverURL + '/aggregate/TestObject', options); | ||
| }).then((resp) => { | ||
| expect(resp.results.length).toBe(0); | ||
| done(); | ||
| }).catch(done.fail); | ||
| }); | ||
|
|
||
| it('distinct array', (done) => { | ||
| const options = Object.assign({}, masterKeyOptions, { | ||
| body: { distinct: 'size' } | ||
| }); | ||
| rp.get(Parse.serverURL + '/aggregate/TestObject', options) | ||
| .then((resp) => { | ||
| expect(resp.results.length).toBe(3); | ||
| expect(resp.results.includes('S')).toBe(true); | ||
| expect(resp.results.includes('M')).toBe(true); | ||
| expect(resp.results.includes('L')).toBe(true); | ||
| done(); | ||
| }).catch(done.fail); | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_id should be transformed (from objectId), which would be true for all keys passed here :)
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So
group: { objectId: null, total: { $sum: '$score' } }And it should return
[ { objectId: null, total: ... } ]instead of[ { _id: null, total: ... } ]and
group: { objectId: '$name', total: { $sum: '$score' } }Should return
[ { name: ..., total: ... } ]There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, in and out, those should not be the internal storage columns but the externals
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should an error be thrown if _id is used?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, most probably!