Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down
37 changes: 37 additions & 0 deletions lib/persisted-model.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'}, ...}
* ```
* <br/>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,
Expand Down Expand Up @@ -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',
Expand Down
8 changes: 8 additions & 0 deletions test/access-control.integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
Expand Down
2 changes: 2 additions & 0 deletions test/data-source.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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);
Expand Down
39 changes: 39 additions & 0 deletions test/model.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -634,6 +672,7 @@ describe.onServer('Remote Methods', function() {
// 'destroyAll', 'deleteAll', 'remove',
'create',
'upsert', 'updateOrCreate', 'patchOrCreate',
'upsertWithWhere', 'patchOrCreateWithWhere',
'exists',
'findById',
'replaceById',
Expand Down
10 changes: 10 additions & 0 deletions test/remoting.integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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',
Expand Down
13 changes: 13 additions & 0 deletions test/replication.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down