feat: Remove deprecation DEPPS1: Native MongoDB syntax in aggregation pipeline (#8362)

BREAKING CHANGE: The MongoDB aggregation pipeline requires native MongoDB syntax instead of the custom Parse Server syntax; for example pipeline stage names require a leading dollar sign like `$match` and the MongoDB document ID is referenced using `_id` instead of `objectId` (#8362)
This commit is contained in:
Daniel
2023-01-06 01:53:43 +11:00
committed by GitHub
parent df00cbebe3
commit d0d30c4f13
8 changed files with 149 additions and 148 deletions

View File

@@ -4,7 +4,7 @@ The following is a list of deprecations, according to the [Deprecation Policy](h
| ID | Change | Issue | Deprecation [][i_deprecation] | Planned Removal [][i_removal] | Status [][i_status] | Notes | | ID | Change | Issue | Deprecation [][i_deprecation] | Planned Removal [][i_removal] | Status [][i_status] | Notes |
|--------|-------------------------------------------------|----------------------------------------------------------------------|---------------------------------|---------------------------------|-----------------------|-------| |--------|-------------------------------------------------|----------------------------------------------------------------------|---------------------------------|---------------------------------|-----------------------|-------|
| DEPPS1 | Native MongoDB syntax in aggregation pipeline | [#7338](https://github.com/parse-community/parse-server/issues/7338) | 5.0.0 (2022) | 6.0.0 (2023) | deprecated | - | | DEPPS1 | Native MongoDB syntax in aggregation pipeline | [#7338](https://github.com/parse-community/parse-server/issues/7338) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - |
| DEPPS2 | Config option `directAccess` defaults to `true` | [#6636](https://github.com/parse-community/parse-server/pull/6636) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - | | DEPPS2 | Config option `directAccess` defaults to `true` | [#6636](https://github.com/parse-community/parse-server/pull/6636) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - |
| DEPPS3 | Config option `enforcePrivateUsers` defaults to `true` | [#7319](https://github.com/parse-community/parse-server/pull/7319) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - | | DEPPS3 | Config option `enforcePrivateUsers` defaults to `true` | [#7319](https://github.com/parse-community/parse-server/pull/7319) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - |
| DEPPS4 | Remove convenience method for http request `Parse.Cloud.httpRequest` | [#7589](https://github.com/parse-community/parse-server/pull/7589) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - | | DEPPS4 | Remove convenience method for http request `Parse.Cloud.httpRequest` | [#7589](https://github.com/parse-community/parse-server/pull/7589) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - |

View File

@@ -1,11 +1,10 @@
const AggregateRouter = require('../lib/Routers/AggregateRouter').AggregateRouter; const AggregateRouter = require('../lib/Routers/AggregateRouter').AggregateRouter;
describe('AggregateRouter', () => { describe('AggregateRouter', () => {
// TODO: update pipeline syntax. See [#7339](https://bit.ly/3incnWx)
it('get pipeline from Array', () => { it('get pipeline from Array', () => {
const body = [ const body = [
{ {
group: { objectId: {} }, $group: { _id: {} },
}, },
]; ];
const expected = [{ $group: { _id: {} } }]; const expected = [{ $group: { _id: {} } }];
@@ -13,22 +12,20 @@ describe('AggregateRouter', () => {
expect(result).toEqual(expected); expect(result).toEqual(expected);
}); });
// TODO: update pipeline syntax. See [#7339](https://bit.ly/3incnWx)
it('get pipeline from Object', () => { it('get pipeline from Object', () => {
const body = { const body = {
group: { objectId: {} }, $group: { _id: {} },
}; };
const expected = [{ $group: { _id: {} } }]; const expected = [{ $group: { _id: {} } }];
const result = AggregateRouter.getPipeline(body); const result = AggregateRouter.getPipeline(body);
expect(result).toEqual(expected); expect(result).toEqual(expected);
}); });
// TODO: update pipeline syntax. See [#7339](https://bit.ly/3incnWx)
it('get pipeline from Pipeline Operator (Array)', () => { it('get pipeline from Pipeline Operator (Array)', () => {
const body = { const body = {
pipeline: [ pipeline: [
{ {
group: { objectId: {} }, $group: { _id: {} },
}, },
], ],
}; };
@@ -37,11 +34,10 @@ describe('AggregateRouter', () => {
expect(result).toEqual(expected); expect(result).toEqual(expected);
}); });
// TODO: update pipeline syntax. See [#7339](https://bit.ly/3incnWx)
it('get pipeline from Pipeline Operator (Object)', () => { it('get pipeline from Pipeline Operator (Object)', () => {
const body = { const body = {
pipeline: { pipeline: {
group: { objectId: {} }, $group: { _id: {} },
}, },
}; };
const expected = [{ $group: { _id: {} } }]; const expected = [{ $group: { _id: {} } }];
@@ -49,43 +45,42 @@ describe('AggregateRouter', () => {
expect(result).toEqual(expected); expect(result).toEqual(expected);
}); });
// TODO: update pipeline syntax. See [#7339](https://bit.ly/3incnWx)
it('get pipeline fails multiple keys in Array stage ', () => { it('get pipeline fails multiple keys in Array stage ', () => {
const body = [ const body = [
{ {
group: { objectId: {} }, $group: { _id: {} },
match: { name: 'Test' }, $match: { name: 'Test' },
}, },
]; ];
try { expect(() => AggregateRouter.getPipeline(body)).toThrow(
AggregateRouter.getPipeline(body); new Parse.Error(
} catch (e) { Parse.Error.INVALID_QUERY,
expect(e.message).toBe('Pipeline stages should only have one key found group, match'); 'Pipeline stages should only have one key but found $group, $match.'
} )
);
}); });
// TODO: update pipeline syntax. See [#7339](https://bit.ly/3incnWx)
it('get pipeline fails multiple keys in Pipeline Operator Array stage ', () => { it('get pipeline fails multiple keys in Pipeline Operator Array stage ', () => {
const body = { const body = {
pipeline: [ pipeline: [
{ {
group: { objectId: {} }, $group: { _id: {} },
match: { name: 'Test' }, $match: { name: 'Test' },
}, },
], ],
}; };
try { expect(() => AggregateRouter.getPipeline(body)).toThrow(
AggregateRouter.getPipeline(body); new Parse.Error(
} catch (e) { Parse.Error.INVALID_QUERY,
expect(e.message).toBe('Pipeline stages should only have one key found group, match'); 'Pipeline stages should only have one key but found $group, $match.'
} )
);
}); });
// TODO: update pipeline syntax. See [#7339](https://bit.ly/3incnWx)
it('get search pipeline from Pipeline Operator (Array)', () => { it('get search pipeline from Pipeline Operator (Array)', () => {
const body = { const body = {
pipeline: { pipeline: {
search: {}, $search: {},
}, },
}; };
const expected = [{ $search: {} }]; const expected = [{ $search: {} }];
@@ -105,7 +100,7 @@ describe('AggregateRouter', () => {
it('support nested stage names starting with `$`', () => { it('support nested stage names starting with `$`', () => {
const body = [ const body = [
{ {
lookup: { $lookup: {
from: 'ACollection', from: 'ACollection',
let: { id: '_id' }, let: { id: '_id' },
as: 'results', as: 'results',
@@ -145,11 +140,11 @@ describe('AggregateRouter', () => {
it('support the use of `_id` in stages', () => { it('support the use of `_id` in stages', () => {
const body = [ const body = [
{ match: { _id: 'randomId' } }, { $match: { _id: 'randomId' } },
{ sort: { _id: -1 } }, { $sort: { _id: -1 } },
{ addFields: { _id: 1 } }, { $addFields: { _id: 1 } },
{ group: { _id: {} } }, { $group: { _id: {} } },
{ project: { _id: 0 } }, { $project: { _id: 0 } },
]; ];
const expected = [ const expected = [
{ $match: { _id: 'randomId' } }, { $match: { _id: 'randomId' } },
@@ -161,4 +156,19 @@ describe('AggregateRouter', () => {
const result = AggregateRouter.getPipeline(body); const result = AggregateRouter.getPipeline(body);
expect(result).toEqual(expected); expect(result).toEqual(expected);
}); });
it('should throw with invalid stage', () => {
expect(() => AggregateRouter.getPipeline([{ foo: 'bar' }])).toThrow(
new Parse.Error(Parse.Error.INVALID_QUERY, `Invalid aggregate stage 'foo'.`)
);
});
it('should throw with invalid group', () => {
expect(() => AggregateRouter.getPipeline([{ $group: { objectId: 'bar' } }])).toThrow(
new Parse.Error(
Parse.Error.INVALID_QUERY,
`Cannot use 'objectId' in aggregation stage $group.`
)
);
});
}); });

View File

@@ -2774,7 +2774,7 @@ describe('afterFind hooks', () => {
const obj = new Parse.Object('MyObject'); const obj = new Parse.Object('MyObject');
const pipeline = [ const pipeline = [
{ {
group: { objectId: {} }, $group: { _id: {} },
}, },
]; ];
obj obj

View File

@@ -23,28 +23,28 @@ const loadTestData = () => {
const data1 = { const data1 = {
score: 10, score: 10,
name: 'foo', name: 'foo',
sender: { group: 'A' }, // TODO: change to `$group`. See [#7339](https://bit.ly/3incnWx) sender: { group: 'A' },
views: 900, views: 900,
size: ['S', 'M'], size: ['S', 'M'],
}; };
const data2 = { const data2 = {
score: 10, score: 10,
name: 'foo', name: 'foo',
sender: { group: 'A' }, // TODO: change to `$group`. See [#7339](https://bit.ly/3incnWx) sender: { group: 'A' },
views: 800, views: 800,
size: ['M', 'L'], size: ['M', 'L'],
}; };
const data3 = { const data3 = {
score: 10, score: 10,
name: 'bar', name: 'bar',
sender: { group: 'B' }, // TODO: change to `$group`. See [#7339](https://bit.ly/3incnWx) sender: { group: 'B' },
views: 700, views: 700,
size: ['S'], size: ['S'],
}; };
const data4 = { const data4 = {
score: 20, score: 20,
name: 'dpl', name: 'dpl',
sender: { group: 'B' }, // TODO: change to `$group`. See [#7339](https://bit.ly/3incnWx) sender: { group: 'B' },
views: 700, views: 700,
size: ['S'], size: ['S'],
}; };
@@ -86,7 +86,7 @@ describe('Parse.Query Aggregate testing', () => {
it('invalid query group _id required', done => { it('invalid query group _id required', done => {
const options = Object.assign({}, masterKeyOptions, { const options = Object.assign({}, masterKeyOptions, {
body: { body: {
group: {}, // TODO: write as `$group`. See [#7339](https://bit.ly/3incnWx) $group: {},
}, },
}); });
get(Parse.serverURL + '/aggregate/TestObject', options).catch(error => { get(Parse.serverURL + '/aggregate/TestObject', options).catch(error => {
@@ -98,7 +98,7 @@ describe('Parse.Query Aggregate testing', () => {
it('group by field', done => { it('group by field', done => {
const options = Object.assign({}, masterKeyOptions, { const options = Object.assign({}, masterKeyOptions, {
body: { body: {
group: { objectId: '$name' }, // TODO: write as `$group`. See [#7339](https://bit.ly/3incnWx) $group: { _id: '$name' },
}, },
}); });
get(Parse.serverURL + '/aggregate/TestObject', options) get(Parse.serverURL + '/aggregate/TestObject', options)
@@ -119,7 +119,7 @@ describe('Parse.Query Aggregate testing', () => {
const options = Object.assign({}, masterKeyOptions, { const options = Object.assign({}, masterKeyOptions, {
body: { body: {
pipeline: { pipeline: {
group: { objectId: '$name' }, // TODO: write as `$group`. See [#7339](https://bit.ly/3incnWx) $group: { _id: '$name' },
}, },
}, },
}); });
@@ -137,7 +137,7 @@ describe('Parse.Query Aggregate testing', () => {
const obj = new TestObject(); const obj = new TestObject();
const pipeline = [ const pipeline = [
{ {
group: { objectId: {} }, // TODO: write as `$group`. See [#7339](https://bit.ly/3incnWx) $group: { _id: {} },
}, },
]; ];
obj obj
@@ -156,7 +156,7 @@ describe('Parse.Query Aggregate testing', () => {
const obj = new TestObject(); const obj = new TestObject();
const pipeline = [ const pipeline = [
{ {
group: { objectId: '' }, // TODO: write as `$group`. See [#7339](https://bit.ly/3incnWx) $group: { _id: '' },
}, },
]; ];
obj obj
@@ -175,7 +175,7 @@ describe('Parse.Query Aggregate testing', () => {
const obj = new TestObject(); const obj = new TestObject();
const pipeline = [ const pipeline = [
{ {
group: { objectId: [] }, // TODO: write as `$group`. See [#7339](https://bit.ly/3incnWx) $group: { _id: [] },
}, },
]; ];
obj obj
@@ -196,9 +196,8 @@ describe('Parse.Query Aggregate testing', () => {
const obj3 = new TestObject(); const obj3 = new TestObject();
const pipeline = [ const pipeline = [
{ {
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx) $group: {
group: { _id: {
objectId: {
score: '$score', score: '$score',
views: '$views', views: '$views',
}, },
@@ -280,7 +279,7 @@ describe('Parse.Query Aggregate testing', () => {
it('group by number', done => { it('group by number', done => {
const options = Object.assign({}, masterKeyOptions, { const options = Object.assign({}, masterKeyOptions, {
body: { body: {
group: { objectId: '$score' }, // TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx) $group: { _id: '$score' },
}, },
}); });
get(Parse.serverURL + '/aggregate/TestObject', options) get(Parse.serverURL + '/aggregate/TestObject', options)
@@ -302,9 +301,8 @@ describe('Parse.Query Aggregate testing', () => {
const obj2 = new TestObject({ name: 'item b', quantity: 5, price: 5 }); const obj2 = new TestObject({ name: 'item b', quantity: 5, price: 5 });
const pipeline = [ const pipeline = [
{ {
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx) $group: {
group: { _id: null,
objectId: null,
total: { $sum: { $multiply: ['$quantity', '$price'] } }, total: { $sum: { $multiply: ['$quantity', '$price'] } },
}, },
}, },
@@ -326,10 +324,10 @@ describe('Parse.Query Aggregate testing', () => {
const obj2 = new TestObject({ name: 'item b', quantity: 5, price: 5 }); const obj2 = new TestObject({ name: 'item b', quantity: 5, price: 5 });
const pipeline = [ const pipeline = [
{ {
match: { quantity: { $exists: true } }, $match: { quantity: { $exists: true } },
}, },
{ {
project: { $project: {
name: 1, name: 1,
total: { $multiply: ['$quantity', '$price'] }, total: { $multiply: ['$quantity', '$price'] },
}, },
@@ -358,16 +356,16 @@ describe('Parse.Query Aggregate testing', () => {
const obj2 = new TestObject({ name: 'item b', quantity: 5, price: 5 }); const obj2 = new TestObject({ name: 'item b', quantity: 5, price: 5 });
const pipeline = [ const pipeline = [
{ {
match: { quantity: { $exists: true } }, $match: { quantity: { $exists: true } },
}, },
{ {
project: { $project: {
objectId: 0, // TODO: change to `_id`. See [#7339](https://bit.ly/3incnWx) _id: 0,
total: { $multiply: ['$quantity', '$price'] }, total: { $multiply: ['$quantity', '$price'] },
}, },
}, },
{ {
sort: { total: 1 }, $sort: { total: 1 },
}, },
]; ];
Parse.Object.saveAll([obj1, obj2]) Parse.Object.saveAll([obj1, obj2])
@@ -388,7 +386,7 @@ describe('Parse.Query Aggregate testing', () => {
it_exclude_dbs(['postgres'])('project updatedAt only transform', done => { it_exclude_dbs(['postgres'])('project updatedAt only transform', done => {
const pipeline = [ const pipeline = [
{ {
project: { objectId: 0, updatedAt: 1 }, $project: { _id: 0, updatedAt: 1 },
}, },
]; ];
const query = new Parse.Query(TestObject); const query = new Parse.Query(TestObject);
@@ -411,13 +409,13 @@ describe('Parse.Query Aggregate testing', () => {
const obj3 = new TestObject({ dateField2019: new Date(1990, 11, 1) }); const obj3 = new TestObject({ dateField2019: new Date(1990, 11, 1) });
const pipeline = [ const pipeline = [
{ {
match: { $match: {
dateField2019: { $exists: true }, dateField2019: { $exists: true },
}, },
}, },
{ {
group: { $group: {
objectId: { _id: {
day: { $dayOfMonth: '$dateField2019' }, day: { $dayOfMonth: '$dateField2019' },
month: { $month: '$dateField2019' }, month: { $month: '$dateField2019' },
year: { $year: '$dateField2019' }, year: { $year: '$dateField2019' },
@@ -449,9 +447,8 @@ describe('Parse.Query Aggregate testing', () => {
const obj3 = new TestObject({ dateField2019: new Date(1990, 11, 1) }); const obj3 = new TestObject({ dateField2019: new Date(1990, 11, 1) });
const pipeline = [ const pipeline = [
{ {
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx) $group: {
group: { _id: {
objectId: {
day: { $dayOfMonth: '$dateField2019' }, day: { $dayOfMonth: '$dateField2019' },
month: { $month: '$dateField2019' }, month: { $month: '$dateField2019' },
year: { $year: '$dateField2019' }, year: { $year: '$dateField2019' },
@@ -481,7 +478,7 @@ describe('Parse.Query Aggregate testing', () => {
const obj1 = new TestObject({ pointer: pointer1 }); const obj1 = new TestObject({ pointer: pointer1 });
const obj2 = new TestObject({ pointer: pointer2 }); const obj2 = new TestObject({ pointer: pointer2 });
const obj3 = new TestObject({ pointer: pointer1 }); const obj3 = new TestObject({ pointer: pointer1 });
const pipeline = [{ group: { objectId: '$pointer' } }]; const pipeline = [{ $group: { _id: '$pointer' } }];
Parse.Object.saveAll([pointer1, pointer2, obj1, obj2, obj3]) Parse.Object.saveAll([pointer1, pointer2, obj1, obj2, obj3])
.then(() => { .then(() => {
const query = new Parse.Query(TestObject); const query = new Parse.Query(TestObject);
@@ -499,8 +496,7 @@ describe('Parse.Query Aggregate testing', () => {
it('group sum query', done => { it('group sum query', done => {
const options = Object.assign({}, masterKeyOptions, { const options = Object.assign({}, masterKeyOptions, {
body: { body: {
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx) $group: { _id: null, total: { $sum: '$score' } },
group: { objectId: null, total: { $sum: '$score' } },
}, },
}); });
get(Parse.serverURL + '/aggregate/TestObject', options) get(Parse.serverURL + '/aggregate/TestObject', options)
@@ -516,8 +512,7 @@ describe('Parse.Query Aggregate testing', () => {
it('group count query', done => { it('group count query', done => {
const options = Object.assign({}, masterKeyOptions, { const options = Object.assign({}, masterKeyOptions, {
body: { body: {
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx) $group: { _id: null, total: { $sum: 1 } },
group: { objectId: null, total: { $sum: 1 } },
}, },
}); });
get(Parse.serverURL + '/aggregate/TestObject', options) get(Parse.serverURL + '/aggregate/TestObject', options)
@@ -533,8 +528,7 @@ describe('Parse.Query Aggregate testing', () => {
it('group min query', done => { it('group min query', done => {
const options = Object.assign({}, masterKeyOptions, { const options = Object.assign({}, masterKeyOptions, {
body: { body: {
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx) $group: { _id: null, minScore: { $min: '$score' } },
group: { objectId: null, minScore: { $min: '$score' } },
}, },
}); });
get(Parse.serverURL + '/aggregate/TestObject', options) get(Parse.serverURL + '/aggregate/TestObject', options)
@@ -550,8 +544,7 @@ describe('Parse.Query Aggregate testing', () => {
it('group max query', done => { it('group max query', done => {
const options = Object.assign({}, masterKeyOptions, { const options = Object.assign({}, masterKeyOptions, {
body: { body: {
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx) $group: { _id: null, maxScore: { $max: '$score' } },
group: { objectId: null, maxScore: { $max: '$score' } },
}, },
}); });
get(Parse.serverURL + '/aggregate/TestObject', options) get(Parse.serverURL + '/aggregate/TestObject', options)
@@ -567,8 +560,7 @@ describe('Parse.Query Aggregate testing', () => {
it('group avg query', done => { it('group avg query', done => {
const options = Object.assign({}, masterKeyOptions, { const options = Object.assign({}, masterKeyOptions, {
body: { body: {
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx) $group: { _id: null, avgScore: { $avg: '$score' } },
group: { objectId: null, avgScore: { $avg: '$score' } },
}, },
}); });
get(Parse.serverURL + '/aggregate/TestObject', options) get(Parse.serverURL + '/aggregate/TestObject', options)
@@ -584,7 +576,7 @@ describe('Parse.Query Aggregate testing', () => {
it('limit query', done => { it('limit query', done => {
const options = Object.assign({}, masterKeyOptions, { const options = Object.assign({}, masterKeyOptions, {
body: { body: {
limit: 2, $limit: 2,
}, },
}); });
get(Parse.serverURL + '/aggregate/TestObject', options) get(Parse.serverURL + '/aggregate/TestObject', options)
@@ -598,7 +590,7 @@ describe('Parse.Query Aggregate testing', () => {
it('sort ascending query', done => { it('sort ascending query', done => {
const options = Object.assign({}, masterKeyOptions, { const options = Object.assign({}, masterKeyOptions, {
body: { body: {
sort: { name: 1 }, $sort: { name: 1 },
}, },
}); });
get(Parse.serverURL + '/aggregate/TestObject', options) get(Parse.serverURL + '/aggregate/TestObject', options)
@@ -616,7 +608,7 @@ describe('Parse.Query Aggregate testing', () => {
it('sort decending query', done => { it('sort decending query', done => {
const options = Object.assign({}, masterKeyOptions, { const options = Object.assign({}, masterKeyOptions, {
body: { body: {
sort: { name: -1 }, $sort: { name: -1 },
}, },
}); });
get(Parse.serverURL + '/aggregate/TestObject', options) get(Parse.serverURL + '/aggregate/TestObject', options)
@@ -634,7 +626,7 @@ describe('Parse.Query Aggregate testing', () => {
it('skip query', done => { it('skip query', done => {
const options = Object.assign({}, masterKeyOptions, { const options = Object.assign({}, masterKeyOptions, {
body: { body: {
skip: 2, $skip: 2,
}, },
}); });
get(Parse.serverURL + '/aggregate/TestObject', options) get(Parse.serverURL + '/aggregate/TestObject', options)
@@ -654,7 +646,7 @@ describe('Parse.Query Aggregate testing', () => {
const obj1 = new TestObject({ dateField: yesterday }); const obj1 = new TestObject({ dateField: yesterday });
const obj2 = new TestObject({ dateField: today }); const obj2 = new TestObject({ dateField: today });
const obj3 = new TestObject({ dateField: tomorrow }); const obj3 = new TestObject({ dateField: tomorrow });
const pipeline = [{ match: { dateField: { $lt: tomorrow } } }]; const pipeline = [{ $match: { dateField: { $lt: tomorrow } } }];
Parse.Object.saveAll([obj1, obj2, obj3]) Parse.Object.saveAll([obj1, obj2, obj3])
.then(() => { .then(() => {
const query = new Parse.Query(TestObject); const query = new Parse.Query(TestObject);
@@ -686,7 +678,7 @@ describe('Parse.Query Aggregate testing', () => {
it('match comparison query', done => { it('match comparison query', done => {
const options = Object.assign({}, masterKeyOptions, { const options = Object.assign({}, masterKeyOptions, {
body: { body: {
match: { score: { $gt: 15 } }, $match: { score: { $gt: 15 } },
}, },
}); });
get(Parse.serverURL + '/aggregate/TestObject', options) get(Parse.serverURL + '/aggregate/TestObject', options)
@@ -701,7 +693,7 @@ describe('Parse.Query Aggregate testing', () => {
it('match multiple comparison query', done => { it('match multiple comparison query', done => {
const options = Object.assign({}, masterKeyOptions, { const options = Object.assign({}, masterKeyOptions, {
body: { body: {
match: { score: { $gt: 5, $lt: 15 } }, $match: { score: { $gt: 5, $lt: 15 } },
}, },
}); });
get(Parse.serverURL + '/aggregate/TestObject', options) get(Parse.serverURL + '/aggregate/TestObject', options)
@@ -718,7 +710,7 @@ describe('Parse.Query Aggregate testing', () => {
it('match complex comparison query', done => { it('match complex comparison query', done => {
const options = Object.assign({}, masterKeyOptions, { const options = Object.assign({}, masterKeyOptions, {
body: { body: {
match: { score: { $gt: 5, $lt: 15 }, views: { $gt: 850, $lt: 1000 } }, $match: { score: { $gt: 5, $lt: 15 }, views: { $gt: 850, $lt: 1000 } },
}, },
}); });
get(Parse.serverURL + '/aggregate/TestObject', options) get(Parse.serverURL + '/aggregate/TestObject', options)
@@ -734,7 +726,7 @@ describe('Parse.Query Aggregate testing', () => {
it('match comparison and equality query', done => { it('match comparison and equality query', done => {
const options = Object.assign({}, masterKeyOptions, { const options = Object.assign({}, masterKeyOptions, {
body: { body: {
match: { score: { $gt: 5, $lt: 15 }, views: 900 }, $match: { score: { $gt: 5, $lt: 15 }, views: 900 },
}, },
}); });
get(Parse.serverURL + '/aggregate/TestObject', options) get(Parse.serverURL + '/aggregate/TestObject', options)
@@ -750,7 +742,7 @@ describe('Parse.Query Aggregate testing', () => {
it('match $or query', done => { it('match $or query', done => {
const options = Object.assign({}, masterKeyOptions, { const options = Object.assign({}, masterKeyOptions, {
body: { body: {
match: { $match: {
$or: [{ score: { $gt: 15, $lt: 25 } }, { views: { $gt: 750, $lt: 850 } }], $or: [{ score: { $gt: 15, $lt: 25 } }, { views: { $gt: 750, $lt: 850 } }],
}, },
}, },
@@ -775,7 +767,7 @@ describe('Parse.Query Aggregate testing', () => {
const obj2 = new TestObject(); const obj2 = new TestObject();
Parse.Object.saveAll([obj1, obj2]) Parse.Object.saveAll([obj1, obj2])
.then(() => { .then(() => {
const pipeline = [{ match: { objectId: obj1.id } }]; const pipeline = [{ $match: { _id: obj1.id } }];
const query = new Parse.Query(TestObject); const query = new Parse.Query(TestObject);
return query.aggregate(pipeline); return query.aggregate(pipeline);
}) })
@@ -791,7 +783,7 @@ describe('Parse.Query Aggregate testing', () => {
const obj2 = new TestObject({ name: 'TestObject2' }); const obj2 = new TestObject({ name: 'TestObject2' });
Parse.Object.saveAll([obj1, obj2]) Parse.Object.saveAll([obj1, obj2])
.then(() => { .then(() => {
const pipeline = [{ match: { name: 'TestObject1' } }]; const pipeline = [{ $match: { name: 'TestObject1' } }];
const query = new Parse.Query(TestObject); const query = new Parse.Query(TestObject);
return query.aggregate(pipeline); return query.aggregate(pipeline);
}) })
@@ -811,7 +803,7 @@ describe('Parse.Query Aggregate testing', () => {
Parse.Object.saveAll([pointer1, pointer2, obj1, obj2, obj3]) Parse.Object.saveAll([pointer1, pointer2, obj1, obj2, obj3])
.then(() => { .then(() => {
const pipeline = [{ match: { pointer: pointer1.id } }]; const pipeline = [{ $match: { pointer: pointer1.id } }];
const query = new Parse.Query(TestObject); const query = new Parse.Query(TestObject);
return query.aggregate(pipeline); return query.aggregate(pipeline);
}) })
@@ -826,7 +818,7 @@ describe('Parse.Query Aggregate testing', () => {
}); });
it_exclude_dbs(['postgres'])('match exists query', done => { it_exclude_dbs(['postgres'])('match exists query', done => {
const pipeline = [{ match: { score: { $exists: true } } }]; const pipeline = [{ $match: { score: { $exists: true } } }];
const query = new Parse.Query(TestObject); const query = new Parse.Query(TestObject);
query.aggregate(pipeline).then(results => { query.aggregate(pipeline).then(results => {
expect(results.length).toEqual(4); expect(results.length).toEqual(4);
@@ -842,7 +834,7 @@ describe('Parse.Query Aggregate testing', () => {
.then(() => { .then(() => {
const now = new Date(); const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const pipeline = [{ match: { createdAt: { $gte: today } } }]; const pipeline = [{ $match: { createdAt: { $gte: today } } }];
const query = new Parse.Query(TestObject); const query = new Parse.Query(TestObject);
return query.aggregate(pipeline); return query.aggregate(pipeline);
}) })
@@ -861,7 +853,7 @@ describe('Parse.Query Aggregate testing', () => {
.then(() => { .then(() => {
const now = new Date(); const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const pipeline = [{ match: { updatedAt: { $gte: today } } }]; const pipeline = [{ $match: { updatedAt: { $gte: today } } }];
const query = new Parse.Query(TestObject); const query = new Parse.Query(TestObject);
return query.aggregate(pipeline); return query.aggregate(pipeline);
}) })
@@ -880,7 +872,7 @@ describe('Parse.Query Aggregate testing', () => {
.then(() => { .then(() => {
const now = new Date(); const now = new Date();
const future = new Date(now.getFullYear(), now.getMonth() + 1, now.getDate()); const future = new Date(now.getFullYear(), now.getMonth() + 1, now.getDate());
const pipeline = [{ match: { createdAt: future } }]; const pipeline = [{ $match: { createdAt: future } }];
const query = new Parse.Query(TestObject); const query = new Parse.Query(TestObject);
return query.aggregate(pipeline); return query.aggregate(pipeline);
}) })
@@ -899,7 +891,7 @@ describe('Parse.Query Aggregate testing', () => {
Parse.Object.saveAll([pointer, obj1, obj2, obj3]) Parse.Object.saveAll([pointer, obj1, obj2, obj3])
.then(() => { .then(() => {
const pipeline = [{ match: { pointer: { $exists: true } } }]; const pipeline = [{ $match: { pointer: { $exists: true } } }];
const query = new Parse.Query(TestObject); const query = new Parse.Query(TestObject);
return query.aggregate(pipeline); return query.aggregate(pipeline);
}) })
@@ -938,7 +930,7 @@ describe('Parse.Query Aggregate testing', () => {
( (
await new Parse.Query('MyCollection').aggregate([ await new Parse.Query('MyCollection').aggregate([
{ {
match: { $match: {
language: { $in: [null, 'en'] }, language: { $in: [null, 'en'] },
}, },
}, },
@@ -952,7 +944,7 @@ describe('Parse.Query Aggregate testing', () => {
( (
await new Parse.Query('MyCollection').aggregate([ await new Parse.Query('MyCollection').aggregate([
{ {
match: { $match: {
$or: [{ language: 'en' }, { language: null }], $or: [{ language: 'en' }, { language: null }],
}, },
}, },
@@ -966,7 +958,7 @@ describe('Parse.Query Aggregate testing', () => {
it('project query', done => { it('project query', done => {
const options = Object.assign({}, masterKeyOptions, { const options = Object.assign({}, masterKeyOptions, {
body: { body: {
project: { name: 1 }, $project: { name: 1 },
}, },
}); });
get(Parse.serverURL + '/aggregate/TestObject', options) get(Parse.serverURL + '/aggregate/TestObject', options)
@@ -986,7 +978,7 @@ describe('Parse.Query Aggregate testing', () => {
it('multiple project query', done => { it('multiple project query', done => {
const options = Object.assign({}, masterKeyOptions, { const options = Object.assign({}, masterKeyOptions, {
body: { body: {
project: { name: 1, score: 1, sender: 1 }, $project: { name: 1, score: 1, sender: 1 },
}, },
}); });
get(Parse.serverURL + '/aggregate/TestObject', options) get(Parse.serverURL + '/aggregate/TestObject', options)
@@ -1011,8 +1003,8 @@ describe('Parse.Query Aggregate testing', () => {
.save() .save()
.then(() => { .then(() => {
const pipeline = [ const pipeline = [
{ match: { objectId: obj.id } }, { $match: { _id: obj.id } },
{ project: { pointer: 1, name: 1, createdAt: 1 } }, { $project: { pointer: 1, name: 1, createdAt: 1 } },
]; ];
const query = new Parse.Query(TestObject); const query = new Parse.Query(TestObject);
return query.aggregate(pipeline); return query.aggregate(pipeline);
@@ -1029,9 +1021,8 @@ describe('Parse.Query Aggregate testing', () => {
it('project with group query', done => { it('project with group query', done => {
const options = Object.assign({}, masterKeyOptions, { const options = Object.assign({}, masterKeyOptions, {
body: { body: {
project: { score: 1 }, $project: { score: 1 },
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx) $group: { _id: '$score', score: { $sum: '$score' } },
group: { objectId: '$score', score: { $sum: '$score' } },
}, },
}); });
get(Parse.serverURL + '/aggregate/TestObject', options) get(Parse.serverURL + '/aggregate/TestObject', options)
@@ -1058,8 +1049,7 @@ describe('Parse.Query Aggregate testing', () => {
it('class does not exist return empty', done => { it('class does not exist return empty', done => {
const options = Object.assign({}, masterKeyOptions, { const options = Object.assign({}, masterKeyOptions, {
body: { body: {
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx) $group: { _id: null, total: { $sum: '$score' } },
group: { objectId: null, total: { $sum: '$score' } },
}, },
}); });
get(Parse.serverURL + '/aggregate/UnknownClass', options) get(Parse.serverURL + '/aggregate/UnknownClass', options)
@@ -1073,8 +1063,7 @@ describe('Parse.Query Aggregate testing', () => {
it('field does not exist return empty', done => { it('field does not exist return empty', done => {
const options = Object.assign({}, masterKeyOptions, { const options = Object.assign({}, masterKeyOptions, {
body: { body: {
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx) $group: { _id: null, total: { $sum: '$unknownfield' } },
group: { objectId: null, total: { $sum: '$unknownfield' } },
}, },
}); });
get(Parse.serverURL + '/aggregate/UnknownClass', options) get(Parse.serverURL + '/aggregate/UnknownClass', options)
@@ -1103,7 +1092,7 @@ describe('Parse.Query Aggregate testing', () => {
const options = Object.assign({}, masterKeyOptions, { const options = Object.assign({}, masterKeyOptions, {
body: { body: {
distinct: 'score', distinct: 'score',
where: { $where: {
name: 'bar', name: 'bar',
}, },
}, },
@@ -1120,7 +1109,7 @@ describe('Parse.Query Aggregate testing', () => {
const options = Object.assign({}, masterKeyOptions, { const options = Object.assign({}, masterKeyOptions, {
body: { body: {
distinct: 'score', distinct: 'score',
where: JSON.stringify({ name: 'bar' }), $where: JSON.stringify({ name: 'bar' }),
}, },
}); });
get(Parse.serverURL + '/aggregate/TestObject', options) get(Parse.serverURL + '/aggregate/TestObject', options)
@@ -1270,7 +1259,7 @@ describe('Parse.Query Aggregate testing', () => {
it('does not return sensitive hidden properties', done => { it('does not return sensitive hidden properties', done => {
const options = Object.assign({}, masterKeyOptions, { const options = Object.assign({}, masterKeyOptions, {
body: { body: {
match: { $match: {
score: { score: {
$gt: 5, $gt: 5,
}, },
@@ -1317,7 +1306,7 @@ describe('Parse.Query Aggregate testing', () => {
}); });
it_exclude_dbs(['postgres'])('aggregate allow multiple of same stage', async done => { it_exclude_dbs(['postgres'])('aggregate allow multiple of same stage', async done => {
await reconfigureServer(); await reconfigureServer({ silent: false });
const pointer1 = new TestObject({ value: 1 }); const pointer1 = new TestObject({ value: 1 });
const pointer2 = new TestObject({ value: 2 }); const pointer2 = new TestObject({ value: 2 });
const pointer3 = new TestObject({ value: 3 }); const pointer3 = new TestObject({ value: 3 });
@@ -1330,17 +1319,17 @@ describe('Parse.Query Aggregate testing', () => {
body: { body: {
pipeline: [ pipeline: [
{ {
match: { name: 'Hello' }, $match: { name: 'Hello' },
}, },
{ {
// Transform className$objectId to objectId and store in new field tempPointer // Transform className$objectId to objectId and store in new field tempPointer
project: { $project: {
tempPointer: { $substr: ['$_p_pointer', 11, -1] }, // Remove TestObject$ tempPointer: { $substr: ['$_p_pointer', 11, -1] }, // Remove TestObject$
}, },
}, },
{ {
// Left Join, replace objectId stored in tempPointer with an actual object // Left Join, replace objectId stored in tempPointer with an actual object
lookup: { $lookup: {
from: 'test_TestObject', from: 'test_TestObject',
localField: 'tempPointer', localField: 'tempPointer',
foreignField: '_id', foreignField: '_id',
@@ -1349,12 +1338,12 @@ describe('Parse.Query Aggregate testing', () => {
}, },
{ {
// lookup returns an array, Deconstructs an array field to objects // lookup returns an array, Deconstructs an array field to objects
unwind: { $unwind: {
path: '$tempPointer', path: '$tempPointer',
}, },
}, },
{ {
match: { 'tempPointer.value': 2 }, $match: { 'tempPointer.value': 2 },
}, },
], ],
}, },
@@ -1398,7 +1387,7 @@ describe('Parse.Query Aggregate testing', () => {
// Create query // Create query
const pipeline = [ const pipeline = [
{ {
geoNear: { $geoNear: {
near: { near: {
type: 'Point', type: 'Point',
coordinates: [1, 1], coordinates: [1, 1],
@@ -1451,7 +1440,7 @@ describe('Parse.Query Aggregate testing', () => {
// Create query // Create query
const pipeline = [ const pipeline = [
{ {
geoNear: { $geoNear: {
near: { near: {
type: 'Point', type: 'Point',
coordinates: [1, 1], coordinates: [1, 1],
@@ -1497,7 +1486,7 @@ describe('Parse.Query Aggregate testing', () => {
// Create query // Create query
const pipeline = [ const pipeline = [
{ {
geoNear: { $geoNear: {
near: [1, 1], near: [1, 1],
key: 'location', key: 'location',
spherical: true, spherical: true,

View File

@@ -322,7 +322,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
url: Parse.serverURL + '/aggregate/TestObject', url: Parse.serverURL + '/aggregate/TestObject',
qs: { qs: {
explain: true, explain: true,
group: JSON.stringify({ objectId: '$foo' }), $group: JSON.stringify({ _id: '$foo' }),
}, },
}); });
let response = await request(options); let response = await request(options);
@@ -334,7 +334,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
qs: { qs: {
explain: true, explain: true,
hint: '_id_', hint: '_id_',
group: JSON.stringify({ objectId: '$foo' }), $group: JSON.stringify({ _id: '$foo' }),
}, },
}); });
response = await request(options); response = await request(options);
@@ -349,7 +349,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
url: Parse.serverURL + '/aggregate/TestObject', url: Parse.serverURL + '/aggregate/TestObject',
qs: { qs: {
explain: true, explain: true,
group: JSON.stringify({ objectId: '$foo' }), $group: JSON.stringify({ _id: '$foo' }),
}, },
}); });
let response = await request(options); let response = await request(options);
@@ -363,7 +363,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
qs: { qs: {
explain: true, explain: true,
hint: '_id_', hint: '_id_',
group: JSON.stringify({ objectId: '$foo' }), $group: JSON.stringify({ _id: '$foo' }),
}, },
}); });
response = await request(options); response = await request(options);
@@ -382,7 +382,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
url: Parse.serverURL + '/aggregate/TestObject', url: Parse.serverURL + '/aggregate/TestObject',
qs: { qs: {
explain: true, explain: true,
group: JSON.stringify({ objectId: '$foo' }), $group: JSON.stringify({ _id: '$foo' }),
}, },
}); });
let response = await request(options); let response = await request(options);
@@ -396,7 +396,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
qs: { qs: {
explain: true, explain: true,
hint: '_id_', hint: '_id_',
group: JSON.stringify({ objectId: '$foo' }), $group: JSON.stringify({ _id: '$foo' }),
}, },
}); });
response = await request(options); response = await request(options);
@@ -415,7 +415,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
url: Parse.serverURL + '/aggregate/TestObject', url: Parse.serverURL + '/aggregate/TestObject',
qs: { qs: {
explain: true, explain: true,
group: JSON.stringify({ objectId: '$foo' }), $group: JSON.stringify({ _id: '$foo' }),
}, },
}); });
let response = await request(options); let response = await request(options);
@@ -429,7 +429,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
qs: { qs: {
explain: true, explain: true,
hint: '_id_', hint: '_id_',
group: JSON.stringify({ objectId: '$foo' }), $group: JSON.stringify({ _id: '$foo' }),
}, },
}); });
response = await request(options); response = await request(options);

View File

@@ -625,7 +625,7 @@ describe_only_db('mongo')('Read preference option', () => {
spyOn(Collection.prototype, 'aggregate').and.callThrough(); spyOn(Collection.prototype, 'aggregate').and.callThrough();
// Query // Query
const query = new Parse.Query('MyObject'); const query = new Parse.Query('MyObject');
const results = await query.aggregate([{ match: { boolKey: false } }]); const results = await query.aggregate([{ $match: { boolKey: false } }]);
// Validate // Validate
expect(results.length).toBe(1); expect(results.length).toBe(1);
let readPreference = null; let readPreference = null;
@@ -678,7 +678,7 @@ describe_only_db('mongo')('Read preference option', () => {
// Query // Query
const query = new Parse.Query('MyObject'); const query = new Parse.Query('MyObject');
query.readPreference('SECONDARY'); query.readPreference('SECONDARY');
const results = await query.aggregate([{ match: { boolKey: false } }]); const results = await query.aggregate([{ $match: { boolKey: false } }]);
// Validate // Validate
expect(results.length).toBe(1); expect(results.length).toBe(1);
let readPreference = null; let readPreference = null;

View File

@@ -2241,8 +2241,11 @@ export class PostgresStorageAdapter implements StorageAdapter {
}); });
stage.$match = collapse; stage.$match = collapse;
} }
for (const field in stage.$match) { for (let field in stage.$match) {
const value = stage.$match[field]; const value = stage.$match[field];
if (field === '_id') {
field = 'objectId';
}
const matchPatterns = []; const matchPatterns = [];
Object.keys(ParseToPosgresComparator).forEach(cmp => { Object.keys(ParseToPosgresComparator).forEach(cmp => {
if (value[cmp]) { if (value[cmp]) {

View File

@@ -3,7 +3,6 @@ import rest from '../rest';
import * as middleware from '../middlewares'; import * as middleware from '../middlewares';
import Parse from 'parse/node'; import Parse from 'parse/node';
import UsersRouter from './UsersRouter'; import UsersRouter from './UsersRouter';
import Deprecator from '../Deprecator/Deprecator';
export class AggregateRouter extends ClassesRouter { export class AggregateRouter extends ClassesRouter {
handleFind(req) { handleFind(req) {
@@ -83,22 +82,30 @@ export class AggregateRouter extends ClassesRouter {
return pipeline.map(stage => { return pipeline.map(stage => {
const keys = Object.keys(stage); const keys = Object.keys(stage);
if (keys.length != 1) { if (keys.length !== 1) {
throw new Error(`Pipeline stages should only have one key found ${keys.join(', ')}`); throw new Parse.Error(
Parse.Error.INVALID_QUERY,
`Pipeline stages should only have one key but found ${keys.join(', ')}.`
);
} }
return AggregateRouter.transformStage(keys[0], stage); return AggregateRouter.transformStage(keys[0], stage);
}); });
} }
static transformStage(stageName, stage) { static transformStage(stageName, stage) {
if (stageName === 'group') { const skipKeys = ['distinct', 'where'];
if (skipKeys.includes(stageName)) {
return;
}
if (stageName[0] !== '$') {
throw new Parse.Error(Parse.Error.INVALID_QUERY, `Invalid aggregate stage '${stageName}'.`);
}
if (stageName === '$group') {
if (Object.prototype.hasOwnProperty.call(stage[stageName], 'objectId')) { if (Object.prototype.hasOwnProperty.call(stage[stageName], 'objectId')) {
Deprecator.logRuntimeDeprecation({ throw new Parse.Error(
usage: 'The use of objectId in aggregation stage $group', Parse.Error.INVALID_QUERY,
solution: 'Use _id instead.', `Cannot use 'objectId' in aggregation stage $group.`
}); );
stage[stageName]._id = stage[stageName].objectId;
delete stage[stageName].objectId;
} }
if (!Object.prototype.hasOwnProperty.call(stage[stageName], '_id')) { if (!Object.prototype.hasOwnProperty.call(stage[stageName], '_id')) {
throw new Parse.Error( throw new Parse.Error(
@@ -107,15 +114,7 @@ export class AggregateRouter extends ClassesRouter {
); );
} }
} }
return { [stageName]: stage[stageName] };
if (stageName[0] !== '$') {
Deprecator.logRuntimeDeprecation({
usage: "Using aggregation stages without a leading '$'",
solution: `Try $${stageName} instead.`,
});
}
const key = stageName[0] === '$' ? stageName : `$${stageName}`;
return { [key]: stage[stageName] };
} }
mountRoutes() { mountRoutes() {