Skip to content

Commit

Permalink
Merge pull request #12680 from jpilgrim/master
Browse files Browse the repository at this point in the history
added overloads for Schema.pre/post with different values for SchemaPreOptions
  • Loading branch information
vkarpov15 committed Apr 27, 2023
2 parents 28e2298 + 37454e9 commit 7a3585e
Show file tree
Hide file tree
Showing 5 changed files with 1,442 additions and 26 deletions.
317 changes: 317 additions & 0 deletions test/model.middleware.preposttypes.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,317 @@
'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 (err) {
return 'unknown';
}
}
} else {
return typeof obj;
}
}

/* Helper for generating tsd test test/types/middleware.preposttypes.test.ts,
* only active when "GEN_TSD" is true
*/
const GEN_TSD = false;
const tsdOut = [];
if (GEN_TSD) {
tsdOut.push('/* The following tests were generated by test/model.middleware.preposttypes.test.js (with GEN_TSD 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');
}
}

/* Tests */
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(); // hook name to be called with types with which it has been called

// register hooks to mongoose and to map in order to check whether hook has been called
function registerHooks(expThisType /* QUERY, DOC, UNION, NEVER */, method /* save, updateOne etc. */, options/* ? {document,query} */) {

for (const hook of ['pre', 'post']) {
// only for TSD test generation ----------------------
if (GEN_TSD) {
const thisTypeStr = ['Query<any, any>', 'HydratedDocument<IDocument>', 'Query<any, any>|HydratedDocument<IDocument>', 'never'][expThisType];
if (hook === 'pre') {
tsdOut.push(`schema.${hook}(${jsfy(method).replace(/"/g, '')}${options ? ', ' + jsfy(options) : ''}, function() {
expectType<${thisTypeStr}>(this);
});`);
} else if (hook === 'post') {
tsdOut.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];
// create signature (for messages)
const hookSignature = [ // not 100% accurate, but good enough for this test
() => `${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());

// the callback checking the type and registering the call
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');
};

// register the hook
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;
}
}
}
}

// checks whether all registered hooks have been called
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) { // UNION
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) { // QUERY
if (!calledWith.has('Query')) {
failures.push(`hook ${hookName} should have been called with Query, was ${calledWithString()}.`);
}
} else if (hookName.indexOf('Document') >= 0) { // DOC
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 - ');
}

// --------------------------------------------------------------------------
// register hooks; here we actually see the correct type annotations in action
const MongooseQueryAndDocumentMiddleware = ['updateOne', 'deleteOne', 'validate'];

const MongooseDistinctDocumentMiddleware = ['save', 'init'];
const MongooseDocumentMiddleware = [...MongooseDistinctDocumentMiddleware, ...MongooseQueryAndDocumentMiddleware];

const MongooseDistinctQueryMiddleware = [
'count', 'estimatedDocumentCount', 'countDocuments',
'deleteMany', 'distinct',
'find', 'findOne', 'findOneAndDelete', 'findOneAndRemove', 'findOneAndReplace', 'findOneAndUpdate',
'replaceOne', 'updateMany'];
const MongooseDefaultQueryMiddleware = [...MongooseDistinctQueryMiddleware, 'updateOne', 'deleteOne'];
const MongooseQueryMiddleware = [...MongooseDistinctQueryMiddleware, ...MongooseQueryAndDocumentMiddleware];

const MongooseQueryOrDocumentMiddleware = [
...MongooseDistinctQueryMiddleware,
...MongooseDistinctDocumentMiddleware,
...MongooseQueryAndDocumentMiddleware];

// first: one method only
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 });
// ------------------------------------------------------------
// always Document (or never, which we do not need to defined in index.d.ts)
}
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 });
// ------------------------------------------------------------
// always Query (or never, which we do not need to defined in index.d.ts)
}
for (const method of ['updateOne', 'deleteOne']) { // MongooseDefaultQueryMiddleware w/o distinct
registerHooks(QUERY, method);
// defaults to Query
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 });
// ------------------------------------------------------------
// When literals are unknown, it is Union of Document|Query (or never, which we do not need to defined in index.d.ts)
}

// method arrays
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(DOC, MongooseDefaultDocumentMiddleware);
// registerHooks(QUERY, MongooseDefaultDocumentMiddleware, { document: false, query: true });
// registerHooks(DOC, MongooseDefaultDocumentMiddleware, { document: true, query: false });
// registerHooks(UNION, MongooseDefaultDocumentMiddleware, { document: true, query: true });
// registerHooks(NEVER, MongooseDefaultDocumentMiddleware, { 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 });

// --------------------------------------------------------------------------
// trigger hooks
try {
const Doc = db.model('Test', schema);
let doc = new Doc({ data: 'value' });
await doc.save(); // triggers save and validate hooks

// MongooseDistinctQueryMiddleware
await Doc.count().exec();
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.findOneAndRemove({}).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();

// MongooseQueryOrDocumentMiddleware, use Query
await Doc.deleteOne({}).exec(); await Doc.create({ data: 'value' });
await Doc.updateOne({ data: 'value' }) // call updateOne and
.setOptions({ runValidators: true }) // validate hook
.exec();

// MongooseQueryOrDocumentMiddleware, use Document
doc = await Doc.create({ data: 'doc2' });
await doc.updateOne({ data: 'value' }); // updateOne
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_TSD) {
tsdOut.push('/* end of generated tests */');
console.log(tsdOut.join('\n\n'));
}
});
});
4 changes: 3 additions & 1 deletion test/types/document.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,9 @@ function gh11435() {
const ItemSchema = new Schema<Item>({ name: String });

ItemSchema.pre('validate', function preValidate() {
expectType<Model<unknown>>(this.$model('Item1'));
if (!(this instanceof Query)) {
expectType<Model<unknown>>(this.$model('Item1'));
}
});
}

Expand Down
Loading

0 comments on commit 7a3585e

Please sign in to comment.