Merge pull request #2435 from strongloop/expose_endpoints_2.x
Expose `Replace*` methods for 2.x
This commit is contained in:
commit
6c9df360b9
|
@ -75,6 +75,12 @@
|
||||||
"permission": "ALLOW",
|
"permission": "ALLOW",
|
||||||
"property": "updateAttributes"
|
"property": "updateAttributes"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"principalType": "ROLE",
|
||||||
|
"principalId": "$owner",
|
||||||
|
"permission": "ALLOW",
|
||||||
|
"property": "replaceById"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"principalType": "ROLE",
|
"principalType": "ROLE",
|
||||||
"principalId": "$everyone",
|
"principalId": "$everyone",
|
||||||
|
|
|
@ -125,10 +125,27 @@ module.exports = function(registry) {
|
||||||
* @param {Object} model Updated model instance.
|
* @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');
|
throwNotAttached(this.modelName, 'upsert');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace or insert a model instance; replace existing record if one is found,
|
||||||
|
* such that parameter `data.id` matches `id` of model instance; otherwise,
|
||||||
|
* insert a new record.
|
||||||
|
* @param {Object} data The model instance data.
|
||||||
|
* @options {Object} [options] Options for replaceOrCreate
|
||||||
|
* @property {Boolean} validate Perform validation before saving. Default is true.
|
||||||
|
* @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 Replaced model instance.
|
||||||
|
*/
|
||||||
|
|
||||||
|
PersistedModel.replaceOrCreate = function replaceOrCreate(data, callback) {
|
||||||
|
throwNotAttached(this.modelName, 'replaceOrCreate');
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds one record matching the optional filter object. If not found, creates
|
* Finds one record matching the optional filter object. If not found, creates
|
||||||
* the object using the data provided as second argument. In this sense it is
|
* the object using the data provided as second argument. In this sense it is
|
||||||
|
@ -492,10 +509,45 @@ module.exports = function(registry) {
|
||||||
* @param {Object} instance Updated instance.
|
* @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');
|
throwNotAttached(this.modelName, 'updateAttributes');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace attributes for a model instance and persist it into the datasource.
|
||||||
|
* Performs validation before replacing.
|
||||||
|
*
|
||||||
|
* @param {Object} data Data to replace.
|
||||||
|
* @options {Object} [options] Options for replace
|
||||||
|
* @property {Boolean} validate Perform validation before saving. Default is true.
|
||||||
|
* @callback {Function} callback Callback function called with `(err, instance)` arguments.
|
||||||
|
* @param {Error} err Error object; see [Error object](http://docs.strongloop.com/display/LB/Error+object).
|
||||||
|
* @param {Object} instance Replaced instance.
|
||||||
|
*/
|
||||||
|
|
||||||
|
PersistedModel.prototype.replaceAttributes = function replaceAttributes(data, cb) {
|
||||||
|
throwNotAttached(this.modelName, 'replaceAttributes');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace attributes for a model instance whose id is the first input
|
||||||
|
* argument and persist it into the datasource.
|
||||||
|
* Performs validation before replacing.
|
||||||
|
*
|
||||||
|
* @param {*} id The ID value of model instance to replace.
|
||||||
|
* @param {Object} data Data to replace.
|
||||||
|
* @options {Object} [options] Options for replace
|
||||||
|
* @property {Boolean} validate Perform validation before saving. Default is true.
|
||||||
|
* @callback {Function} callback Callback function called with `(err, instance)` arguments.
|
||||||
|
* @param {Error} err Error object; see [Error object](http://docs.strongloop.com/display/LB/Error+object).
|
||||||
|
* @param {Object} instance Replaced instance.
|
||||||
|
*/
|
||||||
|
|
||||||
|
PersistedModel.replaceById = function replaceById(id, data, cb) {
|
||||||
|
throwNotAttached(this.modelName, 'replaceById');
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reload object from persistence. Requires `id` member of `object` to be able to call `find`.
|
* Reload object from persistence. Requires `id` member of `object` to be able to call `find`.
|
||||||
* @callback {Function} callback Callback function called with `(err, instance)` arguments. Required.
|
* @callback {Function} callback Callback function called with `(err, instance)` arguments. Required.
|
||||||
|
@ -564,6 +616,9 @@ module.exports = function(registry) {
|
||||||
var typeName = PersistedModel.modelName;
|
var typeName = PersistedModel.modelName;
|
||||||
var options = PersistedModel.settings;
|
var options = PersistedModel.settings;
|
||||||
|
|
||||||
|
// This is just for LB 2.x
|
||||||
|
options.replaceOnPUT = options.replaceOnPUT === true;
|
||||||
|
|
||||||
function setRemoting(scope, name, options) {
|
function setRemoting(scope, name, options) {
|
||||||
var fn = scope[name];
|
var fn = scope[name];
|
||||||
fn._delegate = true;
|
fn._delegate = true;
|
||||||
|
@ -579,15 +634,35 @@ module.exports = function(registry) {
|
||||||
http: {verb: 'post', path: '/'}
|
http: {verb: 'post', path: '/'}
|
||||||
});
|
});
|
||||||
|
|
||||||
setRemoting(PersistedModel, 'upsert', {
|
var upsertOptions = {
|
||||||
aliases: ['updateOrCreate'],
|
aliases: ['patchOrCreate', 'updateOrCreate'],
|
||||||
description: g.s('Update an existing model instance or insert a new one ' +
|
description: g.s('Patch an existing model instance or insert a new one into the data source.'),
|
||||||
'into the data source.'),
|
|
||||||
accessType: 'WRITE',
|
accessType: 'WRITE',
|
||||||
accepts: {arg: 'data', type: 'object', description: 'Model instance data', http: {source: 'body'}},
|
accepts: { arg: 'data', type: 'object', http: { source: 'body' }, description:
|
||||||
|
'Model instance data' },
|
||||||
returns: { arg: 'data', type: typeName, root: true },
|
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, 'upsert', 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', {
|
setRemoting(PersistedModel, 'exists', {
|
||||||
description: g.s('Check whether a model instance exists in the data source.'),
|
description: g.s('Check whether a model instance exists in the data source.'),
|
||||||
|
@ -634,6 +709,25 @@ module.exports = function(registry) {
|
||||||
rest: {after: convertNullToNotFoundError}
|
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', {
|
setRemoting(PersistedModel, 'find', {
|
||||||
description: g.s('Find all instances of the model matched by filter from the data source.'),
|
description: g.s('Find all instances of the model matched by filter from the data source.'),
|
||||||
accessType: 'READ',
|
accessType: 'READ',
|
||||||
|
@ -702,14 +796,21 @@ module.exports = function(registry) {
|
||||||
http: {verb: 'get', path: '/count'}
|
http: {verb: 'get', path: '/count'}
|
||||||
});
|
});
|
||||||
|
|
||||||
setRemoting(PersistedModel.prototype, 'updateAttributes', {
|
var updateAttributesOptions = {
|
||||||
description: g.s('Update attributes for a model instance and persist it into ' +
|
aliases: ['patchAttributes'],
|
||||||
'the data source.'),
|
description: g.s('Patch attributes for a model instance and persist it into the data source.'),
|
||||||
accessType: 'WRITE',
|
accessType: 'WRITE',
|
||||||
|
|
||||||
accepts: { arg: 'data', type: 'object', http: { source: 'body' }, description: 'An object of model property name/value pairs' },
|
accepts: { arg: 'data', type: 'object', http: { source: 'body' }, description: 'An object of model property name/value pairs' },
|
||||||
returns: { arg: 'data', type: typeName, root: true },
|
returns: { arg: 'data', type: typeName, root: true },
|
||||||
http: {verb: 'put', path: '/'}
|
http: [{ verb: 'patch', path: '/' }],
|
||||||
});
|
};
|
||||||
|
|
||||||
|
setRemoting(PersistedModel.prototype, 'updateAttributes', updateAttributesOptions);
|
||||||
|
|
||||||
|
if (!options.replaceOnPUT) {
|
||||||
|
updateAttributesOptions.http.push({ verb: 'put', path: '/' });
|
||||||
|
}
|
||||||
|
|
||||||
if (options.trackChanges || options.enableRemoteReplication) {
|
if (options.trackChanges || options.enableRemoteReplication) {
|
||||||
setRemoting(PersistedModel, 'diff', {
|
setRemoting(PersistedModel, 'diff', {
|
||||||
|
|
|
@ -121,9 +121,15 @@ describe('access control - integration', function() {
|
||||||
assert.equal(user.password, undefined);
|
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.describe.whenCalledRemotely('PUT', '/api/users/:id', function() {
|
||||||
lt.it.shouldBeAllowed();
|
lt.it.shouldBeAllowed();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
lt.describe.whenCalledRemotely('PATCH', '/api/users/:id', function() {
|
||||||
|
lt.it.shouldBeAllowed();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
lt.it.shouldBeDeniedWhenCalledAnonymously('DELETE', urlForUser);
|
lt.it.shouldBeDeniedWhenCalledAnonymously('DELETE', urlForUser);
|
||||||
|
@ -173,7 +179,7 @@ describe('access control - integration', function() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('/accounts', function() {
|
describe('/accounts with replaceOnPUT true', function() {
|
||||||
var count = 0;
|
var count = 0;
|
||||||
before(function() {
|
before(function() {
|
||||||
var roleModel = loopback.getModelByType(loopback.Role);
|
var roleModel = loopback.getModelByType(loopback.Role);
|
||||||
|
@ -187,48 +193,68 @@ describe('access control - integration', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
lt.beforeEach.givenModel('account');
|
lt.beforeEach.givenModel('accountWithReplaceOnPUTtrue');
|
||||||
|
|
||||||
lt.it.shouldBeDeniedWhenCalledAnonymously('GET', '/api/accounts');
|
lt.it.shouldBeDeniedWhenCalledAnonymously('GET', '/api/accounts-replacing');
|
||||||
lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', '/api/accounts');
|
lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', '/api/accounts-replacing');
|
||||||
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'GET', '/api/accounts');
|
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'GET', '/api/accounts-replacing');
|
||||||
|
|
||||||
lt.it.shouldBeDeniedWhenCalledAnonymously('GET', urlForAccount);
|
lt.it.shouldBeDeniedWhenCalledAnonymously('GET', urlForAccount);
|
||||||
lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', urlForAccount);
|
lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', urlForAccount);
|
||||||
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'GET', urlForAccount);
|
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'GET', urlForAccount);
|
||||||
|
|
||||||
lt.it.shouldBeDeniedWhenCalledAnonymously('POST', '/api/accounts');
|
lt.it.shouldBeDeniedWhenCalledAnonymously('POST', '/api/accounts-replacing');
|
||||||
lt.it.shouldBeDeniedWhenCalledUnauthenticated('POST', '/api/accounts');
|
lt.it.shouldBeDeniedWhenCalledUnauthenticated('POST', '/api/accounts-replacing');
|
||||||
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'POST', '/api/accounts');
|
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.shouldBeDeniedWhenCalledAnonymously('PUT', urlForAccount);
|
||||||
lt.it.shouldBeDeniedWhenCalledUnauthenticated('PUT', urlForAccount);
|
lt.it.shouldBeDeniedWhenCalledUnauthenticated('PUT', urlForAccount);
|
||||||
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, '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() {
|
lt.describe.whenLoggedInAsUser(CURRENT_USER, function() {
|
||||||
|
var actId;
|
||||||
beforeEach(function(done) {
|
beforeEach(function(done) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
// Create an account under the given user
|
// Create an account under the given user
|
||||||
app.models.account.create({
|
app.models.accountWithReplaceOnPUTtrue.create({
|
||||||
userId: self.user.id,
|
userId: self.user.id,
|
||||||
balance: 100
|
balance: 100
|
||||||
}, function(err, act) {
|
}, function(err, act) {
|
||||||
self.url = '/api/accounts/' + act.id;
|
actId = act.id;
|
||||||
|
self.url = '/api/accounts-replacing/' + actId;
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
lt.describe.whenCalledRemotely('PATCH', '/api/accounts-replacing/:id', function() {
|
||||||
lt.describe.whenCalledRemotely('PUT', '/api/accounts/:id', function() {
|
|
||||||
lt.it.shouldBeAllowed();
|
lt.it.shouldBeAllowed();
|
||||||
});
|
});
|
||||||
lt.describe.whenCalledRemotely('GET', '/api/accounts/:id', function() {
|
lt.describe.whenCalledRemotely('PUT', '/api/accounts-replacing/:id', function() {
|
||||||
lt.it.shouldBeAllowed();
|
lt.it.shouldBeAllowed();
|
||||||
});
|
});
|
||||||
lt.describe.whenCalledRemotely('DELETE', '/api/accounts/:id', function() {
|
lt.describe.whenCalledRemotely('GET', '/api/accounts-replacing/:id', function() {
|
||||||
|
lt.it.shouldBeAllowed();
|
||||||
|
});
|
||||||
|
lt.describe.whenCalledRemotely('DELETE', '/api/accounts-replacing/:id', function() {
|
||||||
lt.it.shouldBeDenied();
|
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.shouldBeDeniedWhenCalledAnonymously('DELETE', urlForAccount);
|
||||||
|
@ -236,7 +262,77 @@ describe('access control - integration', function() {
|
||||||
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'DELETE', urlForAccount);
|
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'DELETE', urlForAccount);
|
||||||
|
|
||||||
function urlForAccount() {
|
function urlForAccount() {
|
||||||
return '/api/accounts/' + this.account.id;
|
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-updating/:id', function() {
|
||||||
|
lt.it.shouldBeAllowed();
|
||||||
|
});
|
||||||
|
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-updating/' + this.accountWithReplaceOnPUTfalse.id;
|
||||||
|
}
|
||||||
|
function urlForReplaceAccountPOST() {
|
||||||
|
return '/api/accounts-updating/' + this.accountWithReplaceOnPUTfalse.id + '/replace';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "account",
|
"name": "accountWithReplaceOnPUTtrue",
|
||||||
|
"plural": "accounts-replacing",
|
||||||
"relations": {
|
"relations": {
|
||||||
"transactions": {
|
"transactions": {
|
||||||
"model": "transaction",
|
"model": "transaction",
|
||||||
|
@ -38,5 +39,6 @@
|
||||||
"principalId": "$dummy"
|
"principalId": "$dummy"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"properties": {}
|
"properties": {},
|
||||||
|
"replaceOnPUT": true
|
||||||
}
|
}
|
44
test/fixtures/access-control/common/models/accountWithReplaceOnPUTfalse.json
vendored
Normal file
44
test/fixtures/access-control/common/models/accountWithReplaceOnPUTfalse.json
vendored
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -19,5 +19,6 @@
|
||||||
"principalType": "ROLE",
|
"principalType": "ROLE",
|
||||||
"principalId": "$everyone"
|
"principalId": "$everyone"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"replaceOnPUT": false
|
||||||
}
|
}
|
|
@ -34,7 +34,11 @@
|
||||||
"public": true,
|
"public": true,
|
||||||
"dataSource": "db"
|
"dataSource": "db"
|
||||||
},
|
},
|
||||||
"account": {
|
"accountWithReplaceOnPUTtrue": {
|
||||||
|
"public": true,
|
||||||
|
"dataSource": "db"
|
||||||
|
},
|
||||||
|
"accountWithReplaceOnPUTfalse": {
|
||||||
"public": true,
|
"public": true,
|
||||||
"dataSource": "db"
|
"dataSource": "db"
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"name": "storeWithReplaceOnPUTtrue",
|
||||||
|
"plural": "stores-replacing",
|
||||||
|
"properties": {},
|
||||||
|
"scopes": {
|
||||||
|
"superStores": {
|
||||||
|
"where": {
|
||||||
|
"size": "super"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"relations": {
|
||||||
|
"widgets": {
|
||||||
|
"model": "widget",
|
||||||
|
"type": "hasMany"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"replaceOnPUT": true
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"name": "storeWithReplaceOnPUTfalse",
|
||||||
|
"plural": "stores-updating",
|
||||||
|
"properties": {},
|
||||||
|
"scopes": {
|
||||||
|
"superStores": {
|
||||||
|
"where": {
|
||||||
|
"size": "super"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"relations": {
|
||||||
|
"widgets": {
|
||||||
|
"model": "widget",
|
||||||
|
"type": "hasMany"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"replaceOnPUT": false
|
||||||
|
}
|
|
@ -38,6 +38,14 @@
|
||||||
"public": true,
|
"public": true,
|
||||||
"dataSource": "db"
|
"dataSource": "db"
|
||||||
},
|
},
|
||||||
|
"storeWithReplaceOnPUTfalse": {
|
||||||
|
"public": true,
|
||||||
|
"dataSource": "db"
|
||||||
|
},
|
||||||
|
"storeWithReplaceOnPUTtrue": {
|
||||||
|
"public": true,
|
||||||
|
"dataSource": "db"
|
||||||
|
},
|
||||||
"physician": {
|
"physician": {
|
||||||
"dataSource": "db",
|
"dataSource": "db",
|
||||||
"public": true
|
"public": true
|
||||||
|
|
|
@ -627,9 +627,14 @@ describe.onServer('Remote Methods', function() {
|
||||||
var methodNames = [];
|
var methodNames = [];
|
||||||
metadata.methods.forEach(function(method) {
|
metadata.methods.forEach(function(method) {
|
||||||
methodNames.push(method.name);
|
methodNames.push(method.name);
|
||||||
methodNames = methodNames.concat(method.sharedMethod.aliases || []);
|
var aliases = method.sharedMethod.aliases;
|
||||||
|
if (method.name.indexOf('prototype.') === 0) {
|
||||||
|
aliases = aliases.map(function(alias) {
|
||||||
|
return 'prototype.' + alias;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
methodNames = methodNames.concat(aliases || []);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(methodNames).to.have.members([
|
expect(methodNames).to.have.members([
|
||||||
// NOTE(bajtos) These three methods are disabled by default
|
// NOTE(bajtos) These three methods are disabled by default
|
||||||
// Because all tests share the same global registry model
|
// Because all tests share the same global registry model
|
||||||
|
@ -637,9 +642,11 @@ describe.onServer('Remote Methods', function() {
|
||||||
// this test was seeing this method (with all aliases) as public
|
// this test was seeing this method (with all aliases) as public
|
||||||
// 'destroyAll', 'deleteAll', 'remove',
|
// 'destroyAll', 'deleteAll', 'remove',
|
||||||
'create',
|
'create',
|
||||||
'upsert', 'updateOrCreate',
|
'upsert', 'updateOrCreate', 'patchOrCreate',
|
||||||
'exists',
|
'exists',
|
||||||
'findById',
|
'findById',
|
||||||
|
'replaceById',
|
||||||
|
'replaceOrCreate',
|
||||||
'find',
|
'find',
|
||||||
'findOne',
|
'findOne',
|
||||||
'updateAll', 'update',
|
'updateAll', 'update',
|
||||||
|
@ -647,8 +654,8 @@ describe.onServer('Remote Methods', function() {
|
||||||
'destroyById',
|
'destroyById',
|
||||||
'removeById',
|
'removeById',
|
||||||
'count',
|
'count',
|
||||||
'prototype.updateAttributes',
|
'prototype.patchAttributes', 'prototype.updateAttributes',
|
||||||
'createChangeStream'
|
'createChangeStream',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -80,61 +80,28 @@ describe('remoting - integration', function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Model shared classes', function() {
|
describe('Model shared classes', function() {
|
||||||
function formatReturns(m) {
|
it('has expected remote methods with default model.settings.replaceOnPUT' +
|
||||||
var returns = m.returns;
|
'set to false (2.x)',
|
||||||
if (!returns || returns.length === 0) {
|
function() {
|
||||||
return '';
|
|
||||||
}
|
|
||||||
var type = returns[0].type;
|
|
||||||
return type ? ':' + type : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatMethod(m) {
|
|
||||||
return [
|
|
||||||
m.name,
|
|
||||||
'(',
|
|
||||||
m.accepts.map(function(a) {
|
|
||||||
return a.arg + ':' + a.type;
|
|
||||||
}).join(','),
|
|
||||||
')',
|
|
||||||
formatReturns(m),
|
|
||||||
' ',
|
|
||||||
m.getEndpoints()[0].verb,
|
|
||||||
' ',
|
|
||||||
m.getEndpoints()[0].fullPath
|
|
||||||
].join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
function findClass(name) {
|
|
||||||
return app.handler('rest').adapter
|
|
||||||
.getClasses()
|
|
||||||
.filter(function(c) {
|
|
||||||
return c.name === name;
|
|
||||||
})[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
it('has expected remote methods', function() {
|
|
||||||
var storeClass = findClass('store');
|
var storeClass = findClass('store');
|
||||||
var methods = storeClass.methods
|
var methods = getFormattedMethodsExcludingRelations(storeClass.methods);
|
||||||
.filter(function(m) {
|
|
||||||
return m.name.indexOf('__') === -1;
|
|
||||||
})
|
|
||||||
.map(function(m) {
|
|
||||||
return formatMethod(m);
|
|
||||||
});
|
|
||||||
|
|
||||||
var expectedMethods = [
|
var expectedMethods = [
|
||||||
'create(data:object):store POST /stores',
|
'create(data:object):store POST /stores',
|
||||||
|
'upsert(data:object):store PATCH /stores',
|
||||||
'upsert(data:object):store PUT /stores',
|
'upsert(data:object):store PUT /stores',
|
||||||
|
'replaceOrCreate(data:object):store POST /stores/replaceOrCreate',
|
||||||
'exists(id:any):boolean GET /stores/:id/exists',
|
'exists(id:any):boolean GET /stores/:id/exists',
|
||||||
'findById(id:any,filter:object):store GET /stores/:id',
|
'findById(id:any,filter:object):store GET /stores/:id',
|
||||||
|
'prototype.updateAttributes(data:object):store PUT /stores/:id',
|
||||||
|
'replaceById(id:any,data:object):store POST /stores/:id/replace',
|
||||||
'find(filter:object):store GET /stores',
|
'find(filter:object):store GET /stores',
|
||||||
'findOne(filter:object):store GET /stores/findOne',
|
'findOne(filter:object):store GET /stores/findOne',
|
||||||
'updateAll(where:object,data:object):object POST /stores/update',
|
'updateAll(where:object,data:object):object POST /stores/update',
|
||||||
'deleteById(id:any):object DELETE /stores/:id',
|
'deleteById(id:any):object DELETE /stores/:id',
|
||||||
'count(where:object):number GET /stores/count',
|
'count(where:object):number GET /stores/count',
|
||||||
'prototype.updateAttributes(data:object):store PUT /stores/:id',
|
'prototype.updateAttributes(data:object):store PATCH /stores/:id',
|
||||||
'createChangeStream(options:object):ReadableStream POST /stores/change-stream'
|
'createChangeStream(options:object):ReadableStream POST /stores/change-stream',
|
||||||
];
|
];
|
||||||
|
|
||||||
// The list of methods is from docs:
|
// The list of methods is from docs:
|
||||||
|
@ -144,13 +111,7 @@ describe('remoting - integration', function() {
|
||||||
|
|
||||||
it('has expected remote methods for scopes', function() {
|
it('has expected remote methods for scopes', function() {
|
||||||
var storeClass = findClass('store');
|
var storeClass = findClass('store');
|
||||||
var methods = storeClass.methods
|
var methods = getFormattedScopeMethods(storeClass.methods);
|
||||||
.filter(function(m) {
|
|
||||||
return m.name.indexOf('__') === 0;
|
|
||||||
})
|
|
||||||
.map(function(m) {
|
|
||||||
return formatMethod(m);
|
|
||||||
});
|
|
||||||
|
|
||||||
var expectedMethods = [
|
var expectedMethods = [
|
||||||
'__get__superStores(filter:object):store GET /stores/superStores',
|
'__get__superStores(filter:object):store GET /stores/superStores',
|
||||||
|
@ -166,13 +127,7 @@ describe('remoting - integration', function() {
|
||||||
function() {
|
function() {
|
||||||
|
|
||||||
var widgetClass = findClass('widget');
|
var widgetClass = findClass('widget');
|
||||||
var methods = widgetClass.methods
|
var methods = getFormattedPrototypeMethods(widgetClass.methods);
|
||||||
.filter(function(m) {
|
|
||||||
return m.name.indexOf('prototype.__') === 0;
|
|
||||||
})
|
|
||||||
.map(function(m) {
|
|
||||||
return formatMethod(m);
|
|
||||||
});
|
|
||||||
|
|
||||||
var expectedMethods = [
|
var expectedMethods = [
|
||||||
'prototype.__get__store(refresh:boolean):store ' +
|
'prototype.__get__store(refresh:boolean):store ' +
|
||||||
|
@ -185,13 +140,7 @@ describe('remoting - integration', function() {
|
||||||
function() {
|
function() {
|
||||||
|
|
||||||
var physicianClass = findClass('store');
|
var physicianClass = findClass('store');
|
||||||
var methods = physicianClass.methods
|
var methods = getFormattedPrototypeMethods(physicianClass.methods);
|
||||||
.filter(function(m) {
|
|
||||||
return m.name.indexOf('prototype.__') === 0;
|
|
||||||
})
|
|
||||||
.map(function(m) {
|
|
||||||
return formatMethod(m);
|
|
||||||
});
|
|
||||||
|
|
||||||
var expectedMethods = [
|
var expectedMethods = [
|
||||||
'prototype.__findById__widgets(fk:any):widget ' +
|
'prototype.__findById__widgets(fk:any):widget ' +
|
||||||
|
@ -214,15 +163,8 @@ describe('remoting - integration', function() {
|
||||||
|
|
||||||
it('should have correct signatures for hasMany-through methods',
|
it('should have correct signatures for hasMany-through methods',
|
||||||
function() { // jscs:disable validateIndentation
|
function() { // jscs:disable validateIndentation
|
||||||
|
|
||||||
var physicianClass = findClass('physician');
|
var physicianClass = findClass('physician');
|
||||||
var methods = physicianClass.methods
|
var methods = getFormattedPrototypeMethods(physicianClass.methods);
|
||||||
.filter(function(m) {
|
|
||||||
return m.name.indexOf('prototype.__') === 0;
|
|
||||||
})
|
|
||||||
.map(function(m) {
|
|
||||||
return formatMethod(m);
|
|
||||||
});
|
|
||||||
|
|
||||||
var expectedMethods = [
|
var expectedMethods = [
|
||||||
'prototype.__findById__patients(fk:any):patient ' +
|
'prototype.__findById__patients(fk:any):patient ' +
|
||||||
|
@ -251,3 +193,127 @@ describe('remoting - integration', function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('With model.settings.replaceOnPUT false', function() {
|
||||||
|
lt.beforeEach.withApp(app);
|
||||||
|
lt.beforeEach.givenModel('storeWithReplaceOnPUTfalse');
|
||||||
|
afterEach(function(done) {
|
||||||
|
this.app.models.storeWithReplaceOnPUTfalse.destroyAll(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have expected remote methods',
|
||||||
|
function() {
|
||||||
|
var storeClass = findClass('storeWithReplaceOnPUTfalse');
|
||||||
|
var methods = getFormattedMethodsExcludingRelations(storeClass.methods);
|
||||||
|
|
||||||
|
var expectedMethods = [
|
||||||
|
'upsert(data:object):storeWithReplaceOnPUTfalse PATCH /stores-updating',
|
||||||
|
'upsert(data:object):storeWithReplaceOnPUTfalse PUT /stores-updating',
|
||||||
|
'replaceOrCreate(data:object):storeWithReplaceOnPUTfalse POST /stores-updating/replaceOrCreate',
|
||||||
|
'replaceById(id:any,data:object):storeWithReplaceOnPUTfalse POST /stores-updating/:id/replace',
|
||||||
|
'prototype.updateAttributes(data:object):storeWithReplaceOnPUTfalse PATCH /stores-updating/:id',
|
||||||
|
'prototype.updateAttributes(data:object):storeWithReplaceOnPUTfalse PUT /stores-updating/:id',
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(methods).to.include.members(expectedMethods);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('With model.settings.replaceOnPUT true', function() {
|
||||||
|
lt.beforeEach.withApp(app);
|
||||||
|
lt.beforeEach.givenModel('storeWithReplaceOnPUTtrue');
|
||||||
|
afterEach(function(done) {
|
||||||
|
this.app.models.storeWithReplaceOnPUTtrue.destroyAll(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have expected remote methods',
|
||||||
|
function() {
|
||||||
|
var storeClass = findClass('storeWithReplaceOnPUTtrue');
|
||||||
|
var methods = getFormattedMethodsExcludingRelations(storeClass.methods);
|
||||||
|
|
||||||
|
var expectedMethods = [
|
||||||
|
'upsert(data:object):storeWithReplaceOnPUTtrue PATCH /stores-replacing',
|
||||||
|
'replaceOrCreate(data:object):storeWithReplaceOnPUTtrue POST /stores-replacing/replaceOrCreate',
|
||||||
|
'replaceOrCreate(data:object):storeWithReplaceOnPUTtrue PUT /stores-replacing',
|
||||||
|
'replaceById(id:any,data:object):storeWithReplaceOnPUTtrue POST /stores-replacing/:id/replace',
|
||||||
|
'replaceById(id:any,data:object):storeWithReplaceOnPUTtrue PUT /stores-replacing/:id',
|
||||||
|
'prototype.updateAttributes(data:object):storeWithReplaceOnPUTtrue PATCH /stores-replacing/:id',
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(methods).to.include.members(expectedMethods);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function formatReturns(m) {
|
||||||
|
var returns = m.returns;
|
||||||
|
if (!returns || returns.length === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
var type = returns[0].type;
|
||||||
|
return type ? ':' + type : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMethod(m) {
|
||||||
|
var arr = [];
|
||||||
|
var endpoints = m.getEndpoints();
|
||||||
|
for (var i = 0; i < endpoints.length; i++) {
|
||||||
|
arr.push([
|
||||||
|
m.name,
|
||||||
|
'(',
|
||||||
|
m.accepts.map(function(a) {
|
||||||
|
return a.arg + ':' + a.type;
|
||||||
|
}).join(','),
|
||||||
|
')',
|
||||||
|
formatReturns(m),
|
||||||
|
' ',
|
||||||
|
endpoints[i].verb,
|
||||||
|
' ',
|
||||||
|
endpoints[i].fullPath,
|
||||||
|
].join(''));
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findClass(name) {
|
||||||
|
return app.handler('rest').adapter
|
||||||
|
.getClasses()
|
||||||
|
.filter(function(c) {
|
||||||
|
return c.name === name;
|
||||||
|
})[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFormattedMethodsExcludingRelations(methods) {
|
||||||
|
return methods.filter(function(m) {
|
||||||
|
return m.name.indexOf('__') === -1;
|
||||||
|
})
|
||||||
|
.map(function(m) {
|
||||||
|
return formatMethod(m);
|
||||||
|
})
|
||||||
|
.reduce(function(p, c) {
|
||||||
|
return p.concat(c);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFormattedScopeMethods(methods) {
|
||||||
|
return methods.filter(function(m) {
|
||||||
|
return m.name.indexOf('__') === 0;
|
||||||
|
})
|
||||||
|
.map(function(m) {
|
||||||
|
return formatMethod(m);
|
||||||
|
})
|
||||||
|
.reduce(function(p, c) {
|
||||||
|
return p.concat(c);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFormattedPrototypeMethods(methods) {
|
||||||
|
return methods.filter(function(m) {
|
||||||
|
return m.name.indexOf('prototype.__') === 0;
|
||||||
|
})
|
||||||
|
.map(function(m) {
|
||||||
|
return formatMethod(m);
|
||||||
|
})
|
||||||
|
.reduce(function(p, c) {
|
||||||
|
return p.concat(c);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue