diff --git a/lib/connection.js b/lib/connection.js index 61d325a9dd2..797d4f50aef 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -491,6 +491,9 @@ Connection.prototype.transaction = function transaction(fn, options) { doc.set(doc.schema.options.versionKey, state.versionKey); } + if (state.modifiedPaths.length > 0 && doc.$__.activePaths.states.modify == null) { + doc.$__.activePaths.states.modify = {}; + } for (const path of state.modifiedPaths) { doc.$__.activePaths.paths[path] = 'modify'; doc.$__.activePaths.states.modify[path] = true; diff --git a/lib/document.js b/lib/document.js index 4a831cb6b75..aa7fb04be26 100644 --- a/lib/document.js +++ b/lib/document.js @@ -94,7 +94,11 @@ function Document(obj, fields, skipId, options) { } this.$__ = new InternalCache(); - this.$isNew = 'isNew' in options ? options.isNew : true; + + // Avoid setting `isNew` to `true`, because it is `true` by default + if (options.isNew != null && options.isNew !== true) { + this.$isNew = options.isNew; + } if (options.priorDoc != null) { this.$__.priorDoc = options.priorDoc; @@ -117,13 +121,12 @@ function Document(obj, fields, skipId, options) { const schema = this.$__schema; if (typeof fields === 'boolean' || fields === 'throw') { - this.$__.strictMode = fields; + if (fields !== true) { + this.$__.strictMode = fields; + } fields = undefined; - } else { + } else if (schema.options.strict !== true) { this.$__.strictMode = schema.options.strict; - if (fields != null) { - this.$__.selected = fields; - } } const requiredPaths = schema.requiredPaths(true); @@ -135,9 +138,9 @@ function Document(obj, fields, skipId, options) { // determine if this doc is a result of a query with // excluded fields - if (utils.isPOJO(fields)) { + if (utils.isPOJO(fields) && Object.keys(fields).length > 0) { exclude = isExclusive(fields); - this.$__.fields = fields; + this.$__.selected = fields; this.$__.exclude = exclude; } @@ -211,6 +214,13 @@ Object.defineProperty(Document.prototype, 'errors', { this.$errors = value; } }); + +/*! + * ignore + */ + +Document.prototype.$isNew = true; + /*! * Document exposes the NodeJS event emitter API, so you can use * `on`, `once`, etc. @@ -658,10 +668,11 @@ Document.prototype.$__init = function(doc, opts) { this.$emit('init', this); this.constructor.emit('init', this); - const hasIncludedChildren = this.$__.exclude === false && this.$__.fields ? - $__hasIncludedChildren(this.$__.fields) : + const hasIncludedChildren = this.$__.exclude === false && this.$__.selected ? + $__hasIncludedChildren(this.$__.selected) : null; - applyDefaults(this, this.$__.fields, this.$__.exclude, hasIncludedChildren, false, this.$__.skipDefaults); + + applyDefaults(this, this.$__.selected, this.$__.exclude, hasIncludedChildren, false, this.$__.skipDefaults); return this; }; @@ -1505,7 +1516,7 @@ Document.prototype.$__shouldModify = function(pathToMark, path, options, constru return true; } - if (val === void 0 && path in this.$__.activePaths.states.default) { + if (val === void 0 && path in this.$__.activePaths.getStatePaths('default')) { // we're just unsetting the default value which was never saved return false; } @@ -1525,7 +1536,7 @@ Document.prototype.$__shouldModify = function(pathToMark, path, options, constru if (!constructing && val !== null && val !== undefined && - path in this.$__.activePaths.states.default && + path in this.$__.activePaths.getStatePaths('default') && deepEqual(val, schema.getDefault(this, constructing))) { // a path with a default was $unset on the server // and the user is setting it to the same value again @@ -1823,7 +1834,7 @@ Document.prototype.$ignore = function(path) { */ Document.prototype.directModifiedPaths = function() { - return Object.keys(this.$__.activePaths.states.modify); + return Object.keys(this.$__.activePaths.getStatePaths('modify')); }; /** @@ -1901,7 +1912,7 @@ function _isEmpty(v) { Document.prototype.modifiedPaths = function(options) { options = options || {}; - const directModifiedPaths = Object.keys(this.$__.activePaths.states.modify); + const directModifiedPaths = Object.keys(this.$__.activePaths.getStatePaths('modify')); const result = new Set(); let i = 0; @@ -1980,7 +1991,7 @@ Document.prototype[documentModifiedPaths] = Document.prototype.modifiedPaths; Document.prototype.isModified = function(paths, modifiedPaths) { if (paths) { - const directModifiedPaths = Object.keys(this.$__.activePaths.states.modify); + const directModifiedPaths = Object.keys(this.$__.activePaths.getStatePaths('modify')); if (directModifiedPaths.length === 0) { return false; } @@ -2030,7 +2041,7 @@ Document.prototype.$isDefault = function(path) { } if (typeof path === 'string' && path.indexOf(' ') === -1) { - return this.$__.activePaths.states.default.hasOwnProperty(path); + return this.$__.activePaths.getStatePaths('default').hasOwnProperty(path); } let paths = path; @@ -2038,7 +2049,7 @@ Document.prototype.$isDefault = function(path) { paths = paths.split(' '); } - return paths.some(path => this.$__.activePaths.states.default.hasOwnProperty(path)); + return paths.some(path => this.$__.activePaths.getStatePaths('default').hasOwnProperty(path)); }; /** @@ -2091,7 +2102,7 @@ Document.prototype.isDirectModified = function(path) { } if (typeof path === 'string' && path.indexOf(' ') === -1) { - return this.$__.activePaths.states.modify.hasOwnProperty(path); + return this.$__.activePaths.getStatePaths('modify').hasOwnProperty(path); } let paths = path; @@ -2099,7 +2110,7 @@ Document.prototype.isDirectModified = function(path) { paths = paths.split(' '); } - return paths.some(path => this.$__.activePaths.states.modify.hasOwnProperty(path)); + return paths.some(path => this.$__.activePaths.getStatePaths('modify').hasOwnProperty(path)); }; /** @@ -2116,7 +2127,7 @@ Document.prototype.isInit = function(path) { } if (typeof path === 'string' && path.indexOf(' ') === -1) { - return this.$__.activePaths.states.init.hasOwnProperty(path); + return this.$__.activePaths.getStatePaths('init').hasOwnProperty(path); } let paths = path; @@ -2124,7 +2135,7 @@ Document.prototype.isInit = function(path) { paths = paths.split(' '); } - return paths.some(path => this.$__.activePaths.states.init.hasOwnProperty(path)); + return paths.some(path => this.$__.activePaths.getStatePaths('init').hasOwnProperty(path)); }; /** @@ -2352,7 +2363,7 @@ Document.prototype.$validate = Document.prototype.validate; */ function _evaluateRequiredFunctions(doc) { - const requiredFields = Object.keys(doc.$__.activePaths.states.require); + const requiredFields = Object.keys(doc.$__.activePaths.getStatePaths('require')); let i = 0; const len = requiredFields.length; for (i = 0; i < len; ++i) { @@ -2380,7 +2391,7 @@ function _getPathsToValidate(doc) { _evaluateRequiredFunctions(doc); // only validate required fields when necessary - let paths = new Set(Object.keys(doc.$__.activePaths.states.require).filter(function(path) { + let paths = new Set(Object.keys(doc.$__.activePaths.getStatePaths('require')).filter(function(path) { if (!doc.$__isSelected(path) && !doc.$isModified(path)) { return false; } @@ -2390,9 +2401,9 @@ function _getPathsToValidate(doc) { return true; })); - Object.keys(doc.$__.activePaths.states.init).forEach(addToPaths); - Object.keys(doc.$__.activePaths.states.modify).forEach(addToPaths); - Object.keys(doc.$__.activePaths.states.default).forEach(addToPaths); + Object.keys(doc.$__.activePaths.getStatePaths('init')).forEach(addToPaths); + Object.keys(doc.$__.activePaths.getStatePaths('modify')).forEach(addToPaths); + Object.keys(doc.$__.activePaths.getStatePaths('default')).forEach(addToPaths); function addToPaths(p) { paths.add(p); } const subdocs = doc.$getAllSubdocs(); @@ -3114,8 +3125,8 @@ Document.prototype.$__reset = function reset() { this.$__.backup = {}; this.$__.backup.activePaths = { - modify: Object.assign({}, this.$__.activePaths.states.modify), - default: Object.assign({}, this.$__.activePaths.states.default) + modify: Object.assign({}, this.$__.activePaths.getStatePaths('modify')), + default: Object.assign({}, this.$__.activePaths.getStatePaths('default')) }; this.$__.backup.validationError = this.$__.validationError; this.$__.backup.errors = this.$errors; diff --git a/lib/helpers/document/cleanModifiedSubpaths.js b/lib/helpers/document/cleanModifiedSubpaths.js index 29bd18c3ac9..43c225e4fd2 100644 --- a/lib/helpers/document/cleanModifiedSubpaths.js +++ b/lib/helpers/document/cleanModifiedSubpaths.js @@ -13,7 +13,7 @@ module.exports = function cleanModifiedSubpaths(doc, path, options) { return deleted; } - for (const modifiedPath of Object.keys(doc.$__.activePaths.states.modify)) { + for (const modifiedPath of Object.keys(doc.$__.activePaths.getStatePaths('modify'))) { if (skipDocArrays) { const schemaType = doc.$__schema.path(modifiedPath); if (schemaType && schemaType.$isMongooseDocumentArray) { @@ -21,13 +21,13 @@ module.exports = function cleanModifiedSubpaths(doc, path, options) { } } if (modifiedPath.startsWith(path + '.')) { - delete doc.$__.activePaths.states.modify[modifiedPath]; + doc.$__.activePaths.clearPath(modifiedPath); ++deleted; if (doc.$isSubdocument) { const owner = doc.ownerDocument(); const fullPath = doc.$__fullPath(modifiedPath); - delete owner.$__.activePaths.states.modify[fullPath]; + owner.$__.activePaths.clearPath(fullPath); } } } diff --git a/lib/helpers/document/compile.js b/lib/helpers/document/compile.js index 80a0de0b041..71597582535 100644 --- a/lib/helpers/document/compile.js +++ b/lib/helpers/document/compile.js @@ -17,6 +17,13 @@ const isPOJO = utils.isPOJO; exports.compile = compile; exports.defineKey = defineKey; +const _isEmptyOptions = Object.freeze({ + minimize: true, + virtuals: false, + getters: false, + transform: false +}); + /*! * Compiles schemas. */ @@ -130,12 +137,6 @@ function defineKey({ prop, subprops, prototype, prefix, options }) { value: true }); - const _isEmptyOptions = Object.freeze({ - minimize: true, - virtuals: false, - getters: false, - transform: false - }); Object.defineProperty(nested, '$isEmpty', { enumerable: false, configurable: true, diff --git a/lib/helpers/model/castBulkWrite.js b/lib/helpers/model/castBulkWrite.js index 5b882b6d5fe..27723faa6e7 100644 --- a/lib/helpers/model/castBulkWrite.js +++ b/lib/helpers/model/castBulkWrite.js @@ -20,7 +20,7 @@ module.exports = function castBulkWrite(originalModel, op, options) { const model = decideModelByObject(originalModel, op['insertOne']['document']); const doc = new model(op['insertOne']['document']); - if (model.schema.options.timestamps) { + if (model.schema.options.timestamps && options.timestamps !== false) { doc.initializeTimestamps(); } if (options.session != null) { diff --git a/lib/internal.js b/lib/internal.js index a31714825f9..d7fe08d5b87 100644 --- a/lib/internal.js +++ b/lib/internal.js @@ -13,8 +13,9 @@ function InternalCache() { this.activePaths = new ActiveRoster(); } +InternalCache.prototype.strictMode = true; + InternalCache.prototype.fullPath = undefined; -InternalCache.prototype.strictMode = undefined; InternalCache.prototype.selected = undefined; InternalCache.prototype.shardval = undefined; InternalCache.prototype.saveError = undefined; diff --git a/lib/model.js b/lib/model.js index 5a0e9709554..bb79be729b6 100644 --- a/lib/model.js +++ b/lib/model.js @@ -3581,41 +3581,50 @@ Model.bulkWrite = function(ops, options, callback) { * * @param {Array} documents * @param {Object} [options] options passed to the underlying `bulkWrite()` + * @param {Boolean} [options.timestamps] defaults to `null`, when set to false, mongoose will not add/update timestamps to the documents. * @param {ClientSession} [options.session=null] The session associated with this bulk write. See [transactions docs](/docs/transactions.html). * @param {String|number} [options.w=1] The [write concern](https://docs.mongodb.com/manual/reference/write-concern/). See [`Query#w()`](/docs/api.html#query_Query-w) for more information. * @param {number} [options.wtimeout=null] The [write concern timeout](https://docs.mongodb.com/manual/reference/write-concern/#wtimeout). * @param {Boolean} [options.j=true] If false, disable [journal acknowledgement](https://docs.mongodb.com/manual/reference/write-concern/#j-option) * */ -Model.bulkSave = function(documents, options) { - const preSavePromises = documents.map(buildPreSavePromise); - - const writeOperations = this.buildBulkWriteOperations(documents, { skipValidation: true }); - - let bulkWriteResultPromise; - return Promise.all(preSavePromises) - .then(() => bulkWriteResultPromise = this.bulkWrite(writeOperations, options)) - .then(() => documents.map(buildSuccessfulWriteHandlerPromise)) - .then(() => bulkWriteResultPromise) - .catch((err) => { - if (!(err && err.writeErrors && err.writeErrors.length)) { - throw err; - } - return Promise.all( - documents.map((document) => { - const documentError = err.writeErrors.find(writeError => { - const writeErrorDocumentId = writeError.err.op._id || writeError.err.op.q._id; - return writeErrorDocumentId.toString() === document._id.toString(); - }); +Model.bulkSave = async function(documents, options) { + options = options || {}; - if (documentError == null) { - return buildSuccessfulWriteHandlerPromise(document); - } - }) - ).then(() => { - throw err; + const writeOperations = this.buildBulkWriteOperations(documents, { skipValidation: true, timestamps: options.timestamps }); + + if (options.timestamps != null) { + for (const document of documents) { + document.$__.saveOptions = document.$__.saveOptions || {}; + document.$__.saveOptions.timestamps = options.timestamps; + } + } + + await Promise.all(documents.map(buildPreSavePromise)); + + const { bulkWriteResult, bulkWriteError } = await this.bulkWrite(writeOperations, options).then( + (res) => ({ bulkWriteResult: res, bulkWriteError: null }), + (err) => ({ bulkWriteResult: null, bulkWriteError: err }) + ); + + await Promise.all( + documents.map(async(document) => { + const documentError = bulkWriteError && bulkWriteError.writeErrors.find(writeError => { + const writeErrorDocumentId = writeError.err.op._id || writeError.err.op.q._id; + return writeErrorDocumentId.toString() === document._id.toString(); }); - }); + + if (documentError == null) { + await handleSuccessfulWrite(document); + } + }) + ); + + if (bulkWriteError && bulkWriteError.writeErrors && bulkWriteError.writeErrors.length) { + throw bulkWriteError; + } + + return bulkWriteResult; }; function buildPreSavePromise(document) { @@ -3630,24 +3639,21 @@ function buildPreSavePromise(document) { }); } -function buildSuccessfulWriteHandlerPromise(document) { +function handleSuccessfulWrite(document) { return new Promise((resolve, reject) => { - handleSuccessfulWrite(document, resolve, reject); - }); -} + if (document.$isNew) { + _setIsNew(document, false); + } -function handleSuccessfulWrite(document, resolve, reject) { - if (document.$isNew) { - _setIsNew(document, false); - } + document.$__reset(); + document.schema.s.hooks.execPost('save', document, {}, (err) => { + if (err) { + reject(err); + return; + } + resolve(); + }); - document.$__reset(); - document.schema.s.hooks.execPost('save', document, {}, (err) => { - if (err) { - reject(err); - return; - } - resolve(); }); } @@ -3681,6 +3687,7 @@ Model.applyDefaults = function applyDefaults(doc) { * @param {Array} documents The array of documents to build write operations of * @param {Object} options * @param {Boolean} options.skipValidation defaults to `false`, when set to true, building the write operations will bypass validating the documents. + * @param {Boolean} options.timestamps defaults to `null`, when set to false, mongoose will not add/update timestamps to the documents. * @return {Array} Returns a array of all Promises the function executes to be awaited. * @api private */ @@ -3705,9 +3712,9 @@ Model.buildBulkWriteOperations = function buildBulkWriteOperations(documents, op const isANewDocument = document.isNew; if (isANewDocument) { - accumulator.push({ - insertOne: { document } - }); + const writeOperation = { insertOne: { document } }; + utils.injectTimestampsOption(writeOperation.insertOne, options.timestamps); + accumulator.push(writeOperation); return accumulator; } @@ -3722,13 +3729,9 @@ Model.buildBulkWriteOperations = function buildBulkWriteOperations(documents, op _applyCustomWhere(document, where); document.$__version(where, delta); - - accumulator.push({ - updateOne: { - filter: where, - update: changes - } - }); + const writeOperation = { updateOne: { filter: where, update: changes } }; + utils.injectTimestampsOption(writeOperation.updateOne, options.timestamps); + accumulator.push(writeOperation); return accumulator; } @@ -3747,6 +3750,7 @@ Model.buildBulkWriteOperations = function buildBulkWriteOperations(documents, op } }; + /** * Shortcut for creating a new Document from existing raw data, pre-saved in the DB. * The document returned has no paths marked as modified initially. diff --git a/lib/options/SchemaArrayOptions.js b/lib/options/SchemaArrayOptions.js index 9ea87f3e37c..54ad4f09058 100644 --- a/lib/options/SchemaArrayOptions.js +++ b/lib/options/SchemaArrayOptions.js @@ -52,6 +52,25 @@ Object.defineProperty(SchemaArrayOptions.prototype, 'enum', opts); Object.defineProperty(SchemaArrayOptions.prototype, 'of', opts); +/** + * If set to `false`, will always deactivate casting non-array values to arrays. + * If set to `true`, will cast non-array values to arrays if `init` and `SchemaArray.options.castNonArrays` are also `true` + * + * #### Example: + * + * const Model = db.model('Test', new Schema({ x1: { castNonArrays: false, type: [String] } })); + * const doc = new Model({ x1: "some non-array value" }); + * await doc.validate(); // Errors with "CastError" + * + * @api public + * @property castNonArrays + * @memberOf SchemaArrayOptions + * @type {Boolean} + * @instance + */ + +Object.defineProperty(SchemaArrayOptions.prototype, 'castNonArrays', opts); + /*! * ignore */ diff --git a/lib/plugins/trackTransaction.js b/lib/plugins/trackTransaction.js index e0887d65c71..a1726e8ca86 100644 --- a/lib/plugins/trackTransaction.js +++ b/lib/plugins/trackTransaction.js @@ -23,14 +23,14 @@ module.exports = function trackTransaction(schema) { initialState.versionKey = this.get(this.$__schema.options.versionKey); } - initialState.modifiedPaths = new Set(Object.keys(this.$__.activePaths.states.modify)); + initialState.modifiedPaths = new Set(Object.keys(this.$__.activePaths.getStatePaths('modify'))); initialState.atomics = _getAtomics(this); session[sessionNewDocuments].set(this, initialState); } else { const state = session[sessionNewDocuments].get(this); - for (const path of Object.keys(this.$__.activePaths.states.modify)) { + for (const path of Object.keys(this.$__.activePaths.getStatePaths('modify'))) { state.modifiedPaths.add(path); } state.atomics = _getAtomics(this, state.atomics); diff --git a/lib/schema/array.js b/lib/schema/array.js index 33218d3a0aa..1dbe8556708 100644 --- a/lib/schema/array.js +++ b/lib/schema/array.js @@ -393,7 +393,8 @@ SchemaArray.prototype.cast = function(value, doc, init, prev, options) { return value; } - if (init || SchemaArray.options.castNonArrays) { + const castNonArraysOption = this.options.castNonArrays != null ? this.options.castNonArrays : SchemaArray.options.castNonArrays; + if (init || castNonArraysOption) { // gh-2442: if we're loading this from the db and its not an array, mark // the whole array as modified. if (!!doc && !!init) { diff --git a/lib/schema/documentarray.js b/lib/schema/documentarray.js index dbd81b57116..80d7f55b9dd 100644 --- a/lib/schema/documentarray.js +++ b/lib/schema/documentarray.js @@ -36,6 +36,13 @@ let Subdocument; */ function DocumentArrayPath(key, schema, options, schemaOptions) { + const schemaTypeIdOption = DocumentArrayPath.defaultOptions && + DocumentArrayPath.defaultOptions._id; + if (schemaTypeIdOption != null) { + schemaOptions = schemaOptions || {}; + schemaOptions._id = schemaTypeIdOption; + } + if (schemaOptions != null && schemaOptions._id != null) { schema = handleIdOption(schema, schemaOptions); } else if (options != null && options._id != null) { diff --git a/lib/statemachine.js b/lib/statemachine.js index 43da62812e2..a524c999939 100644 --- a/lib/statemachine.js +++ b/lib/statemachine.js @@ -38,19 +38,12 @@ StateMachine.ctor = function() { StateMachine.apply(this, arguments); this.paths = {}; this.states = {}; - this.stateNames = states; - - let i = states.length, - state; - - while (i--) { - state = states[i]; - this.states[state] = {}; - } }; ctor.prototype = new StateMachine(); + ctor.prototype.stateNames = states; + states.forEach(function(state) { // Changes the `path`'s state to `state`. ctor.prototype[state] = function(path) { @@ -76,6 +69,7 @@ StateMachine.prototype._changeState = function _changeState(path, nextState) { if (prevBucket) delete prevBucket[path]; this.paths[path] = nextState; + this.states[nextState] = this.states[nextState] || {}; this.states[nextState][path] = true; }; @@ -84,6 +78,9 @@ StateMachine.prototype._changeState = function _changeState(path, nextState) { */ StateMachine.prototype.clear = function clear(state) { + if (this.states[state] == null) { + return; + } const keys = Object.keys(this.states[state]); let i = keys.length; let path; @@ -108,6 +105,17 @@ StateMachine.prototype.clearPath = function clearPath(path) { delete this.states[state][path]; }; +/*! + * Gets the paths for the given state, or empty object `{}` if none. + */ + +StateMachine.prototype.getStatePaths = function getStatePaths(state) { + if (this.states[state] != null) { + return this.states[state]; + } + return {}; +}; + /*! * Checks to see if at least one path is in the states passed in via `arguments` * e.g., this.some('required', 'inited') @@ -120,6 +128,9 @@ StateMachine.prototype.some = function some() { const _this = this; const what = arguments.length ? arguments : this.stateNames; return Array.prototype.some.call(what, function(state) { + if (_this.states[state] == null) { + return false; + } return Object.keys(_this.states[state]).length; }); }; @@ -143,6 +154,9 @@ StateMachine.prototype._iter = function _iter(iterMethod) { const _this = this; const paths = states.reduce(function(paths, state) { + if (_this.states[state] == null) { + return paths; + } return paths.concat(Object.keys(_this.states[state])); }, []); diff --git a/lib/utils.js b/lib/utils.js index 50058acdc13..8ebd81d8835 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -972,3 +972,11 @@ exports.errorToPOJO = function errorToPOJO(error) { exports.warn = function warn(message) { return process.emitWarning(message, { code: 'MONGOOSE' }); }; + + +exports.injectTimestampsOption = function injectTimestampsOption(writeOperation, timestampsOption) { + if (timestampsOption == null) { + return; + } + writeOperation.timestamps = timestampsOption; +}; diff --git a/test/docs/lean.test.js b/test/docs/lean.test.js index 79e6bd34b51..49cd0c45aad 100644 --- a/test/docs/lean.test.js +++ b/test/docs/lean.test.js @@ -32,15 +32,15 @@ describe('Lean Tutorial', function() { // To enable the `lean` option for a query, use the `lean()` function. const leanDoc = await MyModel.findOne().lean(); - v8Serialize(normalDoc).length; // approximately 300 - v8Serialize(leanDoc).length; // 32, more than 10x smaller! + v8Serialize(normalDoc).length; // approximately 180 + v8Serialize(leanDoc).length; // 32, about 5x smaller! // In case you were wondering, the JSON form of a Mongoose doc is the same // as the POJO. This additional memory only affects how much memory your // Node.js process uses, not how much data is sent over the network. JSON.stringify(normalDoc).length === JSON.stringify(leanDoc).length; // true // acquit:ignore:start - assert.ok(v8Serialize(normalDoc).length >= 300 && v8Serialize(normalDoc).length <= 800, v8Serialize(normalDoc).length); + assert.ok(v8Serialize(normalDoc).length >= 150 && v8Serialize(normalDoc).length <= 200, v8Serialize(normalDoc).length); assert.equal(v8Serialize(leanDoc).length, 32); assert.equal(JSON.stringify(normalDoc).length, JSON.stringify(leanDoc).length); // acquit:ignore:end diff --git a/test/model.test.js b/test/model.test.js index c168800073d..d4ab16ad176 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -8147,6 +8147,73 @@ describe('Model', function() { assert.equal(writeOperations.length, 3); }); + + it('accepts `timestamps: false` (gh-12059)', async() => { + // Arrange + const userSchema = new Schema({ + name: { type: String, minLength: 5 } + }); + + const User = db.model('User', userSchema); + + const newUser = new User({ name: 'Hafez' }); + const userToUpdate = await User.create({ name: 'Hafez' }); + userToUpdate.name = 'John Doe'; + + // Act + const writeOperations = User.buildBulkWriteOperations([newUser, userToUpdate], { timestamps: false, skipValidation: true }); + + // Assert + const timestampsOptions = writeOperations.map(writeOperationContainer => { + const operationObject = writeOperationContainer.updateOne || writeOperationContainer.insertOne; + return operationObject.timestamps; + }); + assert.deepEqual(timestampsOptions, [false, false]); + }); + it('accepts `timestamps: true` (gh-12059)', async() => { + // Arrange + const userSchema = new Schema({ + name: { type: String, minLength: 5 } + }); + + const User = db.model('User', userSchema); + + const newUser = new User({ name: 'Hafez' }); + const userToUpdate = await User.create({ name: 'Hafez' }); + userToUpdate.name = 'John Doe'; + + // Act + const writeOperations = User.buildBulkWriteOperations([newUser, userToUpdate], { timestamps: true, skipValidation: true }); + + // Assert + const timestampsOptions = writeOperations.map(writeOperationContainer => { + const operationObject = writeOperationContainer.updateOne || writeOperationContainer.insertOne; + return operationObject.timestamps; + }); + assert.deepEqual(timestampsOptions, [true, true]); + }); + it('`timestamps` has `undefined` as default value (gh-12059)', async() => { + // Arrange + const userSchema = new Schema({ + name: { type: String, minLength: 5 } + }); + + const User = db.model('User', userSchema); + + const newUser = new User({ name: 'Hafez' }); + const userToUpdate = await User.create({ name: 'Hafez' }); + userToUpdate.name = 'John Doe'; + + // Act + const writeOperations = User.buildBulkWriteOperations([newUser, userToUpdate], { skipValidation: true }); + + // Assert + const timestampsOptions = writeOperations.map(writeOperationContainer => { + const operationObject = writeOperationContainer.updateOne || writeOperationContainer.insertOne; + return operationObject.timestamps; + }); + assert.deepEqual(timestampsOptions, [undefined, undefined]); + }); }); describe('bulkSave() (gh-9673)', function() { @@ -8235,7 +8302,6 @@ describe('Model', function() { }); it('throws an error on failure', async() => { - const userSchema = new Schema({ name: { type: String, unique: true } }); @@ -8255,8 +8321,8 @@ describe('Model', function() { const err = await User.bulkSave(users).then(() => null, err => err); assert.ok(err); - }); + it('changes document state from `isNew` `false` to `true`', async() => { const userSchema = new Schema({ @@ -8411,6 +8477,76 @@ describe('Model', function() { const res = await model.bulkSave(entries); assert.ok(res); }); + + it('accepts `timestamps: false` (gh-12059)', async() => { + // Arrange + const userSchema = new Schema({ + name: { type: String } + }, { timestamps: true }); + + const User = db.model('User', userSchema); + const newUser = new User({ name: 'Sam' }); + + const userToUpdate = await User.create({ name: 'Hafez', createdAt: new Date('1994-12-04'), updatedAt: new Date('1994-12-04') }); + userToUpdate.name = 'John Doe'; + + // Act + await User.bulkSave([newUser, userToUpdate], { timestamps: false }); + + + // Assert + const createdUserPersistedInDB = await User.findOne({ _id: newUser._id }); + assert.deepStrictEqual(newUser.createdAt, undefined); + assert.deepStrictEqual(newUser.updatedAt, undefined); + + assert.deepStrictEqual(createdUserPersistedInDB.createdAt, undefined); + assert.deepStrictEqual(createdUserPersistedInDB.updatedAt, undefined); + assert.deepStrictEqual(userToUpdate.createdAt, new Date('1994-12-04')); + assert.deepStrictEqual(userToUpdate.updatedAt, new Date('1994-12-04')); + }); + + it('accepts `timestamps: true` (gh-12059)', async() => { + // Arrange + const userSchema = new Schema({ + name: { type: String, minLength: 5 } + }, { timestamps: true }); + + const User = db.model('User', userSchema); + + const newUser = new User({ name: 'Hafez' }); + const userToUpdate = await User.create({ name: 'Hafez' }); + userToUpdate.name = 'John Doe'; + + // Act + await User.bulkSave([newUser, userToUpdate], { timestamps: true }); + + // Assert + assert.ok(newUser.createdAt); + assert.ok(newUser.updatedAt); + assert.ok(userToUpdate.createdAt); + assert.ok(userToUpdate.updatedAt); + }); + it('`timestamps` has `undefined` as default value (gh-12059)', async() => { + // Arrange + const userSchema = new Schema({ + name: { type: String, minLength: 5 } + }, { timestamps: true }); + + const User = db.model('User', userSchema); + + const newUser = new User({ name: 'Hafez' }); + const userToUpdate = await User.create({ name: 'Hafez' }); + userToUpdate.name = 'John Doe'; + + // Act + await User.bulkSave([newUser, userToUpdate]); + + // Assert + assert.ok(newUser.createdAt); + assert.ok(newUser.updatedAt); + assert.ok(userToUpdate.createdAt); + assert.ok(userToUpdate.updatedAt); + }); }); describe('Setting the explain flag', function() { diff --git a/test/types.array.test.js b/test/types.array.test.js index 8556fbfb295..68d5b2dcd0f 100644 --- a/test/types.array.test.js +++ b/test/types.array.test.js @@ -1192,6 +1192,20 @@ describe('types array', function() { assert.ifError(doc.validateSync()); assert.deepEqual(doc.arr.toObject(), ['good', 'foo']); + // test also having the property option set + + // the following should work because "castNonArrays" (property option) overwrites global + const bothSchema = new Schema({ arr: { castNonArrays: true, type: [String] }, docArr: { castNonArrays: true, type: [{ name: String }] } }); + const bothModel = db.model('Test2', bothSchema); + let bothdoc = new bothModel({ arr: 'fail', docArr: { name: 'fail' } }); + assert.ifError(doc.validateSync()); + + bothdoc = new bothModel({ arr: ['good'] }); + assert.ifError(bothdoc.validateSync()); + bothdoc.arr.push('foo'); + assert.ifError(bothdoc.validateSync()); + assert.deepEqual(bothdoc.arr.toObject(), ['good', 'foo']); + return Promise.resolve(); }); @@ -2151,4 +2165,45 @@ describe('types array', function() { assert.strictEqual(doc.arr[0], '42'); assert.strictEqual(arr[0], 42); }); + + it('test "castNonArrays" property option', function() { + const Model = db.model('Test', new Schema({ x1: { castNonArrays: false, type: [String] }, x2: { castNonArrays: true, type: [String] }, x3: { type: [String] } })); + + const string = 'hello'; + + // error testing + let doc = new Model({ x1: string }); + const validateErrors = doc.validateSync().errors; + assert.ok(validateErrors); + assert.equal(validateErrors['x1'].name, 'CastError'); + + // good testing + doc = new Model({ x2: string }); + assert.ifError(doc.validateSync()); + doc.x2.push('foo'); + assert.ifError(doc.validateSync()); + assert.deepEqual(doc.x2.toObject(), ['hello', 'foo']); + + // without option (default) + doc = new Model({ x3: string }); + assert.ifError(doc.validateSync()); + doc.x3.push('foo'); + assert.ifError(doc.validateSync()); + assert.deepEqual(doc.x3.toObject(), ['hello', 'foo']); + }); + + it('`castNonArrays` on specific paths takes precedence over global option', function() { + // Arrange + const m = new mongoose.Mongoose(); + m.Schema.Types.Array.options.castNonArrays = false; + + const userSchema = new Schema({ friendsNames: { type: [String], castNonArrays: true } }); + const User = m.model('User', userSchema); + + // Act + const user = new User({ friendsNames: 'Sam' }); + + // Assert + assert.ifError(user.validateSync()); + }); }); diff --git a/test/types/models.test.ts b/test/types/models.test.ts index 8d022594753..46e5920dfbf 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -329,6 +329,24 @@ function gh11911() { }); } + +function gh12059() { + interface IAnimal { + name?: string; + } + + const animalSchema = new Schema({ + name: { type: String } + }); + + const Animal = model('Animal', animalSchema); + const animal = new Animal(); + + Animal.bulkSave([animal], { timestamps: false }); + Animal.bulkSave([animal], { timestamps: true }); + Animal.bulkSave([animal], {}); +} + function gh12100() { const schema = new Schema(); diff --git a/types/models.d.ts b/types/models.d.ts index d4a6f1a1797..6396a4bebf1 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -149,7 +149,7 @@ declare module 'mongoose' { * sending multiple `save()` calls because with `bulkSave()` there is only one * network round trip to the MongoDB server. */ - bulkSave(documents: Array, options?: mongodb.BulkWriteOptions): Promise; + bulkSave(documents: Array, options?: mongodb.BulkWriteOptions & { timestamps?: boolean }): Promise; /** Collection the model uses. */ collection: Collection; diff --git a/types/schemaoptions.d.ts b/types/schemaoptions.d.ts index 8e978bcac2d..02791ab4171 100644 --- a/types/schemaoptions.d.ts +++ b/types/schemaoptions.d.ts @@ -202,5 +202,11 @@ declare module 'mongoose' { * Query helper functions */ query?: Record>(this: T, ...args: any) => T> | QueryHelpers, + + /** + * Set whether to cast non-array values to arrays. + * @default true + */ + castNonArrays?: boolean; } }