From aef6dca30cbb91c8e57f2ed58848cf679a306f39 Mon Sep 17 00:00:00 2001 From: Sonali Samantaray Date: Fri, 2 Sep 2016 18:10:47 +0530 Subject: [PATCH] Expose upsertWithWhere method --- lib/model.js | 2 ++ lib/persisted-model.js | 37 ++++++++++++++++++++++++++++ test/access-control.integration.js | 8 ++++++ test/data-source.test.js | 2 ++ test/model.test.js | 39 ++++++++++++++++++++++++++++++ test/remoting.integration.js | 10 ++++++++ test/replication.test.js | 13 ++++++++++ 7 files changed, 111 insertions(+) diff --git a/lib/model.js b/lib/model.js index e98bf164..310a3f46 100644 --- a/lib/model.js +++ b/lib/model.js @@ -366,6 +366,8 @@ module.exports = function(registry) { return ACL.WRITE; case 'updateOrCreate': return ACL.WRITE; + case 'upsertWithWhere': + return ACL.WRITE; case 'upsert': return ACL.WRITE; case 'exists': diff --git a/lib/persisted-model.js b/lib/persisted-model.js index 5e5f7f33..b01dd6f4 100644 --- a/lib/persisted-model.js +++ b/lib/persisted-model.js @@ -118,6 +118,28 @@ module.exports = function(registry) { throwNotAttached(this.modelName, 'upsert'); }; + /** + * Update or insert a model instance based on the search criteria. + * If there is a single instance retrieved, update the retrieved model. + * Creates a new model if no model instances were found. + * Returns an error if multiple instances are found. + * * @param {Object} [where] `where` filter, like + * ``` + * { key: val, key2: {gt: 'val2'}, ...} + * ``` + *
see + * [Where filter](https://docs.strongloop.com/display/LB/Where+filter#Wherefilter-Whereclauseforothermethods). + * @param {Object} data The model instance data to insert. + * @callback {Function} callback Callback function called with `cb(err, obj)` signature. + * @param {Error} err Error object; see [Error object](http://docs.strongloop.com/display/LB/Error+object). + * @param {Object} model Updated model instance. + */ + + PersistedModel.upsertWithWhere = + PersistedModel.patchOrCreateWithWhere = function upsertWithWhere(where, data, callback) { + throwNotAttached(this.modelName, 'upsertWithWhere'); + }; + /** * Replace or insert a model instance; replace existing record if one is found, * such that parameter `data.id` matches `id` of model instance; otherwise, @@ -654,6 +676,21 @@ module.exports = function(registry) { setRemoting(PersistedModel, 'replaceOrCreate', replaceOrCreateOptions); + setRemoting(PersistedModel, 'upsertWithWhere', { + aliases: ['patchOrCreateWithWhere'], + description: 'Update an existing model instance or insert a new one into ' + + 'the data source based on the where criteria.', + accessType: 'WRITE', + accepts: [ + { arg: 'where', type: 'object', http: { source: 'query' }, + description: 'Criteria to match model instances' }, + { arg: 'data', type: 'object', http: { source: 'body' }, + description: 'An object of model property name/value pairs' }, + ], + returns: { arg: 'data', type: typeName, root: true }, + http: { verb: 'post', path: '/upsertWithWhere' }, + }); + setRemoting(PersistedModel, 'exists', { description: 'Check whether a model instance exists in the data source.', accessType: 'READ', diff --git a/test/access-control.integration.js b/test/access-control.integration.js index 14a9130c..a3b66c82 100644 --- a/test/access-control.integration.js +++ b/test/access-control.integration.js @@ -122,6 +122,10 @@ describe('access control - integration', function() { }); }); + lt.it.shouldBeDeniedWhenCalledAnonymously('POST', '/api/users/upsertWithWhere'); + lt.it.shouldBeDeniedWhenCalledUnauthenticated('POST', '/api/users/upsertWithWhere'); + lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'POST', '/api/users/upsertWithWhere'); + lt.it.shouldBeDeniedWhenCalledAnonymously('DELETE', urlForUser); lt.it.shouldBeDeniedWhenCalledUnauthenticated('DELETE', urlForUser); lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'DELETE', urlForUser); @@ -193,6 +197,10 @@ describe('access control - integration', function() { lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'DELETE', urlForBank); lt.it.shouldBeAllowedWhenCalledByUser(SPECIAL_USER, 'DELETE', urlForBank); + lt.it.shouldBeDeniedWhenCalledAnonymously('POST', '/api/banks/upsertWithWhere'); + lt.it.shouldBeDeniedWhenCalledUnauthenticated('POST', '/api/banks/upsertWithWhere'); + lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'POST', '/api/banks/upsertWithWhere'); + function urlForBank() { return '/api/banks/' + this.bank.id; } diff --git a/test/data-source.test.js b/test/data-source.test.js index de4c9447..4d2fa940 100644 --- a/test/data-source.test.js +++ b/test/data-source.test.js @@ -22,6 +22,7 @@ describe('DataSource', function() { assert.isFunc(Color, 'findOne'); assert.isFunc(Color, 'create'); assert.isFunc(Color, 'updateOrCreate'); + assert.isFunc(Color, 'upsertWithWhere'); assert.isFunc(Color, 'upsert'); assert.isFunc(Color, 'findOrCreate'); assert.isFunc(Color, 'exists'); @@ -82,6 +83,7 @@ describe('DataSource', function() { existsAndShared('_forDB', false); existsAndShared('create', true); existsAndShared('updateOrCreate', true); + existsAndShared('upsertWithWhere', true); existsAndShared('upsert', true); existsAndShared('findOrCreate', false); existsAndShared('exists', true); diff --git a/test/model.test.js b/test/model.test.js index 361ddd85..4ce1504b 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -146,6 +146,43 @@ describe.onServer('Remote Methods', function() { }); }); + describe('Model.upsertWithWhere(where, data, callback)', function() { + it('Updates when a Model instance is retreived from data source', function(done) { + var taskEmitter = new TaskEmitter(); + taskEmitter + .task(User, 'create', { first: 'jill', second: 'pill' }) + .task(User, 'create', { first: 'bob', second: 'sob' }) + .on('done', function() { + User.upsertWithWhere({ second: 'pill' }, { second: 'jones' }, function(err, user) { + if (err) return done(err); + var id = user.id; + User.findById(id, function(err, user) { + if (err) return done(err); + assert.equal(user.second, 'jones'); + done(); + }); + }); + }); + }); + + it('Creates when no Model instance is retreived from data source', function(done) { + var taskEmitter = new TaskEmitter(); + taskEmitter + .task(User, 'create', { first: 'simon', second: 'somers' }) + .on('done', function() { + User.upsertWithWhere({ first: 'somers' }, { first: 'Simon' }, function(err, user) { + if (err) return done(err); + var id = user.id; + User.findById(id, function(err, user) { + if (err) return done(err); + assert.equal(user.first, 'Simon'); + done(); + }); + }); + }); + }); + }); + describe('Example Remote Method', function() { it('Call the method using HTTP / REST', function(done) { request(app) @@ -515,6 +552,7 @@ describe.onServer('Remote Methods', function() { describe('Model.checkAccessTypeForMethod(remoteMethod)', function() { shouldReturn('create', ACL.WRITE); shouldReturn('updateOrCreate', ACL.WRITE); + shouldReturn('upsertWithWhere', ACL.WRITE); shouldReturn('upsert', ACL.WRITE); shouldReturn('exists', ACL.READ); shouldReturn('findById', ACL.READ); @@ -634,6 +672,7 @@ describe.onServer('Remote Methods', function() { // 'destroyAll', 'deleteAll', 'remove', 'create', 'upsert', 'updateOrCreate', 'patchOrCreate', + 'upsertWithWhere', 'patchOrCreateWithWhere', 'exists', 'findById', 'replaceById', diff --git a/test/remoting.integration.js b/test/remoting.integration.js index f6041063..15468c14 100644 --- a/test/remoting.integration.js +++ b/test/remoting.integration.js @@ -183,6 +183,15 @@ describe('remoting - integration', function() { expect(methods).to.include.members(expectedMethods); }); }); + + it('has upsertWithWhere remote method', function() { + var storeClass = findClass('store'); + var methods = getFormattedMethodsExcludingRelations(storeClass.methods); + var expectedMethods = [ + 'upsertWithWhere(where:object,data:object):store POST /stores/upsertWithWhere', + ]; + expect(methods).to.include.members(expectedMethods); + }); }); describe('With model.settings.replaceOnPUT false', function() { @@ -202,6 +211,7 @@ describe('With model.settings.replaceOnPUT false', function() { 'patchOrCreate(data:object):storeWithReplaceOnPUTfalse PUT /stores-updating', 'patchOrCreate(data:object):storeWithReplaceOnPUTfalse PATCH /stores-updating', 'replaceOrCreate(data:object):storeWithReplaceOnPUTfalse POST /stores-updating/replaceOrCreate', + 'upsertWithWhere(where:object,data:object):storeWithReplaceOnPUTfalse POST /stores-updating/upsertWithWhere', 'exists(id:any):boolean GET /stores-updating/:id/exists', 'exists(id:any):boolean HEAD /stores-updating/:id', 'findById(id:any,filter:object):storeWithReplaceOnPUTfalse GET /stores-updating/:id', diff --git a/test/replication.test.js b/test/replication.test.js index 0ad1827c..f017e37c 100644 --- a/test/replication.test.js +++ b/test/replication.test.js @@ -1010,6 +1010,19 @@ describe('Replication / Change APIs', function() { }); }); + it('detects "upsertWithWhere"', function(done) { + givenReplicatedInstance(function(err, inst) { + if (err) return done(err); + SourceModel.upsertWithWhere( + { name: inst.name }, + { name: 'updated' }, + function(err) { + if (err) return done(err); + assertChangeRecordedForId(inst.id, done); + }); + }); + }); + it('detects "findOrCreate"', function(done) { // make sure we bypass find+create and call the connector directly SourceModel.dataSource.connector.findOrCreate =