Skip to main content
Module

x/mongoose/test/aggregate.test.js

MongoDB object modeling designed to work in an asynchronous environment.
Go to Latest
File
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259
'use strict';
/** * Module dependencies */
const start = require('./common');
const assert = require('assert');
const Aggregate = require('../lib/aggregate');
const mongoose = start.mongoose;const Schema = mongoose.Schema;
/** * Test data */
async function setupData(db) { const EmployeeSchema = new Schema({ name: String, sal: Number, dept: String, customers: [String], reportsTo: String });
const emps = [ { name: 'Alice', sal: 18000, dept: 'sales', customers: ['Eve', 'Fred'] }, { name: 'Bob', sal: 15000, dept: 'sales', customers: ['Gary', 'Herbert', 'Isaac'], reportsTo: 'Alice' }, { name: 'Carol', sal: 14000, dept: 'r&d', reportsTo: 'Bob' }, { name: 'Dave', sal: 14500, dept: 'r&d', reportsTo: 'Carol' } ]; const Employee = db.model('Employee', EmployeeSchema);
await Employee.deleteMany({});
await Employee.create(emps);}
/** * Helper function to test operators that only work in a specific version of MongoDB and above (such as some aggregation pipeline operators) * * @param {String} semver, `3.4`, specify minimum compatible mongod version * @param {Object} ctx, `this`, so that mocha tests can be skipped * @return {Promise<void>} */async function onlyTestAtOrAbove(semver, ctx) { const versions = { 3.4: [3, 4], 3.6: [3, 6] };
if (semver.length !== 3 || Object.keys(versions).indexOf(semver) === -1) { throw new TypeError('onlyTestAtOrAbove expects either ' + Object.keys(versions).join(', ') + ' as first parameter.'); }
const version = await start.mongodVersion();
const desired = versions[semver];
const meetsMinimum = version[0] > desired[0] || (version[0] === desired[0] && version[1] >= desired[1]);
if (!meetsMinimum) { ctx.skip(); }}
/** * Test. */
describe('aggregate: ', function() { let db;
before(function startConnection() { db = start(); });
after(async function closeConnection() { await db.close(); });
beforeEach(() => db.deleteModel(/.*/)); afterEach(() => require('./util').clearTestData(db)); afterEach(() => require('./util').stopRemainingOps(db));
describe('append', function() { it('(pipeline)', function() { const aggregate = new Aggregate();
assert.equal(aggregate.append({ $a: 1 }, { $b: 2 }, { $c: 3 }), aggregate); assert.deepEqual(aggregate._pipeline, [{ $a: 1 }, { $b: 2 }, { $c: 3 }]);
aggregate.append({ $d: 4 }, { $c: 5 }); assert.deepEqual(aggregate._pipeline, [{ $a: 1 }, { $b: 2 }, { $c: 3 }, { $d: 4 }, { $c: 5 }]); });
it('supports array as single argument', function() { const aggregate = new Aggregate();
assert.equal(aggregate.append([{ $a: 1 }, { $b: 2 }, { $c: 3 }]), aggregate); assert.deepEqual(aggregate._pipeline, [{ $a: 1 }, { $b: 2 }, { $c: 3 }]);
aggregate.append([{ $d: 4 }, { $c: 5 }]); assert.deepEqual(aggregate._pipeline, [{ $a: 1 }, { $b: 2 }, { $c: 3 }, { $d: 4 }, { $c: 5 }]); });
it('throws if non-operator parameter is passed', function() { const aggregate = new Aggregate(); const regexp = /Arguments must be aggregate pipeline operators/;
assert.throws(function() { aggregate.append({ $a: 1 }, 'string'); }, regexp);
assert.throws(function() { aggregate.append({ $a: 1 }, ['array']); }, regexp);
assert.throws(function() { aggregate.append({ $a: 1 }, { a: 1 }); }, regexp);
assert.throws(function() { aggregate.append([{ $a: 1 }, { a: 1 }]); }, regexp); });
it('does not throw when 0 args passed', function() { const aggregate = new Aggregate();
assert.doesNotThrow(function() { aggregate.append(); }); });
it('does not throw when empty array is passed as single argument', function() { const aggregate = new Aggregate();
assert.doesNotThrow(function() { aggregate.append([]); }); }); });
describe('project', function() { it('(object)', function() { const aggregate = new Aggregate();
assert.equal(aggregate.project({ a: 1, b: 1, c: 0 }), aggregate); assert.deepEqual(aggregate._pipeline, [{ $project: { a: 1, b: 1, c: 0 } }]);
aggregate.project({ b: 1 }); assert.deepEqual(aggregate._pipeline, [{ $project: { a: 1, b: 1, c: 0 } }, { $project: { b: 1 } }]); });
it('(string)', function() { const aggregate = new Aggregate();
aggregate.project(' a b -c '); assert.deepEqual(aggregate._pipeline, [{ $project: { a: 1, b: 1, c: 0 } }]);
aggregate.project('b'); assert.deepEqual(aggregate._pipeline, [{ $project: { a: 1, b: 1, c: 0 } }, { $project: { b: 1 } }]); });
it('("a","b","c")', function() { assert.throws(function() { const aggregate = new Aggregate(); aggregate.project('a', 'b', 'c'); }, /Invalid project/); });
it('["a","b","c"]', function() { assert.throws(function() { const aggregate = new Aggregate(); aggregate.project(['a', 'b', 'c']); }, /Invalid project/); }); });
describe('group', function() { it('works', function() { const aggregate = new Aggregate();
assert.equal(aggregate.group({ a: 1, b: 2 }), aggregate); assert.deepEqual(aggregate._pipeline, [{ $group: { a: 1, b: 2 } }]);
aggregate.group({ c: 3 }); assert.deepEqual(aggregate._pipeline, [{ $group: { a: 1, b: 2 } }, { $group: { c: 3 } }]); }); });
describe('skip', function() { it('works', function() { const aggregate = new Aggregate();
assert.equal(aggregate.skip(42), aggregate); assert.deepEqual(aggregate._pipeline, [{ $skip: 42 }]);
aggregate.skip(42); assert.deepEqual(aggregate._pipeline, [{ $skip: 42 }, { $skip: 42 }]); }); });
describe('limit', function() { it('works', function() { const aggregate = new Aggregate();
assert.equal(aggregate.limit(42), aggregate); assert.deepEqual(aggregate._pipeline, [{ $limit: 42 }]);
aggregate.limit(42); assert.deepEqual(aggregate._pipeline, [{ $limit: 42 }, { $limit: 42 }]); }); });
describe('unwind', function() { it('("field")', function() { const aggregate = new Aggregate();
assert.equal(aggregate.unwind('field'), aggregate); assert.deepEqual(aggregate._pipeline, [{ $unwind: '$field' }]);
aggregate.unwind('a', 'b', 'c'); assert.deepEqual(aggregate._pipeline, [ { $unwind: '$field' }, { $unwind: '$a' }, { $unwind: '$b' }, { $unwind: '$c' } ]); }); });
describe('match', function() { it('works', function() { const aggregate = new Aggregate();
assert.equal(aggregate.match({ a: 1 }), aggregate); assert.deepEqual(aggregate._pipeline, [{ $match: { a: 1 } }]);
aggregate.match({ b: 2 }); assert.deepEqual(aggregate._pipeline, [{ $match: { a: 1 } }, { $match: { b: 2 } }]); }); });
describe('sort', function() { it('(object)', function() { const aggregate = new Aggregate();
assert.equal(aggregate.sort({ a: 1, b: 'asc', c: 'descending' }), aggregate); assert.deepEqual(aggregate._pipeline, [{ $sort: { a: 1, b: 1, c: -1 } }]);
aggregate.sort({ b: 'desc' }); assert.deepEqual(aggregate._pipeline, [{ $sort: { a: 1, b: 1, c: -1 } }, { $sort: { b: -1 } }]); });
it('(string)', function() { const aggregate = new Aggregate();
aggregate.sort(' a b -c '); assert.deepEqual(aggregate._pipeline, [{ $sort: { a: 1, b: 1, c: -1 } }]);
aggregate.sort('b'); assert.deepEqual(aggregate._pipeline, [{ $sort: { a: 1, b: 1, c: -1 } }, { $sort: { b: 1 } }]); });
it('("a","b","c")', function() { assert.throws(function() { const aggregate = new Aggregate(); aggregate.sort('a', 'b', 'c'); }, /Invalid sort/); });
it('["a","b","c"]', function() { assert.throws(function() { const aggregate = new Aggregate(); aggregate.sort(['a', 'b', 'c']); }, /Invalid sort/); }); });
describe('near', function() { it('works', function() { const aggregate = new Aggregate();
assert.equal(aggregate.near({ a: 1 }), aggregate); assert.deepEqual(aggregate._pipeline, [{ $geoNear: { a: 1 } }]);
aggregate.near({ b: 2 }); assert.deepEqual(aggregate._pipeline, [{ $geoNear: { a: 1 } }, { $geoNear: { b: 2 } }]); });
it('works with discriminators (gh-3304)', function() { let aggregate = new Aggregate(); const stub = { schema: { discriminatorMapping: { key: '__t', value: 'subschema', isRoot: false } } };
aggregate._model = stub;
assert.equal(aggregate.near({ a: 1 }), aggregate); // Run exec so we apply discriminator pipeline Aggregate._prepareDiscriminatorPipeline(aggregate._pipeline, stub.schema); assert.deepEqual(aggregate._pipeline, [{ $geoNear: { a: 1, query: { __t: 'subschema' } } }]);
aggregate = new Aggregate(); aggregate._model = stub;
aggregate.near({ b: 2, query: { x: 1 } }); Aggregate._prepareDiscriminatorPipeline(aggregate._pipeline, stub.schema); assert.deepEqual(aggregate._pipeline, [{ $geoNear: { b: 2, query: { x: 1, __t: 'subschema' } } }]); }); });
describe('lookup', function() { it('works', function() { const aggregate = new Aggregate(); const obj = { from: 'users', localField: 'userId', foreignField: '_id', as: 'users' };
aggregate.lookup(obj);
assert.equal(aggregate._pipeline.length, 1); assert.deepEqual(aggregate._pipeline[0].$lookup, obj); }); });
describe('unionWith', function() { it('works', function() { const aggregate = new Aggregate(); const obj = { coll: 'users', pipeline: [ { $match: { _id: 1 } } ] };
aggregate.unionWith(obj);
assert.equal(aggregate._pipeline.length, 1); assert.deepEqual(aggregate._pipeline[0].$unionWith, obj); }); });
describe('sample', function() { it('works', function() { const aggregate = new Aggregate();
aggregate.sample(3);
assert.equal(aggregate._pipeline.length, 1); assert.deepEqual(aggregate._pipeline[0].$sample, { size: 3 }); }); });
describe('densify', function() { it('works', function() { const aggregate = new Aggregate(); const obj = { field: 'timestamp', range: { step: 1, unit: 'hour', bounds: [new Date('2021-05-18T00:00:00.000Z'), new Date('2021-05-18T08:00:00.000Z')] } };
aggregate.densify(obj);
assert.equal(aggregate._pipeline.length, 1); assert.deepEqual(aggregate._pipeline[0].$densify, obj); }); });
describe('fill', function() { it('works', function() { const aggregate = new Aggregate(); const obj = { output: { bootsSold: { value: 0 }, sandalsSold: { value: 0 }, sneakersSold: { value: 0 } } };
aggregate.fill(obj);
assert.equal(aggregate._pipeline.length, 1); assert.deepEqual(aggregate._pipeline[0].$fill, obj); }); });
describe('model()', function() { it('works', function() { const aggregate = new Aggregate(); const model = { foo: 42 };
assert.equal(aggregate._model, null); assert.equal(aggregate.model(), null);
assert.equal(aggregate.model(model), model); assert.equal(aggregate._model, model); assert.equal(aggregate.model(), model); }); });
describe('redact', function() { const pipelineResult = [{ $redact: { $cond: { if: { $eq: ['$level', 5] }, then: '$$PRUNE', else: '$$DESCEND' } } }]; it('works', function() { const aggregate = new Aggregate(); aggregate.redact({ $cond: { if: { $eq: ['$level', 5] }, then: '$$PRUNE', else: '$$DESCEND' } }); assert.deepEqual(aggregate._pipeline, pipelineResult); }); it('works with (condition, string, string)', function() { const aggregate = new Aggregate(); aggregate.redact({ $eq: ['$level', 5] }, '$$PRUNE', '$$DESCEND'); assert.deepEqual(aggregate._pipeline, pipelineResult); }); });
describe('Mongo 3.4 operators', function() { before(async function() { await onlyTestAtOrAbove('3.4', this); });
describe('graphLookup', function() { it('works', function() { const aggregate = new Aggregate(); aggregate.graphLookup({ startWith: '$test', from: 'sourceCollection', connectFromField: 'testFromField', connectToField: '_id' });
assert.equal(aggregate._pipeline.length, 1); assert.deepEqual(aggregate._pipeline[0].$graphLookup, { startWith: '$test', from: 'sourceCollection', connectFromField: 'testFromField', connectToField: '_id' }); });
it('automatically prepends $ to the startWith field', function() { const aggregate = new Aggregate(); aggregate.graphLookup({ startWith: 'test' });
assert.deepEqual(aggregate._pipeline[0].$graphLookup, { startWith: '$test' }); });
it('Throws if no options are passed to graphLookup', function() { const aggregate = new Aggregate(); assert.throws(function() { aggregate.graphLookup('invalid options'); }, TypeError); }); });
describe('addFields', function() { it('should throw if passed a non object', function() { const aggregate = new Aggregate(); assert.throws(() => {aggregate.addFields('invalid');}, /Invalid addFields\(\) argument\. Must be an object/); }); it('should throw if passed null', function() { const aggregate = new Aggregate(); assert.throws(() => {aggregate.addFields(null);}, /Invalid addFields\(\) argument\. Must be an object/); }); it('should throw if passed an Array', function() { const aggregate = new Aggregate(); assert.throws(() => {aggregate.addFields([]);}, /Invalid addFields\(\) argument\. Must be an object/); }); it('(object)', function() { const aggregate = new Aggregate();
assert.equal(aggregate.addFields({ a: 1, b: 1, c: 0 }), aggregate); assert.deepEqual(aggregate._pipeline, [{ $addFields: { a: 1, b: 1, c: 0 } }]);
aggregate.addFields({ d: { $add: ['$a', '$b'] } }); assert.deepEqual(aggregate._pipeline, [{ $addFields: { a: 1, b: 1, c: 0 } }, { $addFields: { d: { $add: ['$a', '$b'] } } }]); }); });
describe('facet', function() { it('works', function() { const aggregate = new Aggregate();
aggregate.facet({ heights: [ // This will group documents by their `height` property { $group: { _id: '$height', count: { $sum: 1 } } }, // This will sort by descending height { $sort: { count: -1, _id: -1 } } ], players: [ // This will group documents by their `firstName` property { $group: { _id: '$firstName', count: { $sum: 1 } } }, // This will sort documents by their firstName descending { $sort: { count: -1, _id: -1 } } ] });
assert.equal(aggregate._pipeline.length, 1); assert.deepEqual(aggregate._pipeline[0].$facet, { heights: [ // This will group documents by their `height` property { $group: { _id: '$height', count: { $sum: 1 } } }, // This will sort by descending height { $sort: { count: -1, _id: -1 } } ], players: [ // This will group documents by their `firstName` property { $group: { _id: '$firstName', count: { $sum: 1 } } },
// This will sort documents by their firstName descending { $sort: { count: -1, _id: -1 } } ] }); }); });
describe('replaceRoot', function() { it('works with a string', function() { const aggregate = new Aggregate();
aggregate.replaceRoot('myNewRoot');
assert.deepEqual(aggregate._pipeline, [{ $replaceRoot: { newRoot: '$myNewRoot' } }]); }); it('works with an object (gh-6474)', function() { const aggregate = new Aggregate();
aggregate.replaceRoot({ x: { $concat: ['$this', '$that'] } });
assert.deepEqual(aggregate._pipeline, [{ $replaceRoot: { newRoot: { x: { $concat: ['$this', '$that'] } } } }]); }); });
describe('count', function() { it('works', function() { const aggregate = new Aggregate();
aggregate.count('countResult');
assert.deepEqual(aggregate._pipeline, [{ $count: 'countResult' }]); }); });
describe('sortByCount', function() { it('works with a string argument', function() { const aggregate = new Aggregate();
aggregate.sortByCount('countedField');
assert.deepEqual(aggregate._pipeline, [{ $sortByCount: '$countedField' }]); });
it('works with an object argument', function() { const aggregate = new Aggregate();
aggregate.sortByCount({ lname: '$employee.last' });
assert.deepEqual(aggregate._pipeline, [{ $sortByCount: { lname: '$employee.last' } }]); });
it('throws if the argument is neither a string or object', function() { const aggregate = new Aggregate(); assert.throws(function() { aggregate.sortByCount(1); }, TypeError); }); }); });
describe('exec', function() { beforeEach(async function() { await setupData(db); });
it('project', async function() { const aggregate = new Aggregate([], db.model('Employee'));
const docs = await aggregate.project({ sal: 1, sal_k: { $divide: ['$sal', 1000] } }).exec();
docs.forEach(function(doc) { assert.equal(doc.sal / 1000, doc.sal_k); });
});
it('group', async function() { const aggregate = new Aggregate([], db.model('Employee'));
const docs = await aggregate. group({ _id: '$dept' }). exec();
assert.equal(docs.length, 2);
const depts = docs.map((doc) => doc._id); assert.notEqual(depts.indexOf('sales'), -1); assert.notEqual(depts.indexOf('r&d'), -1); });
it('skip', async function() { const aggregate = new Aggregate([], db.model('Employee'));
const docs = await aggregate. skip(1). exec();
assert.equal(docs.length, 3); });
it('limit', async function() { const aggregate = new Aggregate([], db.model('Employee'));
const docs = await aggregate. limit(3). exec();
assert.equal(docs.length, 3); });
it('unwind', async function() { const aggregate = new Aggregate([], db.model('Employee'));
const docs = await aggregate. unwind('customers'). exec();
assert.equal(docs.length, 5); });
it('unwind with obj', function() { const aggregate = new Aggregate();
const agg = aggregate. unwind({ path: '$customers', preserveNullAndEmptyArrays: true });
assert.equal(agg._pipeline.length, 1); assert.strictEqual(agg._pipeline[0].$unwind.preserveNullAndEmptyArrays, true); });
it('unwind throws with bad arg', function() { const aggregate = new Aggregate();
let threw = false; try { aggregate. unwind(36); } catch (err) { assert.ok(err.message.indexOf('to unwind()') !== -1); threw = true; } assert.ok(threw); });
it('match', async function() { const aggregate = new Aggregate([], db.model('Employee'));
const docs = await aggregate.match({ sal: { $gt: 15000 } });
assert.equal(docs.length, 1); });
it('sort', async function() { const aggregate = new Aggregate([], db.model('Employee'));
const docs = await aggregate.sort('sal');
assert.equal(docs[0].sal, 14000); });
it('graphLookup', async function() { const _this = this; const version = await start.mongodVersion();
const mongo34 = version[0] > 3 || (version[0] === 3 && version[1] >= 4); if (!mongo34) { _this.skip(); }
const aggregate = new Aggregate([], db.model('Employee'));
const docs = await aggregate. graphLookup({ from: 'Employee', startWith: '$reportsTo', connectFromField: 'reportsTo', connectToField: 'name', as: 'employeeHierarchy' }). sort({ name: 1 }). exec();
const lowest = docs[3]; assert.equal(lowest.name, 'Dave'); assert.equal(lowest.employeeHierarchy.length, 3);
// First result in array is max depth result const names = lowest.employeeHierarchy.map((doc) => doc.name).sort(); assert.equal(names[0], 'Alice'); assert.equal(names[1], 'Bob'); assert.equal(names[2], 'Carol'); });
it('facet', async function() { const _this = this; const version = await start.mongodVersion();
const mongo34 = version[0] > 3 || (version[0] === 3 && version[1] >= 4); if (!mongo34) { _this.skip(); }

const aggregate = new Aggregate([], db.model('Employee'));
const docs = await aggregate. facet({ departments: [ { $group: { _id: '$dept', count: { $sum: 1 } } } ], employeesPerCustomer: [ { $unwind: '$customers' }, { $sortByCount: '$customers' }, { $sort: { _id: 1 } } ] }). exec();
assert.deepEqual(docs[0].departments.map(d => d.count), [2, 2]);
assert.deepEqual(docs[0].employeesPerCustomer, [ { _id: 'Eve', count: 1 }, { _id: 'Fred', count: 1 }, { _id: 'Gary', count: 1 }, { _id: 'Herbert', count: 1 }, { _id: 'Isaac', count: 1 } ]); });
it('complex pipeline', async function() { const aggregate = new Aggregate([], db.model('Employee'));
const docs = await aggregate. match({ sal: { $lt: 16000 } }). unwind('customers'). project({ emp: '$name', cust: '$customers' }). sort('-cust'). skip(2). exec();
assert.equal(docs.length, 1); assert.equal(docs[0].cust, 'Gary'); assert.equal(docs[0].emp, 'Bob'); });
it('pipeline() (gh-5825)', function() { const aggregate = new Aggregate();
const pipeline = aggregate. match({ sal: { $lt: 16000 } }). pipeline();
assert.deepEqual(pipeline, [{ $match: { sal: { $lt: 16000 } } }]); });
it('explain()', async function() { const aggregate = new Aggregate([], db.model('Employee')); const version = await start.mongodVersion();
const mongo26 = version[0] > 2 || (version[0] === 2 && version[1] >= 6); if (!mongo26) { return; }
const output = await aggregate. match({ sal: { $lt: 16000 } }). explain();
assert.ok(output); // make sure we got explain output assert.ok(output.stages || output.queryPlanner); });
describe('error when empty pipeline', function() { it('without a callback', function() { const agg = new Aggregate([], db.model('Employee'));
const promise = agg.exec(); assert.ok(promise instanceof mongoose.Promise);
return promise.catch(error => { assert.ok(error); assert.ok(error.message.indexOf('empty pipeline') !== -1, error.message); }); });
it('with a callback', function(done) { const aggregate = new Aggregate([], db.model('Employee'));
const callback = function(err) { assert.ok(err); assert.equal(err.message, 'Aggregate has empty pipeline'); done(); };
aggregate.exec(callback); }); });
describe('error when not bound to a model', function() { it('with callback', function() { const aggregate = new Aggregate();
aggregate.skip(0); let threw = false; try { aggregate.exec(); } catch (error) { threw = true; assert.equal(error.message, 'Aggregate not bound to any Model'); } assert.ok(threw); }); });
it('handles aggregation options', async function() { const version = await start.mongodVersion();
const m = db.model('Employee'); const match = { $match: { sal: { $gt: 15000 } } }; const pref = 'primaryPreferred'; const aggregate = m.aggregate([match]).read(pref); const mongo26_or_greater = version[0] > 2 || (version[0] === 2 && version[1] >= 6); const mongo32_or_greater = version[0] > 3 || (version[0] === 3 && version[1] >= 2);
assert.equal(aggregate.options.readPreference.mode, pref); if (mongo26_or_greater) { aggregate.allowDiskUse(true); aggregate.option({ maxTimeMS: 1000 }); assert.equal(aggregate.options.allowDiskUse, true); assert.equal(aggregate.options.maxTimeMS, 1000); }
if (mongo32_or_greater) { aggregate.readConcern('m'); assert.deepEqual(aggregate.options.readConcern, { level: 'majority' }); }
const docs = await aggregate.exec();
assert.equal(1, docs.length); assert.equal(docs[0].sal, 18000); });
describe('middleware (gh-5251)', function() { it('pre', async function() { const s = new Schema({ name: String });
let called = 0; s.pre('aggregate', function(next) { ++called; next(); });
const M = db.model('Test', s);
const res = await M.aggregate([{ $match: { name: 'test' } }]);
assert.deepEqual(res, []); assert.equal(called, 1); });
it('setting option in pre (gh-7606)', async function() { const s = new Schema({ name: String });
s.pre('aggregate', function(next) { this.options.collation = { locale: 'en_US', strength: 1 }; next(); });
const M = db.model('Test', s);
await M.create([{ name: 'alpha' }, { name: 'Zeta' }]);
const docs = await M.aggregate([{ $sort: { name: 1 } }]);
assert.equal(docs[0].name, 'alpha'); assert.equal(docs[1].name, 'Zeta'); });
it('adding to pipeline in pre (gh-8017)', async function() { const s = new Schema({ name: String });
s.pre('aggregate', function(next) { this.append({ $limit: 1 }); next(); });
const M = db.model('Test', s);
await M.create([{ name: 'alpha' }, { name: 'Zeta' }]);
const docs = await M.aggregate([{ $sort: { name: 1 } }]);
assert.equal(docs.length, 1); assert.equal(docs[0].name, 'Zeta'); });
it('post', async function() { const s = new Schema({ name: String });
const calledWith = []; s.post('aggregate', function(res, next) { calledWith.push(res); next(); });
const M = db.model('Test', s);
const res = await M.aggregate([{ $match: { name: 'test' } }]);
assert.deepEqual(res, []); assert.equal(calledWith.length, 1); assert.deepEqual(calledWith[0], []); });
it('error handler with agg error', async function() { const s = new Schema({ name: String });
const calledWith = []; s.post('aggregate', function(error, res, next) { calledWith.push(error); next(); });
const M = db.model('Test', s);
const error = await M.aggregate([{ $fakeStage: { name: 'test' } }]).then(() => null, err => err);
assert.ok(error); assert.ok( error.message.indexOf('Unrecognized pipeline stage') !== -1, error.message );
assert.equal(calledWith.length, 1); assert.equal(calledWith[0], error); });
it('error handler with pre error', async function() { const s = new Schema({ name: String });
const calledWith = []; s.pre('aggregate', function(next) { next(new Error('woops')); }); s.post('aggregate', function(error, res, next) { calledWith.push(error); next(); });
const M = db.model('Test', s);
const error = await M.aggregate([{ $match: { name: 'test' } }]).then(() => null, err => err);
assert.ok(error); assert.equal(error.message, 'woops'); assert.equal(calledWith.length, 1); assert.equal(calledWith[0], error); });
it('with agg cursor', async function() { const s = new Schema({ name: String });
let calledPre = 0; let calledPost = 0; s.pre('aggregate', function(next) { ++calledPre; next(); }); s.post('aggregate', function(res, next) { ++calledPost; next(); });
const M = db.model('Test', s);
let numDocs = 0; await M. aggregate([{ $match: { name: 'test' } }]). cursor({ useMongooseAggCursor: true }). eachAsync(function() { ++numDocs; });
assert.equal(numDocs, 0); assert.equal(calledPre, 1); assert.equal(calledPost, 0); });
it('with explain() (gh-5887)', function() { const s = new Schema({ name: String });
let calledPre = 0; const calledPost = []; s.pre('aggregate', function(next) { ++calledPre; next(); }); s.post('aggregate', function(res, next) { calledPost.push(res); next(); });
const M = db.model('Test', s);
return M.aggregate([{ $match: { name: 'test' } }]).explain(). then(() => { assert.equal(calledPre, 1); assert.equal(calledPost.length, 1); assert.ok(calledPost[0].stages || calledPost[0].queryPlanner); }); }); });
it('readPref from schema (gh-5522)', function() { const schema = new Schema({ name: String }, { read: 'secondary' }); const M = db.model('Test', schema); const a = M.aggregate(); assert.equal(a.options.readPreference.mode, 'secondary');
a.read('secondaryPreferred');
assert.equal(a.options.readPreference.mode, 'secondaryPreferred'); }); });
it('cursor (gh-3160)', async function() { const MyModel = db.model('Test', { name: String });
await MyModel.create({ name: 'test' });
const cursor = MyModel. aggregate([{ $match: { name: 'test' } }, { $project: { name: '$name' } }]). allowDiskUse(true). cursor({ batchSize: 2500 });
assert.ok(cursor.eachAsync); });
it('catch() (gh-7267)', async function() { const MyModel = db.model('Test', {});
const err = await MyModel.aggregate([{ $group: { foo: 'bar' } }]) .then(() => null, err => err); assert.ok(err instanceof Error); assert.equal(err.name, 'MongoServerError'); });
it('cursor() without options (gh-3855)', function() { const MyModel = db.model('Test', { name: String });
const cursor = MyModel. aggregate([{ $match: { name: 'test' } }]). cursor(); assert.ok(cursor instanceof require('stream').Readable); });
it('cursor() with useMongooseAggCursor (gh-5145)', function() { const MyModel = db.model('Test', { name: String });
const cursor = MyModel. aggregate([{ $match: { name: 'test' } }]). cursor({ useMongooseAggCursor: true }); assert.ok(cursor instanceof require('stream').Readable); });
it('cursor() with useMongooseAggCursor works (gh-5145) (gh-5394)', async function() { const MyModel = db.model('Test', { name: String });
await MyModel.create({ name: 'test' });
const docs = []; await MyModel. aggregate([{ $match: { name: 'test' } }]). cursor({ useMongooseAggCursor: true }). eachAsync(function(doc) { docs.push(doc); });
assert.equal(docs.length, 1); assert.equal(docs[0].name, 'test'); });
it('cursor() eachAsync (gh-4300)', async function() { const MyModel = db.model('Test', { name: String });
let cur = 0; const expectedNames = ['Axl', 'Slash'];
await MyModel.create([{ name: 'Axl' }, { name: 'Slash' }]);
await MyModel.aggregate([{ $sort: { name: 1 } }]). cursor(). eachAsync(function(doc) { const _cur = cur; assert.equal(doc.name, expectedNames[cur]); return { then: function(resolve) { setTimeout(function() { assert.equal(_cur, cur++); resolve(); }, 50); } }; }); });
it('cursor() eachAsync with options (parallel)', async function() { const MyModel = db.model('Test', { name: String });
const names = []; const startedAt = []; const expectedNames = ['Axl', 'Slash']; const checkDoc = function(doc) { names.push(doc.name); startedAt.push(Date.now()); return { then: function(resolve) { setTimeout(function() { resolve(); }, 100); } }; };
await MyModel.create([{ name: 'Axl' }, { name: 'Slash' }]);
await MyModel.aggregate([{ $sort: { name: 1 } }]). cursor(). eachAsync(checkDoc, { parallel: 2 }).then(function() { assert.ok(Date.now() - startedAt[1] >= 75, Date.now() - startedAt[1]); assert.equal(startedAt.length, 2); assert.ok(startedAt[1] - startedAt[0] < 50, `${startedAt[1] - startedAt[0]}`); assert.deepEqual(names.sort(), expectedNames); }); });
it('is now a proper aggregate cursor vs what it was before gh-10410', function() { const MyModel = db.model('Test', { name: String }); assert.throws(() => { MyModel.aggregate([]).cursor({ batchSize: 1000 }).exec(); }); });
it('query by document (gh-4866)', async function() { const MyModel = db.model('Test', { name: String });
const doc = await MyModel.create({ name: 'test' }); const res = await MyModel.aggregate([{ $match: doc }]); assert.equal(res.length, 1); });
it('sort by text score (gh-5258)', async function() { const mySchema = new Schema({ test: String }); mySchema.index({ test: 'text' }); const M = db.model('Test', mySchema);
await M.init();
await M.create([{ test: 'test test' }, { test: 'a test' }]);
const aggregate = M.aggregate(); aggregate.match({ $text: { $search: 'test' } }); aggregate.sort({ score: { $meta: 'textScore' } });
const res = await aggregate.exec();
assert.equal(res.length, 2); assert.equal(res[0].test, 'test test'); assert.equal(res[1].test, 'a test'); });
describe('Mongo 3.6 options', function() { before(async function() { await onlyTestAtOrAbove('3.6', this); });
it('adds hint option', async function() { const mySchema = new Schema({ name: String, qty: Number }); mySchema.index({ qty: -1, name: -1 }); const M = db.model('Test', mySchema); await M.init();
const docs = [ { name: 'Andrew', qty: 4 }, { name: 'Betty', qty: 5 }, { name: 'Charlie', qty: 4 } ]; await M.create(docs);
const foundDocs = await M.aggregate().match({}) .hint({ qty: -1, name: -1 }).exec();
assert.equal(foundDocs[0].name, 'Betty'); assert.equal(foundDocs[1].name, 'Charlie'); assert.equal(foundDocs[2].name, 'Andrew'); }); });
});