diff --git a/lib/dao.js b/lib/dao.js index 9b1afbbb..a55d9c9c 100644 --- a/lib/dao.js +++ b/lib/dao.js @@ -418,6 +418,7 @@ DataAccessObject.updateOrCreate = DataAccessObject.upsert = function upsert(data * @param {Function} cb Callback called with (err, instance, created) */ DataAccessObject.findOrCreate = function findOrCreate(query, data, options, cb) { + if (stillConnecting(this.getDataSource(), this, arguments)) return; assert(arguments.length >= 2, 'At least two arguments are required'); if (options === undefined && cb === undefined) { @@ -447,13 +448,85 @@ DataAccessObject.findOrCreate = function findOrCreate(query, data, options, cb) assert(typeof cb === 'function', 'The cb argument must be a function'); var Model = this; - Model.findOne(query, function (err, record) { - if (err) return cb(err); - if (record) return cb(null, record, false); - Model.create(data, options, function (err, record) { - cb(err, record, record != null); + var self = this; + + function _findOrCreate(query, data) { + var modelName = self.modelName; + data = removeUndefined(data); + self.getDataSource().connector.findOrCreate(modelName, query, + self._forDB(data), + function(err, data, created) { + var obj, Model = self.lookupModel(data); + + if (data) { + obj = new Model(data, {fields: query.fields, applySetters: false, + persisted: true}); + } + + if (created) { + Model.notifyObserversOf('after save', { Model: Model, instance: obj }, + function(err) { + cb(err, obj, created); + if (!err) Model.emit('changed', obj); + }); + } else { + cb(err, obj, created); + } + }); + } + + if (this.getDataSource().connector.findOrCreate) { + query.limit = 1; + + try { + this._normalize(query); + } catch (err) { + return process.nextTick(function () { + cb(err); + }); + } + + this.applyScope(query); + + Model.notifyObserversOf('access', { Model: Model, query: query }, + function (err, ctx) { + if (err) return cb(err); + + var query = ctx.query; + + var enforced = {}; + var Model = self.lookupModel(data); + var obj = data instanceof Model ? data : new Model(data); + + Model.applyProperties(enforced, obj); + obj.setAttributes(enforced); + + Model.notifyObserversOf('before save', { Model: Model, instance: obj }, + function(err, ctx) { + if (err) return cb(err); + + var obj = ctx.instance; + var data = obj.toObject(true); + + // validation required + obj.isValid(function (valid) { + if (valid) { + _findOrCreate(query, data); + } else { + cb(new ValidationError(obj), obj); + } + }, data); + }); }); - }); + } else { + Model.findOne(query, options, function (err, record) { + if (err) return cb(err); + if (record) return cb(null, record, false); + Model.create(data, options, function (err, record) { + cb(err, record, record != null); + }); + }); + } }; /** diff --git a/test/memory.test.js b/test/memory.test.js index 341913e7..a9cc627a 100644 --- a/test/memory.test.js +++ b/test/memory.test.js @@ -399,16 +399,29 @@ describe('Memory connector', function() { }); }); }); +}); - require('./persistence-hooks.suite')( - new DataSource({ connector: Memory }), - should); +describe('Optimized connector', function() { + var ds = new DataSource({ connector: Memory }); + + // optimized methods + ds.connector.findOrCreate = function (model, query, data, callback) { + this.all(model, query, function (err, list) { + if (err || (list && list[0])) return callback(err, list && list[0], false); + this.create(model, data, function (err) { + callback(err, data, true); + }); + }.bind(this)); + }; + + require('./persistence-hooks.suite')(ds, should); }); describe('Unoptimized connector', function() { var ds = new DataSource({ connector: Memory }); // disable optimized methods ds.connector.updateOrCreate = false; + ds.connector.findOrCreate = false; require('./persistence-hooks.suite')(ds, should); }); diff --git a/test/persistence-hooks.suite.js b/test/persistence-hooks.suite.js index 1df8699d..6a6d3538 100644 --- a/test/persistence-hooks.suite.js +++ b/test/persistence-hooks.suite.js @@ -274,26 +274,25 @@ module.exports = function(dataSource, should) { }); }); - // TODO(bajtos) Enable this test for all connectors that - // provide optimized implementation of findOrCreate. - // The unoptimized implementation does not trigger the hook - // when an existing model was found. - it.skip('triggers `before save` hook when found', function(done) { - TestModel.observe('before save', pushContextAndNext()); + if (dataSource.connector.findOrCreate) { + it('triggers `before save` hook when found', function(done) { + TestModel.observe('before save', pushContextAndNext()); - TestModel.findOrCreate( - { where: { name: existingInstance.name } }, - { name: existingInstance.name }, - function(err, record, created) { - if (err) return done(err); - observedContexts.should.eql(aTestModelCtx({ instance: { - id: record.id, - name: existingInstance.name, - extra: undefined - }})); - done(); - }); - }); + TestModel.findOrCreate( + { where: { name: existingInstance.name } }, + { name: existingInstance.name }, + function(err, record, created) { + if (err) return done(err); + record.id.should.eql(existingInstance.id); + observedContexts.should.eql(aTestModelCtx({ instance: { + id: getLastGeneratedUid(), + name: existingInstance.name, + extra: undefined + }})); + done(); + }); + }); + } it('triggers `before save` hook when not found', function(done) { TestModel.observe('before save', pushContextAndNext()); @@ -1250,6 +1249,10 @@ module.exports = function(dataSource, should) { lastId += 1; return '' + lastId; } + + function getLastGeneratedUid() { + return '' + lastId; + } }); function deepCloneToObject(obj) {