diff --git a/lib/helpers/omitUndefined.js b/lib/helpers/omitUndefined.js new file mode 100644 index 00000000000..5c9eb88564a --- /dev/null +++ b/lib/helpers/omitUndefined.js @@ -0,0 +1,20 @@ +'use strict'; + +module.exports = function omitUndefined(val) { + if (val == null || typeof val !== 'object') { + return val; + } + if (Array.isArray(val)) { + for (let i = val.length - 1; i >= 0; --i) { + if (val[i] === undefined) { + val.splice(i, 1); + } + } + } + for (const key of Object.keys(val)) { + if (val[key] === void 0) { + delete val[key]; + } + } + return val; +}; diff --git a/lib/helpers/query/cast$expr.js b/lib/helpers/query/cast$expr.js index a13190b1c41..9889d47ada1 100644 --- a/lib/helpers/query/cast$expr.js +++ b/lib/helpers/query/cast$expr.js @@ -3,6 +3,7 @@ const CastError = require('../../error/cast'); const StrictModeError = require('../../error/strict'); const castNumber = require('../../cast/number'); +const omitUndefined = require('../omitUndefined'); const booleanComparison = new Set(['$and', '$or']); const comparisonOperator = new Set(['$cmp', '$eq', '$lt', '$lte', '$gt', '$gte']); @@ -125,18 +126,11 @@ function _castExpression(val, schema, strictQuery) { val.$round = $round.map(v => castNumberOperator(v, schema, strictQuery)); } - _omitUndefined(val); + omitUndefined(val); return val; } -function _omitUndefined(val) { - const keys = Object.keys(val); - for (let i = 0, len = keys.length; i < len; ++i) { - (val[keys[i]] === void 0) && delete val[keys[i]]; - } -} - // { $op: } function castNumberOperator(val) { if (!isLiteral(val)) { diff --git a/lib/mongoose.js b/lib/mongoose.js index 49392bfd70f..89cb4348c2b 100644 --- a/lib/mongoose.js +++ b/lib/mongoose.js @@ -1281,6 +1281,28 @@ Mongoose.prototype.skipMiddlewareFunction = Kareem.skipWrappedFunction; Mongoose.prototype.overwriteMiddlewareResult = Kareem.overwriteResult; +/** + * Takes in an object and deletes any keys from the object whose values + * are strictly equal to `undefined`. + * This function is useful for query filters because Mongoose treats + * `TestModel.find({ name: undefined })` as `TestModel.find({ name: null })`. + * + * #### Example: + * + * const filter = { name: 'John', age: undefined, status: 'active' }; + * mongoose.omitUndefined(filter); // { name: 'John', status: 'active' } + * filter; // { name: 'John', status: 'active' } + * + * await UserModel.findOne(mongoose.omitUndefined(filter)); + * + * @method omitUndefined + * @param {Object} [val] the object to remove undefined keys from + * @returns {Object} the object passed in + * @api public + */ + +Mongoose.prototype.omitUndefined = require('./helpers/omitUndefined'); + /** * The exports object is an instance of Mongoose. * diff --git a/test/types/base.test.ts b/test/types/base.test.ts index fba2acf37b0..9d25cfe30f3 100644 --- a/test/types/base.test.ts +++ b/test/types/base.test.ts @@ -69,3 +69,5 @@ function setAsObject() { expectError(mongoose.set({ invalid: true })); } + +const x: { name: string } = mongoose.omitUndefined({ name: 'foo' }); diff --git a/types/index.d.ts b/types/index.d.ts index cd5695b35f2..6f851d23dfa 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -68,6 +68,8 @@ declare module 'mongoose' { /** Gets mongoose options */ export function get(key: K): MongooseOptions[K]; + export function omitUndefined>(val: T): T; + /* ! ignore */ export type CompileModelOptions = { overwriteModels?: boolean,