Skip to content

Commit ae6f4a2

Browse files
authored
Merge pull request #15293 from Automattic/vkarpov15/gh-15279
fix(AggregationCursor): make next() error if schema pre('aggregate') middleware throws error
2 parents 633ff8d + c50cb37 commit ae6f4a2

File tree

2 files changed

+75
-2
lines changed

2 files changed

+75
-2
lines changed

lib/cursor/aggregationCursor.js

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const MongooseError = require('../error/mongooseError');
88
const Readable = require('stream').Readable;
99
const eachAsync = require('../helpers/cursor/eachAsync');
1010
const immediate = require('../helpers/immediate');
11+
const kareem = require('kareem');
1112
const util = require('util');
1213

1314
/**
@@ -62,7 +63,11 @@ util.inherits(AggregationCursor, Readable);
6263

6364
function _init(model, c, agg) {
6465
if (!model.collection.buffer) {
65-
model.hooks.execPre('aggregate', agg, function() {
66+
model.hooks.execPre('aggregate', agg, function(err) {
67+
if (err != null) {
68+
_handlePreHookError(c, err);
69+
return;
70+
}
6671
if (typeof agg.options?.cursor?.transform === 'function') {
6772
c._transforms.push(agg.options.cursor.transform);
6873
}
@@ -72,7 +77,12 @@ function _init(model, c, agg) {
7277
});
7378
} else {
7479
model.collection.emitter.once('queue', function() {
75-
model.hooks.execPre('aggregate', agg, function() {
80+
model.hooks.execPre('aggregate', agg, function(err) {
81+
if (err != null) {
82+
_handlePreHookError(c, err);
83+
return;
84+
}
85+
7686
if (typeof agg.options?.cursor?.transform === 'function') {
7787
c._transforms.push(agg.options.cursor.transform);
7888
}
@@ -84,6 +94,38 @@ function _init(model, c, agg) {
8494
}
8595
}
8696

97+
/**
98+
* Handles error emitted from pre middleware. In particular, checks for `skipWrappedFunction`, which allows skipping
99+
* the actual aggregation and overwriting the function's return value. Because aggregation cursors don't return a value,
100+
* we need to make sure the user doesn't accidentally set a value in skipWrappedFunction.
101+
*
102+
* @param {QueryCursor} queryCursor
103+
* @param {Error} err
104+
* @returns
105+
*/
106+
107+
function _handlePreHookError(queryCursor, err) {
108+
if (err instanceof kareem.skipWrappedFunction) {
109+
const resultValue = err.args[0];
110+
if (resultValue != null && (!Array.isArray(resultValue) || resultValue.length)) {
111+
const err = new MongooseError(
112+
'Cannot `skipMiddlewareFunction()` with a value when using ' +
113+
'`.aggregate().cursor()`, value must be nullish or empty array, got "' +
114+
util.inspect(resultValue) +
115+
'".'
116+
);
117+
queryCursor._markError(err);
118+
queryCursor.listeners('error').length > 0 && queryCursor.emit('error', err);
119+
return;
120+
}
121+
queryCursor.emit('cursor', null);
122+
return;
123+
}
124+
queryCursor._markError(err);
125+
queryCursor.listeners('error').length > 0 && queryCursor.emit('error', err);
126+
}
127+
128+
87129
/**
88130
* Necessary to satisfy the Readable API
89131
* @method _read
@@ -424,6 +466,7 @@ function _next(ctx, cb) {
424466
err => callback(err)
425467
);
426468
} else {
469+
ctx.once('error', cb);
427470
ctx.once('cursor', function() {
428471
_next(ctx, cb);
429472
});

test/aggregate.test.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1300,4 +1300,34 @@ describe('aggregate: ', function() {
13001300
});
13011301
}, /Aggregate `near\(\)` argument has invalid coordinates, got ""/);
13021302
});
1303+
1304+
it('cursor() errors out if schema pre aggregate hook throws an error (gh-15279)', async function() {
1305+
const schema = new Schema({ name: String });
1306+
1307+
schema.pre('aggregate', function(next) {
1308+
if (!this.options.allowed) {
1309+
throw new Error('Unauthorized aggregate operation: only allowed operations are permitted');
1310+
}
1311+
next();
1312+
});
1313+
1314+
const Test = db.model('Test', schema);
1315+
1316+
await Test.create({ name: 'test1' });
1317+
1318+
await assert.rejects(
1319+
async() => {
1320+
await Test.aggregate([{ $limit: 1 }], { allowed: false }).exec();
1321+
},
1322+
err => err.message === 'Unauthorized aggregate operation: only allowed operations are permitted'
1323+
);
1324+
1325+
const cursor = Test.aggregate([{ $limit: 1 }], { allowed: false }).cursor();
1326+
await assert.rejects(
1327+
async() => {
1328+
await cursor.next();
1329+
},
1330+
err => err.message === 'Unauthorized aggregate operation: only allowed operations are permitted'
1331+
);
1332+
});
13031333
});

0 commit comments

Comments
 (0)