Skip to content

Commit 8fca1aa

Browse files
authored
feat(NODE-3639): add a general stage to the aggregation pipeline builder (#4079)
1 parent 2645513 commit 8fca1aa

File tree

2 files changed

+97
-35
lines changed

2 files changed

+97
-35
lines changed

src/cursor/aggregation_cursor.ts

Lines changed: 31 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -86,33 +86,45 @@ export class AggregationCursor<TSchema = any> extends AbstractCursor<TSchema> {
8686
);
8787
}
8888

89+
/** Add a stage to the aggregation pipeline
90+
* @example
91+
* ```
92+
* const documents = await users.aggregate().addStage({ $match: { name: /Mike/ } }).toArray();
93+
* ```
94+
* @example
95+
* ```
96+
* const documents = await users.aggregate()
97+
* .addStage<{ name: string }>({ $project: { name: true } })
98+
* .toArray(); // type of documents is { name: string }[]
99+
* ```
100+
*/
101+
addStage(stage: Document): this;
102+
addStage<T = Document>(stage: Document): AggregationCursor<T>;
103+
addStage<T = Document>(stage: Document): AggregationCursor<T> {
104+
assertUninitialized(this);
105+
this[kPipeline].push(stage);
106+
return this as unknown as AggregationCursor<T>;
107+
}
108+
89109
/** Add a group stage to the aggregation pipeline */
90110
group<T = TSchema>($group: Document): AggregationCursor<T>;
91111
group($group: Document): this {
92-
assertUninitialized(this);
93-
this[kPipeline].push({ $group });
94-
return this;
112+
return this.addStage({ $group });
95113
}
96114

97115
/** Add a limit stage to the aggregation pipeline */
98116
limit($limit: number): this {
99-
assertUninitialized(this);
100-
this[kPipeline].push({ $limit });
101-
return this;
117+
return this.addStage({ $limit });
102118
}
103119

104120
/** Add a match stage to the aggregation pipeline */
105121
match($match: Document): this {
106-
assertUninitialized(this);
107-
this[kPipeline].push({ $match });
108-
return this;
122+
return this.addStage({ $match });
109123
}
110124

111125
/** Add an out stage to the aggregation pipeline */
112126
out($out: { db: string; coll: string } | string): this {
113-
assertUninitialized(this);
114-
this[kPipeline].push({ $out });
115-
return this;
127+
return this.addStage({ $out });
116128
}
117129

118130
/**
@@ -157,50 +169,36 @@ export class AggregationCursor<TSchema = any> extends AbstractCursor<TSchema> {
157169
* ```
158170
*/
159171
project<T extends Document = Document>($project: Document): AggregationCursor<T> {
160-
assertUninitialized(this);
161-
this[kPipeline].push({ $project });
162-
return this as unknown as AggregationCursor<T>;
172+
return this.addStage<T>({ $project });
163173
}
164174

165175
/** Add a lookup stage to the aggregation pipeline */
166176
lookup($lookup: Document): this {
167-
assertUninitialized(this);
168-
this[kPipeline].push({ $lookup });
169-
return this;
177+
return this.addStage({ $lookup });
170178
}
171179

172180
/** Add a redact stage to the aggregation pipeline */
173181
redact($redact: Document): this {
174-
assertUninitialized(this);
175-
this[kPipeline].push({ $redact });
176-
return this;
182+
return this.addStage({ $redact });
177183
}
178184

179185
/** Add a skip stage to the aggregation pipeline */
180186
skip($skip: number): this {
181-
assertUninitialized(this);
182-
this[kPipeline].push({ $skip });
183-
return this;
187+
return this.addStage({ $skip });
184188
}
185189

186190
/** Add a sort stage to the aggregation pipeline */
187191
sort($sort: Sort): this {
188-
assertUninitialized(this);
189-
this[kPipeline].push({ $sort });
190-
return this;
192+
return this.addStage({ $sort });
191193
}
192194

193195
/** Add a unwind stage to the aggregation pipeline */
194196
unwind($unwind: Document | string): this {
195-
assertUninitialized(this);
196-
this[kPipeline].push({ $unwind });
197-
return this;
197+
return this.addStage({ $unwind });
198198
}
199199

200200
/** Add a geoNear stage to the aggregation pipeline */
201201
geoNear($geoNear: Document): this {
202-
assertUninitialized(this);
203-
this[kPipeline].push({ $geoNear });
204-
return this;
202+
return this.addStage({ $geoNear });
205203
}
206204
}

test/integration/crud/aggregation.test.ts

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { expect } from 'chai';
22

3-
import { MongoInvalidArgumentError } from '../../mongodb';
3+
import { type MongoClient, MongoInvalidArgumentError } from '../../mongodb';
44
import { filterForCommands } from '../shared';
55

66
describe('Aggregation', function () {
7-
let client;
7+
let client: MongoClient;
88

99
beforeEach(async function () {
1010
client = this.configuration.newClient();
@@ -939,4 +939,68 @@ describe('Aggregation', function () {
939939
.finally(() => client.close());
940940
}
941941
});
942+
943+
it('should return identical results for array aggregations and builder aggregations', async function () {
944+
const databaseName = this.configuration.db;
945+
const db = client.db(databaseName);
946+
const collection = db.collection(
947+
'shouldReturnIdenticalResultsForArrayAggregationsAndBuilderAggregations'
948+
);
949+
950+
const docs = [
951+
{
952+
title: 'this is my title',
953+
author: 'bob',
954+
posted: new Date(),
955+
pageViews: 5,
956+
tags: ['fun', 'good', 'fun'],
957+
other: { foo: 5 },
958+
comments: [
959+
{ author: 'joe', text: 'this is cool' },
960+
{ author: 'sam', text: 'this is bad' }
961+
]
962+
}
963+
];
964+
965+
await collection.insertMany(docs, { writeConcern: { w: 1 } });
966+
const arrayPipelineCursor = collection.aggregate([
967+
{
968+
$project: {
969+
author: 1,
970+
tags: 1
971+
}
972+
},
973+
{ $unwind: '$tags' },
974+
{
975+
$group: {
976+
_id: { tags: '$tags' },
977+
authors: { $addToSet: '$author' }
978+
}
979+
},
980+
{ $sort: { _id: -1 } }
981+
]);
982+
983+
const builderPipelineCursor = collection
984+
.aggregate()
985+
.project({ author: 1, tags: 1 })
986+
.unwind('$tags')
987+
.group({ _id: { tags: '$tags' }, authors: { $addToSet: '$author' } })
988+
.sort({ _id: -1 });
989+
990+
const builderGenericStageCursor = collection
991+
.aggregate()
992+
.addStage({ $project: { author: 1, tags: 1 } })
993+
.addStage({ $unwind: '$tags' })
994+
.addStage({ $group: { _id: { tags: '$tags' }, authors: { $addToSet: '$author' } } })
995+
.addStage({ $sort: { _id: -1 } });
996+
997+
const expectedResults = [
998+
{ _id: { tags: 'good' }, authors: ['bob'] },
999+
{ _id: { tags: 'fun' }, authors: ['bob'] }
1000+
];
1001+
1002+
expect(await arrayPipelineCursor.toArray()).to.deep.equal(expectedResults);
1003+
expect(await builderPipelineCursor.toArray()).to.deep.equal(expectedResults);
1004+
expect(await builderGenericStageCursor.toArray()).to.deep.equal(expectedResults);
1005+
});
9421006
});

0 commit comments

Comments
 (0)