From 1fce12ec75fca9e3542c27d65cf6e1421037303b Mon Sep 17 00:00:00 2001 From: Amir Jafarian Date: Mon, 7 Dec 2015 11:41:08 -0500 Subject: [PATCH] Implementation for replace This includes: *Implementation for replaceById *Implementation for replaceOrCreate --- lib/mongodb.js | 72 ++++++++++++++++++++++ test/mongodb.test.js | 107 +++++++++++++++++++++++++++++++++ test/persistence-hooks.test.js | 13 +++- 3 files changed, 191 insertions(+), 1 deletion(-) diff --git a/lib/mongodb.js b/lib/mongodb.js index 19b12c353..1e12ccf53 100644 --- a/lib/mongodb.js +++ b/lib/mongodb.js @@ -493,6 +493,25 @@ MongoDB.prototype.updateOrCreate = function updateOrCreate(model, data, options, }); }; +/** + * Replace model instance if it exists or create a new one if it doesn't + * + * @param {String} model The name of the model + * @param {Object} data The model instance data + * @param {Object} options The options object + * @param {Function} [cb] The callback function + */ +MongoDB.prototype.replaceOrCreate = function(model, data, options, cb) { + if (this.debug) debug('replaceOrCreate', model, data); + + var id = this.getIdValue(model, data); + var oid = this.coerceId(model, id); + var idName = this.idName(model); + data._id = data[idName]; + delete data[idName]; + this.replaceWithOptions(model, oid, data, {upsert: true}, cb); +}; + /** * Delete a model instance by id * @param {String} model The model name @@ -812,6 +831,59 @@ MongoDB.prototype.count = function count(model, where, options, callback) { }); }; +/** + * Replace properties for the model instance data + * @param {String} model The name of the model + * @param {*} id The instance id + * @param {Object} data The model data + * @param {Object} options The options object + * @param {Function} [cb] The callback function + */ +MongoDB.prototype.replaceById = function replace(model, id, data, options, cb) { + if (this.debug) debug('replace', model, id, data); + var oid = this.coerceId(model, id); + this.replaceWithOptions(model, oid, data, {upsert: false}, function(err, data) { + cb(err, data); + }); +}; + +/** + * Update a model instance with id + * @param {String} model The name of the model + * @param {Object} id The id of the model instance + * @param {Object} data The property/value pairs to be updated or inserted if {upsert: true} is passed as options + * @param {Object} options The options you want to pass for update, e.g, {upsert: true} + * @callback {Function} [cb] Callback function + */ +MongoDB.prototype.replaceWithOptions = function(model, id, data, options, cb) { + var self = this; + this.execute(model, 'update', {_id: id}, data, options, function(err, info) { + if (this.debug) debug('updateWithOptions.callback', model, {_id: id}, data, err, info); + if (err) return cb && cb(err); + var result; + var cbInfo = {}; + if (info.result && info.result.n == 1) { + result = data; + delete result._id; + var idName = self.idName(model); + result[idName] = id; + // create result formats: + // 2.4.x :{ ok: 1, n: 1, upserted: [ Object ] } + // { ok: 1, nModified: 0, n: 1, upserted: [ Object ] } + // + // replace result formats: + // 2.4.x: { ok: 1, n: 1 } + // { ok: 1, nModified: 1, n: 1 } + if (info.result.nModified !== undefined) { + cbInfo.isNewInstance = info.result.nModified === 0; + } + } else { + result = undefined; + } + cb && cb(err, result, cbInfo); + }); +}; + /** * Update properties for the model instance data * @param {String} model The model name diff --git a/test/mongodb.test.js b/test/mongodb.test.js index 13bf9f291..9bccf5183 100644 --- a/test/mongodb.test.js +++ b/test/mongodb.test.js @@ -1072,6 +1072,113 @@ describe('mongodb connector', function () { }); }); + describe('replaceOrCreate', function() { + it('should create a model instance even if it already exists', function(done) { + Product.replaceOrCreate({name: 'newFoo'}, function(err, updatedProduct) { + if (err) return done(err); + should.not.exist(updatedProduct._id); + should.exist(updatedProduct.id); + verifyData(updatedProduct.id); + }); + function verifyData(id) { + Product.findById(id, function(err, data) { + data.name.should.be.equal('newFoo'); + done(err); + }); + }; + }); + + it('should replace a model instance if the passing key already exists', function(done) { + Product.create({name: 'bread', price: 100}, function(err, product) { + if (err) return done(err); + replaceOrCreate({id: product.id, name: 'milk'}); + }); + function replaceOrCreate(data) { + Product.replaceOrCreate(data, function(err, updatedProduct) { + if (err) return done(err); + should.not.exist(updatedProduct._id); + updatedProduct.name.should.be.equal('milk'); + should.exist(updatedProduct.id); + verify(data.id); + }); + } + function verify(id) { + Product.findById(id, function(err, data) { + data.name.should.be.equal('milk'); + should.not.exist(data.price); + done(err); + }); + } + }); + + it('should remove extraneous properties that are not defined in the model', function(done) { + Product.create({name: 'bread', price: 100, bar: 'baz'}, function(err, product) { + if (err) return done(err); + replaceOrCreate({id: product.id, name: 'milk'}); + }); + function replaceOrCreate(data) { + Product.replaceOrCreate(data, function(err, updatedProduct) { + if (err) return done(err); + should.not.exist(updatedProduct.bar); + verify(data.id); + }); + } + function verify(id) { + Product.findById(id, function(err, data) { + should.not.exist(data.bar); + done(err); + }); + } + }); + }); + + describe('replace', function() { + it('should replace the model instance if the provided key already exists', function(done) { + Product.create({name: 'bread', price: 100}, function(err, product) { + if (err) return done(err); + replace(product, {name: 'milk'}, product.id); + }); + function replace(product, data, id) { + product.replaceAttributes(data, function(err, updatedProduct) { + if (err) return done(err); + should.not.exist(updatedProduct._id); + updatedProduct.name.should.be.equal('milk'); + should.exist(updatedProduct.id); + verify(id); + }); + } + function verify(id) { + Product.findById(id, function(err, data) { + data.name.should.be.equal('milk'); + should.not.exist(data.price); + done(err); + }); + } + }); + + it('should remove extraneous properties that are not defined in the model', function(done) { + Product.create({name: 'bread', price: 100, bar: 'baz'}, function(err, product) { + if (err) return done(err); + replace(product, {name: 'milk'}, product.id); + + }); + function replace(product, data, id) { + product.replaceAttributes(data, function(err, updatedProduct) { + if (err) return done(err); + should.not.exist(updatedProduct.bar); + verify(id); + }); + } + function verify(id) { + Product.findById(id, function(err, data) { + data.name.should.be.equal('milk'); + should.not.exist(data.bar); + done(err); + }); + } + }); + }); + it('updateOrCreate: should handle combination of operators and top level properties without errors', function (done) { Product.dataSource.settings.allowExtendedOperators = true; Product.create({name: 'bread', price: 100, ingredients:['flour'],pricehistory:[{'2014-11-11':90},{ '2014-10-10':80 }]}, function (err, product) { diff --git a/test/persistence-hooks.test.js b/test/persistence-hooks.test.js index 498a27f6e..c08cb97ad 100644 --- a/test/persistence-hooks.test.js +++ b/test/persistence-hooks.test.js @@ -14,5 +14,16 @@ if (process.env.MONGODB_VERSION && for(var i in config) { customConfig[i] = config[i]; } +var DB_VERSION = process.env.MONGODB_VERSION; -suite(global.getDataSource(customConfig), should); +if (!DB_VERSION) { + console.log('The ENV variable MONGODB_VERSION is not set.' + + ' Assuming MongoDB version 2.6 or newer.'); +} + +var DB_HAS_2_6_FEATURES = (!DB_VERSION || + semver.satisfies(DB_VERSION, '>=2.6.0')); + +suite(global.getDataSource(customConfig), should, { + replaceOrCreateReportsNewInstance: DB_HAS_2_6_FEATURES +});