From 9d19525f07b61c9de6d180d7b8e4d0f19d1b31f1 Mon Sep 17 00:00:00 2001 From: Ivan Schwarz Date: Fri, 3 Aug 2018 16:01:26 +0200 Subject: [PATCH] Expose replace methods for hasMany relationship --- lib/model.js | 37 ++++++++++++++++-- test/relations.integration.js | 4 +- test/remoting.integration.js | 72 ++++++++++++++++++++++++++++++++--- 3 files changed, 102 insertions(+), 11 deletions(-) diff --git a/lib/model.js b/lib/model.js index eb955619b..676975cad 100644 --- a/lib/model.js +++ b/lib/model.js @@ -620,9 +620,13 @@ module.exports = function(registry) { }; Model.hasManyRemoting = function(relationName, relation, define) { + var Model = this; + var options = Model.settings; var pathName = (relation.options.http && relation.options.http.path) || relationName; var toModelName = relation.modelTo.modelName; + options.replaceOnPUT = options.replaceOnPUT !== false; + var findByIdFunc = this.prototype['__findById__' + relationName]; define('__findById__' + relationName, { isStatic: false, @@ -660,10 +664,31 @@ module.exports = function(registry) { returns: [], }, destroyByIdFunc); + var replaceByIdFunc = this.prototype['__replaceById__' + relationName]; + var replaceByIdOptions = { + isStatic: false, + http: [{verb: 'post', path: '/' + pathName + '/:fk/replace'}], + accepts: [ + {arg: 'fk', type: 'any', + description: format('Foreign key for %s', relationName), + required: true, + http: {source: 'path'}}, + {arg: 'data', type: 'object', model: toModelName, http: {source: 'body'}}, + {arg: 'options', type: 'object', http: 'optionsFromRequest'}, + ], + description: format('Replace a related item by id for %s.', relationName), + accessType: 'WRITE', + returns: {arg: 'result', type: toModelName, root: true}, + }; + if (options.replaceOnPUT) { + replaceByIdOptions.http.push({verb: 'put', path: '/' + pathName + '/:fk'}); + } + define('__replaceById__' + relationName, replaceByIdOptions, replaceByIdFunc); + var updateByIdFunc = this.prototype['__updateById__' + relationName]; - define('__updateById__' + relationName, { + var updateByIdOptions = { isStatic: false, - http: {verb: 'put', path: '/' + pathName + '/:fk'}, + http: [{verb: 'patch', path: '/' + pathName + '/:fk'}], accepts: [ {arg: 'fk', type: 'any', description: format('Foreign key for %s', relationName), @@ -672,10 +697,14 @@ module.exports = function(registry) { {arg: 'data', type: 'object', model: toModelName, http: {source: 'body'}}, {arg: 'options', type: 'object', http: 'optionsFromRequest'}, ], - description: format('Update a related item by id for %s.', relationName), + description: format('Patch a related item by id for %s.', relationName), accessType: 'WRITE', returns: {arg: 'result', type: toModelName, root: true}, - }, updateByIdFunc); + }; + if (!options.replaceOnPUT) { + updateByIdOptions.http.push({verb: 'put', path: '/' + pathName + '/:fk'}); + } + define('__updateById__' + relationName, updateByIdOptions, updateByIdFunc); if (relation.modelThrough || relation.type === 'referencesMany') { var modelThrough = relation.modelThrough || relation.modelTo; diff --git a/test/relations.integration.js b/test/relations.integration.js index b8b77f874..7bdc3db63 100644 --- a/test/relations.integration.js +++ b/test/relations.integration.js @@ -225,7 +225,7 @@ describe('relations - integration', function() { }); }); - describe('PUT /api/store/:id/widgets/:fk', function() { + describe('PATCH /api/store/:id/widgets/:fk', function() { beforeEach(function(done) { var self = this; this.store.widgets.create({ @@ -238,7 +238,7 @@ describe('relations - integration', function() { }); it('does not add default properties to request body', function(done) { var self = this; - self.request.put(self.url) + self.request.patch(self.url) .send({active: true}) .end(function(err) { if (err) return done(err); diff --git a/test/remoting.integration.js b/test/remoting.integration.js index a41305db4..892bf19c1 100644 --- a/test/remoting.integration.js +++ b/test/remoting.integration.js @@ -135,14 +135,17 @@ describe('remoting - integration', function() { function() { var storeClass = findClass('store'); var methods = getFormattedPrototypeMethods(storeClass.methods); - 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:object:widget):widget ' + + 'prototype.__replaceById__widgets(fk:any,data:object:widget):widget ' + + 'POST /stores/:id/widgets/:fk/replace', + 'prototype.__replaceById__widgets(fk:any,data:object:widget):widget ' + 'PUT /stores/:id/widgets/:fk', + 'prototype.__updateById__widgets(fk:any,data:object:widget):widget ' + + 'PATCH /stores/:id/widgets/:fk', 'prototype.__get__widgets(filter:object):widget ' + 'GET /stores/:id/widgets', 'prototype.__create__widgets(data:object:widget):widget ' + @@ -165,8 +168,12 @@ describe('remoting - integration', function() { 'GET /physicians/:id/patients/:fk', 'prototype.__destroyById__patients(fk:any) ' + 'DELETE /physicians/:id/patients/:fk', - 'prototype.__updateById__patients(fk:any,data:object:patient):patient ' + + 'prototype.__replaceById__patients(fk:any,data:object:patient):patient ' + + 'POST /physicians/:id/patients/:fk/replace', + 'prototype.__replaceById__patients(fk:any,data:object:patient):patient ' + 'PUT /physicians/:id/patients/:fk', + 'prototype.__updateById__patients(fk:any,data:object:patient):patient ' + + 'PATCH /physicians/:id/patients/:fk', 'prototype.__link__patients(fk:any,data:object:appointment):appointment ' + 'PUT /physicians/:id/patients/rel/:fk', 'prototype.__unlink__patients(fk:any) ' + @@ -231,7 +238,6 @@ describe('With model.settings.replaceOnPUT false', function() { function() { var storeClass = findClass('storeWithReplaceOnPUTfalse'); var methods = getFormattedMethodsExcludingRelations(storeClass.methods); - var expectedMethods = [ 'create(data:object:storeWithReplaceOnPUTfalse):storeWithReplaceOnPUTfalse POST /stores-updating', 'patchOrCreate(data:object:storeWithReplaceOnPUTfalse):storeWithReplaceOnPUTfalse PUT /stores-updating', @@ -252,9 +258,37 @@ describe('With model.settings.replaceOnPUT false', function() { 'createChangeStream(options:object):ReadableStream POST /stores-updating/change-stream', 'createChangeStream(options:object):ReadableStream GET /stores-updating/change-stream', ]; - expect(methods).to.eql(expectedMethods); }); + + it('should have correct signatures for hasMany methods', + function() { + var storeClass = findClass('storeWithReplaceOnPUTfalse'); + var methods = getFormattedPrototypeMethods(storeClass.methods); + + var expectedMethods = [ + 'prototype.__findById__widgets(fk:any):widget ' + + 'GET /stores-updating/:id/widgets/:fk', + 'prototype.__destroyById__widgets(fk:any) ' + + 'DELETE /stores-updating/:id/widgets/:fk', + 'prototype.__replaceById__widgets(fk:any,data:object:widget):widget ' + + 'POST /stores-updating/:id/widgets/:fk/replace', + 'prototype.__updateById__widgets(fk:any,data:object:widget):widget ' + + 'PATCH /stores-updating/:id/widgets/:fk', + 'prototype.__updateById__widgets(fk:any,data:object:widget):widget ' + + 'PUT /stores-updating/:id/widgets/:fk', + 'prototype.__get__widgets(filter:object):widget ' + + 'GET /stores-updating/:id/widgets', + 'prototype.__create__widgets(data:object:widget):widget ' + + 'POST /stores-updating/:id/widgets', + 'prototype.__delete__widgets() ' + + 'DELETE /stores-updating/:id/widgets', + 'prototype.__count__widgets(where:object):number ' + + 'GET /stores-updating/:id/widgets/count', + ]; + + expect(methods).to.include.members(expectedMethods); + }); }); describe('With model.settings.replaceOnPUT true', function() { @@ -278,6 +312,34 @@ describe('With model.settings.replaceOnPUT true', function() { 'prototype.patchAttributes(data:object:storeWithReplaceOnPUTtrue):storeWithReplaceOnPUTtrue PATCH /stores-replacing/:id', ]; + expect(methods).to.include.members(expectedMethods); + }); + + it('should have correct signatures for hasMany methods', + function() { + var storeClass = findClass('storeWithReplaceOnPUTtrue'); + var methods = getFormattedPrototypeMethods(storeClass.methods); + var expectedMethods = [ + 'prototype.__findById__widgets(fk:any):widget ' + + 'GET /stores-replacing/:id/widgets/:fk', + 'prototype.__destroyById__widgets(fk:any) ' + + 'DELETE /stores-replacing/:id/widgets/:fk', + 'prototype.__replaceById__widgets(fk:any,data:object:widget):widget ' + + 'POST /stores-replacing/:id/widgets/:fk/replace', + 'prototype.__replaceById__widgets(fk:any,data:object:widget):widget ' + + 'PUT /stores-replacing/:id/widgets/:fk', + 'prototype.__updateById__widgets(fk:any,data:object:widget):widget ' + + 'PATCH /stores-replacing/:id/widgets/:fk', + 'prototype.__get__widgets(filter:object):widget ' + + 'GET /stores-replacing/:id/widgets', + 'prototype.__create__widgets(data:object:widget):widget ' + + 'POST /stores-replacing/:id/widgets', + 'prototype.__delete__widgets() ' + + 'DELETE /stores-replacing/:id/widgets', + 'prototype.__count__widgets(where:object):number ' + + 'GET /stores-replacing/:id/widgets/count', + ]; + expect(methods).to.include.members(expectedMethods); }); });