feat(AggregateRouter): support native mongodb syntax in aggregation pipelines (#7339)
This commit is contained in:
committed by
GitHub
parent
381e9bf014
commit
8fddac39bf
@@ -105,6 +105,7 @@ ___
|
||||
- Add official support for MongoDB 5.0 (Manuel Trezza) [#7469](https://github.com/parse-community/parse-server/pull/7469)
|
||||
|
||||
### Other Changes
|
||||
- Support native mongodb syntax in aggregation pipelines (Raschid JF Rafeally) [#7339](https://github.com/parse-community/parse-server/pull/7339)
|
||||
- Fix error when a not yet inserted job is updated (Antonio Davi Macedo Coelho de Castro) [#7196](https://github.com/parse-community/parse-server/pull/7196)
|
||||
- request.context for afterFind triggers (dblythy) [#7078](https://github.com/parse-community/parse-server/pull/7078)
|
||||
- Winston Logger interpolating stdout to console (dplewis) [#7114](https://github.com/parse-community/parse-server/pull/7114)
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
The following is a list of deprecations, according to the [Deprecation Policy](https://github.com/parse-community/parse-server/blob/master/CONTRIBUTING.md#deprecation-policy). After a feature becomes deprecated, and giving developers time to adapt to the change, the deprecated feature will eventually be removed, leading to a breaking change. Developer feedback during the deprecation period may postpone the introduction of the breaking change.
|
||||
|
||||
| Feature | Issue | Deprecation [ℹ️][i_deprecation] | Planned Removal [ℹ️][i_removal] | Status [ℹ️][i_status] | Notes |
|
||||
|---------|----|------------------|----------------------|----------|-------|
|
||||
(none)
|
||||
|
||||
|-----------------------------------------------|----------------------------------------------------------------------|---------------------------------|---------------------------------|-----------------------|-------|
|
||||
| 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 | - |
|
||||
|
||||
[i_deprecation]: ## "The version and date of the deprecation."
|
||||
[i_removal]: ## "The version and date of the planned removal."
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const AggregateRouter = require('../lib/Routers/AggregateRouter').AggregateRouter;
|
||||
|
||||
describe('AggregateRouter', () => {
|
||||
// TODO: update pipeline syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
it('get pipeline from Array', () => {
|
||||
const body = [
|
||||
{
|
||||
@@ -12,6 +13,7 @@ describe('AggregateRouter', () => {
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
// TODO: update pipeline syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
it('get pipeline from Object', () => {
|
||||
const body = {
|
||||
group: { objectId: {} },
|
||||
@@ -21,6 +23,7 @@ describe('AggregateRouter', () => {
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
// TODO: update pipeline syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
it('get pipeline from Pipeline Operator (Array)', () => {
|
||||
const body = {
|
||||
pipeline: [
|
||||
@@ -34,6 +37,7 @@ describe('AggregateRouter', () => {
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
// TODO: update pipeline syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
it('get pipeline from Pipeline Operator (Object)', () => {
|
||||
const body = {
|
||||
pipeline: {
|
||||
@@ -45,6 +49,7 @@ describe('AggregateRouter', () => {
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
// TODO: update pipeline syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
it('get pipeline fails multiple keys in Array stage ', () => {
|
||||
const body = [
|
||||
{
|
||||
@@ -59,6 +64,7 @@ describe('AggregateRouter', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: update pipeline syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
it('get pipeline fails multiple keys in Pipeline Operator Array stage ', () => {
|
||||
const body = {
|
||||
pipeline: [
|
||||
@@ -75,6 +81,7 @@ describe('AggregateRouter', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: update pipeline syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
it('get search pipeline from Pipeline Operator (Array)', () => {
|
||||
const body = {
|
||||
pipeline: {
|
||||
@@ -85,4 +92,73 @@ describe('AggregateRouter', () => {
|
||||
const result = AggregateRouter.getPipeline(body);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('support stage name starting with `$`', () => {
|
||||
const body = {
|
||||
$match: { someKey: 'whatever' },
|
||||
};
|
||||
const expected = [{ $match: { someKey: 'whatever' } }];
|
||||
const result = AggregateRouter.getPipeline(body);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('support nested stage names starting with `$`', () => {
|
||||
const body = [
|
||||
{
|
||||
lookup: {
|
||||
from: 'ACollection',
|
||||
let: { id: '_id' },
|
||||
as: 'results',
|
||||
pipeline: [
|
||||
{
|
||||
$match: {
|
||||
$expr: {
|
||||
$eq: ['$_id', '$$id'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
const expected = [
|
||||
{
|
||||
$lookup: {
|
||||
from: 'ACollection',
|
||||
let: { id: '_id' },
|
||||
as: 'results',
|
||||
pipeline: [
|
||||
{
|
||||
$match: {
|
||||
$expr: {
|
||||
$eq: ['$_id', '$$id'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
const result = AggregateRouter.getPipeline(body);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('support the use of `_id` in stages', () => {
|
||||
const body = [
|
||||
{ match: { _id: 'randomId' } },
|
||||
{ sort: { _id: -1 } },
|
||||
{ addFields: { _id: 1 } },
|
||||
{ group: { _id: {} } },
|
||||
{ project: { _id: 0 } },
|
||||
];
|
||||
const expected = [
|
||||
{ $match: { _id: 'randomId' } },
|
||||
{ $sort: { _id: -1 } },
|
||||
{ $addFields: { _id: 1 } },
|
||||
{ $group: { _id: {} } },
|
||||
{ $project: { _id: 0 } },
|
||||
];
|
||||
const result = AggregateRouter.getPipeline(body);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,28 +23,28 @@ const loadTestData = () => {
|
||||
const data1 = {
|
||||
score: 10,
|
||||
name: 'foo',
|
||||
sender: { group: 'A' },
|
||||
sender: { group: 'A' }, // TODO: change to `$group`. See [#7339](https://bit.ly/3incnWx)
|
||||
views: 900,
|
||||
size: ['S', 'M'],
|
||||
};
|
||||
const data2 = {
|
||||
score: 10,
|
||||
name: 'foo',
|
||||
sender: { group: 'A' },
|
||||
sender: { group: 'A' }, // TODO: change to `$group`. See [#7339](https://bit.ly/3incnWx)
|
||||
views: 800,
|
||||
size: ['M', 'L'],
|
||||
};
|
||||
const data3 = {
|
||||
score: 10,
|
||||
name: 'bar',
|
||||
sender: { group: 'B' },
|
||||
sender: { group: 'B' }, // TODO: change to `$group`. See [#7339](https://bit.ly/3incnWx)
|
||||
views: 700,
|
||||
size: ['S'],
|
||||
};
|
||||
const data4 = {
|
||||
score: 20,
|
||||
name: 'dpl',
|
||||
sender: { group: 'B' },
|
||||
sender: { group: 'B' }, // TODO: change to `$group`. See [#7339](https://bit.ly/3incnWx)
|
||||
views: 700,
|
||||
size: ['S'],
|
||||
};
|
||||
@@ -83,22 +83,10 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('invalid query group _id', done => {
|
||||
it('invalid query group _id required', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
group: { _id: null },
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options).catch(error => {
|
||||
expect(error.error.code).toEqual(Parse.Error.INVALID_QUERY);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('invalid query group objectId required', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
group: {},
|
||||
group: {}, // TODO: write as `$group`. See [#7339](https://bit.ly/3incnWx)
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options).catch(error => {
|
||||
@@ -110,7 +98,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('group by field', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
group: { objectId: '$name' },
|
||||
group: { objectId: '$name' }, // TODO: write as `$group`. See [#7339](https://bit.ly/3incnWx)
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options)
|
||||
@@ -131,7 +119,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
pipeline: {
|
||||
group: { objectId: '$name' },
|
||||
group: { objectId: '$name' }, // TODO: write as `$group`. See [#7339](https://bit.ly/3incnWx)
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -149,7 +137,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const obj = new TestObject();
|
||||
const pipeline = [
|
||||
{
|
||||
group: { objectId: {} },
|
||||
group: { objectId: {} }, // TODO: write as `$group`. See [#7339](https://bit.ly/3incnWx)
|
||||
},
|
||||
];
|
||||
obj
|
||||
@@ -168,7 +156,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const obj = new TestObject();
|
||||
const pipeline = [
|
||||
{
|
||||
group: { objectId: '' },
|
||||
group: { objectId: '' }, // TODO: write as `$group`. See [#7339](https://bit.ly/3incnWx)
|
||||
},
|
||||
];
|
||||
obj
|
||||
@@ -187,7 +175,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const obj = new TestObject();
|
||||
const pipeline = [
|
||||
{
|
||||
group: { objectId: [] },
|
||||
group: { objectId: [] }, // TODO: write as `$group`. See [#7339](https://bit.ly/3incnWx)
|
||||
},
|
||||
];
|
||||
obj
|
||||
@@ -208,6 +196,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const obj3 = new TestObject();
|
||||
const pipeline = [
|
||||
{
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: {
|
||||
objectId: {
|
||||
score: '$score',
|
||||
@@ -234,6 +223,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const obj3 = new TestObject();
|
||||
const pipeline = [
|
||||
{
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: {
|
||||
objectId: {
|
||||
day: { $dayOfMonth: '$_updated_at' },
|
||||
@@ -264,6 +254,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const obj3 = new TestObject();
|
||||
const pipeline = [
|
||||
{
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: {
|
||||
objectId: {
|
||||
day: { $dayOfMonth: '$updatedAt' },
|
||||
@@ -291,7 +282,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('group by number', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
group: { objectId: '$score' },
|
||||
group: { objectId: '$score' }, // TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options)
|
||||
@@ -313,6 +304,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const obj2 = new TestObject({ name: 'item b', quantity: 5, price: 5 });
|
||||
const pipeline = [
|
||||
{
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: {
|
||||
objectId: null,
|
||||
total: { $sum: { $multiply: ['$quantity', '$price'] } },
|
||||
@@ -372,7 +364,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
},
|
||||
{
|
||||
project: {
|
||||
objectId: 0,
|
||||
objectId: 0, // TODO: change to `_id`. See [#7339](https://bit.ly/3incnWx)
|
||||
total: { $multiply: ['$quantity', '$price'] },
|
||||
},
|
||||
},
|
||||
@@ -459,6 +451,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const obj3 = new TestObject({ dateField2019: new Date(1990, 11, 1) });
|
||||
const pipeline = [
|
||||
{
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: {
|
||||
objectId: {
|
||||
day: { $dayOfMonth: '$dateField2019' },
|
||||
@@ -508,6 +501,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('group sum query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: { objectId: null, total: { $sum: '$score' } },
|
||||
},
|
||||
});
|
||||
@@ -524,6 +518,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('group count query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: { objectId: null, total: { $sum: 1 } },
|
||||
},
|
||||
});
|
||||
@@ -540,6 +535,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('group min query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: { objectId: null, minScore: { $min: '$score' } },
|
||||
},
|
||||
});
|
||||
@@ -556,6 +552,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('group max query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: { objectId: null, maxScore: { $max: '$score' } },
|
||||
},
|
||||
});
|
||||
@@ -572,6 +569,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('group avg query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: { objectId: null, avgScore: { $avg: '$score' } },
|
||||
},
|
||||
});
|
||||
@@ -1017,6 +1015,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
project: { score: 1 },
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: { objectId: '$score', score: { $sum: '$score' } },
|
||||
},
|
||||
});
|
||||
@@ -1044,6 +1043,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('class does not exist return empty', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: { objectId: null, total: { $sum: '$score' } },
|
||||
},
|
||||
});
|
||||
@@ -1058,6 +1058,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('field does not exist return empty', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: { objectId: null, total: { $sum: '$unknownfield' } },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import rest from '../rest';
|
||||
import * as middleware from '../middlewares';
|
||||
import Parse from 'parse/node';
|
||||
import UsersRouter from './UsersRouter';
|
||||
import Deprecator from '../Deprecator/Deprecator';
|
||||
|
||||
export class AggregateRouter extends ClassesRouter {
|
||||
handleFind(req) {
|
||||
@@ -91,22 +92,30 @@ export class AggregateRouter extends ClassesRouter {
|
||||
|
||||
static transformStage(stageName, stage) {
|
||||
if (stageName === 'group') {
|
||||
if (Object.prototype.hasOwnProperty.call(stage[stageName], '_id')) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_QUERY,
|
||||
`Invalid parameter for query: group. Please use objectId instead of _id`
|
||||
);
|
||||
}
|
||||
if (!Object.prototype.hasOwnProperty.call(stage[stageName], 'objectId')) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_QUERY,
|
||||
`Invalid parameter for query: group. objectId is required`
|
||||
);
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(stage[stageName], 'objectId')) {
|
||||
Deprecator.logRuntimeDeprecation({
|
||||
usage: 'The use of objectId in aggregation stage $group',
|
||||
solution: 'Use _id instead.',
|
||||
});
|
||||
stage[stageName]._id = stage[stageName].objectId;
|
||||
delete stage[stageName].objectId;
|
||||
}
|
||||
return { [`$${stageName}`]: stage[stageName] };
|
||||
if (!Object.prototype.hasOwnProperty.call(stage[stageName], '_id')) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_QUERY,
|
||||
`Invalid parameter for query: group. Missing key _id`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
|
||||
Reference in New Issue
Block a user