From 4c7c8901ffa2d9f30e07f7ded1ebd5416dddfc32 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Mon, 17 Nov 2014 09:44:20 -0800 Subject: [PATCH] Fix the model name for hasMany/through relation --- lib/model.js | 13 ++- test/relations.integration.js | 8 +- test/remoting.integration.js | 183 ++++++++++++++++++++++++++++++---- 3 files changed, 178 insertions(+), 26 deletions(-) diff --git a/lib/model.js b/lib/model.js index 9423d75a..f6a3e9af 100644 --- a/lib/model.js +++ b/lib/model.js @@ -457,7 +457,7 @@ Model.hasManyRemoting = function(relationName, relation, define) { description: 'Foreign key for ' + relationName, required: true, http: {source: 'path'}}, description: 'Delete a related item by id for ' + relationName, - returns: {} + returns: [] }, destroyByIdFunc); var updateByIdFunc = this.prototype['__updateById__' + relationName]; @@ -502,7 +502,7 @@ Model.hasManyRemoting = function(relationName, relation, define) { description: 'Foreign key for ' + relationName, required: true, http: {source: 'path'}}, description: 'Remove the ' + relationName + ' relation to an item by id', - returns: {} + returns: [] }, removeFunc); // FIXME: [rfeng] How to map a function with callback(err, true|false) to HEAD? @@ -542,6 +542,15 @@ Model.scopeRemoting = function(scopeName, scope, define) { var isStatic = scope.isStatic; var toModelName = scope.modelTo.modelName; + // https://github.com/strongloop/loopback/issues/811 + // Check if the scope is for a hasMany relation + var relation = this.relations[scopeName]; + if (relation && relation.modelTo) { + // For a relation with through model, the toModelName should be the one + // from the target model + toModelName = relation.modelTo.modelName; + } + define('__get__' + scopeName, { isStatic: isStatic, http: {verb: 'get', path: '/' + pathName}, diff --git a/test/relations.integration.js b/test/relations.integration.js index 306f949f..04460223 100644 --- a/test/relations.integration.js +++ b/test/relations.integration.js @@ -411,8 +411,8 @@ describe('relations - integration', function () { }); lt.describe.whenCalledRemotely('DELETE', '/api/physicians/:id/patients/rel/:fk', function () { - it('should succeed with statusCode 200', function () { - assert.equal(this.res.statusCode, 200); + it('should succeed with statusCode 204', function () { + assert.equal(this.res.statusCode, 204); }); it('should remove the record in appointment', function (done) { @@ -469,8 +469,8 @@ describe('relations - integration', function () { }); lt.describe.whenCalledRemotely('DELETE', '/api/physicians/:id/patients/:fk', function () { - it('should succeed with statusCode 200', function () { - assert.equal(this.res.statusCode, 200); + it('should succeed with statusCode 204', function () { + assert.equal(this.res.statusCode, 204); }); it('should remove the record in appointment', function (done) { diff --git a/test/remoting.integration.js b/test/remoting.integration.js index aa017079..0ba0e90f 100644 --- a/test/remoting.integration.js +++ b/test/remoting.integration.js @@ -66,33 +66,176 @@ describe('remoting - integration', function () { }); }); - describe('Model', function() { - it('has expected remote methods', function() { - var storeClass = app.handler('rest').adapter + describe('Model shared classes', function() { + function formatReturns(m) { + var returns = m.returns; + if (!returns || returns.length === 0) { + 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.getHttpMethod(), + ' ', + m.getFullPath() + ].join(''); + } + + function findClass(name) { + return app.handler('rest').adapter .getClasses() - .filter(function(c) { return c.name === 'store'; })[0]; + .filter(function(c) { + return c.name === name; + })[0]; + } + + it('has expected remote methods', function() { + var storeClass = findClass('store'); var methods = storeClass.methods + .filter(function(m) { + return m.name.indexOf('__') === -1; + }) .map(function(m) { - return [ - m.name + '()', - m.getHttpMethod(), - m.getFullPath() - ].join(' '); + return formatMethod(m); }); + var expectedMethods = [ + 'create(data:object):store POST /stores', + 'upsert(data:object):store PUT /stores', + 'exists(id:any):boolean GET /stores/:id/exists', + 'findById(id:any):store GET /stores/:id', + 'find(filter:object):store GET /stores', + 'findOne(filter:object):store GET /stores/findOne', + 'updateAll(where:object,data:object) POST /stores/update', + 'deleteById(id:any) DELETE /stores/:id', + 'count(where:object):number GET /stores/count', + 'prototype.updateAttributes(data:object):store PUT /stores/:id' + ]; + // The list of methods is from docs: // http://docs.strongloop.com/display/LB/Exposing+models+over+a+REST+API - expect(methods).to.include.members([ - 'create() POST /stores', - 'upsert() PUT /stores', - 'exists() GET /stores/:id/exists', - 'findById() GET /stores/:id', - 'find() GET /stores', - 'findOne() GET /stores/findOne', - 'deleteById() DELETE /stores/:id', - 'count() GET /stores/count', - 'prototype.updateAttributes() PUT /stores/:id', - ]); + expect(methods).to.include.members(expectedMethods); + }); + + it('has expected remote methods for scopes', function() { + var storeClass = findClass('store'); + var methods = storeClass.methods + .filter(function(m) { + return m.name.indexOf('__') === 0; + }) + .map(function(m) { + return formatMethod(m); + }); + + var expectedMethods = [ + '__get__superStores(filter:object):store GET /stores/superStores', + '__create__superStores(data:store):store POST /stores/superStores', + '__delete__superStores() DELETE /stores/superStores', + '__count__superStores(where:object):number GET /stores/superStores/count' + ]; + + expect(methods).to.include.members(expectedMethods); + }); + + + it('should have correct signatures for belongsTo methods', + function() { + + var widgetClass = findClass('widget'); + var methods = widgetClass.methods + .filter(function(m) { + return m.name.indexOf('prototype.__') === 0; + }) + .map(function(m) { + return formatMethod(m); + }); + + var expectedMethods = [ + 'prototype.__get__store(refresh:boolean):store ' + + 'GET /widgets/:id/store' + ]; + expect(methods).to.include.members(expectedMethods); + }); + + + it('should have correct signatures for hasMany methods', + function() { + + var physicianClass = findClass('store'); + var methods = physicianClass.methods + .filter(function(m) { + return m.name.indexOf('prototype.__') === 0; + }) + .map(function(m) { + return formatMethod(m); + }); + + var expectedMethods = [ + 'prototype.__findById__widgets(fk:any):widget ' + + 'GET /stores/:id/widgets/:fk', + 'prototype.__destroyById__widgets(fk:any) ' + + 'DELETE /stores/:id/widgets/:fk', + 'prototype.__updateById__widgets(fk:any,data:widget):widget ' + + 'PUT /stores/:id/widgets/:fk', + 'prototype.__get__widgets(filter:object):widget ' + + 'GET /stores/:id/widgets', + 'prototype.__create__widgets(data:widget):widget ' + + 'POST /stores/:id/widgets', + 'prototype.__delete__widgets() ' + + 'DELETE /stores/:id/widgets', + 'prototype.__count__widgets(where:object):number ' + + 'GET /stores/:id/widgets/count' + ]; + expect(methods).to.include.members(expectedMethods); + }); + + it('should have correct signatures for hasMany-through methods', + function() { + + var physicianClass = findClass('physician'); + var methods = physicianClass.methods + .filter(function(m) { + return m.name.indexOf('prototype.__') === 0; + }) + .map(function(m) { + return formatMethod(m); + }); + + var expectedMethods = [ + 'prototype.__findById__patients(fk:any):patient ' + + 'GET /physicians/:id/patients/:fk', + 'prototype.__destroyById__patients(fk:any) ' + + 'DELETE /physicians/:id/patients/:fk', + 'prototype.__updateById__patients(fk:any,data:patient):patient ' + + 'PUT /physicians/:id/patients/:fk', + 'prototype.__link__patients(fk:any,data:appointment):appointment ' + + 'PUT /physicians/:id/patients/rel/:fk', + 'prototype.__unlink__patients(fk:any) ' + + 'DELETE /physicians/:id/patients/rel/:fk', + 'prototype.__exists__patients(fk:any):boolean ' + + 'HEAD /physicians/:id/patients/rel/:fk', + 'prototype.__get__patients(filter:object):patient ' + + 'GET /physicians/:id/patients', + 'prototype.__create__patients(data:patient):patient ' + + 'POST /physicians/:id/patients', + 'prototype.__delete__patients() ' + + 'DELETE /physicians/:id/patients', + 'prototype.__count__patients(where:object):number ' + + 'GET /physicians/:id/patients/count' + ]; + expect(methods).to.include.members(expectedMethods); }); }); + });