Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
343 changes: 343 additions & 0 deletions spec/ParseQuery.Aggregate.spec.js
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' } },
Copy link
Contributor

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 :)

Copy link
Member Author

@dplewis dplewis Sep 24, 2017

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: ... } ]

Copy link
Contributor

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

Copy link
Member Author

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?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, most probably!

}
});
rp.get(Parse.serverURL + '/aggregate/TestObject', options)
.then((resp) => {
expect(resp.results[0].total).toBe(50);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious what happens if one of the score values happens to be unset/null? I'm assuming it just omits it but would be nice to know for sure.

Copy link
Member Author

Choose a reason for hiding this comment

The 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) => {
Copy link
Contributor

@montymxb montymxb Nov 4, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like it duplicates functionality we already have in standard queries, is this needed for something in particular where the original won't do? Read through the rest, nvm this comment.

Copy link
Member Author

Choose a reason for hiding this comment

The 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

Copy link
Contributor

Choose a reason for hiding this comment

The 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) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo on distint

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) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo on distint again

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);
});
});
8 changes: 8 additions & 0 deletions src/Adapters/Storage/Mongo/MongoCollection.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ export default class MongoCollection {
return countOperation;
}

distinct(field, query) {
return this._mongoCollection.distinct(field, query);
}

aggregate(pipeline, { maxTimeMS, readPreference } = {}) {
return this._mongoCollection.aggregate(pipeline, { maxTimeMS, readPreference }).toArray();
}

insertOne(object) {
return this._mongoCollection.insertOne(object);
}
Expand Down
12 changes: 12 additions & 0 deletions src/Adapters/Storage/Mongo/MongoStorageAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,18 @@ export class MongoStorageAdapter {
}));
}

distinct(className, schema, query, fieldName) {
schema = convertParseSchemaToMongoSchema(schema);
return this._adaptiveCollection(className)
.then(collection => collection.distinct(fieldName, transformWhere(className, query, schema)));
}

aggregate(className, pipeline, readPreference) {
readPreference = this._parseReadPreference(readPreference);
return this._adaptiveCollection(className)
.then(collection => collection.aggregate(pipeline, { readPreference, maxTimeMS: this._maxTimeMS }));
}

_parseReadPreference(readPreference) {
if (readPreference) {
switch (readPreference) {
Expand Down
Loading