'use strict'; const MongooseError = require('../../error/mongooseError'); const getDiscriminatorByValue = require('../../helpers/discriminator/getDiscriminatorByValue'); const applyTimestampsToChildren = require('../update/applyTimestampsToChildren'); const applyTimestampsToUpdate = require('../update/applyTimestampsToUpdate'); const cast = require('../../cast'); const castUpdate = require('../query/castUpdate'); const clone = require('../clone'); const decorateUpdateWithVersionKey = require('../update/decorateUpdateWithVersionKey'); const { inspect } = require('util'); const setDefaultsOnInsert = require('../setDefaultsOnInsert'); /** * Given a model and a bulkWrite op, return a thunk that handles casting and * validating the individual op. * @param {Model} originalModel * @param {Object} op * @param {Object} [options] * @api private */ module.exports = function castBulkWrite(originalModel, op, options) { const now = originalModel.base.now(); if (op['insertOne']) { return callback => module.exports.castInsertOne(originalModel, op['insertOne'], options).then(() => callback(null), err => callback(err)); } else if (op['updateOne']) { return (callback) => { try { module.exports.castUpdateOne(originalModel, op['updateOne'], options, now); callback(null); } catch (err) { callback(err); } }; } else if (op['updateMany']) { return (callback) => { try { module.exports.castUpdateMany(originalModel, op['updateMany'], options, now); callback(null); } catch (err) { callback(err); } }; } else if (op['replaceOne']) { return (callback) => { module.exports.castReplaceOne(originalModel, op['replaceOne'], options).then(() => callback(null), err => callback(err)); }; } else if (op['deleteOne']) { return (callback) => { try { module.exports.castDeleteOne(originalModel, op['deleteOne']); callback(null); } catch (err) { callback(err); } }; } else if (op['deleteMany']) { return (callback) => { try { module.exports.castDeleteMany(originalModel, op['deleteMany']); callback(null); } catch (err) { callback(err); } }; } else { return (callback) => { const error = new MongooseError(`Invalid op passed to \`bulkWrite()\`: ${inspect(op)}`); callback(error, null); }; } }; module.exports.castInsertOne = async function castInsertOne(originalModel, insertOne, options) { const model = decideModelByObject(originalModel, insertOne['document']); const doc = new model(insertOne['document']); if (model.schema.options.timestamps && getTimestampsOpt(insertOne, options)) { doc.initializeTimestamps(); } if (options.session != null) { doc.$session(options.session); } const versionKey = model?.schema?.options?.versionKey; if (versionKey && doc[versionKey] == null) { doc[versionKey] = 0; } insertOne['document'] = doc; if (options.skipValidation || insertOne.skipValidation) { return insertOne; } await insertOne['document'].$validate(); return insertOne; }; module.exports.castUpdateOne = function castUpdateOne(originalModel, updateOne, options, now) { if (!updateOne['filter']) { throw new Error('Must provide a filter object.'); } if (!updateOne['update']) { throw new Error('Must provide an update object.'); } const model = decideModelByObject(originalModel, updateOne['filter']); const schema = model.schema; const strict = options.strict != null ? options.strict : model.schema.options.strict; const update = clone(updateOne['update']); _addDiscriminatorToObject(schema, updateOne['filter']); const doInitTimestamps = getTimestampsOpt(updateOne, options); if (model.schema.$timestamps != null && doInitTimestamps) { const createdAt = model.schema.$timestamps.createdAt; const updatedAt = model.schema.$timestamps.updatedAt; applyTimestampsToUpdate(now, createdAt, updatedAt, update, {}); } if (doInitTimestamps) { applyTimestampsToChildren(now, update, model.schema); } const globalSetDefaultsOnInsert = originalModel.base.options.setDefaultsOnInsert; const shouldSetDefaultsOnInsert = updateOne.setDefaultsOnInsert == null ? globalSetDefaultsOnInsert : updateOne.setDefaultsOnInsert; if (shouldSetDefaultsOnInsert !== false) { setDefaultsOnInsert(updateOne['filter'], model.schema, update, { setDefaultsOnInsert: true, upsert: updateOne.upsert }); } decorateUpdateWithVersionKey( update, updateOne, model.schema.options.versionKey ); updateOne['filter'] = cast(model.schema, updateOne['filter'], { strict: strict, upsert: updateOne.upsert }); updateOne['update'] = castUpdate(model.schema, update, { strict: strict, upsert: updateOne.upsert, arrayFilters: updateOne.arrayFilters, overwriteDiscriminatorKey: updateOne.overwriteDiscriminatorKey }, model, updateOne['filter']); return updateOne; }; module.exports.castUpdateMany = function castUpdateMany(originalModel, updateMany, options, now) { if (!updateMany['filter']) { throw new Error('Must provide a filter object.'); } if (!updateMany['update']) { throw new Error('Must provide an update object.'); } const model = decideModelByObject(originalModel, updateMany['filter']); const schema = model.schema; const strict = options.strict != null ? options.strict : model.schema.options.strict; const globalSetDefaultsOnInsert = originalModel.base.options.setDefaultsOnInsert; const shouldSetDefaultsOnInsert = updateMany.setDefaultsOnInsert == null ? globalSetDefaultsOnInsert : updateMany.setDefaultsOnInsert; if (shouldSetDefaultsOnInsert !== false) { setDefaultsOnInsert(updateMany['filter'], model.schema, updateMany['update'], { setDefaultsOnInsert: true, upsert: updateMany.upsert }); } const doInitTimestamps = getTimestampsOpt(updateMany, options); if (model.schema.$timestamps != null && doInitTimestamps) { const createdAt = model.schema.$timestamps.createdAt; const updatedAt = model.schema.$timestamps.updatedAt; applyTimestampsToUpdate(now, createdAt, updatedAt, updateMany['update'], {}); } if (doInitTimestamps) { applyTimestampsToChildren(now, updateMany['update'], model.schema); } _addDiscriminatorToObject(schema, updateMany['filter']); decorateUpdateWithVersionKey( updateMany['update'], updateMany, model.schema.options.versionKey ); updateMany['filter'] = cast(model.schema, updateMany['filter'], { strict: strict, upsert: updateMany.upsert }); updateMany['update'] = castUpdate(model.schema, updateMany['update'], { strict: strict, upsert: updateMany.upsert, arrayFilters: updateMany.arrayFilters, overwriteDiscriminatorKey: updateMany.overwriteDiscriminatorKey }, model, updateMany['filter']); }; module.exports.castReplaceOne = async function castReplaceOne(originalModel, replaceOne, options) { const model = decideModelByObject(originalModel, replaceOne['filter']); const schema = model.schema; const strict = options.strict != null ? options.strict : model.schema.options.strict; _addDiscriminatorToObject(schema, replaceOne['filter']); replaceOne['filter'] = cast(model.schema, replaceOne['filter'], { strict: strict, upsert: replaceOne.upsert }); // set `skipId`, otherwise we get "_id field cannot be changed" const doc = new model(replaceOne['replacement'], strict, true); if (model.schema.options.timestamps && getTimestampsOpt(replaceOne, options)) { doc.initializeTimestamps(); } if (options.session != null) { doc.$session(options.session); } const versionKey = model?.schema?.options?.versionKey; if (versionKey && doc[versionKey] == null) { doc[versionKey] = 0; } replaceOne['replacement'] = doc; if (options.skipValidation || replaceOne.skipValidation) { replaceOne['replacement'] = replaceOne['replacement'].toBSON(); return; } await replaceOne['replacement'].$validate(); replaceOne['replacement'] = replaceOne['replacement'].toBSON(); }; module.exports.castDeleteOne = function castDeleteOne(originalModel, deleteOne) { const model = decideModelByObject(originalModel, deleteOne['filter']); const schema = model.schema; _addDiscriminatorToObject(schema, deleteOne['filter']); deleteOne['filter'] = cast(model.schema, deleteOne['filter']); }; module.exports.castDeleteMany = function castDeleteMany(originalModel, deleteMany) { const model = decideModelByObject(originalModel, deleteMany['filter']); const schema = model.schema; _addDiscriminatorToObject(schema, deleteMany['filter']); deleteMany['filter'] = cast(model.schema, deleteMany['filter']); }; module.exports.cast = { insertOne: module.exports.castInsertOne, updateOne: module.exports.castUpdateOne, updateMany: module.exports.castUpdateMany, replaceOne: module.exports.castReplaceOne, deleteOne: module.exports.castDeleteOne, deleteMany: module.exports.castDeleteMany }; function _addDiscriminatorToObject(schema, obj) { if (schema == null) { return; } if (schema.discriminatorMapping && !schema.discriminatorMapping.isRoot) { obj[schema.discriminatorMapping.key] = schema.discriminatorMapping.value; } } /** * gets discriminator model if discriminator key is present in object * @api private */ function decideModelByObject(model, object) { const discriminatorKey = model.schema.options.discriminatorKey; if (object != null && object.hasOwnProperty(discriminatorKey)) { model = getDiscriminatorByValue(model.discriminators, object[discriminatorKey]) || model; } return model; } /** * gets timestamps option for a given operation. If the option is set within an individual operation, use it. Otherwise, use the global timestamps option configured in the `bulkWrite` options. Overall default is `true`. * @api private */ function getTimestampsOpt(opCommand, options) { const opLevelOpt = opCommand.timestamps; const bulkLevelOpt = options.timestamps; if (opLevelOpt != null) { return opLevelOpt; } else if (bulkLevelOpt != null) { return bulkLevelOpt; } return true; }