diff --git a/lib/aggregate.js b/lib/aggregate.js index 2426a256ca1..7178cce7526 100644 --- a/lib/aggregate.js +++ b/lib/aggregate.js @@ -111,11 +111,11 @@ Aggregate.prototype.model = function(model) { this._model = model; if (model.schema != null) { if (this.options.readPreference == null && - model.schema.options.read != null) { + model.schema.options.read != null) { this.options.readPreference = model.schema.options.read; } if (this.options.collation == null && - model.schema.options.collation != null) { + model.schema.options.collation != null) { this.options.collation = model.schema.options.collation; } } @@ -158,7 +158,7 @@ Aggregate.prototype.append = function() { * Requires MongoDB v3.4+ to work * * #### Example: - * + * * // adding new fields based on existing fields * aggregate.addFields({ * newField: '$b.nested' @@ -328,6 +328,28 @@ Aggregate.prototype.project = function(arg) { * @api public */ +/** + * Appends a new $fill operator to this aggregate pipeline. + * + * #### Example: + * + * aggregate.fill({ + * output: { + * bootsSold: { value: 0 }, + * sandalsSold: { value: 0 }, + * sneakersSold: { value: 0 } + * } + * }); + * + * @see $fill https://www.mongodb.com/docs/manual/reference/operator/aggregation/fill/ + * @method fill + * @memberOf Aggregate + * @instance + * @param {Object} arg $fill operator contents + * @return {Aggregate} + * @api public + */ + /** * Appends a new $geoNear operator to this aggregate pipeline. * @@ -366,7 +388,7 @@ Aggregate.prototype.near = function(arg) { * define methods */ -'group match skip limit out densify'.split(' ').forEach(function($operator) { +'group match skip limit out densify fill'.split(' ').forEach(function($operator) { Aggregate.prototype[$operator] = function(arg) { const op = {}; op['$' + $operator] = arg; @@ -702,7 +724,7 @@ Aggregate.prototype.readConcern = function(level) { Aggregate.prototype.redact = function(expression, thenExpr, elseExpr) { if (arguments.length === 3) { if ((typeof thenExpr === 'string' && !validRedactStringValues.has(thenExpr)) || - (typeof elseExpr === 'string' && !validRedactStringValues.has(elseExpr))) { + (typeof elseExpr === 'string' && !validRedactStringValues.has(elseExpr))) { throw new Error('If thenExpr or elseExpr is string, it must be either $$DESCEND, $$PRUNE or $$KEEP'); } @@ -1099,9 +1121,7 @@ Aggregate.prototype.catch = function(reject) { if (Symbol.asyncIterator != null) { Aggregate.prototype[Symbol.asyncIterator] = function() { - return this.cursor({ useMongooseAggCursor: true }). - transformNull(). - _transformForAsyncIterator(); + return this.cursor({ useMongooseAggCursor: true }).transformNull()._transformForAsyncIterator(); }; } diff --git a/lib/document.js b/lib/document.js index 5b9d3fcc5c7..824ac6cf96f 100644 --- a/lib/document.js +++ b/lib/document.js @@ -948,6 +948,48 @@ Document.prototype.$session = function $session(session) { return session; }; +/** + * Getter/setter around whether this document will apply timestamps by + * default when using `save()` and `bulkSave()`. + * + * #### Example: + * + * const TestModel = mongoose.model('Test', new Schema({ name: String }, { timestamps: true })); + * const doc = new TestModel({ name: 'John Smith' }); + * + * doc.$timestamps(); // true + * + * doc.$timestamps(false); + * await doc.save(); // Does **not** apply timestamps + * + * @param {Boolean} [value] overwrite the current session + * @return {Document|boolean|undefined} When used as a getter (no argument), a boolean will be returned indicating the timestamps option state or if unset "undefined" will be used, otherwise will return "this" + * @method $timestamps + * @api public + * @memberOf Document + */ + +Document.prototype.$timestamps = function $timestamps(value) { + if (arguments.length === 0) { + if (this.$__.timestamps != null) { + return this.$__.timestamps; + } + + if (this.$__schema) { + return this.$__schema.options.timestamps; + } + + return undefined; + } + + const currentValue = this.$timestamps(); + if (value !== currentValue) { + this.$__.timestamps = value; + } + + return this; +}; + /** * Overwrite all values in this document with the values of `obj`, except * for immutable properties. Behaves similarly to `set()`, except for it diff --git a/lib/error/setOptionError.js b/lib/error/setOptionError.js new file mode 100644 index 00000000000..b38a0d30244 --- /dev/null +++ b/lib/error/setOptionError.js @@ -0,0 +1,101 @@ +/*! + * Module requirements + */ + +'use strict'; + +const MongooseError = require('./mongooseError'); +const util = require('util'); +const combinePathErrors = require('../helpers/error/combinePathErrors'); + +class SetOptionError extends MongooseError { + /** + * Mongoose.set Error + * + * @api private + * @inherits MongooseError + */ + constructor() { + super(''); + + this.errors = {}; + } + + /** + * Console.log helper + */ + toString() { + return combinePathErrors(this); + } + + /** + * inspect helper + * @api private + */ + inspect() { + return Object.assign(new Error(this.message), this); + } + + /** + * add message + * @param {String} key + * @param {String|Error} error + * @api private + */ + addError(key, error) { + if (error instanceof SetOptionError) { + const { errors } = error; + for (const optionKey of Object.keys(errors)) { + this.addError(optionKey, errors[optionKey]); + } + + return; + } + + this.errors[key] = error; + this.message = combinePathErrors(this); + } +} + + +if (util.inspect.custom) { + // Avoid Node deprecation warning DEP0079 + SetOptionError.prototype[util.inspect.custom] = SetOptionError.prototype.inspect; +} + +/** + * Helper for JSON.stringify + * Ensure `name` and `message` show up in toJSON output re: gh-9847 + * @api private + */ +Object.defineProperty(SetOptionError.prototype, 'toJSON', { + enumerable: false, + writable: false, + configurable: true, + value: function() { + return Object.assign({}, this, { name: this.name, message: this.message }); + } +}); + + +Object.defineProperty(SetOptionError.prototype, 'name', { + value: 'SetOptionError' +}); + +class SetOptionInnerError extends MongooseError { + /** + * Error for the "errors" array in "SetOptionError" with consistent message + * @param {String} key + */ + constructor(key) { + super(`"${key}" is not a valid option to set`); + } +} + +SetOptionError.SetOptionInnerError = SetOptionInnerError; + +/*! + * Module exports + */ + +module.exports = SetOptionError; diff --git a/lib/error/validation.js b/lib/error/validation.js index 1c3e8639a83..5e222e980f9 100644 --- a/lib/error/validation.js +++ b/lib/error/validation.js @@ -7,6 +7,7 @@ const MongooseError = require('./mongooseError'); const getConstructorName = require('../helpers/getConstructorName'); const util = require('util'); +const combinePathErrors = require('../helpers/error/combinePathErrors'); class ValidationError extends MongooseError { /** @@ -38,7 +39,7 @@ class ValidationError extends MongooseError { * Console.log helper */ toString() { - return this.name + ': ' + _generateMessage(this); + return this.name + ': ' + combinePathErrors(this); } /** @@ -66,7 +67,7 @@ class ValidationError extends MongooseError { } this.errors[path] = error; - this.message = this._message + ': ' + _generateMessage(this); + this.message = this._message + ': ' + combinePathErrors(this); } } @@ -95,27 +96,6 @@ Object.defineProperty(ValidationError.prototype, 'name', { value: 'ValidationError' }); -/*! - * ignore - */ - -function _generateMessage(err) { - const keys = Object.keys(err.errors || {}); - const len = keys.length; - const msgs = []; - let key; - - for (let i = 0; i < len; ++i) { - key = keys[i]; - if (err === err.errors[key]) { - continue; - } - msgs.push(key + ': ' + err.errors[key].message); - } - - return msgs.join(', '); -} - /*! * Module exports */ diff --git a/lib/helpers/error/combinePathErrors.js b/lib/helpers/error/combinePathErrors.js new file mode 100644 index 00000000000..841dbc0aa6e --- /dev/null +++ b/lib/helpers/error/combinePathErrors.js @@ -0,0 +1,22 @@ +'use strict'; + +/*! + * ignore + */ + +module.exports = function combinePathErrors(err) { + const keys = Object.keys(err.errors || {}); + const len = keys.length; + const msgs = []; + let key; + + for (let i = 0; i < len; ++i) { + key = keys[i]; + if (err === err.errors[key]) { + continue; + } + msgs.push(key + ': ' + err.errors[key].message); + } + + return msgs.join(', '); +}; diff --git a/lib/helpers/model/discriminator.js b/lib/helpers/model/discriminator.js index a0abc4d5672..a178093a6dc 100644 --- a/lib/helpers/model/discriminator.js +++ b/lib/helpers/model/discriminator.js @@ -19,11 +19,13 @@ const CUSTOMIZABLE_DISCRIMINATOR_OPTIONS = { * ignore */ -module.exports = function discriminator(model, name, schema, tiedValue, applyPlugins) { +module.exports = function discriminator(model, name, schema, tiedValue, applyPlugins, mergeHooks) { if (!(schema && schema.instanceOfSchema)) { throw new Error('You must pass a valid discriminator Schema'); } + mergeHooks = mergeHooks == null ? true : mergeHooks; + if (model.schema.discriminatorMapping && !model.schema.discriminatorMapping.isRoot) { throw new Error('Discriminator "' + name + @@ -32,7 +34,7 @@ module.exports = function discriminator(model, name, schema, tiedValue, applyPlu if (applyPlugins) { const applyPluginsToDiscriminators = get(model.base, - 'options.applyPluginsToDiscriminators', false); + 'options.applyPluginsToDiscriminators', false) || !mergeHooks; // Even if `applyPluginsToDiscriminators` isn't set, we should still apply // global plugins to schemas embedded in the discriminator schema (gh-7370) model.base._applyPlugins(schema, { @@ -179,7 +181,9 @@ module.exports = function discriminator(model, name, schema, tiedValue, applyPlu schema.options._id = _id; } schema.options.id = id; - schema.s.hooks = model.schema.s.hooks.merge(schema.s.hooks); + if (mergeHooks) { + schema.s.hooks = model.schema.s.hooks.merge(schema.s.hooks); + } schema.plugins = Array.prototype.slice.call(baseSchema.plugins); schema.callQueue = baseSchema.callQueue.concat(schema.callQueue); diff --git a/lib/index.js b/lib/index.js index 4bc62f65619..29aec767819 100644 --- a/lib/index.js +++ b/lib/index.js @@ -37,6 +37,7 @@ const trusted = require('./helpers/query/trusted').trusted; const sanitizeFilter = require('./helpers/query/sanitizeFilter'); const isBsonType = require('./helpers/isBsonType'); const MongooseError = require('./error/mongooseError'); +const SetOptionError = require('./error/setOptionError'); const defaultMongooseSymbol = Symbol.for('mongoose:default'); @@ -182,6 +183,9 @@ Mongoose.prototype.setDriver = function setDriver(driver) { /** * Sets mongoose options * + * `key` can be used a object to set multiple options at once. + * If a error gets thrown for one option, other options will still be evaluated. + * * #### Example: * * mongoose.set('test', value) // sets the 'test' option to `value` @@ -190,6 +194,8 @@ Mongoose.prototype.setDriver = function setDriver(driver) { * * mongoose.set('debug', function(collectionName, methodName, ...methodArgs) {}); // use custom function to log collection methods + arguments * + * mongoose.set({ debug: true, autoIndex: false }); // set multiple options at once + * * Currently supported options are: * - 'applyPluginsToChildSchemas': `true` by default. Set to false to skip applying global plugins to child schemas * - 'applyPluginsToDiscriminators': `false` by default. Set to true to apply global plugins to discriminator schemas. This typically isn't necessary because plugins are applied to the base schema and discriminators copy all middleware, methods, statics, and properties from the base schema. @@ -213,36 +219,66 @@ Mongoose.prototype.setDriver = function setDriver(driver) { * - 'toJSON': `{ transform: true, flattenDecimals: true }` by default. Overwrites default objects to [`toJSON()`](/docs/api.html#document_Document-toJSON), for determining how Mongoose documents get serialized by `JSON.stringify()` * - 'toObject': `{ transform: true, flattenDecimals: true }` by default. Overwrites default objects to [`toObject()`](/docs/api.html#document_Document-toObject) * - * @param {String} key - * @param {String|Function|Boolean} value + * @param {String|Object} key The name of the option or a object of multiple key-value pairs + * @param {String|Function|Boolean} value The value of the option, unused if "key" is a object + * @returns {Mongoose} The used Mongoose instnace * @api public */ Mongoose.prototype.set = function(key, value) { const _mongoose = this instanceof Mongoose ? this : mongoose; - if (VALID_OPTIONS.indexOf(key) === -1) { - throw new Error(`\`${key}\` is an invalid option.`); - } + if (arguments.length === 1 && typeof key !== 'object') { + if (VALID_OPTIONS.indexOf(key) === -1) { + const error = new SetOptionError(); + error.addError(key, new SetOptionError.SetOptionInnerError(key)); + throw error; + } - if (arguments.length === 1) { return _mongoose.options[key]; } - _mongoose.options[key] = value; + let options = {}; - if (key === 'objectIdGetter') { - if (value) { - Object.defineProperty(mongoose.Types.ObjectId.prototype, '_id', { - enumerable: false, - configurable: true, - get: function() { - return this; - } - }); - } else { - delete mongoose.Types.ObjectId.prototype._id; + if (arguments.length === 2) { + options = { [key]: value }; + } + + if (arguments.length === 1 && typeof key === 'object') { + options = key; + } + + // array for errors to collect all errors for all key-value pairs, like ".validate" + let error = undefined; + + for (const [optionKey, optionValue] of Object.entries(options)) { + if (VALID_OPTIONS.indexOf(optionKey) === -1) { + if (!error) { + error = new SetOptionError(); + } + error.addError(optionKey, new SetOptionError.SetOptionInnerError(optionKey)); + continue; } + + _mongoose.options[optionKey] = optionValue; + + if (optionKey === 'objectIdGetter') { + if (optionValue) { + Object.defineProperty(mongoose.Types.ObjectId.prototype, '_id', { + enumerable: false, + configurable: true, + get: function() { + return this; + } + }); + } else { + delete mongoose.Types.ObjectId.prototype._id; + } + } + } + + if (error) { + throw error; } return _mongoose; diff --git a/lib/model.js b/lib/model.js index 9c3bb658b04..8fd7bdfbdd2 100644 --- a/lib/model.js +++ b/lib/model.js @@ -511,6 +511,9 @@ Model.prototype.save = function(options, fn) { if (options.hasOwnProperty('session')) { this.$session(options.session); } + if (this.$__.timestamps != null) { + options.timestamps = this.$__.timestamps; + } this.$__.$versionError = generateVersionError(this, this.modifiedPaths()); fn = this.constructor.$handleCallbackError(fn); @@ -1211,6 +1214,7 @@ Model.exists = function exists(filter, options, callback) { * @param {String} [options.value] the string stored in the `discriminatorKey` property. If not specified, Mongoose uses the `name` parameter. * @param {Boolean} [options.clone=true] By default, `discriminator()` clones the given `schema`. Set to `false` to skip cloning. * @param {Boolean} [options.overwriteModels=false] by default, Mongoose does not allow you to define a discriminator with the same name as another discriminator. Set this to allow overwriting discriminators with the same name. + * @param {Boolean} [options.mergeHooks=true] By default, Mongoose merges the base schema's hooks with the discriminator schema's hooks. Set this option to `false` to make Mongoose use the discriminator schema's hooks instead. * @return {Model} The newly created discriminator model * @api public */ @@ -1238,7 +1242,7 @@ Model.discriminator = function(name, schema, options) { schema = schema.clone(); } - schema = discriminator(this, name, schema, value, true); + schema = discriminator(this, name, schema, value, true, options.mergeHooks); if (this.db.models[name] && !schema.options.overwriteModels) { throw new OverwriteModelError(name); } @@ -3455,7 +3459,8 @@ Model.$__insertMany = function(arr, options, callback) { if (doc.$__schema.options.versionKey) { doc[doc.$__schema.options.versionKey] = 0; } - if ((!options || options.timestamps !== false) && doc.initializeTimestamps) { + const shouldSetTimestamps = (!options || options.timestamps !== false) && doc.initializeTimestamps && (!doc.$__ || doc.$__.timestamps !== false); + if (shouldSetTimestamps) { return doc.initializeTimestamps().toObject(internalToObjectOptions); } return doc.toObject(internalToObjectOptions); @@ -3696,6 +3701,13 @@ Model.bulkSave = async function(documents, options) { document.$__.saveOptions = document.$__.saveOptions || {}; document.$__.saveOptions.timestamps = options.timestamps; } + } else { + for (const document of documents) { + if (document.$__.timestamps != null) { + document.$__.saveOptions = document.$__.saveOptions || {}; + document.$__.saveOptions.timestamps = document.$__.timestamps; + } + } } await Promise.all(documents.map(buildPreSavePromise)); diff --git a/lib/schema.js b/lib/schema.js index 8185831f9b0..ba8435e0e66 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -170,17 +170,49 @@ function Schema(obj, options) { * @api private */ function aliasFields(schema, paths) { - paths = paths || Object.keys(schema.paths); - for (const path of paths) { - const options = get(schema.paths[path], 'options'); - if (options == null) { + for (const path of Object.keys(paths)) { + let alias = null; + if (paths[path] != null) { + alias = paths[path]; + } else { + const options = get(schema.paths[path], 'options'); + if (options == null) { + continue; + } + + alias = options.alias; + } + + if (!alias) { continue; } const prop = schema.paths[path].path; - const alias = options.alias; + if (Array.isArray(alias)) { + for (const a of alias) { + if (typeof a !== 'string') { + throw new Error('Invalid value for alias option on ' + prop + ', got ' + a); + } + + schema.aliases[a] = prop; + + schema. + virtual(a). + get((function(p) { + return function() { + if (typeof this.get === 'function') { + return this.get(p); + } + return this[p]; + }; + })(prop)). + set((function(p) { + return function(v) { + return this.$set(p, v); + }; + })(prop)); + } - if (!alias) { continue; } @@ -659,9 +691,44 @@ Schema.prototype.add = function add(obj, prefix) { } } - const addedKeys = Object.keys(obj). - map(key => prefix ? prefix + key : key); - aliasFields(this, addedKeys); + const aliasObj = Object.fromEntries( + Object.entries(obj).map(([key]) => ([prefix + key, null])) + ); + aliasFields(this, aliasObj); + return this; +}; + +/** + * Add an alias for `path`. This means getting or setting the `alias` + * is equivalent to getting or setting the `path`. + * + * #### Example: + * + * const toySchema = new Schema({ n: String }); + * + * // Make 'name' an alias for 'n' + * toySchema.alias('n', 'name'); + * + * const Toy = mongoose.model('Toy', toySchema); + * const turboMan = new Toy({ n: 'Turbo Man' }); + * + * turboMan.name; // 'Turbo Man' + * turboMan.n; // 'Turbo Man' + * + * turboMan.name = 'Turbo Man Action Figure'; + * turboMan.n; // 'Turbo Man Action Figure' + * + * await turboMan.save(); // Saves { _id: ..., n: 'Turbo Man Action Figure' } + * + * + * @param {String} path real path to alias + * @param {String|String[]} alias the path(s) to use as an alias for `path` + * @return {Schema} the Schema instance + * @api public + */ + +Schema.prototype.alias = function alias(path, alias) { + aliasFields(this, { [path]: alias }); return this; }; @@ -1106,6 +1173,7 @@ Schema.prototype.interpretAsType = function(path, obj, options) { // If this schema has an associated Mongoose object, use the Mongoose object's // copy of SchemaTypes re: gh-7158 gh-6933 const MongooseTypes = this.base != null ? this.base.Schema.Types : Schema.Types; + const Types = this.base != null ? this.base.Types : require('./types'); if (!utils.isPOJO(obj) && !(obj instanceof SchemaTypeOptions)) { const constructorName = utils.getFunctionName(obj.constructor); @@ -1240,6 +1308,10 @@ Schema.prototype.interpretAsType = function(path, obj, options) { name = 'Buffer'; } else if (typeof type === 'function' || typeof type === 'object') { name = type.schemaName || utils.getFunctionName(type); + } else if (type === Types.ObjectId) { + name = 'ObjectId'; + } else if (type === Types.Decimal128) { + name = 'Decimal128'; } else { name = type == null ? '' + type : type.toString(); } @@ -1717,6 +1789,7 @@ Schema.prototype.post = function(name) { * * @param {Function} plugin The Plugin's callback * @param {Object} [opts] Options to pass to the plugin + * @param {Boolean} [opts.deduplicate=false] If true, ignore duplicate plugins (same `fn` argument using `===`) * @see plugins /docs/plugins.html * @api public */ diff --git a/lib/schema/index.js b/lib/schema/index.js index 999bef7c693..f3eb9851ea6 100644 --- a/lib/schema/index.js +++ b/lib/schema/index.js @@ -29,6 +29,8 @@ exports.Decimal128 = exports.Decimal = require('./decimal128'); exports.Map = require('./map'); +exports.UUID = require('./uuid'); + // alias exports.Oid = exports.ObjectId; diff --git a/lib/schema/uuid.js b/lib/schema/uuid.js new file mode 100644 index 00000000000..b621fa942d8 --- /dev/null +++ b/lib/schema/uuid.js @@ -0,0 +1,329 @@ +/*! + * Module dependencies. + */ + +'use strict'; + +const MongooseBuffer = require('../types/buffer'); +const SchemaType = require('../schematype'); +const CastError = SchemaType.CastError; +const utils = require('../utils'); +const isBsonType = require('../helpers/isBsonType'); +const handleBitwiseOperator = require('./operators/bitwise'); + +const UUID_FORMAT = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/i; +const Binary = MongooseBuffer.Binary; + +/** + * Helper function to convert the input hex-string to a buffer + * @param {String} hex The hex string to convert + * @returns {Buffer} The hex as buffer + * @api private + */ + +function hex2buffer(hex) { + // use buffer built-in function to convert from hex-string to buffer + const buff = Buffer.from(hex, 'hex'); + return buff; +} + +/** + * Helper function to convert the buffer input to a string + * @param {Buffer} buf The buffer to convert to a hex-string + * @returns {String} The buffer as a hex-string + * @api private + */ + +function binary2hex(buf) { + // use buffer built-in function to convert from buffer to hex-string + const hex = buf.toString('hex'); + return hex; +} + +/** + * Convert a String to Binary + * @param {String} uuidStr The value to process + * @returns {MongooseBuffer} The binary to store + * @api private + */ + +function stringToBinary(uuidStr) { + // Protect against undefined & throwing err + if (typeof uuidStr !== 'string') uuidStr = ''; + const hex = uuidStr.replace(/[{}-]/g, ''); // remove extra characters + const bytes = hex2buffer(hex); + const buff = new MongooseBuffer(bytes); + buff._subtype = 4; + + return buff; +} + +/** + * Convert binary to a uuid string + * @param {Buffer|Binary|String} uuidBin The value to process + * @returns {String} The completed uuid-string + * @api private + */ +function binaryToString(uuidBin) { + // i(hasezoey) dont quite know why, but "uuidBin" may sometimes also be the already processed string + let hex; + if (typeof uuidBin !== 'string') { + hex = binary2hex(uuidBin); + const uuidStr = hex.substring(0, 8) + '-' + hex.substring(8, 8 + 4) + '-' + hex.substring(12, 12 + 4) + '-' + hex.substring(16, 16 + 4) + '-' + hex.substring(20, 20 + 12); + return uuidStr; + } + return uuidBin; +} + +/** + * UUIDv1 SchemaType constructor. + * + * @param {String} key + * @param {Object} options + * @inherits SchemaType + * @api public + */ + +function SchemaUUID(key, options) { + SchemaType.call(this, key, options, 'UUID'); + this.getters.push(binaryToString); +} + +/** + * This schema type's name, to defend against minifiers that mangle + * function names. + * + * @api public + */ +SchemaUUID.schemaName = 'UUID'; + +SchemaUUID.defaultOptions = {}; + +/*! + * Inherits from SchemaType. + */ +SchemaUUID.prototype = Object.create(SchemaType.prototype); +SchemaUUID.prototype.constructor = SchemaUUID; + +/*! + * ignore + */ + +SchemaUUID._cast = function(value) { + if (value === null) { + return value; + } + + function newBuffer(initbuff) { + const buff = new MongooseBuffer(initbuff); + buff._subtype = 4; + return buff; + } + + if (typeof value === 'string') { + if (UUID_FORMAT.test(value)) { + return stringToBinary(value); + } else { + throw new CastError(SchemaUUID.schemaName, value, this.path); + } + } + + if (Buffer.isBuffer(value)) { + return newBuffer(value); + } + + if (value instanceof Binary) { + return newBuffer(value.value(true)); + } + + // Re: gh-647 and gh-3030, we're ok with casting using `toString()` + // **unless** its the default Object.toString, because "[object Object]" + // doesn't really qualify as useful data + if (value.toString && value.toString !== Object.prototype.toString) { + if (UUID_FORMAT.test(value.toString())) { + return stringToBinary(value.toString()); + } + } + + throw new CastError(SchemaUUID.schemaName, value, this.path); +}; + +/** + * Sets a default option for all UUID instances. + * + * #### Example: + * + * // Make all UUIDs have `required` of true by default. + * mongoose.Schema.UUID.set('required', true); + * + * const User = mongoose.model('User', new Schema({ test: mongoose.UUID })); + * new User({ }).validateSync().errors.test.message; // Path `test` is required. + * + * @param {String} option The option you'd like to set the value for + * @param {Any} value value for option + * @return {undefined} + * @function set + * @static + * @api public + */ + +SchemaUUID.set = SchemaType.set; + +/** + * Get/set the function used to cast arbitrary values to UUIDs. + * + * #### Example: + * + * // Make Mongoose refuse to cast UUIDs with 0 length + * const original = mongoose.Schema.Types.UUID.cast(); + * mongoose.UUID.cast(v => { + * assert.ok(typeof v === "string" && v.length > 0); + * return original(v); + * }); + * + * // Or disable casting entirely + * mongoose.UUID.cast(false); + * + * @param {Function} [caster] + * @return {Function} + * @function get + * @static + * @api public + */ + +SchemaUUID.cast = function cast(caster) { + if (arguments.length === 0) { + return this._cast; + } + if (caster === false) { + caster = this._defaultCaster; + } + this._cast = caster; + + return this._cast; +}; + +/*! + * ignore + */ + +SchemaUUID._checkRequired = v => v != null; + +/** + * Override the function the required validator uses to check whether a string + * passes the `required` check. + * + * @param {Function} fn + * @return {Function} + * @function checkRequired + * @static + * @api public + */ + +SchemaUUID.checkRequired = SchemaType.checkRequired; + +/** + * Check if the given value satisfies a required validator. + * + * @param {Any} value + * @return {Boolean} + * @api public + */ + +SchemaUUID.prototype.checkRequired = function checkRequired(value) { + return UUID_FORMAT.test(value); +}; + +/** + * Casts to UUID + * + * @param {Object} value + * @param {Object} doc + * @param {Boolean} init whether this is an initialization cast + * @api private + */ + +SchemaUUID.prototype.cast = function(value, doc, init) { + if (SchemaType._isRef(this, value, doc, init)) { + if (isBsonType(value, 'UUID')) { + return value; + } + + return this._castRef(value, doc, init); + } + + let castFn; + if (typeof this._castFunction === 'function') { + castFn = this._castFunction; + } else if (typeof this.constructor.cast === 'function') { + castFn = this.constructor.cast(); + } else { + castFn = SchemaUUID.cast(); + } + + try { + return castFn(value); + } catch (error) { + throw new CastError(SchemaUUID.schemaName, value, this.path, error, this); + } +}; + +/*! + * ignore + */ + +function handleSingle(val) { + return this.cast(val); +} + +/*! + * ignore + */ + +function handleArray(val) { + return val.map((m) => { + return this.cast(m); + }); +} + +SchemaUUID.prototype.$conditionalHandlers = +utils.options(SchemaType.prototype.$conditionalHandlers, { + $bitsAllClear: handleBitwiseOperator, + $bitsAnyClear: handleBitwiseOperator, + $bitsAllSet: handleBitwiseOperator, + $bitsAnySet: handleBitwiseOperator, + $all: handleArray, + $gt: handleSingle, + $gte: handleSingle, + $in: handleArray, + $lt: handleSingle, + $lte: handleSingle, + $ne: handleSingle, + $nin: handleArray +}); + +/** + * Casts contents for queries. + * + * @param {String} $conditional + * @param {any} val + * @api private + */ + +SchemaUUID.prototype.castForQuery = function($conditional, val) { + let handler; + if (arguments.length === 2) { + handler = this.$conditionalHandlers[$conditional]; + if (!handler) + throw new Error('Can\'t use ' + $conditional + ' with UUID.'); + return handler.call(this, val); + } else { + return this.cast($conditional); + } +}; + +/*! + * Module exports. + */ + +module.exports = SchemaUUID; diff --git a/package.json b/package.json index cd9531f5c50..6665453cf44 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "dependencies": { "bson": "^4.6.5", "kareem": "2.4.1", - "mongodb": "4.9.1", + "mongodb": "4.11.0", "mpath": "0.9.0", "mquery": "4.0.3", "ms": "2.1.3", diff --git a/test/aggregate.test.js b/test/aggregate.test.js index 168db7a7e34..91acbf8716e 100644 --- a/test/aggregate.test.js +++ b/test/aggregate.test.js @@ -389,6 +389,25 @@ describe('aggregate: ', function() { }); }); + describe('fill', function() { + it('works', function() { + const aggregate = new Aggregate(); + const obj = { + output: + { + bootsSold: { value: 0 }, + sandalsSold: { value: 0 }, + sneakersSold: { value: 0 } + } + }; + + aggregate.fill(obj); + + assert.equal(aggregate._pipeline.length, 1); + assert.deepEqual(aggregate._pipeline[0].$fill, obj); + }); + }); + describe('model()', function() { it('works', function() { const aggregate = new Aggregate(); diff --git a/test/index.test.js b/test/index.test.js index 49ae3c65242..723bc817234 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -11,6 +11,7 @@ const { EventEmitter } = require('events'); const collection = 'blogposts_' + random(); +const SetOptionError = require('../lib/error/setOptionError'); const mongoose = start.mongoose; const Mongoose = mongoose.Mongoose; const Schema = mongoose.Schema; @@ -500,16 +501,13 @@ describe('mongoose module:', function() { }); it('throws an error on setting invalid options (gh-6899)', function() { - let threw = false; try { mongoose.set('someInvalidOption', true); + assert.fail('Expected mongoose.set to throw'); } catch (err) { - assert.equal(err.message, '`someInvalidOption` is an invalid option.'); - threw = true; - } - finally { - assert.equal(threw, true); + assert.ok(err instanceof SetOptionError); + assert.equal(err.message, 'someInvalidOption: "someInvalidOption" is not a valid option to set'); } }); @@ -1101,4 +1099,85 @@ describe('mongoose module:', function() { assert.equal(entry.id, undefined); }); }); + + describe('set()', function() { + let m; + + beforeEach(() => { + m = new mongoose.Mongoose(); + }); + + it('should be able to set a option through set with (key, value)', function() { + // also test the getter behavior of the function + assert.strictEqual(m.options['debug'], undefined); + assert.strictEqual(m.set('debug'), undefined); + m.set('debug', true); + + assert.strictEqual(m.options['debug'], true); + assert.strictEqual(m.set('debug'), true); + }); + + it('should be able to set a option through a object with {key: value}', function() { + assert.strictEqual(m.options['debug'], undefined); + m.set({ debug: true }); + + assert.strictEqual(m.options['debug'], true); + }); + + it('should throw a single error when using a invalid key', function() { + try { + m.set('invalid', true); + assert.fail('Expected .set to throw'); + } catch (err) { + assert.ok(err instanceof SetOptionError); + assert.strictEqual(Object.keys(err.errors).length, 1); + assert.strictEqual(err.message, 'invalid: "invalid" is not a valid option to set'); + } + }); + + it('should throw a error with many errors when using multiple invalid keys', function() { + try { + m.set({ + invalid1: true, + invalid2: true + }); + assert.fail('Expected .set to throw'); + } catch (err) { + assert.ok(err instanceof SetOptionError); + assert.strictEqual(Object.keys(err.errors).length, 2); + assert.strictEqual(err.message, 'invalid1: "invalid1" is not a valid option to set, invalid2: "invalid2" is not a valid option to set'); + assert.ok(err.errors['invalid1'] instanceof SetOptionError.SetOptionInnerError); + assert.strictEqual(err.errors['invalid1'].message, '"invalid1" is not a valid option to set'); + assert.ok(err.errors['invalid2'] instanceof SetOptionError.SetOptionInnerError); + assert.strictEqual(err.errors['invalid2'].message, '"invalid2" is not a valid option to set'); + } + }); + + it('should apply all values, even if there are errors', function() { + assert.strictEqual(m.options['debug'], undefined); + try { + m.set({ + invalid: true, + debug: true + }); + assert.fail('Expected .set to throw'); + } catch (err) { + assert.ok(err instanceof SetOptionError); + assert.ok(err.errors['invalid'] instanceof SetOptionError.SetOptionInnerError); + assert.strictEqual(err.message, 'invalid: "invalid" is not a valid option to set'); + assert.strictEqual(m.options['debug'], true); + } + }); + + it('should throw a single error when using a invalid key when getting', function() { + try { + m.set('invalid'); + assert.fail('Expected .set to throw'); + } catch (err) { + assert.ok(err instanceof SetOptionError); + assert.ok(err.errors['invalid'] instanceof SetOptionError.SetOptionInnerError); + assert.strictEqual(err.message, 'invalid: "invalid" is not a valid option to set'); + } + }); + }); }); diff --git a/test/model.discriminator.test.js b/test/model.discriminator.test.js index fa9826f809f..919101de4bf 100644 --- a/test/model.discriminator.test.js +++ b/test/model.discriminator.test.js @@ -2011,4 +2011,32 @@ describe('model', function() { assert.strictEqual(doc.shape.get('a').radius, '5'); assert.strictEqual(doc.shape.get('b').side, 10); }); + + it('supports `mergeHooks` option to use the discriminator schema\'s hooks over the base schema\'s (gh-12472)', function() { + const shapeSchema = Schema({ name: String }, { discriminatorKey: 'kind' }); + shapeSchema.plugin(myPlugin); + + const Shape = db.model('Test', shapeSchema); + + const triangleSchema = Schema({ sides: { type: Number, enum: [3] } }); + triangleSchema.plugin(myPlugin); + const Triangle = Shape.discriminator( + 'Triangle', + triangleSchema + ); + const squareSchema = Schema({ sides: { type: Number, enum: [4] } }); + squareSchema.plugin(myPlugin); + const Square = Shape.discriminator( + 'Square', + squareSchema, + { mergeHooks: false } + ); + + assert.equal(Triangle.schema.s.hooks._pres.get('save').filter(hook => hook.fn.name === 'testHook12472').length, 2); + assert.equal(Square.schema.s.hooks._pres.get('save').filter(hook => hook.fn.name === 'testHook12472').length, 1); + + function myPlugin(schema) { + schema.pre('save', function testHook12472() {}); + } + }); }); diff --git a/test/model.test.js b/test/model.test.js index 36de9589f18..3c6585db5ce 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -4388,6 +4388,23 @@ describe('Model', function() { }); }); + it('timestamps respect $timestamps() (gh-12117)', async function() { + const schema = new Schema({ name: String }, { timestamps: true }); + const Movie = db.model('Movie', schema); + const start = Date.now(); + + const arr = [ + new Movie({ name: 'Star Wars' }), + new Movie({ name: 'The Empire Strikes Back' }) + ]; + arr[1].$timestamps(false); + + await Movie.insertMany(arr); + const docs = await Movie.find().sort({ name: 1 }); + assert.ok(docs[0].createdAt.valueOf() >= start); + assert.ok(!docs[1].createdAt); + }); + it('insertMany() with nested timestamps (gh-12060)', async function() { const childSchema = new Schema({ name: { type: String } }, { _id: false, @@ -8441,6 +8458,7 @@ describe('Model', function() { assert.ok(userToUpdate.createdAt); assert.ok(userToUpdate.updatedAt); }); + it('`timestamps` has `undefined` as default value (gh-12059)', async() => { // Arrange const userSchema = new Schema({ @@ -8462,6 +8480,27 @@ describe('Model', function() { assert.ok(userToUpdate.createdAt); assert.ok(userToUpdate.updatedAt); }); + + it('respects `$timestamps()` (gh-12117)', async function() { + // Arrange + const userSchema = new Schema({ name: String }, { timestamps: true }); + + const User = db.model('User', userSchema); + + const newUser1 = new User({ name: 'John' }); + const newUser2 = new User({ name: 'Bill' }); + + newUser2.$timestamps(false); + + // Act + await User.bulkSave([newUser1, newUser2]); + + // Assert + assert.ok(newUser1.createdAt); + assert.ok(newUser1.updatedAt); + assert.ok(!newUser2.createdAt); + assert.ok(!newUser2.updatedAt); + }); }); describe('Setting the explain flag', function() { diff --git a/test/schema.alias.test.js b/test/schema.alias.test.js index 5cdfdab9306..ca25511e2e5 100644 --- a/test/schema.alias.test.js +++ b/test/schema.alias.test.js @@ -159,4 +159,34 @@ describe('schema alias option', function() { done(); // acquit:ignore:end }); + + it('array of aliases (gh-12368)', function() { + const productSchema = new Schema({ + n: { + type: String, + alias: ['name', 'product_name'] + } + }); + + const Product = db.model('Test', productSchema); + const doc = new Product({}); + + doc['product_name'] = 'Turbo Man'; + assert.equal(doc.n, 'Turbo Man'); + assert.equal(doc.name, 'Turbo Man'); + }); + + it('alias() method (gh-12368)', function() { + const schema = new Schema({ name: String }); + + schema.alias('name', 'otherName'); + assert.equal(schema.aliases['otherName'], 'name'); + assert.ok(schema.virtuals['otherName']); + + schema.alias('name', ['name1', 'name2']); + assert.equal(schema.aliases['name1'], 'name'); + assert.equal(schema.aliases['name2'], 'name'); + assert.ok(schema.virtuals['name1']); + assert.ok(schema.virtuals['name2']); + }); }); diff --git a/test/schema.test.js b/test/schema.test.js index c09895de3b3..36368c58b2d 100644 --- a/test/schema.test.js +++ b/test/schema.test.js @@ -2867,4 +2867,22 @@ describe('schema', function() { assert.equal(doc1.domain, mongooseDomain); assert.equal(doc1.domain, doc2.domain); }); + + it('allows defining ObjectIds and Decimal128s using Types.* (gh-12205)', function() { + const schema = new Schema({ + testId: mongoose.Types.ObjectId, + testId2: { + type: mongoose.Types.ObjectId + }, + num: mongoose.Types.Decimal128, + num2: { + type: mongoose.Types.Decimal128 + } + }); + + assert.equal(schema.path('testId').instance, 'ObjectID'); + assert.equal(schema.path('testId2').instance, 'ObjectID'); + assert.equal(schema.path('num').instance, 'Decimal128'); + assert.equal(schema.path('num2').instance, 'Decimal128'); + }); }); diff --git a/test/schema.uuid.test.js b/test/schema.uuid.test.js new file mode 100644 index 00000000000..ee06af9b3b7 --- /dev/null +++ b/test/schema.uuid.test.js @@ -0,0 +1,108 @@ +'use strict'; + +const start = require('./common'); +const util = require('./util'); + +const bson = require('bson'); + +const assert = require('assert'); + +const mongoose = start.mongoose; +const Schema = mongoose.Schema; + +describe('SchemaUUID', function() { + let Model; + let TestSchema; + let db; + + before(async function() { + TestSchema = new Schema({ x: { type: mongoose.Schema.Types.UUID }, y: [{ type: mongoose.Schema.Types.UUID }] }); + db = await start().asPromise(); + }); + + after(async function() { + await db.close(); + }); + + beforeEach(async() => { + await db.deleteModel(/.*/); + Model = db.model('Test', TestSchema); + }); + afterEach(() => util.clearTestData(db)); + afterEach(() => util.stopRemainingOps(db)); + + it('basic functionality should work', async function() { + const doc = new Model({ x: '09190f70-3d30-11e5-8814-0f4df9a59c41' }); + assert.ifError(doc.validateSync()); + assert.ok(typeof doc.x === 'string'); + assert.strictEqual(doc.x, '09190f70-3d30-11e5-8814-0f4df9a59c41'); + await doc.save(); + + const query = Model.findOne({ x: '09190f70-3d30-11e5-8814-0f4df9a59c41' }); + assert.ok(typeof query._conditions.x === 'string'); + + const res = await query; + assert.ifError(res.validateSync()); + assert.ok(typeof res.x === 'string'); + assert.strictEqual(res.x, '09190f70-3d30-11e5-8814-0f4df9a59c41'); + + // check that the data is actually a buffer in the database with the correct subtype + const col = db.client.db(db.name).collection(Model.collection.name); + const rawDoc = await col.findOne({ x: new bson.Binary(Buffer.from('09190f703d3011e588140f4df9a59c41', 'hex'), 4) }); + assert.ok(rawDoc); + assert.ok(rawDoc.x instanceof bson.Binary); + assert.strictEqual(rawDoc.x.sub_type, 4); + }); + + it('should throw error in case of invalid string', function() { + const doc = new Model({ x: 'invalid' }); + const res = doc.validateSync(); + assert.ok(res !== null && res !== undefined); + const errors = res.errors; + assert.strictEqual(Object.keys(errors).length, 1); + assert.ok(errors.x instanceof mongoose.Error.CastError); + }); + + it('should work with $in and $nin and $all', async function() { + const doc1 = new Model({ y: ['f8010af3-bc2c-45e6-85c6-caa30c4a7d34', 'c6f59133-4f84-45a8-bc1d-8f172803e4fe', 'df1309e0-58c5-427a-b22f-6c0fc445ccc0'] }); + const doc2 = new Model({ y: ['13d51406-cd06-4fc2-93d1-4fad9b3eecd7', 'f004416b-e02a-4212-ac77-2d3fcf04898b', '5b544b71-8988-422b-a4df-bf691939fe4e'] }); + + await doc1.save(); + await doc2.save(); + + // test $in + const foundDocIn = await Model.find({ y: { $in: ['f8010af3-bc2c-45e6-85c6-caa30c4a7d34'] } }); + assert.ok(foundDocIn); + assert.strictEqual(foundDocIn.length, 1); + assert.ok(foundDocIn[0].y); + assert.strictEqual(foundDocIn[0].y.length, 3); + assert.strictEqual(foundDocIn[0].y[0], 'f8010af3-bc2c-45e6-85c6-caa30c4a7d34'); + assert.strictEqual(foundDocIn[0].y[1], 'c6f59133-4f84-45a8-bc1d-8f172803e4fe'); + assert.strictEqual(foundDocIn[0].y[2], 'df1309e0-58c5-427a-b22f-6c0fc445ccc0'); + + // test $nin + const foundDocNin = await Model.find({ y: { $nin: ['f8010af3-bc2c-45e6-85c6-caa30c4a7d34'] } }); + assert.ok(foundDocNin); + assert.strictEqual(foundDocNin.length, 1); + assert.ok(foundDocNin[0].y); + assert.strictEqual(foundDocNin[0].y.length, 3); + assert.strictEqual(foundDocNin[0].y[0], '13d51406-cd06-4fc2-93d1-4fad9b3eecd7'); + assert.strictEqual(foundDocNin[0].y[1], 'f004416b-e02a-4212-ac77-2d3fcf04898b'); + assert.strictEqual(foundDocNin[0].y[2], '5b544b71-8988-422b-a4df-bf691939fe4e'); + + // test for $all + const foundDocAll = await Model.find({ y: { $all: ['13d51406-cd06-4fc2-93d1-4fad9b3eecd7', 'f004416b-e02a-4212-ac77-2d3fcf04898b'] } }); + assert.ok(foundDocAll); + assert.strictEqual(foundDocAll.length, 1); + assert.ok(foundDocAll[0].y); + assert.strictEqual(foundDocAll[0].y.length, 3); + assert.strictEqual(foundDocAll[0].y[0], '13d51406-cd06-4fc2-93d1-4fad9b3eecd7'); + assert.strictEqual(foundDocAll[0].y[1], 'f004416b-e02a-4212-ac77-2d3fcf04898b'); + assert.strictEqual(foundDocAll[0].y[2], '5b544b71-8988-422b-a4df-bf691939fe4e'); + }); + + // the following are TODOs based on SchemaUUID.prototype.$conditionalHandlers which are not tested yet + it('should work with $bits* operators'); + it('should work with $all operator'); + it('should work with $lt, $lte, $gt, $gte operators'); +}); diff --git a/test/timestamps.test.js b/test/timestamps.test.js index a12b29192cd..1315c16af16 100644 --- a/test/timestamps.test.js +++ b/test/timestamps.test.js @@ -626,6 +626,20 @@ describe('timestamps', function() { assert.strictEqual(cat.updatedAt, old); }); + it('can skip with `$timestamps(false)` (gh-12117)', async function() { + const cat = await Cat.findOne(); + const old = cat.updatedAt; + + await delay(10); + + cat.hobby = 'fishing'; + + cat.$timestamps(false); + await cat.save(); + + assert.strictEqual(cat.updatedAt, old); + }); + it('should change updatedAt when findOneAndUpdate', function(done) { Cat.create({ name: 'test123' }, function(err) { assert.ifError(err); diff --git a/test/types/PipelineStage.test.ts b/test/types/PipelineStage.test.ts index 29c4696caca..28670945721 100644 --- a/test/types/PipelineStage.test.ts +++ b/test/types/PipelineStage.test.ts @@ -311,6 +311,85 @@ const setWindowFields4: PipelineStage = { } }; +const setWindowFieldsLinearFill: PipelineStage = { + $setWindowFields: { + partitionBy: '$stock', + sortBy: { date: 1 }, + output: { + price: { $linearFill: '$price' } + } + } +}; + +const setWindowFieldsLocf: PipelineStage = { + $setWindowFields: { + partitionBy: '$stock', + sortBy: { date: 1 }, + output: { + price: { $locf: '$price' } + } + } +}; + +const fillWithOutput: PipelineStage = { + $fill: { + output: { + bootsSold: { value: 0 } + } + } +}; + +const fillWithPartitionBy: PipelineStage = { + $fill: { + partitionBy: 'date', + output: { + bootsSold: { value: 0 } + } + } +}; + +const fillWithPartitionByFields: PipelineStage = { + $fill: { + partitionByFields: ['date'], + output: { + bootsSold: { value: 0 } + } + } +}; + +const fillWithSortBy: PipelineStage = { + $fill: { + sortBy: { + date: -1 + }, + output: { + bootsSold: { value: 0 } + } + } +}; + +const fillWithOutputMethodLinear: PipelineStage = { + $fill: { + sortBy: { + date: -1 + }, + output: { + bootsSold: { method: 'linear' } + } + } +}; + +const fillWithOutputMethodLocf: PipelineStage = { + $fill: { + sortBy: { + date: -1 + }, + output: { + bootsSold: { method: 'locf' } + } + } +}; + const group1: PipelineStage = { $group: { _id: null, ageStdDev: { $stdDevSamp: '$age' } } }; const group2: PipelineStage = { $group: { diff --git a/test/types/base.test.ts b/test/types/base.test.ts index c8198d02287..bf4aa723d92 100644 --- a/test/types/base.test.ts +++ b/test/types/base.test.ts @@ -1,5 +1,5 @@ import * as mongoose from 'mongoose'; -import { expectType } from 'tsd'; +import { expectError, expectType } from 'tsd'; Object.values(mongoose.models).forEach(model => { model.modelName; @@ -51,3 +51,12 @@ function gh12100() { mongoose.syncIndexes({ continueOnError: true, noResponse: true }); mongoose.syncIndexes({ continueOnError: false, noResponse: true }); } + +function setAsObject() { + mongoose.set({ + debug: true, + autoIndex: false + }); + + expectError(mongoose.set({ invalid: true })); +} diff --git a/test/types/discriminator.test.ts b/test/types/discriminator.test.ts index 052e3c3ba3c..1f32fd1149b 100644 --- a/test/types/discriminator.test.ts +++ b/test/types/discriminator.test.ts @@ -18,6 +18,12 @@ const doc: IDiscriminatorTest = new Disc({ name: 'foo', email: 'hi' }); doc.name = 'bar'; doc.email = 'hello'; +const Disc2 = Base.discriminator( + 'Disc2', + new Schema({ email: { type: String } }), + { value: 'test', mergeHooks: false } +); + function test(): void { enum CardType { Artifact = 'artifact', diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index e400dfa0ccf..1eddc97363d 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -8,10 +8,14 @@ import { InferSchemaType, SchemaType, Query, + model, HydratedDocument, - SchemaOptions + SchemaOptions, + ObtainDocumentType, + ObtainSchemaGeneric } from 'mongoose'; import { expectType, expectError, expectAssignable } from 'tsd'; +import { ObtainDocumentPathType, ResolvePathType } from '../../types/inferschematype'; enum Genre { Action, @@ -661,6 +665,28 @@ function gh12030() { ] }); + type A = ResolvePathType<[ + { + username: { type: String } + } + ]>; + expectType<{ + username?: string + }[]>({} as A); + + type B = ObtainDocumentType<{ + users: [ + { + username: { type: String } + } + ] + }>; + expectType<{ + users: { + username?: string + }[]; + }>({} as B); + expectType<{ users: { username?: string @@ -759,6 +785,71 @@ function pluginOptions() { expectError(schema.plugin(pluginFunction2, {})); // should error because "option2" is not optional } +function gh12205() { + const campaignSchema = new Schema( + { + client: { + type: new Types.ObjectId(), + required: true + } + }, + { timestamps: true } + ); + + const Campaign = model('Campaign', campaignSchema); + const doc = new Campaign(); + expectType(doc.client); + + type ICampaign = InferSchemaType; + expectType<{ client: Types.ObjectId }>({} as ICampaign); + + expectType<'type'>({} as ObtainSchemaGeneric); + + type A = ObtainDocumentType<{ client: { type: Schema.Types.ObjectId, required: true } }>; + expectType<{ client: Types.ObjectId }>({} as A); + + type Foo = ObtainDocumentPathType<{ type: Schema.Types.ObjectId, required: true }, 'type'>; + expectType({} as Foo); + + type Bar = ResolvePathType; + expectType({} as Bar); + + /* type Baz = Schema.Types.ObjectId extends typeof Schema.Types.ObjectId ? string : number; + expectType({} as Baz); */ +} + + +function gh12450() { + const ObjectIdSchema = new Schema({ + user: { type: Schema.Types.ObjectId } + }); + + expectType<{ + user?: Types.ObjectId; + }>({} as InferSchemaType); + + const Schema2 = new Schema({ + createdAt: { type: Date, required: true }, + decimalValue: { type: Schema.Types.Decimal128, required: true } + }); + + expectType<{ createdAt: Date, decimalValue: Types.Decimal128 }>({} as InferSchemaType); + + const Schema3 = new Schema({ + createdAt: { type: Date, required: true }, + decimalValue: { type: Schema.Types.Decimal128 } + }); + + expectType<{ createdAt: Date, decimalValue?: Types.Decimal128 }>({} as InferSchemaType); + + const Schema4 = new Schema({ + createdAt: { type: Date }, + decimalValue: { type: Schema.Types.Decimal128 } + }); + + expectType<{ createdAt?: Date, decimalValue?: Types.Decimal128 }>({} as InferSchemaType); +} + function gh12242() { const dbExample = new Schema( { diff --git a/types/aggregate.d.ts b/types/aggregate.d.ts index df5b4efdcc6..5352e22d0f6 100644 --- a/types/aggregate.d.ts +++ b/types/aggregate.d.ts @@ -133,6 +133,9 @@ declare module 'mongoose' { /** Combines multiple aggregation pipelines. */ facet(options: PipelineStage.Facet['$facet']): this; + /** Appends a new $fill operator to this aggregate pipeline */ + fill(arg: PipelineStage.Fill['$fill']): this; + /** Appends new custom $graphLookup operator(s) to this aggregate pipeline, performing a recursive search on a collection. */ graphLookup(options: PipelineStage.GraphLookup['$graphLookup']): this; diff --git a/types/expressions.d.ts b/types/expressions.d.ts index ecd7b3e6b79..b17659528c6 100644 --- a/types/expressions.d.ts +++ b/types/expressions.d.ts @@ -1190,6 +1190,26 @@ declare module 'mongoose' { $last: Expression; } + export interface LinearFill { + /** + * Fills null and missing fields in a window using linear interpolation based on surrounding field values. + * + * @version 5.3 + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/linearFill + */ + $linearFill: Expression + } + + export interface Locf { + /** + * Last observation carried forward. Sets values for null and missing fields in a window to the last non-null value for the field. + * + * @version 5.2 + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/locf + */ + $locf: Expression + } + export interface Map { /** * Applies a subexpression to each element of an array and returns the array of resulting values in order. Accepts named parameters. @@ -2745,6 +2765,8 @@ declare module 'mongoose' { Expression.First | Expression.Integral | Expression.Last | + Expression.LinearFill | + Expression.Locf | Expression.Max | Expression.Min | Expression.Push | diff --git a/types/index.d.ts b/types/index.d.ts index 5fc0853987b..3c1e7c2563e 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -109,6 +109,7 @@ declare module 'mongoose' { /** Sets mongoose options */ export function set(key: K, value: MongooseOptions[K]): Mongoose; + export function set(options: { [K in keyof MongooseOptions]: MongooseOptions[K] }): Mongoose; /** The Mongoose version */ export const version: string; @@ -195,6 +196,12 @@ declare module 'mongoose' { /** Adds key path / schema type pairs to this schema. */ add(obj: SchemaDefinition> | Schema, prefix?: string): this; + /** + * Add an alias for `path`. This means getting or setting the `alias` + * is equivalent to getting or setting the `path`. + */ + alias(path: string, alias: string | string[]): this; + /** * Array of child schemas (from document arrays and single nested subdocs) * and their corresponding compiled models. Each element of the array is diff --git a/types/inferschematype.d.ts b/types/inferschematype.d.ts index d590b5d65c9..a0406388f00 100644 --- a/types/inferschematype.d.ts +++ b/types/inferschematype.d.ts @@ -31,14 +31,14 @@ declare module 'mongoose' { /** * @summary Obtains document schema type from Schema instance. - * @param {SchemaType} SchemaType A generic of schema type instance. + * @param {Schema} TSchema `typeof` a schema instance. * @example * const userSchema = new Schema({userName:String}); * type UserType = InferSchemaType; * // result * type UserType = {userName?: string} */ - type InferSchemaType = ObtainSchemaGeneric; + type InferSchemaType = ObtainSchemaGeneric; /** * @summary Obtains schema Generic type by using generic alias. @@ -65,7 +65,7 @@ declare module 'mongoose' { * @param {P} P Document path. * @param {TypeKey} TypeKey A generic of literal string type."Refers to the property used for path type definition". */ -type IsPathRequired = +type IsPathRequired = P extends { required: true | [true, string | undefined] } | ArrayConstructor | any[] ? true : P extends (Record) @@ -83,7 +83,7 @@ type IsPathRequired = * @description It helps to check if a path is defined by TypeKey OR not. * @param {TypeKey} TypeKey A literal string refers to path type property key. */ -type PathWithTypePropertyBaseType = { [k in TypeKey]: any }; +type PathWithTypePropertyBaseType = { [k in TypeKey]: any }; /** * @summary A Utility to obtain schema's required path keys. @@ -91,7 +91,7 @@ type PathWithTypePropertyBaseType = { [k in Typ * @param {TypeKey} TypeKey A generic of literal string type."Refers to the property used for path type definition". * @returns required paths keys of document definition. */ -type RequiredPathKeys = { +type RequiredPathKeys = { [K in keyof T]: IsPathRequired extends true ? IfEquals : never; }[keyof T]; @@ -101,7 +101,7 @@ type RequiredPathKeys = { * @param {TypeKey} TypeKey A generic of literal string type."Refers to the property used for path type definition". * @returns a record contains required paths with the corresponding type. */ -type RequiredPaths = { +type RequiredPaths = { [K in RequiredPathKeys]: T[K]; }; @@ -111,7 +111,7 @@ type RequiredPaths = { * @param {TypeKey} TypeKey A generic of literal string type."Refers to the property used for path type definition". * @returns optional paths keys of document definition. */ -type OptionalPathKeys = { +type OptionalPathKeys = { [K in keyof T]: IsPathRequired extends true ? never : K; }[keyof T]; @@ -121,7 +121,7 @@ type OptionalPathKeys = { * @param {TypeKey} TypeKey A generic of literal string type."Refers to the property used for path type definition". * @returns a record contains optional paths with the corresponding type. */ -type OptionalPaths = { +type OptionalPaths = { [K in OptionalPathKeys]?: T[K]; }; @@ -131,7 +131,7 @@ type OptionalPaths = { * @param {PathValueType} PathValueType Document definition path type. * @param {TypeKey} TypeKey A generic refers to document definition. */ -type ObtainDocumentPathType = PathValueType extends Schema +type ObtainDocumentPathType = PathValueType extends Schema ? InferSchemaType : ResolvePathType< PathValueType extends PathWithTypePropertyBaseType ? PathValueType[TypeKey] : PathValueType, @@ -154,19 +154,28 @@ type PathEnumOrString['enum']> = T extends ( */ type ResolvePathType = {}, TypeKey extends TypeKeyBaseType = DefaultTypeKey> = PathValueType extends Schema ? InferSchemaType : - PathValueType extends (infer Item)[] ? IfEquals> : ResolvePathType[]> : + PathValueType extends (infer Item)[] ? IfEquals> : ObtainDocumentPathType[]> : PathValueType extends StringSchemaDefinition ? PathEnumOrString : - PathValueType extends NumberSchemaDefinition ? Options['enum'] extends ReadonlyArray ? Options['enum'][number] : number : - PathValueType extends DateSchemaDefinition ? Date : - PathValueType extends typeof Buffer | 'buffer' | 'Buffer' | typeof Schema.Types.Buffer ? Buffer : - PathValueType extends BooleanSchemaDefinition ? boolean : - PathValueType extends ObjectIdSchemaDefinition ? Types.ObjectId : - PathValueType extends 'decimal128' | 'Decimal128' | typeof Schema.Types.Decimal128 ? Types.Decimal128 : - PathValueType extends MapConstructor ? Map> : - PathValueType extends ArrayConstructor ? any[] : - PathValueType extends typeof Schema.Types.Mixed ? any: - IfEquals extends true ? any: - IfEquals extends true ? any: - PathValueType extends typeof SchemaType ? PathValueType['prototype'] : - PathValueType extends Record ? ObtainDocumentType : - unknown; + IfEquals extends true ? PathEnumOrString : + IfEquals extends true ? PathEnumOrString : + PathValueType extends NumberSchemaDefinition ? Options['enum'] extends ReadonlyArray ? Options['enum'][number] : number : + IfEquals extends true ? number : + PathValueType extends DateSchemaDefinition ? Date : + IfEquals extends true ? Date : + PathValueType extends typeof Buffer | 'buffer' | 'Buffer' | typeof Schema.Types.Buffer ? Buffer : + PathValueType extends BooleanSchemaDefinition ? boolean : + IfEquals extends true ? boolean : + PathValueType extends ObjectIdSchemaDefinition ? Types.ObjectId : + IfEquals extends true ? Types.ObjectId : + IfEquals extends true ? Types.ObjectId : + PathValueType extends 'decimal128' | 'Decimal128' | typeof Schema.Types.Decimal128 ? Types.Decimal128 : + IfEquals extends true ? Types.Decimal128 : + IfEquals extends true ? Types.Decimal128 : + PathValueType extends MapConstructor ? Map> : + PathValueType extends ArrayConstructor ? any[] : + PathValueType extends typeof Schema.Types.Mixed ? any: + IfEquals extends true ? any: + IfEquals extends true ? any: + PathValueType extends typeof SchemaType ? PathValueType['prototype'] : + PathValueType extends Record ? ObtainDocumentType : + unknown; diff --git a/types/models.d.ts b/types/models.d.ts index d3cbbcf1338..1f4cd3cc2ce 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -1,10 +1,17 @@ declare module 'mongoose' { import mongodb = require('mongodb'); + export interface DiscriminatorOptions { + value?: string | number | ObjectId; + clone?: boolean; + overwriteModels?: boolean; + mergeHooks?: boolean; + } + export interface AcceptsDiscriminator { /** Adds a discriminator type. */ - discriminator(name: string | number, schema: Schema, value?: string | number | ObjectId): Model; - discriminator(name: string | number, schema: Schema, value?: string | number | ObjectId): U; + discriminator(name: string | number, schema: Schema, value?: string | number | ObjectId | DiscriminatorOptions): Model; + discriminator(name: string | number, schema: Schema, value?: string | number | ObjectId | DiscriminatorOptions): U; } interface MongooseBulkWriteOptions { diff --git a/types/pipelinestage.d.ts b/types/pipelinestage.d.ts index bbc98324a75..f58e4b61f27 100644 --- a/types/pipelinestage.d.ts +++ b/types/pipelinestage.d.ts @@ -10,6 +10,7 @@ declare module 'mongoose' { | PipelineStage.Count | PipelineStage.Densify | PipelineStage.Facet + | PipelineStage.Fill | PipelineStage.GeoNear | PipelineStage.GraphLookup | PipelineStage.Group @@ -90,6 +91,16 @@ declare module 'mongoose' { } } + export interface Fill { + /** [`$fill` reference](https://docs.mongodb.com/manual/reference/operator/aggregation/fill/) */ + $fill: { + partitionBy?: Expression, + partitionByFields?: string[], + sortBy?: Record, + output: Record + } + } + export interface Facet { /** [`$facet` reference](https://docs.mongodb.com/manual/reference/operator/aggregation/facet/) */ $facet: Record; diff --git a/types/schematypes.d.ts b/types/schematypes.d.ts index 75e2258a7a4..899660d9fe6 100644 --- a/types/schematypes.d.ts +++ b/types/schematypes.d.ts @@ -57,7 +57,7 @@ declare module 'mongoose' { T | typeof SchemaType | Schema | SchemaDefinition | Function | AnyArray; /** Defines a virtual with the given name that gets/sets this path. */ - alias?: string; + alias?: string | string[]; /** Function or object describing how to validate this schematype. See [validation docs](https://mongoosejs.com/docs/validation.html). */ validate?: SchemaValidator | AnyArray>;