Skip to content

Commit

Permalink
Expose Replace* methods
Browse files Browse the repository at this point in the history
*Re-mapping `updateAttributes` endpoint to use
`PATCH` and `PUT`(configurable) verb
*Exposing `replaceById` and `replaceOrCreate` via
`POST` and `PUT`(configurable) verb
  • Loading branch information
Amir-61 committed Jun 10, 2016
1 parent ed45358 commit 6502309
Show file tree
Hide file tree
Showing 12 changed files with 429 additions and 105 deletions.
6 changes: 6 additions & 0 deletions common/models/user.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@
"permission": "ALLOW",
"property": "updateAttributes"
},
{
"principalType": "ROLE",
"principalId": "$owner",
"permission": "ALLOW",
"property": "replaceById"
},
{
"principalType": "ROLE",
"principalId": "$everyone",
Expand Down
74 changes: 63 additions & 11 deletions lib/persisted-model.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ module.exports = function(registry) {
* @param {Object} model Updated model instance.
*/

PersistedModel.upsert = PersistedModel.updateOrCreate = function upsert(data, callback) {
PersistedModel.upsert = PersistedModel.updateOrCreate = PersistedModel.patchOrCreate =
function upsert(data, callback) {
throwNotAttached(this.modelName, 'upsert');
};

Expand Down Expand Up @@ -494,7 +495,8 @@ module.exports = function(registry) {
* @param {Object} instance Updated instance.
*/

PersistedModel.prototype.updateAttributes = function updateAttributes(data, cb) {
PersistedModel.prototype.updateAttributes = PersistedModel.prototype.patchAttributes =
function updateAttributes(data, cb) {
throwNotAttached(this.modelName, 'updateAttributes');
};

Expand Down Expand Up @@ -600,6 +602,9 @@ module.exports = function(registry) {
var typeName = PersistedModel.modelName;
var options = PersistedModel.settings;

// This is just for LB 3.x
options.replaceOnPUT = options.replaceOnPUT !== false;

function setRemoting(scope, name, options) {
var fn = scope[name];
fn._delegate = true;
Expand All @@ -616,15 +621,35 @@ module.exports = function(registry) {
http: { verb: 'post', path: '/' },
});

setRemoting(PersistedModel, 'upsert', {
aliases: ['updateOrCreate'],
description: 'Update an existing model instance or insert a new one into the data source.',
var upsertOptions = {
aliases: ['upsert', 'updateOrCreate'],
description: 'Patch an existing model instance or insert a new one into the data source.',
accessType: 'WRITE',
accepts: { arg: 'data', type: 'object', http: { source: 'body' }, description:
'Model instance data' },
returns: { arg: 'data', type: typeName, root: true },
http: { verb: 'put', path: '/' },
});
http: [{ verb: 'patch', path: '/' }],
};

if (!options.replaceOnPUT) {
upsertOptions.http.push({ verb: 'put', path: '/' });
}
setRemoting(PersistedModel, 'patchOrCreate', upsertOptions);

var replaceOrCreateOptions = {
description: 'Replace an existing model instance or insert a new one into the data source.',
accessType: 'WRITE',
accepts: { arg: 'data', type: 'object', http: { source: 'body' }, description:
'Model instance data' },
returns: { arg: 'data', type: typeName, root: true },
http: [{ verb: 'post', path: '/replaceOrCreate' }],
};

if (options.replaceOnPUT) {
replaceOrCreateOptions.http.push({ verb: 'put', path: '/' });
}

setRemoting(PersistedModel, 'replaceOrCreate', replaceOrCreateOptions);

setRemoting(PersistedModel, 'exists', {
description: 'Check whether a model instance exists in the data source.',
Expand Down Expand Up @@ -671,6 +696,26 @@ module.exports = function(registry) {
rest: { after: convertNullToNotFoundError },
});

var replaceByIdOptions = {
description: 'Replace attributes for a model instance and persist it into the data source.',
accessType: 'WRITE',
accepts: [
{ arg: 'id', type: 'any', description: 'Model id', required: true,
http: { source: 'path' }},
{ arg: 'data', type: 'object', http: { source: 'body' }, description:
'Model instance data' },
],
returns: { arg: 'data', type: typeName, root: true },
http: [{ verb: 'post', path: '/:id/replace' }],
};

if (options.replaceOnPUT) {
replaceByIdOptions.http.push({ verb: 'put', path: '/:id' });
}

setRemoting(PersistedModel, 'replaceById', replaceByIdOptions);


setRemoting(PersistedModel, 'find', {
description: 'Find all instances of the model matched by filter from the data source.',
accessType: 'READ',
Expand Down Expand Up @@ -741,13 +786,20 @@ module.exports = function(registry) {
http: { verb: 'get', path: '/count' },
});

setRemoting(PersistedModel.prototype, 'updateAttributes', {
description: 'Update attributes for a model instance and persist it into the data source.',
var updateAttributesOptions = {
aliases: ['updateAttributes'],
description: 'Patch attributes for a model instance and persist it into the data source.',
accessType: 'WRITE',
accepts: { 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: 'put', path: '/' },
});
http: [{ verb: 'patch', path: '/' }],
};

setRemoting(PersistedModel.prototype, 'patchAttributes', updateAttributesOptions);

if (!options.replaceOnPUT) {
updateAttributesOptions.http.push({ verb: 'put', path: '/' });
}

if (options.trackChanges || options.enableRemoteReplication) {
setRemoting(PersistedModel, 'diff', {
Expand Down
125 changes: 110 additions & 15 deletions test/access-control.integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,15 @@ describe('access control - integration', function() {
assert.equal(user.password, undefined);
});
});

// user has replaceOnPUT = false; so then both PUT and PATCH should be allowed for update
lt.describe.whenCalledRemotely('PUT', '/api/users/:id', function() {
lt.it.shouldBeAllowed();
});

lt.describe.whenCalledRemotely('PATCH', '/api/users/:id', function() {
lt.it.shouldBeAllowed();
});
});

lt.it.shouldBeDeniedWhenCalledAnonymously('DELETE', urlForUser);
Expand Down Expand Up @@ -163,7 +169,7 @@ describe('access control - integration', function() {
}
});

describe('/accounts', function() {
describe('/accounts with replaceOnPUT true', function() {
var count = 0;
before(function() {
var roleModel = loopback.getModelByType(loopback.Role);
Expand All @@ -177,56 +183,145 @@ describe('access control - integration', function() {
});
});

lt.beforeEach.givenModel('account');
lt.beforeEach.givenModel('accountWithReplaceOnPUTtrue');

lt.it.shouldBeDeniedWhenCalledAnonymously('GET', '/api/accounts');
lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', '/api/accounts');
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'GET', '/api/accounts');
lt.it.shouldBeDeniedWhenCalledAnonymously('GET', '/api/accounts-replacing');
lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', '/api/accounts-replacing');
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'GET', '/api/accounts-replacing');

lt.it.shouldBeDeniedWhenCalledAnonymously('GET', urlForAccount);
lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', urlForAccount);
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'GET', urlForAccount);

lt.it.shouldBeDeniedWhenCalledAnonymously('POST', '/api/accounts');
lt.it.shouldBeDeniedWhenCalledUnauthenticated('POST', '/api/accounts');
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'POST', '/api/accounts');
lt.it.shouldBeDeniedWhenCalledAnonymously('POST', '/api/accounts-replacing');
lt.it.shouldBeDeniedWhenCalledUnauthenticated('POST', '/api/accounts-replacing');
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'POST', '/api/accounts-replacing');

lt.it.shouldBeDeniedWhenCalledAnonymously('POST', urlForReplaceAccountPOST);
lt.it.shouldBeDeniedWhenCalledUnauthenticated('POST', urlForReplaceAccountPOST);
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'POST', urlForReplaceAccountPOST);

lt.it.shouldBeDeniedWhenCalledAnonymously('PUT', urlForAccount);
lt.it.shouldBeDeniedWhenCalledUnauthenticated('PUT', urlForAccount);
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'PUT', urlForAccount);

lt.it.shouldBeDeniedWhenCalledAnonymously('PATCH', urlForAccount);
lt.it.shouldBeDeniedWhenCalledUnauthenticated('PATCH', urlForAccount);
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'PATCH', urlForAccount);

lt.describe.whenLoggedInAsUser(CURRENT_USER, function() {
var actId;
beforeEach(function(done) {
var self = this;

// Create an account under the given user
app.models.account.create({
app.models.accountWithReplaceOnPUTtrue.create({
userId: self.user.id,
balance: 100,
}, function(err, act) {
self.url = '/api/accounts/' + act.id;
actId = act.id;
self.url = '/api/accounts-replacing/' + actId;
done();
});
});

lt.describe.whenCalledRemotely('PATCH', '/api/accounts-replacing/:id', function() {
lt.it.shouldBeAllowed();
});
lt.describe.whenCalledRemotely('PUT', '/api/accounts-replacing/:id', function() {
lt.it.shouldBeAllowed();
});
lt.describe.whenCalledRemotely('GET', '/api/accounts-replacing/:id', function() {
lt.it.shouldBeAllowed();
});
lt.describe.whenCalledRemotely('DELETE', '/api/accounts-replacing/:id', function() {
lt.it.shouldBeDenied();
});
describe('replace on POST verb', function() {
beforeEach(function(done) {
this.url = '/api/accounts-replacing/' + actId + '/replace';
done();
});
lt.describe.whenCalledRemotely('POST', '/api/accounts-replacing/:id/replace', function() {
lt.it.shouldBeAllowed();
});
});
});

lt.it.shouldBeDeniedWhenCalledAnonymously('DELETE', urlForAccount);
lt.it.shouldBeDeniedWhenCalledUnauthenticated('DELETE', urlForAccount);
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'DELETE', urlForAccount);

lt.describe.whenCalledRemotely('PUT', '/api/accounts/:id', function() {
function urlForAccount() {
return '/api/accounts-replacing/' + this.accountWithReplaceOnPUTtrue.id;
}
function urlForReplaceAccountPOST() {
return '/api/accounts-replacing/' + this.accountWithReplaceOnPUTtrue.id + '/replace';
}
});

describe('/accounts with replaceOnPUT false', function() {
lt.beforeEach.givenModel('accountWithReplaceOnPUTfalse');
lt.it.shouldBeDeniedWhenCalledAnonymously('POST', urlForReplaceAccountPOST);
lt.it.shouldBeDeniedWhenCalledUnauthenticated('POST', urlForReplaceAccountPOST);
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'POST', urlForReplaceAccountPOST);

lt.it.shouldBeDeniedWhenCalledAnonymously('PUT', urlForAccount);
lt.it.shouldBeDeniedWhenCalledUnauthenticated('PUT', urlForAccount);
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'PUT', urlForAccount);

lt.it.shouldBeDeniedWhenCalledAnonymously('PATCH', urlForAccount);
lt.it.shouldBeDeniedWhenCalledUnauthenticated('PATCH', urlForAccount);
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'PATCH', urlForAccount);

lt.describe.whenLoggedInAsUser(CURRENT_USER, function() {
var actId;
beforeEach(function(done) {
var self = this;
// Create an account under the given user
app.models.accountWithReplaceOnPUTfalse.create({
userId: self.user.id,
balance: 100,
}, function(err, act) {
actId = act.id;
self.url = '/api/accounts-updating/' + actId;
done();
});
});

lt.describe.whenCalledRemotely('PATCH', '/api/accounts-updating/:id', function() {
lt.it.shouldBeAllowed();
});

lt.describe.whenCalledRemotely('PUT', '/api/accounts-updating/:id', function() {
lt.it.shouldBeAllowed();
});
lt.describe.whenCalledRemotely('GET', '/api/accounts/:id', function() {
lt.describe.whenCalledRemotely('GET', '/api/accounts-updating/:id', function() {
lt.it.shouldBeAllowed();
});
lt.describe.whenCalledRemotely('DELETE', '/api/accounts/:id', function() {
lt.describe.whenCalledRemotely('DELETE', '/api/accounts-updating/:id', function() {
lt.it.shouldBeDenied();
});

describe('replace on POST verb', function() {
beforeEach(function(done) {
this.url = '/api/accounts-updating/' + actId + '/replace';
done();
});
lt.describe.whenCalledRemotely('POST', '/api/accounts-updating/:id/replace', function() {
lt.it.shouldBeAllowed();
});
});
});

lt.it.shouldBeDeniedWhenCalledAnonymously('DELETE', urlForAccount);
lt.it.shouldBeDeniedWhenCalledUnauthenticated('DELETE', urlForAccount);
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'DELETE', urlForAccount);

function urlForAccount() {
return '/api/accounts/' + this.account.id;
return '/api/accounts-updating/' + this.accountWithReplaceOnPUTfalse.id;
}
function urlForReplaceAccountPOST() {
return '/api/accounts-updating/' + this.accountWithReplaceOnPUTfalse.id + '/replace';
}
});
});
6 changes: 4 additions & 2 deletions test/fixtures/access-control/common/models/account.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"name": "account",
"name": "accountWithReplaceOnPUTtrue",
"plural": "accounts-replacing",
"relations": {
"transactions": {
"model": "transaction",
Expand Down Expand Up @@ -38,5 +39,6 @@
"principalId": "$dummy"
}
],
"properties": {}
"properties": {},
"replaceOnPUT": true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "accountWithReplaceOnPUTfalse",
"plural": "accounts-updating",
"relations": {
"transactions": {
"model": "transaction",
"type": "hasMany"
},
"user": {
"model": "user",
"type": "belongsTo",
"foreignKey": "userId"
}
},
"acls": [
{
"accessType": "*",
"permission": "DENY",
"principalType": "ROLE",
"principalId": "$everyone"
},
{
"accessType": "*",
"permission": "ALLOW",
"principalType": "ROLE",
"principalId": "$owner"
},
{
"permission": "DENY",
"principalType": "ROLE",
"principalId": "$owner",
"property": "deleteById"
},
{
"accessType": "*",
"permission": "DENY",
"property": "find",
"principalType": "ROLE",
"principalId": "$dummy"
}
],
"properties": {},
"replaceOnPUT": false
}
3 changes: 2 additions & 1 deletion test/fixtures/access-control/common/models/user.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@
"principalType": "ROLE",
"principalId": "$everyone"
}
]
],
"replaceOnPUT": false
}
Loading

0 comments on commit 6502309

Please sign in to comment.