Skip to content

Commit

Permalink
fix(updateOne/updateMany): ensure that update documents contain atomi…
Browse files Browse the repository at this point in the history
…c operators

NODE-965
  • Loading branch information
kmahar authored and mbroadst committed Sep 12, 2017
1 parent e9c4ffc commit 8b4255a
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 9 deletions.
28 changes: 28 additions & 0 deletions lib/collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -938,7 +938,15 @@ define.classMethod('insert', { callback: true, promise: true });
*/
Collection.prototype.updateOne = function(filter, update, options, callback) {
var self = this;

if (typeof options === 'function') (callback = options), (options = {});

var err = checkForAtomicOperators(update);
if (err) {
if (typeof callback === 'function') return callback(err);
return this.s.promiseLibrary.reject(err);
}

options = shallowClone(options);

// Add ignoreUndfined
Expand All @@ -959,6 +967,19 @@ Collection.prototype.updateOne = function(filter, update, options, callback) {
});
};

var checkForAtomicOperators = function(update) {

This comment has been minimized.

Copy link
@4Z4T4R

4Z4T4R Oct 6, 2017

Does the updateOne method no longer support passing in a document as update? That won't have a "$" and implicitly an "atomic" operator?

This comment has been minimized.

Copy link
@mbroadst

mbroadst Oct 10, 2017

Member

@toszter yes, this has been deprecated for some time now, and is specified as having to work this way. You can read the ticket for more information as to why this behavior is dangerous. Also, to answer your question from the SUPPORT ticket, this is a breaking change, but the 3.x release of the driver will indeed break a number of other things being that its a major release, so this is to be expected. We are documenting these changes in the CHANGES_3.0.0 file, which I will remind one of our developers to update with this information tomorrow

var keys = Object.keys(update);

// same errors as the server would give for update doc lacking atomic operators
if (keys.length === 0) {
return toError('The update operation document must contain at least one atomic operator.');
}

if (keys[0][0] !== '$') {
return toError('the update operation document must contain atomic operators.');
}
};

var updateOne = function(self, filter, update, options, callback) {
// Set single document update
options.multi = false;
Expand Down Expand Up @@ -1061,6 +1082,13 @@ define.classMethod('replaceOne', { callback: true, promise: true });
Collection.prototype.updateMany = function(filter, update, options, callback) {
var self = this;
if (typeof options === 'function') (callback = options), (options = {});

var err = checkForAtomicOperators(update);
if (err) {
if (typeof callback === 'function') return callback(err);
return this.s.promiseLibrary.reject(err);
}

options = shallowClone(options);

// Add ignoreUndfined
Expand Down
82 changes: 80 additions & 2 deletions test/functional/crud_api_tests.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';
var test = require('./shared').assert;
var setupDatabase = require('./shared').setupDatabase;
var test = require('./shared').assert,
setupDatabase = require('./shared').setupDatabase,
expect = require('chai').expect;

// instanceof cannot be use reliably to detect the new models in js due to scoping and new
// contexts killing class info find/distinct/count thus cannot be overloaded without breaking
Expand Down Expand Up @@ -1011,4 +1012,81 @@ describe('CRUD API', function() {
});
}
});

it('should correctly throw error if update doc for updateOne lacks atomic operator', {
// Add a tag that our runner can trigger on
// in this case we are setting that node needs to be higher than 0.10.X to run
metadata: {
requires: { topology: ['single', 'replicaset', 'sharded', 'ssl', 'heap', 'wiredtiger'] }
},

// The actual test we wish to run
test: function(done) {
var configuration = this.configuration;
var client = configuration.newClient(configuration.writeConcernMax(), { poolSize: 1 });
client.connect(function(err, client) {
expect(err).to.not.exist;
var db = client.db(configuration.db);
var col = db.collection('t21_1');
col.insertOne({ a: 1, b: 2, c: 3 }, function(err, r) {
expect(err).to.not.exist;
expect(r.insertedCount).to.equal(1);

// empty update document
col.updateOne({ a: 1 }, {}, function(err, r) {
expect(err).to.exist;
expect(r).to.not.exist;

// update document non empty but still lacks atomic operator
col.updateOne({ a: 1 }, { b: 5 }, function(err, r) {
expect(err).to.exist;
expect(r).to.not.exist;

client.close();
done();
});
});
});
});
}
});

it('should correctly throw error if update doc for updateMany lacks atomic operator', {
// Add a tag that our runner can trigger on
// in this case we are setting that node needs to be higher than 0.10.X to run
metadata: {
requires: { topology: ['single', 'replicaset', 'sharded', 'ssl', 'heap', 'wiredtiger'] }
},

// The actual test we wish to run
test: function(done) {
var configuration = this.configuration;
var client = configuration.newClient(configuration.writeConcernMax(), { poolSize: 1 });
client.connect(function(err, client) {
expect(err).to.not.exist;
var db = client.db(configuration.db);
var col = db.collection('t22_1');
col.insertMany([{ a: 1, b: 2 }, { a: 1, b: 3 }, { a: 1, b: 4 }], function(err, r) {
console.dir(err);
expect(err).to.not.exist;
expect(r.insertedCount).to.equal(3);

// empty update document
col.updateMany({ a: 1 }, {}, function(err, r) {
expect(err).to.exist;
expect(r).to.not.exist;

// update document non empty but still lacks atomic operator
col.updateMany({ a: 1 }, { b: 5 }, function(err, r) {
expect(err).to.exist;
expect(r).to.not.exist;

client.close();
done();
});
});
});
});
}
});
});
12 changes: 10 additions & 2 deletions test/functional/examples_tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -1035,8 +1035,16 @@ describe('Examples', function() {
.updateOne(
{ item: 'paper' },
{
item: 'paper',
instock: [{ warehouse: 'A', qty: 60 }, { warehouse: 'B', qty: 40 }]
$set: {
item: 'paper',
instock: [{ warehouse: 'A', qty: 60 }, { warehouse: 'B', qty: 40 }]
},
$unset: {
qty: '',
size: '',
status: '',
lastModified: ''
}
}
)
.then(function(result) {
Expand Down
4 changes: 2 additions & 2 deletions test/functional/operation_example_tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -3531,7 +3531,7 @@ describe('Operation Examples', function() {
// Get a collection
var collection = db.collection('update_a_simple_document_upsert');
// Update the document using an upsert operation, ensuring creation if it does not exist
collection.updateOne({ a: 1 }, { b: 2, a: 1 }, { upsert: true, w: 1 }, function(
collection.updateOne({ a: 1 }, { $set: { b: 2, a: 1 } }, { upsert: true, w: 1 }, function(
err,
result
) {
Expand Down Expand Up @@ -6777,7 +6777,7 @@ describe('Operation Examples', function() {

db
.collection('mongoclient_test')
.updateOne({ a: 1 }, { b: 1 }, { upsert: true }, function(err, result) {
.updateOne({ a: 1 }, { $set: { b: 1 } }, { upsert: true }, function(err, result) {
test.equal(null, err);
test.equal(1, result.result.n);

Expand Down
6 changes: 5 additions & 1 deletion test/functional/operation_generators_example_tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -2697,7 +2697,11 @@ describe('Operation (Generators)', function() {
// Get a collection
var collection = db.collection('update_a_simple_document_upsert_with_generators');
// Update the document using an upsert operation, ensuring creation if it does not exist
var result = yield collection.updateOne({ a: 1 }, { b: 2, a: 1 }, { upsert: true, w: 1 });
var result = yield collection.updateOne(
{ a: 1 },
{ $set: { b: 2, a: 1 } },
{ upsert: true, w: 1 }
);
test.equal(1, result.result.n);

// Fetch the document that we modified and check if it got inserted correctly
Expand Down
4 changes: 2 additions & 2 deletions test/functional/operation_promises_example_tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -2789,7 +2789,7 @@ describe('Operation (Promises)', function() {

// Update the document using an upsert operation, ensuring creation if it does not exist
return collection
.updateOne({ a: 1 }, { b: 2, a: 1 }, { upsert: true, w: 1 })
.updateOne({ a: 1 }, { $set: { b: 2, a: 1 } }, { upsert: true, w: 1 })
.then(function(result) {
test.equal(1, result.result.n);

Expand Down Expand Up @@ -5077,7 +5077,7 @@ describe('Operation (Promises)', function() {
// BEGIN
return db
.collection('mongoclient_test_with_promise')
.updateOne({ a: 1 }, { b: 1 }, { upsert: true })
.updateOne({ a: 1 }, { $set: { b: 1 } }, { upsert: true })
.then(function(result) {
test.equal(1, result.result.n);
client.close();
Expand Down

0 comments on commit 8b4255a

Please sign in to comment.