Skip to content

Commit b156819

Browse files
authored
Merge pull request #1052 from strongloop/upsertWithWhere_2x
upsertWithWhere feature support in juggler DAO
2 parents fce1662 + 12aadd4 commit b156819

File tree

5 files changed

+879
-0
lines changed

5 files changed

+879
-0
lines changed

lib/connectors/memory.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,36 @@ Memory.prototype.updateOrCreate = function(model, data, options, callback) {
252252
});
253253
};
254254

255+
Memory.prototype.patchOrCreateWithWhere =
256+
Memory.prototype.upsertWithWhere = function(model, where, data, options, callback) {
257+
var self = this;
258+
var primaryKey = this.idName(model);
259+
var filter = { where: where };
260+
var nodes = self._findAllSkippingIncludes(model, filter);
261+
if (nodes.length === 0) {
262+
return self._createSync(model, data, function(err, id) {
263+
if (err) return process.nextTick(function() { callback(err); });
264+
self.saveToFile(id, function(err, id) {
265+
self.setIdValue(model, data, id);
266+
callback(err, self.fromDb(model, data), { isNewInstance: true });
267+
});
268+
});
269+
}
270+
if (nodes.length === 1) {
271+
var primaryKeyValue = nodes[0][primaryKey];
272+
self.updateAttributes(model, primaryKeyValue, data, options, function(err, data) {
273+
callback(err, data, { isNewInstance: false });
274+
});
275+
} else {
276+
process.nextTick(function() {
277+
var error = new Error('There are multiple instances found.' +
278+
'Upsert Operation will not be performed!');
279+
error.statusCode = 400;
280+
callback(error);
281+
});
282+
}
283+
};
284+
255285
Memory.prototype.findOrCreate = function(model, filter, data, callback) {
256286
var self = this;
257287
var nodes = self._findAllSkippingIncludes(model, filter);

lib/dao.js

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -634,7 +634,173 @@ DataAccessObject.upsert = function(data, options, cb) {
634634
}
635635
return cb.promise;
636636
};
637+
/**
638+
* Update or insert a model instance based on the search criteria.
639+
* If there is a single instance retrieved, update the retrieved model.
640+
* Creates a new model if no model instances were found.
641+
* Returns an error if multiple instances are found.
642+
* * @param {Object} [where] `where` filter, like
643+
* ```
644+
* { key: val, key2: {gt: 'val2'}, ...}
645+
* ```
646+
* <br/>see
647+
* [Where filter](https://docs.strongloop.com/display/LB/Where+filter#Wherefilter-Whereclauseforothermethods).
648+
* @param {Object} data The model instance data to insert.
649+
* @callback {Function} callback Callback function called with `cb(err, obj)` signature.
650+
* @param {Error} err Error object; see [Error object](http://docs.strongloop.com/display/LB/Error+object).
651+
* @param {Object} model Updated model instance.
652+
*/
653+
DataAccessObject.patchOrCreateWithWhere =
654+
DataAccessObject.upsertWithWhere = function(where, data, options, cb) {
655+
var connectionPromise = stillConnecting(this.getDataSource(), this, arguments);
656+
if (connectionPromise) { return connectionPromise; }
657+
if (cb === undefined) {
658+
if (typeof options === 'function') {
659+
// upsertWithWhere(where, data, cb)
660+
cb = options;
661+
options = {};
662+
}
663+
}
664+
cb = cb || utils.createPromiseCallback();
665+
options = options || {};
666+
assert(typeof where === 'object', 'The where argument must be an object');
667+
assert(typeof data === 'object', 'The data argument must be an object');
668+
assert(typeof options === 'object', 'The options argument must be an object');
669+
assert(typeof cb === 'function', 'The cb argument must be a function');
670+
if (Object.keys(data).length === 0) {
671+
var err = new Error('data object cannot be empty!');
672+
err.statusCode = 400;
673+
process.nextTick(function() { cb(err); });
674+
return cb.promise;
675+
}
676+
var hookState = {};
677+
var self = this;
678+
var Model = this;
679+
var connector = Model.getConnector();
680+
var modelName = Model.modelName;
681+
var query = { where: where };
682+
var context = {
683+
Model: Model,
684+
query: query,
685+
hookState: hookState,
686+
options: options,
687+
};
688+
Model.notifyObserversOf('access', context, doUpsertWithWhere);
689+
function doUpsertWithWhere(err, ctx) {
690+
if (err) return cb(err);
691+
ctx.data = data;
692+
if (connector.upsertWithWhere) {
693+
var context = {
694+
Model: Model,
695+
where: ctx.query.where,
696+
data: ctx.data,
697+
hookState: hookState,
698+
options: options,
699+
};
700+
Model.notifyObserversOf('before save', context, function(err, ctx) {
701+
if (err) return cb(err);
702+
data = ctx.data;
703+
var update = data;
704+
var inst = data;
705+
if (!(data instanceof Model)) {
706+
inst = new Model(data, { applyDefaultValues: false });
707+
}
708+
update = inst.toObject(false);
709+
Model.applyScope(query);
710+
Model.applyProperties(update, inst);
711+
Model = Model.lookupModel(update);
712+
if (options.validate === false) {
713+
return callConnector();
714+
}
715+
if (options.validate === undefined && Model.settings.automaticValidation === false) {
716+
return callConnector();
717+
}
718+
inst.isValid(function(valid) {
719+
if (!valid) return cb(new ValidationError(inst), inst);
720+
callConnector();
721+
}, update, options);
637722

723+
function callConnector() {
724+
try {
725+
ctx.where = removeUndefined(ctx.where);
726+
ctx.where = Model._coerce(ctx.where);
727+
update = removeUndefined(update);
728+
update = Model._coerce(update);
729+
} catch (err) {
730+
return process.nextTick(function() {
731+
cb(err);
732+
});
733+
}
734+
context = {
735+
Model: Model,
736+
where: ctx.where,
737+
data: update,
738+
currentInstance: inst,
739+
hookState: ctx.hookState,
740+
options: options,
741+
};
742+
Model.notifyObserversOf('persist', context, function(err) {
743+
if (err) return done(err);
744+
connector.upsertWithWhere(modelName, ctx.where, update, options, done);
745+
});
746+
}
747+
function done(err, data, info) {
748+
if (err) return cb(err);
749+
var contxt = {
750+
Model: Model,
751+
data: data,
752+
isNewInstance: info && info.isNewInstance,
753+
hookState: ctx.hookState,
754+
options: options,
755+
};
756+
Model.notifyObserversOf('loaded', contxt, function(err) {
757+
if (err) return cb(err);
758+
var obj;
759+
if (contxt.data && !(contxt.data instanceof Model)) {
760+
inst._initProperties(contxt.data, { persisted: true });
761+
obj = inst;
762+
} else {
763+
obj = contxt.data;
764+
}
765+
var context = {
766+
Model: Model,
767+
instance: obj,
768+
isNewInstance: info ? info.isNewInstance : undefined,
769+
hookState: hookState,
770+
options: options,
771+
};
772+
Model.notifyObserversOf('after save', context, function(err) {
773+
cb(err, obj);
774+
});
775+
});
776+
}
777+
});
778+
} else {
779+
var opts = { notify: false };
780+
if (ctx.options && ctx.options.transaction) {
781+
opts.transaction = ctx.options.transaction;
782+
}
783+
self.find({ where: ctx.query.where }, opts, function(err, instances) {
784+
if (err) return cb(err);
785+
var modelsLength = instances.length;
786+
if (modelsLength === 0) {
787+
self.create(data, options, cb);
788+
} else if (modelsLength === 1) {
789+
var modelInst = instances[0];
790+
modelInst.updateAttributes(data, options, cb);
791+
} else {
792+
process.nextTick(function() {
793+
var error = new Error('There are multiple instances found.' +
794+
'Upsert Operation will not be performed!');
795+
error.statusCode = 400;
796+
cb(error);
797+
});
798+
}
799+
});
800+
}
801+
}
802+
return cb.promise;
803+
};
638804
/**
639805
* Replace or insert a model instance: replace exiting record if one is found, such that parameter `data.id` matches `id` of model instance;
640806
* otherwise, insert a new record.

test/crud-with-options.test.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,69 @@ describe('crud-with-options', function() {
517517
});
518518
});
519519

520+
describe('upsertWithWhere', function() {
521+
beforeEach(seed);
522+
it('rejects upsertWithWhere (options,cb)', function(done) {
523+
try {
524+
User.upsertWithWhere({}, function(err) {
525+
if (err) return done(err);
526+
});
527+
} catch (ex) {
528+
ex.message.should.equal('The data argument must be an object');
529+
done();
530+
}
531+
});
532+
533+
it('rejects upsertWithWhere (cb)', function(done) {
534+
try {
535+
User.upsertWithWhere(function(err) {
536+
if (err) return done(err);
537+
});
538+
} catch (ex) {
539+
ex.message.should.equal('The where argument must be an object');
540+
done();
541+
}
542+
});
543+
544+
it('allows upsertWithWhere by accepting where,data and cb as arguments', function(done) {
545+
User.upsertWithWhere({ name: 'John Lennon' }, { name: 'John Smith' }, function(err) {
546+
if (err) return done(err);
547+
User.find({ where: { name: 'John Lennon' }}, function(err, data) {
548+
if (err) return done(err);
549+
data.length.should.equal(0);
550+
User.find({ where: { name: 'John Smith' }}, function(err, data) {
551+
if (err) return done(err);
552+
data.length.should.equal(1);
553+
data[0].name.should.equal('John Smith');
554+
data[0].email.should.equal('[email protected]');
555+
data[0].role.should.equal('lead');
556+
data[0].order.should.equal(2);
557+
data[0].vip.should.equal(true);
558+
done();
559+
});
560+
});
561+
});
562+
});
563+
564+
it('allows upsertWithWhere by accepting where, data, options, and cb as arguments', function(done) {
565+
options = {};
566+
User.upsertWithWhere({ name: 'John Lennon' }, { name: 'John Smith' }, options, function(err) {
567+
if (err) return done(err);
568+
User.find({ where: { name: 'John Smith' }}, function(err, data) {
569+
if (err) return done(err);
570+
data.length.should.equal(1);
571+
data[0].name.should.equal('John Smith');
572+
data[0].seq.should.equal(0);
573+
data[0].email.should.equal('[email protected]');
574+
data[0].role.should.equal('lead');
575+
data[0].order.should.equal(2);
576+
data[0].vip.should.equal(true);
577+
done();
578+
});
579+
});
580+
});
581+
});
582+
520583
function seed(done) {
521584
var beatles = [
522585
{

0 commit comments

Comments
 (0)