Skip to content

Commit 31ae3c9

Browse files
authored
fix: assert update/replace atomic requirements in bulk operations
`checkForAtomicOperators` was refactored into the more versatile `hasAtomicOperators` and the logic for asserting atomic operator requirements was streamlined across all crud operations as well as bulk operations. NODE-2660
1 parent 188c23e commit 31ae3c9

36 files changed

+1054
-402
lines changed

lib/bulk/common.js

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ const applyRetryableWrites = require('../utils').applyRetryableWrites;
1111
const applyWriteConcern = require('../utils').applyWriteConcern;
1212
const executeLegacyOperation = require('../utils').executeLegacyOperation;
1313
const isPromiseLike = require('../utils').isPromiseLike;
14+
const hasAtomicOperators = require('../utils').hasAtomicOperators;
15+
const maxWireVersion = require('../core/utils').maxWireVersion;
1416

1517
// Error codes
1618
const WRITE_CONCERN_ERROR = 64;
@@ -641,6 +643,10 @@ class FindOperators {
641643
document.hint = updateDocument.hint;
642644
}
643645

646+
if (!hasAtomicOperators(updateDocument)) {
647+
throw new TypeError('Update document requires atomic operators');
648+
}
649+
644650
// Clear out current Op
645651
this.s.currentOp = null;
646652
return this.s.options.addToOperationsList(this, UPDATE, document);
@@ -650,12 +656,33 @@ class FindOperators {
650656
* Add a replace one operation to the bulk operation
651657
*
652658
* @method
653-
* @param {object} updateDocument the new document to replace the existing one with
659+
* @param {object} replacement the new document to replace the existing one with
654660
* @throws {MongoError} If operation cannot be added to bulk write
655661
* @return {OrderedBulkOperation|UnorderedBulkOperation} A reference to the parent BulkOperation
656662
*/
657-
replaceOne(updateDocument) {
658-
this.updateOne(updateDocument);
663+
replaceOne(replacement) {
664+
// Perform upsert
665+
const upsert = typeof this.s.currentOp.upsert === 'boolean' ? this.s.currentOp.upsert : false;
666+
667+
// Establish the update command
668+
const document = {
669+
q: this.s.currentOp.selector,
670+
u: replacement,
671+
multi: false,
672+
upsert: upsert
673+
};
674+
675+
if (replacement.hint) {
676+
document.hint = replacement.hint;
677+
}
678+
679+
if (hasAtomicOperators(replacement)) {
680+
throw new TypeError('Replacement document must not use atomic operators');
681+
}
682+
683+
// Clear out current Op
684+
this.s.currentOp = null;
685+
return this.s.options.addToOperationsList(this, UPDATE, document);
659686
}
660687

661688
/**
@@ -943,6 +970,12 @@ class BulkOperationBase {
943970

944971
// Crud spec update format
945972
if (op.updateOne || op.updateMany || op.replaceOne) {
973+
if (op.replaceOne && hasAtomicOperators(op[key].replacement)) {
974+
throw new TypeError('Replacement document must not use atomic operators');
975+
} else if ((op.updateOne || op.updateMany) && !hasAtomicOperators(op[key].update)) {
976+
throw new TypeError('Update document requires atomic operators');
977+
}
978+
946979
const multi = op.updateOne || op.replaceOne ? false : true;
947980
const operation = {
948981
q: op[key].filter,
@@ -960,7 +993,15 @@ class BulkOperationBase {
960993
} else {
961994
if (op[key].upsert) operation.upsert = true;
962995
}
963-
if (op[key].arrayFilters) operation.arrayFilters = op[key].arrayFilters;
996+
if (op[key].arrayFilters) {
997+
// TODO: this check should be done at command construction against a connection, not a topology
998+
if (maxWireVersion(this.s.topology) < 6) {
999+
throw new TypeError('arrayFilters are only supported on MongoDB 3.6+');
1000+
}
1001+
1002+
operation.arrayFilters = op[key].arrayFilters;
1003+
}
1004+
9641005
return this.s.options.addToOperationsList(this, UPDATE, operation);
9651006
}
9661007

lib/collection.js

Lines changed: 29 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ const deprecateOptions = require('./utils').deprecateOptions;
55
const checkCollectionName = require('./utils').checkCollectionName;
66
const ObjectID = require('./core').BSON.ObjectID;
77
const MongoError = require('./core').MongoError;
8-
const toError = require('./utils').toError;
98
const normalizeHintField = require('./utils').normalizeHintField;
109
const decorateCommand = require('./utils').decorateCommand;
1110
const decorateWithCollation = require('./utils').decorateWithCollation;
@@ -23,7 +22,6 @@ const AggregationCursor = require('./aggregation_cursor');
2322
const CommandCursor = require('./command_cursor');
2423

2524
// Operations
26-
const checkForAtomicOperators = require('./operations/collection_ops').checkForAtomicOperators;
2725
const ensureIndex = require('./operations/collection_ops').ensureIndex;
2826
const group = require('./operations/collection_ops').group;
2927
const parallelCollectionScan = require('./operations/collection_ops').parallelCollectionScan;
@@ -747,14 +745,6 @@ Collection.prototype.insert = deprecate(function(docs, options, callback) {
747745
*/
748746
Collection.prototype.updateOne = function(filter, update, options, callback) {
749747
if (typeof options === 'function') (callback = options), (options = {});
750-
options = options || {};
751-
752-
const err = checkForAtomicOperators(update);
753-
if (err) {
754-
if (typeof callback === 'function') return callback(err);
755-
return this.s.promiseLibrary.reject(err);
756-
}
757-
758748
options = Object.assign({}, options);
759749

760750
// Add ignoreUndefined
@@ -763,9 +753,11 @@ Collection.prototype.updateOne = function(filter, update, options, callback) {
763753
options.ignoreUndefined = this.s.options.ignoreUndefined;
764754
}
765755

766-
const updateOneOperation = new UpdateOneOperation(this, filter, update, options);
767-
768-
return executeOperation(this.s.topology, updateOneOperation, callback);
756+
return executeOperation(
757+
this.s.topology,
758+
new UpdateOneOperation(this, filter, update, options),
759+
callback
760+
);
769761
};
770762

771763
/**
@@ -798,9 +790,11 @@ Collection.prototype.replaceOne = function(filter, doc, options, callback) {
798790
options.ignoreUndefined = this.s.options.ignoreUndefined;
799791
}
800792

801-
const replaceOneOperation = new ReplaceOneOperation(this, filter, doc, options);
802-
803-
return executeOperation(this.s.topology, replaceOneOperation, callback);
793+
return executeOperation(
794+
this.s.topology,
795+
new ReplaceOneOperation(this, filter, doc, options),
796+
callback
797+
);
804798
};
805799

806800
/**
@@ -826,14 +820,6 @@ Collection.prototype.replaceOne = function(filter, doc, options, callback) {
826820
*/
827821
Collection.prototype.updateMany = function(filter, update, options, callback) {
828822
if (typeof options === 'function') (callback = options), (options = {});
829-
options = options || {};
830-
831-
const err = checkForAtomicOperators(update);
832-
if (err) {
833-
if (typeof callback === 'function') return callback(err);
834-
return this.s.promiseLibrary.reject(err);
835-
}
836-
837823
options = Object.assign({}, options);
838824

839825
// Add ignoreUndefined
@@ -842,9 +828,11 @@ Collection.prototype.updateMany = function(filter, update, options, callback) {
842828
options.ignoreUndefined = this.s.options.ignoreUndefined;
843829
}
844830

845-
const updateManyOperation = new UpdateManyOperation(this, filter, update, options);
846-
847-
return executeOperation(this.s.topology, updateManyOperation, callback);
831+
return executeOperation(
832+
this.s.topology,
833+
new UpdateManyOperation(this, filter, update, options),
834+
callback
835+
);
848836
};
849837

850838
/**
@@ -1679,13 +1667,11 @@ Collection.prototype.findOneAndDelete = function(filter, options, callback) {
16791667
if (typeof options === 'function') (callback = options), (options = {});
16801668
options = options || {};
16811669

1682-
// Basic validation
1683-
if (filter == null || typeof filter !== 'object')
1684-
throw toError('filter parameter must be an object');
1685-
1686-
const findOneAndDeleteOperation = new FindOneAndDeleteOperation(this, filter, options);
1687-
1688-
return executeOperation(this.s.topology, findOneAndDeleteOperation, callback);
1670+
return executeOperation(
1671+
this.s.topology,
1672+
new FindOneAndDeleteOperation(this, filter, options),
1673+
callback
1674+
);
16891675
};
16901676

16911677
/**
@@ -1714,27 +1700,11 @@ Collection.prototype.findOneAndReplace = function(filter, replacement, options,
17141700
if (typeof options === 'function') (callback = options), (options = {});
17151701
options = options || {};
17161702

1717-
// Basic validation
1718-
if (filter == null || typeof filter !== 'object')
1719-
throw toError('filter parameter must be an object');
1720-
if (replacement == null || typeof replacement !== 'object')
1721-
throw toError('replacement parameter must be an object');
1722-
1723-
// Check that there are no atomic operators
1724-
const keys = Object.keys(replacement);
1725-
1726-
if (keys[0] && keys[0][0] === '$') {
1727-
throw toError('The replacement document must not contain atomic operators.');
1728-
}
1729-
1730-
const findOneAndReplaceOperation = new FindOneAndReplaceOperation(
1731-
this,
1732-
filter,
1733-
replacement,
1734-
options
1703+
return executeOperation(
1704+
this.s.topology,
1705+
new FindOneAndReplaceOperation(this, filter, replacement, options),
1706+
callback
17351707
);
1736-
1737-
return executeOperation(this.s.topology, findOneAndReplaceOperation, callback);
17381708
};
17391709

17401710
/**
@@ -1764,21 +1734,11 @@ Collection.prototype.findOneAndUpdate = function(filter, update, options, callba
17641734
if (typeof options === 'function') (callback = options), (options = {});
17651735
options = options || {};
17661736

1767-
// Basic validation
1768-
if (filter == null || typeof filter !== 'object')
1769-
throw toError('filter parameter must be an object');
1770-
if (update == null || typeof update !== 'object')
1771-
throw toError('update parameter must be an object');
1772-
1773-
const err = checkForAtomicOperators(update);
1774-
if (err) {
1775-
if (typeof callback === 'function') return callback(err);
1776-
return this.s.promiseLibrary.reject(err);
1777-
}
1778-
1779-
const findOneAndUpdateOperation = new FindOneAndUpdateOperation(this, filter, update, options);
1780-
1781-
return executeOperation(this.s.topology, findOneAndUpdateOperation, callback);
1737+
return executeOperation(
1738+
this.s.topology,
1739+
new FindOneAndUpdateOperation(this, filter, update, options),
1740+
callback
1741+
);
17821742
};
17831743

17841744
/**

lib/operations/collection_ops.js

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ const indexInformationDb = require('./db_ops').indexInformation;
1313
const Long = require('../core').BSON.Long;
1414
const MongoError = require('../core').MongoError;
1515
const ReadPreference = require('../core').ReadPreference;
16-
const toError = require('../utils').toError;
1716
const insertDocuments = require('./common_functions').insertDocuments;
1817
const updateDocuments = require('./common_functions').updateDocuments;
1918

@@ -51,24 +50,6 @@ const updateDocuments = require('./common_functions').updateDocuments;
5150
const groupFunction =
5251
'function () {\nvar c = db[ns].find(condition);\nvar map = new Map();\nvar reduce_function = reduce;\n\nwhile (c.hasNext()) {\nvar obj = c.next();\nvar key = {};\n\nfor (var i = 0, len = keys.length; i < len; ++i) {\nvar k = keys[i];\nkey[k] = obj[k];\n}\n\nvar aggObj = map.get(key);\n\nif (aggObj == null) {\nvar newObj = Object.extend({}, key);\naggObj = Object.extend(newObj, initial);\nmap.put(key, aggObj);\n}\n\nreduce_function(obj, aggObj);\n}\n\nreturn { "result": map.values() };\n}';
5352

54-
// Check the update operation to ensure it has atomic operators.
55-
function checkForAtomicOperators(update) {
56-
if (Array.isArray(update)) {
57-
return update.reduce((err, u) => err || checkForAtomicOperators(u), null);
58-
}
59-
60-
const keys = Object.keys(update);
61-
62-
// same errors as the server would give for update doc lacking atomic operators
63-
if (keys.length === 0) {
64-
return toError('The update operation document must contain at least one atomic operator.');
65-
}
66-
67-
if (keys[0][0] !== '$') {
68-
return toError('the update operation document must contain atomic operators.');
69-
}
70-
}
71-
7253
/**
7354
* Create an index on the db and collection.
7455
*
@@ -360,7 +341,6 @@ function save(coll, doc, options, callback) {
360341
}
361342

362343
module.exports = {
363-
checkForAtomicOperators,
364344
createIndex,
365345
createIndexes,
366346
ensureIndex,

lib/operations/find_one_and_delete.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ class FindOneAndDeleteOperation extends FindAndModifyOperation {
99
finalOptions.fields = options.projection;
1010
finalOptions.remove = true;
1111

12+
// Basic validation
13+
if (filter == null || typeof filter !== 'object') {
14+
throw new TypeError('Filter parameter must be an object');
15+
}
16+
1217
super(collection, filter, finalOptions.sort, null, finalOptions);
1318
}
1419
}

lib/operations/find_one_and_replace.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
22

33
const FindAndModifyOperation = require('./find_and_modify');
4+
const hasAtomicOperators = require('../utils').hasAtomicOperators;
45

56
class FindOneAndReplaceOperation extends FindAndModifyOperation {
67
constructor(collection, filter, replacement, options) {
@@ -11,6 +12,18 @@ class FindOneAndReplaceOperation extends FindAndModifyOperation {
1112
finalOptions.new = options.returnOriginal !== void 0 ? !options.returnOriginal : false;
1213
finalOptions.upsert = options.upsert !== void 0 ? !!options.upsert : false;
1314

15+
if (filter == null || typeof filter !== 'object') {
16+
throw new TypeError('Filter parameter must be an object');
17+
}
18+
19+
if (replacement == null || typeof replacement !== 'object') {
20+
throw new TypeError('Replacement parameter must be an object');
21+
}
22+
23+
if (hasAtomicOperators(replacement)) {
24+
throw new TypeError('Replacement document must not contain atomic operators');
25+
}
26+
1427
super(collection, filter, finalOptions.sort, replacement, finalOptions);
1528
}
1629
}

lib/operations/find_one_and_update.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
22

33
const FindAndModifyOperation = require('./find_and_modify');
4+
const { hasAtomicOperators } = require('../utils');
45

56
class FindOneAndUpdateOperation extends FindAndModifyOperation {
67
constructor(collection, filter, update, options) {
@@ -12,6 +13,18 @@ class FindOneAndUpdateOperation extends FindAndModifyOperation {
1213
typeof options.returnOriginal === 'boolean' ? !options.returnOriginal : false;
1314
finalOptions.upsert = typeof options.upsert === 'boolean' ? options.upsert : false;
1415

16+
if (filter == null || typeof filter !== 'object') {
17+
throw new TypeError('Filter parameter must be an object');
18+
}
19+
20+
if (update == null || typeof update !== 'object') {
21+
throw new TypeError('Update parameter must be an object');
22+
}
23+
24+
if (!hasAtomicOperators(update)) {
25+
throw new TypeError('Update document requires atomic operators');
26+
}
27+
1528
super(collection, filter, finalOptions.sort, update, finalOptions);
1629
}
1730
}

0 commit comments

Comments
 (0)