Merge pull request #2435 from strongloop/expose_endpoints_2.x

Expose `Replace*` methods for 2.x
This commit is contained in:
Amir-61 2016-08-15 15:05:32 -04:00 committed by GitHub
commit 6c9df360b9
12 changed files with 489 additions and 116 deletions

View File

@ -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",

View File

@ -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:
returns: {arg: 'data', type: typeName, root: true}, 'Model instance data' },
http: {verb: 'put', path: '/'} returns: { arg: 'data', type: typeName, root: true },
}); 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'},
returns: {arg: 'data', type: typeName, root: true}, accepts: { arg: 'data', type: 'object', http: { source: 'body' }, description: 'An object of model property name/value pairs' },
http: {verb: 'put', path: '/'} returns: { arg: 'data', type: typeName, root: true },
}); 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', {

View File

@ -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';
} }
}); });

View File

@ -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
} }

View 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
}

View File

@ -19,5 +19,6 @@
"principalType": "ROLE", "principalType": "ROLE",
"principalId": "$everyone" "principalId": "$everyone"
} }
] ],
"replaceOnPUT": false
} }

View File

@ -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"
}, },

View File

@ -0,0 +1,19 @@
{
"name": "storeWithReplaceOnPUTtrue",
"plural": "stores-replacing",
"properties": {},
"scopes": {
"superStores": {
"where": {
"size": "super"
}
}
},
"relations": {
"widgets": {
"model": "widget",
"type": "hasMany"
}
},
"replaceOnPUT": true
}

View File

@ -0,0 +1,19 @@
{
"name": "storeWithReplaceOnPUTfalse",
"plural": "stores-updating",
"properties": {},
"scopes": {
"superStores": {
"where": {
"size": "super"
}
}
},
"relations": {
"widgets": {
"model": "widget",
"type": "hasMany"
}
},
"replaceOnPUT": false
}

View File

@ -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

View File

@ -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',
]); ]);
}); });

View File

@ -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 = getFormattedPrototypeMethods(physicianClass.methods);
var methods = 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);
});
}