├── index.js ├── .editorconfig ├── .eslintrc ├── lib └── utils.js ├── .gitignore ├── package.json ├── LICENSE ├── setup-remote-methods.js └── README.md /index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "loopback", 3 | "globals": { 4 | "expect": false, 5 | "jasmine": false, 6 | "beforeAll": false, 7 | "afterAll": false 8 | }, 9 | "rules": { 10 | "max-len": ["error", 100, 4, {"ignoreComments": true, "ignoreUrls": true, 11 | "ignorePattern": "^\\s*var\\s.+=\\s*require\\s*\\("}], 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | getAuthorizedAclMethods, 5 | }; 6 | 7 | function getAuthorizedAclMethods(Model) { 8 | let authorizedMethods = []; 9 | let acls = Model.settings.acls || []; 10 | 11 | acls.forEach((acl) => { 12 | if (acl.permission === 'ALLOW' && acl.property) { 13 | if (!Array.isArray(acl.property)) { 14 | acl.property = [acl.property]; 15 | } 16 | authorizedMethods = authorizedMethods.concat(acl.property); 17 | } 18 | }); 19 | 20 | return authorizedMethods; 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # IDE 40 | .idea 41 | .DS_Store 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loopback-setup-remote-methods-mixin", 3 | "version": "1.2.1", 4 | "description": "Mixin for Loopback, to easily disable remote methods and setup new ones from the model configuration file.", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "eslint .", 8 | "posttest": "npm run lint" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/devsu/loopback-setup-remote-methods-mixin.git" 13 | }, 14 | "keywords": [ 15 | "loopback", 16 | "mixins", 17 | "mixin", 18 | "remote", 19 | "methods" 20 | ], 21 | "author": "César Salazar", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/devsu/loopback-setup-remote-methods-mixin/issues" 25 | }, 26 | "homepage": "https://github.com/devsu/loopback-setup-remote-methods-mixin#readme", 27 | "devDependencies": { 28 | "eslint": "^3.11.1", 29 | "eslint-config-loopback": "^6.0.0", 30 | "jasmine": "^2.5.2" 31 | }, 32 | "dependencies": { 33 | "debug": "^2.3.3", 34 | "lodash": "^4.17.2" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Devsu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /setup-remote-methods.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Utils = require('./lib/utils'); 3 | const _ = require('lodash'); 4 | const path = require('path'); 5 | const debug = require('debug')('loopback:contrib:setup-remote-methods-mixin'); 6 | 7 | module.exports = (Model, options) => { 8 | if (!Model || !Model.sharedClass) { 9 | return; 10 | } 11 | 12 | let methodsAdded = []; 13 | 14 | if (options.add) { 15 | processAdd(); 16 | } 17 | 18 | if (options.addFromFile) { 19 | processAddFromFile(); 20 | } 21 | 22 | // wait for all models to be attached so sharedClass.methods() returns all methods 23 | Model.on('attached', function() { 24 | if (options.disable || options.disableAllExcept || options.relations) { 25 | processDisable(); 26 | } 27 | }); 28 | 29 | function processAdd() { 30 | let definitions = {}; 31 | let keys = Object.keys(options.add); 32 | keys.forEach(key => { 33 | definitions[key] = options.add[key]; 34 | if (isString(options.add[key])) { 35 | let definitionMethodName = options.add[key]; 36 | definitions[key] = getMethodWithName(definitionMethodName)(); 37 | } 38 | }); 39 | addRemoteMethods(definitions); 40 | } 41 | 42 | function processAddFromFile() { 43 | let opt = options.addFromFile; 44 | let addAll = false; 45 | 46 | if (isString(opt)) { 47 | opt = {filename: opt}; 48 | addAll = true; 49 | } 50 | 51 | let filename = path.join(process.cwd(), opt.filename); 52 | let definitions = require(filename); 53 | 54 | if (!addAll) { 55 | definitions = _.pickBy(definitions, (definition, key) => { 56 | return _.includes(opt.methods, key); 57 | }); 58 | } 59 | 60 | definitions = _.mapValues(definitions, definitionMethod => { 61 | return definitionMethod(); 62 | }); 63 | 64 | addRemoteMethods(definitions); 65 | } 66 | 67 | function addRemoteMethods(methodsToAdd) { 68 | let methodNames = Object.keys(methodsToAdd); 69 | methodNames.forEach(methodName => { 70 | Model.remoteMethod(methodName, methodsToAdd[methodName]); 71 | }); 72 | debug('Model `%s`: Add remote methods: `%s`', Model.modelName, methodNames.join(', ')); 73 | methodsAdded = methodsAdded.concat(methodNames); 74 | } 75 | 76 | function processDisable() { 77 | const allMethods = getAllMethods(); 78 | let methodsToDisable = []; 79 | let methodsToKeep = []; 80 | 81 | if (options.disable) { 82 | methodsToDisable = expandWildCards(options.disable); 83 | } 84 | 85 | if (options.relations) { 86 | const allRelations = Object.keys(Model.settings.relations); 87 | let relationNames = Object.keys(options.relations); 88 | allMethods.forEach(method => { 89 | _.intersection(allRelations, relationNames).forEach(relationName => { 90 | if (options.relations[relationName].disableAllExcept) { 91 | let allRelationMethods = getRelationMethodsByRelationName(relationName); 92 | options.relations[relationName].disableAllExcept.forEach(relationMethod => { 93 | if (method === 'prototype.__' + relationMethod + '__' + relationName) { 94 | methodsToKeep.push(method); 95 | } 96 | }); 97 | methodsToDisable = methodsToDisable.concat(_.difference(allRelationMethods, methodsToKeep)); 98 | } 99 | }); 100 | }); 101 | } 102 | 103 | if (options.disableAllExcept) { 104 | methodsToKeep = methodsToKeep.concat(expandWildCards(options.disableAllExcept)); 105 | methodsToDisable = _.difference(allMethods, methodsToKeep); 106 | methodsToDisable = _.difference(methodsToDisable, methodsAdded); 107 | } 108 | 109 | if (options.ignoreACL !== true) { 110 | let authorizedAclMethods = Utils.getAuthorizedAclMethods(Model); 111 | methodsToDisable = _.differenceWith(methodsToDisable, authorizedAclMethods, (a, b) => { 112 | return a === b || a === 'prototype.' + b; 113 | }); 114 | } 115 | 116 | disableRemoteMethods(methodsToDisable); 117 | } 118 | 119 | function getMethodWithName(methodName) { 120 | let components = methodName.split('.'); 121 | let method = components.reduce((obj, currentComponent) => { 122 | return obj[currentComponent]; 123 | }, Model); 124 | return method; 125 | } 126 | 127 | function getAllMethods() { 128 | const allMethods = Model.sharedClass.methods().map(m => { 129 | return m.isStatic ? m.name : 'prototype.' + m.name; 130 | }); 131 | return allMethods.concat(getAllRelationsMethods()); 132 | } 133 | 134 | function expandWildCards(methods) { 135 | let results = []; 136 | methods.forEach(methodName => { 137 | let pattern = methodName.indexOf('*') !== -1 && 138 | new RegExp('^' + methodName.replace(/\./g, '\\.').replace(/\*/g, '(.*?)') + '$'); 139 | if (pattern) { 140 | let matched = getAllMethods().filter(name => pattern.test(name)); 141 | results = results.concat(matched); 142 | } else { 143 | results.push(methodName); 144 | } 145 | }); 146 | return results; 147 | } 148 | 149 | // Not fully tested, but based on documentation from 150 | // https://loopback.io/doc/en/lb2/Accessing-related-models.html 151 | // and https://loopback.io/doc/en/lb3/Accessing-related-models.html 152 | const methodsByRelationType = { 153 | 'belongsTo': ['get'], 154 | 'hasOne': ['create', 'get', 'update', 'destroy'], 155 | 'hasMany': ['count', 'create', 'delete', 'destroyById', 'findById', 'get', 'updateById'], 156 | 'hasManyThrough': ['count', 'create', 'delete', 'destroyById', 'exists', 'findById', 157 | 'get', 'link', 'updateById', 'unlink'], 158 | 'hasAndBelongsToMany': ['count', 'create', 'delete', 'destroyById', 'exists', 'findById', 159 | 'get', 'link', 'updateById', 'unlink'], 160 | 'embedsOne': ['create', 'get', 'update', 'destroy'], 161 | 'embedsMany': ['count', 'create', 'delete', 'destroyById', 'findById', 'get', 'updateById'], 162 | }; 163 | 164 | function getAllRelationsMethods() { 165 | let relationMethods = []; 166 | Object.keys(Model.definition.settings.relations).forEach(function(relationName) { 167 | relationMethods = relationMethods.concat(getRelationMethodsByRelationName(relationName)); 168 | }); 169 | return relationMethods; 170 | } 171 | 172 | function getRelationMethodsByRelationName(relationName) { 173 | const relationMethods = []; 174 | const relation = Model.definition.settings.relations[relationName]; 175 | let relationType = relation.type; 176 | if (relationType === 'hasMany' && relation.through !== undefined) { 177 | relationType = 'hasManyThrough'; 178 | } 179 | const methodsByRelation = methodsByRelationType[relationType]; 180 | if (!methodsByRelation) { 181 | return; 182 | } 183 | methodsByRelation.forEach(function(methodName) { 184 | relationMethods.push('prototype.__' + methodName + '__' + relationName); 185 | }); 186 | return relationMethods; 187 | } 188 | 189 | function disableRemoteMethods(methodsToDisable) { 190 | methodsToDisable.forEach(methodName => { 191 | Model.disableRemoteMethodByName(methodName); 192 | }); 193 | if (methodsToDisable.length) { 194 | debug('Model `%s`: Disable remote methods: `%s`', Model.modelName, 195 | methodsToDisable.join(', ')); 196 | } 197 | } 198 | 199 | function isString(value) { 200 | return (typeof value === 'string' || value instanceof String); 201 | } 202 | }; 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # loopback-setup-remote-methods-mixin 2 | 3 | Mixins for Loopback, to easily disable or setup new remote methods from the model definition file. It works with both Loopback 2 and 3. 4 | 5 | ## Installation 6 | 7 | ```bash 8 | npm install --save loopback-setup-remote-methods-mixin 9 | ``` 10 | 11 | ## Usage 12 | 13 | ### As a mixin (recommended) 14 | 15 | First, modify your server/model-config.json to include the path to this module: 16 | 17 | For LB3, mixins should be declared in the `_meta.mixins` property. For LB2, mixins should be declared in the `mixins` property. 18 | 19 | ```json 20 | { 21 | "_meta": { 22 | "mixins": [ 23 | "loopback/common/mixins", 24 | "loopback/server/mixins", 25 | "../common/mixins", 26 | "./mixins", 27 | "../node_modules/loopback-setup-remote-methods-mixin" 28 | ] 29 | } 30 | } 31 | ``` 32 | 33 | Then you can [use the mixin](https://loopback.io/doc/en/lb3/Defining-mixins.html#enable-a-model-with-mixins) from your model definition files: 34 | 35 | ```json 36 | ... 37 | "mixins": { 38 | "SetupRemoteMethods": { 39 | "disableAllExcept": ["create", "prototype.updateAttributes"], 40 | "addFromFile": "./common/models/mymodel-remotes.js" 41 | } 42 | } 43 | ... 44 | ``` 45 | List of default remote methods 46 | 47 | http://loopback.io/doc/en/lb3/Exposing-models-over-REST.html#predefined-remote-methods 48 | 49 | ## Options 50 | 51 | - [disable](#disable) 52 | - [disableAllExcept](#disableallexcept) 53 | - [relations](#relations) 54 | - disableAllExcept 55 | - [ignoreACL](#ignoreacl) 56 | - [add](#add) 57 | - [add using JSON](#add-using-json) 58 | - [add using a JS file](#add-using-js-in-the-model) (Deprecated, use **addFromFile** instead) 59 | - [addFromFile](#addfromfile) 60 | 61 | ### disable 62 | 63 | Disable the defined remote methods. For example, to disable the `create` and the `updateAttributes` remote methods: 64 | 65 | ```json 66 | "mixins": { 67 | "SetupRemoteMethods": { 68 | "disable": ["create", "prototype.updateAttributes"] 69 | } 70 | } 71 | ``` 72 | 73 | Allows wildcards with `*` (not fully tested though) 74 | 75 | ### disableAllExcept 76 | 77 | Disable all the remote methods, except the defined on the options. For example, to disable all except `create` and `updateAttributes` remote methods: 78 | 79 | ```json 80 | "mixins": { 81 | "SetupRemoteMethods": { 82 | "disableAllExcept": ["create", "prototype.updateAttributes"] 83 | } 84 | } 85 | ``` 86 | 87 | Allows wildcards with `*` (not fully tested though) 88 | 89 | ### relations 90 | 91 | Allows to setup some options per relation. Currently only `disableAllExcept` is supported. 92 | 93 | ```json 94 | "mixins": { 95 | "SetupRemoteMethods": { 96 | "relations": { 97 | "customer": { 98 | "disableAllExcept": ["create", "get"] 99 | } 100 | } 101 | } 102 | } 103 | ``` 104 | 105 | ### ignoreACL 106 | 107 | **Default value:** false 108 | 109 | This option works together with `disable` and `disableAllExcept`. If **true**, it forces to disable the methods, even if they are configured in the ACL to be allowed. 110 | 111 | ```json 112 | "mixins": { 113 | "SetupRemoteMethods": { 114 | "ignoreACL": true, 115 | "disableAllExcept": ["create", "prototype.updateAttributes"] 116 | } 117 | } 118 | ``` 119 | 120 | ### add 121 | 122 | It adds new remote methods to the model. This is similar to what's planned for the [Methods](https://loopback.io/doc/en/lb3/Model-definition-JSON-file.html#methods) section. (Which is not yet implemented. This option will be deprecated when that happens.) 123 | 124 | #### Add using JSON 125 | 126 | ```json 127 | "mixins": { 128 | "SetupRemoteMethods": { 129 | "add": { 130 | "sayHello": { 131 | "accepts": {"arg": "msg", "type": "string"}, 132 | "returns": {"arg": "greeting", "type": "string"} 133 | }, 134 | "sayBye": { 135 | "accepts": {"arg": "msg", "type": "string"}, 136 | "returns": {"arg": "farewell", "type": "string"} 137 | } 138 | } 139 | } 140 | } 141 | ``` 142 | 143 | Then you can have the methods implemented in your model as usual: 144 | 145 | ```javascript 146 | const Promise = require('bluebird'); 147 | 148 | module.exports = function(Employee) { 149 | Employee.sayHello = msg => { 150 | return new Promise((resolve) => { 151 | resolve('Hello ' + msg); 152 | }); 153 | }; 154 | 155 | Employee.sayBye = msg => { 156 | return new Promise((resolve) => { 157 | resolve('Goodbye ' + msg); 158 | }); 159 | }; 160 | }; 161 | ``` 162 | 163 | #### Add using JS in the model 164 | 165 | **Deprecated**, use [addFromFile](#addfromfile) instead. 166 | 167 | You can define the name of the methods in the model that will provide the remote method definition. 168 | 169 | ```json 170 | "mixins": { 171 | "SetupRemoteMethods": { 172 | "add": { 173 | "greet": "remotesDefinitions.greet" 174 | } 175 | } 176 | } 177 | ``` 178 | 179 | In order to avoid having this definition in the model file, we can have the definition on a different file, let's say we name it **remote-methods.js** 180 | 181 | ```javascript 182 | module.exports = { 183 | greet 184 | }; 185 | 186 | function greet() { 187 | return { 188 | accepts: {arg: 'msg', type: 'string'}, 189 | returns: {arg: 'greeting', type: 'string'}, 190 | }; 191 | } 192 | 193 | ``` 194 | 195 | Then, on your model, you would need to have something like: 196 | 197 | ```javascript 198 | module.exports = function(Employee) { 199 | // Include the definitions in the model for the mixin to be able to get them 200 | Employee.remotesDefinitions = require('./remote-methods'); 201 | 202 | // The implementation of your remote method 203 | Employee.greet = msg => { 204 | return new Promise((resolve) => { 205 | resolve('Hello ' + msg); 206 | }); 207 | }; 208 | }; 209 | ``` 210 | 211 | ### addFromFile 212 | 213 | There are some cases that you might want to call a method to return the definition. This happens for example if one of the properties should be calculated. 214 | 215 | You can add **all** the methods from the file: 216 | 217 | ```json 218 | "mixins": { 219 | "SetupRemoteMethods": { 220 | "addFromFile": "./common/models/employee-remotes.js" 221 | } 222 | } 223 | ``` 224 | 225 | Or just some of them: 226 | 227 | ```json 228 | "mixins": { 229 | "SetupRemoteMethods": { 230 | "addFromFile": { 231 | "filename": "./common/models/employee-remotes.js", 232 | "methods": [ "sayHello" ] 233 | } 234 | } 235 | } 236 | ``` 237 | 238 | The path of the file should be relative to `process.cwd()`. 239 | 240 | The file (`employee-remotes.js` in our example) would contain the remotes definitions: 241 | 242 | ```javascript 243 | module.exports = { 244 | sayHello, 245 | sayBye 246 | }; 247 | 248 | function sayHello() { 249 | return { 250 | accepts: {arg: 'msg', type: 'string'}, 251 | returns: {arg: 'greeting', type: 'string'}, 252 | }; 253 | } 254 | 255 | function sayBye() { 256 | return { 257 | accepts: {arg: 'msg', type: 'string'}, 258 | returns: {arg: 'farewell', type: 'string'}, 259 | }; 260 | } 261 | ``` 262 | 263 | Then, in the model, you will only need the implementation: 264 | 265 | ```javascript 266 | module.exports = function(Employee) { 267 | Employee.sayHello = msg => { 268 | return new Promise((resolve) => { 269 | resolve('Hello ' + msg); 270 | }); 271 | }; 272 | 273 | Employee.sayBye = msg => { 274 | return new Promise((resolve) => { 275 | resolve('Goodbye ' + msg); 276 | }); 277 | }; 278 | }; 279 | ``` 280 | 281 | ## Credits 282 | 283 | Disabling remote methods feature is based on the discussion at [https://github.com/strongloop/loopback/issues/651](https://github.com/strongloop/loopback/issues/651). 284 | 285 | The code for `disable`, `disableAllExcept` and `ignoreACL` options is based on this [gist](https://gist.github.com/ebarault/1c3e43e19735f03dee8260471f8d3545) from [ebarault](https://github.com/ebarault), which was based on another [gist](https://gist.github.com/drmikecrowe/7ec75265fda2788e1c08249ece505a44) from [drmikecrowe](https://github.com/drmikecrowe). 286 | 287 | Module created by [c3s4r](https://github.com/c3s4r) for [Devsu](http://devsu.com/). 288 | --------------------------------------------------------------------------------