Skip to content

Commit 911c25d

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 49d45b3 commit 911c25d

32 files changed

+1128
-488
lines changed

src/bulk/common.ts

+47-6
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@ import {
77
applyWriteConcern,
88
applyRetryableWrites,
99
executeLegacyOperation,
10-
isPromiseLike
10+
isPromiseLike,
11+
hasAtomicOperators,
12+
maxWireVersion
1113
} from '../utils';
14+
1215
// Error codes
1316
const WRITE_CONCERN_ERROR = 64;
1417

@@ -663,6 +666,10 @@ class FindOperators {
663666
document.hint = updateDocument.hint;
664667
}
665668

669+
if (!hasAtomicOperators(updateDocument)) {
670+
throw new TypeError('Update document requires atomic operators');
671+
}
672+
666673
// Clear out current Op
667674
this.s.currentOp = null;
668675
return this.s.options.addToOperationsList(this, UPDATE, document);
@@ -671,13 +678,33 @@ class FindOperators {
671678
/**
672679
* Add a replace one operation to the bulk operation
673680
*
674-
* @function
675-
* @param {object} updateDocument the new document to replace the existing one with
681+
* @param {object} replacement the new document to replace the existing one with
676682
* @throws {MongoError} If operation cannot be added to bulk write
677683
* @returns {void} A reference to the parent BulkOperation
678684
*/
679-
replaceOne(updateDocument: object): void {
680-
this.updateOne(updateDocument);
685+
replaceOne(replacement: any) {
686+
// Perform upsert
687+
const upsert = typeof this.s.currentOp.upsert === 'boolean' ? this.s.currentOp.upsert : false;
688+
689+
// Establish the update command
690+
const document = {
691+
q: this.s.currentOp.selector,
692+
u: replacement,
693+
multi: false,
694+
upsert: upsert
695+
} as any;
696+
697+
if (replacement.hint) {
698+
document.hint = replacement.hint;
699+
}
700+
701+
if (hasAtomicOperators(replacement)) {
702+
throw new TypeError('Replacement document must not use atomic operators');
703+
}
704+
705+
// Clear out current Op
706+
this.s.currentOp = null;
707+
return this.s.options.addToOperationsList(this, UPDATE, document);
681708
}
682709

683710
/**
@@ -965,6 +992,12 @@ class BulkOperationBase {
965992

966993
// Crud spec update format
967994
if (op.updateOne || op.updateMany || op.replaceOne) {
995+
if (op.replaceOne && hasAtomicOperators(op[key].replacement)) {
996+
throw new TypeError('Replacement document must not use atomic operators');
997+
} else if ((op.updateOne || op.updateMany) && !hasAtomicOperators(op[key].update)) {
998+
throw new TypeError('Update document requires atomic operators');
999+
}
1000+
9681001
const multi = op.updateOne || op.replaceOne ? false : true;
9691002
const operation = {
9701003
q: op[key].filter,
@@ -982,7 +1015,15 @@ class BulkOperationBase {
9821015
} else {
9831016
if (op[key].upsert) operation.upsert = true;
9841017
}
985-
if (op[key].arrayFilters) operation.arrayFilters = op[key].arrayFilters;
1018+
if (op[key].arrayFilters) {
1019+
// TODO: this check should be done at command construction against a connection, not a topology
1020+
if (maxWireVersion(this.s.topology) < 6) {
1021+
throw new TypeError('arrayFilters are only supported on MongoDB 3.6+');
1022+
}
1023+
1024+
operation.arrayFilters = op[key].arrayFilters;
1025+
}
1026+
9861027
return this.s.options.addToOperationsList(this, UPDATE, operation);
9871028
}
9881029

0 commit comments

Comments
 (0)