Skip to content

Commit 56aeeeb

Browse files
authored
Merge pull request #1001 from mountain1234585/upsertNPK
upsertWithWhere method
2 parents 552f50b + 37541dd commit 56aeeeb

File tree

5 files changed

+888
-0
lines changed

5 files changed

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

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

540540
});
541541

542+
describe('upsertWithWhere', function() {
543+
beforeEach(seed);
544+
it('rejects upsertWithWhere (options,cb)', function(done) {
545+
try {
546+
User.upsertWithWhere({}, function(err) {
547+
if (err) return done(err);
548+
});
549+
} catch (ex) {
550+
ex.message.should.equal('The data argument must be an object');
551+
done();
552+
}
553+
});
554+
555+
it('rejects upsertWithWhere (cb)', function(done) {
556+
try {
557+
User.upsertWithWhere(function(err) {
558+
if (err) return done(err);
559+
});
560+
} catch (ex) {
561+
ex.message.should.equal('The where argument must be an object');
562+
done();
563+
}
564+
});
565+
566+
it('allows upsertWithWhere by accepting where,data and cb as arguments', function(done) {
567+
User.upsertWithWhere({ name: 'John Lennon' }, { name: 'John Smith' }, function(err) {
568+
if (err) return done(err);
569+
User.find({ where: { name: 'John Lennon' }}, function(err, data) {
570+
if (err) return done(err);
571+
data.length.should.equal(0);
572+
User.find({ where: { name: 'John Smith' }}, function(err, data) {
573+
if (err) return done(err);
574+
data.length.should.equal(1);
575+
data[0].name.should.equal('John Smith');
576+
data[0].email.should.equal('[email protected]');
577+
data[0].role.should.equal('lead');
578+
data[0].order.should.equal(2);
579+
data[0].vip.should.equal(true);
580+
done();
581+
});
582+
});
583+
});
584+
});
585+
586+
it('allows upsertWithWhere by accepting where, data, options, and cb as arguments', function(done) {
587+
options = {};
588+
User.upsertWithWhere({ name: 'John Lennon' }, { name: 'John Smith' }, options, function(err) {
589+
if (err) return done(err);
590+
User.find({ where: { name: 'John Smith' }}, function(err, data) {
591+
if (err) return done(err);
592+
data.length.should.equal(1);
593+
data[0].name.should.equal('John Smith');
594+
data[0].seq.should.equal(0);
595+
data[0].email.should.equal('[email protected]');
596+
data[0].role.should.equal('lead');
597+
data[0].order.should.equal(2);
598+
data[0].vip.should.equal(true);
599+
done();
600+
});
601+
});
602+
});
603+
});
604+
542605
function seed(done) {
543606
var beatles = [
544607
{

0 commit comments

Comments
 (0)