'use strict';
* Test dependencies.
*/
const start = require('./common');
const assert = require('assert');
const { fail } = require('assert');
const mongoose = start.mongoose;
const Schema = mongoose.Schema;
* Constants and helpers
*/
const QUERY = 0;
const DOC = 1;
const UNION = 2;
const NEVER = 3;
const TYPE_TO_NAME = ['Query', 'Document', 'Document|Query', 'never'];
function getTypeName(obj) {
if (typeof obj === 'object') {
if (obj instanceof mongoose.Query) {
return 'Query';
} else if (obj instanceof mongoose.Document) {
return 'Document';
} else {
try {
return this.constructor.name;
} catch {
return 'unknown';
}
}
} else {
return typeof obj;
}
}
* only active when "GEN_TYPE_TEST" is true
*/
const GEN_TYPE_TEST = false;
const typeTestText = [];
if (GEN_TYPE_TEST) {
typeTestText.push('/* The following tests were generated by test/model.middleware.preposttypes.test.js (with GEN_TYPE_TEST hardcoded set to true) */');
}
function jsfy(obj) {
if (typeof obj === 'string') {
return `'${obj}'`;
} else if (obj instanceof Array) {
return `[${obj.map(jsfy).join(', ')}]`;
} else {
return JSON.stringify(obj).replace(/"/g, '').replace(/([{,:])/g, '$1 ').replace(/([}])/g, ' $1');
}
}
describe('pre/post hooks, type of this', function() {
let db;
before(function() {
db = start();
});
after(async function() {
await db.close();
});
afterEach(() => require('./util').clearTestData(db));
afterEach(() => require('./util').stopRemainingOps(db));
* One single test for all the different types of hooks. This test is checking the type annotations of hooks in index.d.ts.
*/
it('dynamic type of this in pre/post hooks', async function() {
const schema = new Schema({ data: String });
const signatures = new Map();
function registerHooks(expThisType , method , options) {
for (const hook of ['pre', 'post']) {
if (GEN_TYPE_TEST) {
const thisTypeStr = ['Query<any, any>', 'HydratedDocument<IDocument>', 'Query<any, any>|HydratedDocument<IDocument>', 'never'][expThisType];
if (hook === 'pre') {
typeTestText.push(`schema.${hook}(${jsfy(method).replace(/"/g, '')}${options ? ', ' + jsfy(options) : ''}, function() {
ExpectType<${thisTypeStr}>(this);
});`);
} else if (hook === 'post') {
typeTestText.push(`schema.${hook}(${jsfy(method).replace(/"/g, '')}${options ? ', ' + jsfy(options) : ''}, function(res) {
ExpectType<${thisTypeStr}>(this);
ExpectNotType<Query<any, any>>(res);
});`);
}
}
const methods = method instanceof Array ? method : [method];
const hookSignature = [
() => `${hook}<T = Query<any, any>>(method: '${methods.join('\'|\'')}'${options ? ', ' + JSON.stringify(options) : ''}, fn: ${hook[0].toUpperCase()}${hook.slice(1)}MiddlewareFunction<T>): this;`,
() => `${hook}<T = HydratedDocument<DocType, TInstanceMethods>>(method: '${methods.join('\'|\'')}'${options ? ', ' + JSON.stringify(options) : ''}, fn: ${hook[0].toUpperCase()}${hook.slice(1)}MiddlewareFunction<T>): this;`,
() => `${hook}<T = HydratedDocument<DocType, TInstanceMethods>|Query<any, any>>(method: '${methods.join('\'|\'')}'${options ? ', ' + JSON.stringify(options) : ''}, fn: ${hook[0].toUpperCase()}${hook.slice(1)}MiddlewareFunction<T>): this;`,
() => `${hook}<T = never>(method: '${methods.join('\'|\'')}'${options ? ', ' + JSON.stringify(options) : ''}, fn: ${hook[0].toUpperCase()}${hook.slice(1)}MiddlewareFunction<T>): this;`
][expThisType]();
assert(!signatures.has(hookSignature), `hook already registered: ${hookSignature}`);
signatures.set(hookSignature, new Set());
const fn = function() {
const actThisType = getTypeName(this);
switch (expThisType) {
case QUERY:
case DOC:
assert(actThisType === TYPE_TO_NAME[expThisType], `this was ${actThisType}, should be ${TYPE_TO_NAME[expThisType]} for hook ${hookSignature}`); break;
case UNION: assert(actThisType === 'Document' || actThisType === 'Query', `this was ${actThisType}, should be ${TYPE_TO_NAME[expThisType]} for hook ${hookSignature}`); break;
case NEVER: fail(`this was ${actThisType}, hook ${hookSignature} should never have been called`); break;
}
const calledWith = signatures.get(hookSignature);
calledWith.add(actThisType);
};
const fnPost = function(res) {
fn.call(this);
const resType = getTypeName(res);
assert(resType !== 'Query', 'type of res in post middleware is not expected to be a Query');
};
if (options) {
switch (hook) {
case 'pre': schema.pre(method, options, fn); break;
case 'post': schema.post(method, options, fnPost); break;
}
} else {
switch (hook) {
case 'pre': schema.pre(method, fn); break;
case 'post': schema.post(method, fnPost); break;
}
}
}
}
function checkCalls() {
const failures = [];
for (const [hookName, calledWith] of signatures.entries()) {
const calledWithString = () => {
if (calledWith.size == 0) return 'never called';
return 'called with ' + [...calledWith].join(', ');
};
if (hookName.indexOf('never') >= 0) {
if (calledWith.size > 0) {
failures.push(`hook ${hookName} should never have been called but was ${calledWithString()}.`);
}
} else if (hookName.indexOf('|Query') >= 0) {
if (!(calledWith.has('Query') && calledWith.has('Document'))) {
failures.push(`hook ${hookName} should have been called with Document and Query, was ${calledWithString()}.`);
}
} else if (hookName.indexOf('Query') >= 0) {
if (!calledWith.has('Query')) {
failures.push(`hook ${hookName} should have been called with Query, was ${calledWithString()}.`);
}
} else if (hookName.indexOf('Document') >= 0) {
if (!calledWith.has('Document')) {
failures.push(`hook ${hookName} should have been called with Document, was ${calledWithString()}.`);
}
} else {
failures.push(`Error in test, do not recognize type of hook ${hookName}`);
}
}
return failures.join('\n - ');
}
const MongooseQueryAndDocumentMiddleware = ['updateOne', 'deleteOne', 'validate'];
const MongooseDistinctDocumentMiddleware = ['save', 'init'];
const MongooseDocumentMiddleware = [...MongooseDistinctDocumentMiddleware, ...MongooseQueryAndDocumentMiddleware];
const MongooseDistinctQueryMiddleware = [
'estimatedDocumentCount', 'countDocuments',
'deleteMany', 'distinct',
'find', 'findOne', 'findOneAndDelete', 'findOneAndReplace', 'findOneAndUpdate',
'replaceOne', 'updateMany'];
const MongooseDefaultQueryMiddleware = [...MongooseDistinctQueryMiddleware, 'updateOne', 'deleteOne'];
const MongooseQueryMiddleware = [...MongooseDistinctQueryMiddleware, ...MongooseQueryAndDocumentMiddleware];
const MongooseQueryOrDocumentMiddleware = [
...MongooseDistinctQueryMiddleware,
...MongooseDistinctDocumentMiddleware,
...MongooseQueryAndDocumentMiddleware];
for (const method of MongooseDistinctDocumentMiddleware) {
registerHooks(DOC, method);
registerHooks(DOC, method, { document: true, query: false });
registerHooks(DOC, method, { document: true, query: true });
registerHooks(NEVER, method, { document: false, query: true });
registerHooks(NEVER, method, { document: false, query: false });
}
for (const method of MongooseDistinctQueryMiddleware) {
registerHooks(QUERY, method);
registerHooks(QUERY, method, { document: false, query: true });
registerHooks(QUERY, method, { document: true, query: true });
registerHooks(NEVER, method, { document: true, query: false });
registerHooks(NEVER, method, { document: false, query: false });
}
for (const method of ['updateOne', 'deleteOne']) {
registerHooks(QUERY, method);
registerHooks(QUERY, method, { document: false, query: true });
registerHooks(DOC, method, { document: true, query: false });
registerHooks(UNION, method, { document: true, query: true });
registerHooks(NEVER, method, { document: false, query: false });
}
registerHooks(DOC, MongooseDistinctDocumentMiddleware);
registerHooks(DOC, MongooseDistinctDocumentMiddleware, { document: true, query: false });
registerHooks(DOC, MongooseDistinctDocumentMiddleware, { document: true, query: true });
registerHooks(NEVER, MongooseDistinctDocumentMiddleware, { document: false, query: true });
registerHooks(NEVER, MongooseDistinctDocumentMiddleware, { document: false, query: false });
registerHooks(QUERY, MongooseDistinctQueryMiddleware);
registerHooks(QUERY, MongooseDistinctQueryMiddleware, { document: false, query: true });
registerHooks(QUERY, MongooseDistinctQueryMiddleware, { document: true, query: true });
registerHooks(NEVER, MongooseDistinctQueryMiddleware, { document: true, query: false });
registerHooks(NEVER, MongooseDistinctQueryMiddleware, { document: false, query: false });
registerHooks(QUERY, MongooseDefaultQueryMiddleware);
registerHooks(QUERY, MongooseDefaultQueryMiddleware, { document: false, query: true });
registerHooks(DOC, MongooseDefaultQueryMiddleware, { document: true, query: false });
registerHooks(UNION, MongooseDefaultQueryMiddleware, { document: true, query: true });
registerHooks(NEVER, MongooseDefaultQueryMiddleware, { document: false, query: false });
registerHooks(UNION, MongooseDocumentMiddleware);
registerHooks(QUERY, MongooseDocumentMiddleware, { document: false, query: true });
registerHooks(DOC, MongooseDocumentMiddleware, { document: true, query: false });
registerHooks(UNION, MongooseDocumentMiddleware, { document: true, query: true });
registerHooks(NEVER, MongooseDocumentMiddleware, { document: false, query: false });
registerHooks(UNION, MongooseQueryMiddleware);
registerHooks(QUERY, MongooseQueryMiddleware, { document: false, query: true });
registerHooks(DOC, MongooseQueryMiddleware, { document: true, query: false });
registerHooks(UNION, MongooseQueryMiddleware, { document: true, query: true });
registerHooks(NEVER, MongooseQueryMiddleware, { document: false, query: false });
registerHooks(UNION, MongooseQueryOrDocumentMiddleware);
registerHooks(QUERY, MongooseQueryOrDocumentMiddleware, { document: false, query: true });
registerHooks(DOC, MongooseQueryOrDocumentMiddleware, { document: true, query: false });
registerHooks(UNION, MongooseQueryOrDocumentMiddleware, { document: true, query: true });
registerHooks(NEVER, MongooseQueryOrDocumentMiddleware, { document: false, query: false });
try {
const Doc = db.model('Test', schema);
let doc = new Doc({ data: 'value' });
await doc.save();
await Doc.estimatedDocumentCount().exec();
await Doc.countDocuments().exec();
await Doc.deleteMany().exec(); await Doc.create({ data: 'value' });
await Doc.distinct('data').exec();
await Doc.find({}).exec();
await Doc.findOne({}).exec();
await Doc.findOneAndDelete({}).exec(); await Doc.create({ data: 'value' });
await Doc.findOneAndReplace({}, { data: 'valueRep' }).exec();
await Doc.findOneAndUpdate({}, { data: 'valueUpd' }).exec();
await Doc.replaceOne({}, { data: 'value' }).exec();
await Doc.updateOne({}, { data: 'value' }).exec();
await Doc.updateMany({}, { data: 'value' }).exec();
await Doc.deleteOne({}).exec(); await Doc.create({ data: 'value' });
await Doc.updateOne({ data: 'value' })
.setOptions({ runValidators: true })
.exec();
doc = await Doc.create({ data: 'doc2' });
await doc.updateOne({ data: 'value' });
await doc.deleteOne(); doc = await Doc.create({ data: 'doc3' });
const callResult = checkCalls();
assert(callResult.length == 0, 'Unexpected hook calls:\n - ' + callResult);
} catch (err) {
assert.fail(err);
}
if (GEN_TYPE_TEST) {
typeTestText.push('/* end of generated tests */');
console.log(typeTestText.join('\n\n'));
}
});
});