castBulkWrite.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. 'use strict';
  2. const MongooseError = require('../../error/mongooseError');
  3. const getDiscriminatorByValue = require('../../helpers/discriminator/getDiscriminatorByValue');
  4. const applyTimestampsToChildren = require('../update/applyTimestampsToChildren');
  5. const applyTimestampsToUpdate = require('../update/applyTimestampsToUpdate');
  6. const cast = require('../../cast');
  7. const castUpdate = require('../query/castUpdate');
  8. const clone = require('../clone');
  9. const decorateUpdateWithVersionKey = require('../update/decorateUpdateWithVersionKey');
  10. const { inspect } = require('util');
  11. const setDefaultsOnInsert = require('../setDefaultsOnInsert');
  12. /**
  13. * Given a model and a bulkWrite op, return a thunk that handles casting and
  14. * validating the individual op.
  15. * @param {Model} originalModel
  16. * @param {Object} op
  17. * @param {Object} [options]
  18. * @api private
  19. */
  20. module.exports = function castBulkWrite(originalModel, op, options) {
  21. const now = originalModel.base.now();
  22. if (op['insertOne']) {
  23. return callback => module.exports.castInsertOne(originalModel, op['insertOne'], options).then(() => callback(null), err => callback(err));
  24. } else if (op['updateOne']) {
  25. return (callback) => {
  26. try {
  27. module.exports.castUpdateOne(originalModel, op['updateOne'], options, now);
  28. callback(null);
  29. } catch (err) {
  30. callback(err);
  31. }
  32. };
  33. } else if (op['updateMany']) {
  34. return (callback) => {
  35. try {
  36. module.exports.castUpdateMany(originalModel, op['updateMany'], options, now);
  37. callback(null);
  38. } catch (err) {
  39. callback(err);
  40. }
  41. };
  42. } else if (op['replaceOne']) {
  43. return (callback) => {
  44. module.exports.castReplaceOne(originalModel, op['replaceOne'], options).then(() => callback(null), err => callback(err));
  45. };
  46. } else if (op['deleteOne']) {
  47. return (callback) => {
  48. try {
  49. module.exports.castDeleteOne(originalModel, op['deleteOne']);
  50. callback(null);
  51. } catch (err) {
  52. callback(err);
  53. }
  54. };
  55. } else if (op['deleteMany']) {
  56. return (callback) => {
  57. try {
  58. module.exports.castDeleteMany(originalModel, op['deleteMany']);
  59. callback(null);
  60. } catch (err) {
  61. callback(err);
  62. }
  63. };
  64. } else {
  65. return (callback) => {
  66. const error = new MongooseError(`Invalid op passed to \`bulkWrite()\`: ${inspect(op)}`);
  67. callback(error, null);
  68. };
  69. }
  70. };
  71. module.exports.castInsertOne = async function castInsertOne(originalModel, insertOne, options) {
  72. const model = decideModelByObject(originalModel, insertOne['document']);
  73. const doc = new model(insertOne['document']);
  74. if (model.schema.options.timestamps && getTimestampsOpt(insertOne, options)) {
  75. doc.initializeTimestamps();
  76. }
  77. if (options.session != null) {
  78. doc.$session(options.session);
  79. }
  80. const versionKey = model?.schema?.options?.versionKey;
  81. if (versionKey && doc[versionKey] == null) {
  82. doc[versionKey] = 0;
  83. }
  84. insertOne['document'] = doc;
  85. if (options.skipValidation || insertOne.skipValidation) {
  86. return insertOne;
  87. }
  88. await insertOne['document'].$validate();
  89. return insertOne;
  90. };
  91. module.exports.castUpdateOne = function castUpdateOne(originalModel, updateOne, options, now) {
  92. if (!updateOne['filter']) {
  93. throw new Error('Must provide a filter object.');
  94. }
  95. if (!updateOne['update']) {
  96. throw new Error('Must provide an update object.');
  97. }
  98. const model = decideModelByObject(originalModel, updateOne['filter']);
  99. const schema = model.schema;
  100. const strict = options.strict ?? model.schema.options.strict;
  101. const update = clone(updateOne['update']);
  102. _addDiscriminatorToObject(schema, updateOne['filter']);
  103. const doInitTimestamps = getTimestampsOpt(updateOne, options);
  104. if (model.schema.$timestamps != null && doInitTimestamps) {
  105. const createdAt = model.schema.$timestamps.createdAt;
  106. const updatedAt = model.schema.$timestamps.updatedAt;
  107. applyTimestampsToUpdate(now, createdAt, updatedAt, update, {
  108. timestamps: updateOne.timestamps,
  109. overwriteImmutable: updateOne.overwriteImmutable
  110. });
  111. }
  112. if (doInitTimestamps) {
  113. applyTimestampsToChildren(now, update, model.schema);
  114. }
  115. const globalSetDefaultsOnInsert = originalModel.base.options.setDefaultsOnInsert;
  116. const shouldSetDefaultsOnInsert = updateOne.setDefaultsOnInsert ?? globalSetDefaultsOnInsert;
  117. if (shouldSetDefaultsOnInsert !== false) {
  118. setDefaultsOnInsert(updateOne['filter'], model.schema, update, {
  119. setDefaultsOnInsert: true,
  120. upsert: updateOne.upsert
  121. });
  122. }
  123. decorateUpdateWithVersionKey(
  124. update,
  125. updateOne,
  126. model.schema.options.versionKey
  127. );
  128. updateOne['filter'] = cast(model.schema, updateOne['filter'], {
  129. strict: strict,
  130. upsert: updateOne.upsert
  131. });
  132. updateOne['update'] = castUpdate(model.schema, update, {
  133. strict: strict,
  134. upsert: updateOne.upsert,
  135. arrayFilters: updateOne.arrayFilters,
  136. overwriteDiscriminatorKey: updateOne.overwriteDiscriminatorKey,
  137. overwriteImmutable: updateOne.overwriteImmutable
  138. }, model, updateOne['filter']);
  139. return updateOne;
  140. };
  141. module.exports.castUpdateMany = function castUpdateMany(originalModel, updateMany, options, now) {
  142. if (!updateMany['filter']) {
  143. throw new Error('Must provide a filter object.');
  144. }
  145. if (!updateMany['update']) {
  146. throw new Error('Must provide an update object.');
  147. }
  148. const model = decideModelByObject(originalModel, updateMany['filter']);
  149. const schema = model.schema;
  150. const strict = options.strict ?? model.schema.options.strict;
  151. const globalSetDefaultsOnInsert = originalModel.base.options.setDefaultsOnInsert;
  152. const shouldSetDefaultsOnInsert = updateMany.setDefaultsOnInsert ?? globalSetDefaultsOnInsert;
  153. if (shouldSetDefaultsOnInsert !== false) {
  154. setDefaultsOnInsert(updateMany['filter'], model.schema, updateMany['update'], {
  155. setDefaultsOnInsert: true,
  156. upsert: updateMany.upsert
  157. });
  158. }
  159. const doInitTimestamps = getTimestampsOpt(updateMany, options);
  160. if (model.schema.$timestamps != null && doInitTimestamps) {
  161. const createdAt = model.schema.$timestamps.createdAt;
  162. const updatedAt = model.schema.$timestamps.updatedAt;
  163. applyTimestampsToUpdate(now, createdAt, updatedAt, updateMany['update'], {
  164. timestamps: updateMany.timestamps,
  165. overwriteImmutable: updateMany.overwriteImmutable
  166. });
  167. }
  168. if (doInitTimestamps) {
  169. applyTimestampsToChildren(now, updateMany['update'], model.schema);
  170. }
  171. _addDiscriminatorToObject(schema, updateMany['filter']);
  172. decorateUpdateWithVersionKey(
  173. updateMany['update'],
  174. updateMany,
  175. model.schema.options.versionKey
  176. );
  177. updateMany['filter'] = cast(model.schema, updateMany['filter'], {
  178. strict: strict,
  179. upsert: updateMany.upsert
  180. });
  181. updateMany['update'] = castUpdate(model.schema, updateMany['update'], {
  182. strict: strict,
  183. upsert: updateMany.upsert,
  184. arrayFilters: updateMany.arrayFilters,
  185. overwriteDiscriminatorKey: updateMany.overwriteDiscriminatorKey,
  186. overwriteImmutable: updateMany.overwriteImmutable
  187. }, model, updateMany['filter']);
  188. };
  189. module.exports.castReplaceOne = async function castReplaceOne(originalModel, replaceOne, options) {
  190. const model = decideModelByObject(originalModel, replaceOne['filter']);
  191. const schema = model.schema;
  192. const strict = options.strict ?? model.schema.options.strict;
  193. _addDiscriminatorToObject(schema, replaceOne['filter']);
  194. replaceOne['filter'] = cast(model.schema, replaceOne['filter'], {
  195. strict: strict,
  196. upsert: replaceOne.upsert
  197. });
  198. // set `skipId`, otherwise we get "_id field cannot be changed"
  199. const doc = new model(replaceOne['replacement'], strict, { skipId: true });
  200. if (model.schema.options.timestamps && getTimestampsOpt(replaceOne, options)) {
  201. doc.initializeTimestamps();
  202. }
  203. if (options.session != null) {
  204. doc.$session(options.session);
  205. }
  206. const versionKey = model?.schema?.options?.versionKey;
  207. if (versionKey && doc[versionKey] == null) {
  208. doc[versionKey] = 0;
  209. }
  210. replaceOne['replacement'] = doc;
  211. if (options.skipValidation || replaceOne.skipValidation) {
  212. replaceOne['replacement'] = replaceOne['replacement'].toBSON();
  213. return;
  214. }
  215. await replaceOne['replacement'].$validate();
  216. replaceOne['replacement'] = replaceOne['replacement'].toBSON();
  217. };
  218. module.exports.castDeleteOne = function castDeleteOne(originalModel, deleteOne) {
  219. const model = decideModelByObject(originalModel, deleteOne['filter']);
  220. const schema = model.schema;
  221. _addDiscriminatorToObject(schema, deleteOne['filter']);
  222. deleteOne['filter'] = cast(model.schema, deleteOne['filter']);
  223. };
  224. module.exports.castDeleteMany = function castDeleteMany(originalModel, deleteMany) {
  225. const model = decideModelByObject(originalModel, deleteMany['filter']);
  226. const schema = model.schema;
  227. _addDiscriminatorToObject(schema, deleteMany['filter']);
  228. deleteMany['filter'] = cast(model.schema, deleteMany['filter']);
  229. };
  230. module.exports.cast = {
  231. insertOne: module.exports.castInsertOne,
  232. updateOne: module.exports.castUpdateOne,
  233. updateMany: module.exports.castUpdateMany,
  234. replaceOne: module.exports.castReplaceOne,
  235. deleteOne: module.exports.castDeleteOne,
  236. deleteMany: module.exports.castDeleteMany
  237. };
  238. function _addDiscriminatorToObject(schema, obj) {
  239. if (schema == null) {
  240. return;
  241. }
  242. if (schema.discriminatorMapping && !schema.discriminatorMapping.isRoot) {
  243. obj[schema.discriminatorMapping.key] = schema.discriminatorMapping.value;
  244. }
  245. }
  246. /**
  247. * gets discriminator model if discriminator key is present in object
  248. * @api private
  249. */
  250. function decideModelByObject(model, object) {
  251. const discriminatorKey = model.schema.options.discriminatorKey;
  252. if (object != null && Object.hasOwn(object, discriminatorKey)) {
  253. model = getDiscriminatorByValue(model.discriminators, object[discriminatorKey]) || model;
  254. }
  255. return model;
  256. }
  257. /**
  258. * 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`.
  259. * @api private
  260. */
  261. function getTimestampsOpt(opCommand, options) {
  262. const opLevelOpt = opCommand.timestamps;
  263. const bulkLevelOpt = options.timestamps;
  264. if (opLevelOpt != null) {
  265. return opLevelOpt;
  266. } else if (bulkLevelOpt != null) {
  267. return bulkLevelOpt;
  268. }
  269. return true;
  270. }