1. 들어가며
- 이전 글에서 작업한 PR이 머지되었고, aggregate 뿐만 아니라 다른 middleware에 대한 작업 제안도 받아들여졌습니다.

- 이에 따라서 다른 middleware에 대해서도 작업을 추가적으로 진행하고 PR을 올리고자 합니다.
2. 해결하기
'use strict';
const middlewareFunctions = require('../../constants').queryMiddlewareFunctions;
const promiseOrCallback = require('../promiseOrCallback');
module.exports = function applyStaticHooks(model, hooks, statics) {
const kareemOptions = {
useErrorHandlers: true,
numCallbackParams: 1
};
hooks = hooks.filter(hook => {
// If the custom static overwrites an existing query middleware, don't apply
// middleware to it by default. This avoids a potential backwards breaking
// change with plugins like `mongoose-delete` that use statics to overwrite
// built-in Mongoose functions.
if (middlewareFunctions.indexOf(hook.name) !== -1) {
return !!hook.model;
}
return hook.model !== false;
});
model.$__insertMany = hooks.createWrapper('insertMany',
model.$__insertMany, model, kareemOptions);
for (const key of Object.keys(statics)) {
...
}
};
- 문제는 이전 글에서 언급한 것과 같이 hooks.filter하는 부분에 있어서 특정 middleware만 필터링하기 때문에 발생하는 문제입니다.
- 따라서 문제와 해결 방안의 맥락은 동일합니다. 대상만 바뀌었습니다.
- query middleware과 동일한 이름을 가진 static method와 제 작업을 통해서 추가된 aggreate middleware과 동일한 이름을 가진 static method 경우는 pre/post 훅이 적용되지 않습니다.
- 하지만 여전히 다른 middleware와 동일한 이름을 가진 static method의 경우는 필터링되지 않아서 pre/post 훅이 적용됩니다.
해결 방법
lib/constants.js
'use strict';
/*!
* ignore
*/
const queryOperations = Object.freeze([
// Read
'countDocuments',
'distinct',
'estimatedDocumentCount',
'find',
'findOne',
// Update
'findOneAndReplace',
'findOneAndUpdate',
'replaceOne',
'updateMany',
'updateOne',
// Delete
'deleteMany',
'deleteOne',
'findOneAndDelete'
]);
exports.queryOperations = queryOperations;
/*!
* ignore
*/
const queryMiddlewareFunctions = queryOperations.concat([
'validate'
]);
exports.queryMiddlewareFunctions = queryMiddlewareFunctions;
/*!
* ignore
*/
const aggregateMiddlewareFunctions = [
'aggregate'
];
exports.aggregateMiddlewareFunctions = aggregateMiddlewareFunctions;
/*!
* ignore
*/
const modelMiddlewareFunctions = [
'bulkWrite',
'createCollection',
'insertMany'
];
exports.modelMiddlewareFunctions = modelMiddlewareFunctions;
/*!
* ignore
*/
const documentMiddlewareFunctions = [
'validate',
'save',
'remove',
'updateOne',
'deleteOne',
'init'
];
exports.documentMiddlewareFunctions = documentMiddlewareFunctions;
- 다음과 같이 modelMiddlewareFunctions / documentMiddlewareFunctions 배열을 추가합니다.
lib/helpers/model/applyStaticHooks.js
'use strict';
const promiseOrCallback = require('../promiseOrCallback');
const { queryMiddlewareFunctions, aggregateMiddlewareFunctions, modelMiddlewareFunctions, documentMiddlewareFunctions } = require('../../constants');
const middlewareFunctions = Array.from(
new Set([
...queryMiddlewareFunctions,
...aggregateMiddlewareFunctions,
...modelMiddlewareFunctions,
...documentMiddlewareFunctions
])
);
module.exports = function applyStaticHooks(model, hooks, statics) {
const kareemOptions = {
useErrorHandlers: true,
numCallbackParams: 1
};
model.$__insertMany = hooks.createWrapper('insertMany',
model.$__insertMany, model, kareemOptions);
hooks = hooks.filter(hook => {
// If the custom static overwrites an existing middleware, don't apply
// middleware to it by default. This avoids a potential backwards breaking
// change with plugins like `mongoose-delete` that use statics to overwrite
// built-in Mongoose functions.
if (middlewareFunctions.indexOf(hook.name) !== -1) {
return !!hook.model;
}
return hook.model !== false;
});
for (const key of Object.keys(statics)) {
if (hooks.hasHooks(key)) {
const original = model[key];
model[key] = function() {
const numArgs = arguments.length;
const lastArg = numArgs > 0 ? arguments[numArgs - 1] : null;
const cb = typeof lastArg === 'function' ? lastArg : null;
const args = Array.prototype.slice.
call(arguments, 0, cb == null ? numArgs : numArgs - 1);
// Special case: can't use `Kareem#wrap()` because it doesn't currently
// support wrapped functions that return a promise.
return promiseOrCallback(cb, callback => {
hooks.execPre(key, model, args, function(err) {
if (err != null) {
return callback(err);
}
let postCalled = 0;
const ret = original.apply(model, args.concat(post));
if (ret != null && typeof ret.then === 'function') {
ret.then(res => post(null, res), err => post(err));
}
function post(error, res) {
if (postCalled++ > 0) {
return;
}
if (error != null) {
return callback(error);
}
hooks.execPost(key, model, [res], function(error) {
if (error != null) {
return callback(error);
}
callback(null, res);
});
}
});
}, model.events);
};
}
}
};
- 모든 middleware를 합친 middlewareFunctions 배열을 만듭니다.
- 이때 Set을 두는데요, 아래의 메서드들의 이름이 중복되기 떄문입니다.
- Document middleware의 updateOne / Query middleware의 updateOne
- Document middleware의 deleteOne / Query middleware의 deleteOne
- Document middleware의 validate / Query middleware의 validate
- 이제는 hooks.filter 로직에서 필터링되어 mongoose middleware과 동일한 이름을 가진 모든 static method이 pre/post 훅이 적용되지 않습니다.
- Set에 대한 부분은 코드리뷰를 받았습니다. 😊
5. 해결한 부분을 테스트하기
- 이제 모든 middleware에 대해서 잘 작동하는지에 대한 테스트를 작성하고 통과하는지 확인해봐야 합니다.
테스트
- 우선 기존 테스트를 돌리고, 모두 통과하는지 확인합니다.
$ npm run test
테스트 추가
it('custom statics that overwrite model functions dont get hooks by default', async function() {
const schema = new Schema({ name: String });
schema.statics.insertMany = function(docs) {
return model.insertMany.apply(this, [docs]);
};
let called = 0;
schema.pre('insertMany', function(next) {
++called;
next();
});
const Model = db.model('Test', schema);
const res = await Model.insertMany([
{ name: 'foo' },
{ name: 'boo' }
]);
assert.ok(res[0].name);
assert.ok(res[1].name);
assert.equal(called, 1);
});
it('custom statics that overwrite document functions dont get hooks by default', async function() {
const schema = new Schema({ name: String });
schema.statics.save = function() {
return 'foo';
};
let called = 0;
schema.pre('save', function(next) {
++called;
next();
});
const Model = db.model('Test', schema);
const doc = await Model.save();
assert.ok(doc);
assert.equal(doc, 'foo');
assert.equal(called, 0);
});
- 대표적으로 model middleware의 insertMany / document middleware의 save에 대한 테스트 코드를 작성하였습니다.
- 각각 동일한 이름을 가진 static method를 만들고, 해당 메서드에 pre 훅을 걸어두었습니다.
- 첫번째 케이스에 대해서는 static method가 실행하는 실제 메서드 호출로 인해서 한번 호출되어야합니다.
- 두번째 케이스에 대해서는 한번도 호출되어서는 안됩니다.
테스트 확인
- 똑같이 테스트를 돌리고, 모두 통과하는지 확인합니다.

- 테스트가 모두 통과하는 것을 확인하였습니다. 이제 PR만 작성하면 됩니다.
6. PR을 작성하기
- 작성한 코드에 대해 PR을 작성하였습니다.
- 메인테이너도 인지하고 있는 이슈라서, 이전 맥락에 대한 내용을 잘 적어두었습니다.
8. 기다리기
- 또 다시 메인테이너분들의 반응을 기다립니다! 이미 알고 있는 컨텍스트라서 빠르게 확인해 주실 것입니다.
9. 기여 완료!
- PR이 머지되었습니다. 꽤 빠르게 확인하고 머지해주셨네요!

10. 마치며
- 컨트리뷰션을 쪼개서 할 수 있다는 점이 참 좋았는데요, 도입이 필요한지 명확히 확인할 수 있었고 나중에 히스토리 파악 측면에서도 좋은 것 같습니다.
- 개인적으로 좋았던 점은 두개의 작업을 기여했다는 점입니다! 🚀