-
-
Notifications
You must be signed in to change notification settings - Fork 3.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #12680 from jpilgrim/master
added overloads for Schema.pre/post with different values for SchemaPreOptions
- Loading branch information
Showing
5 changed files
with
1,442 additions
and
26 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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')); | ||
} | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.