diff --git a/lib/application.js b/lib/application.js index 31c05a82..fcc9be2e 100644 --- a/lib/application.js +++ b/lib/application.js @@ -533,9 +533,9 @@ function configureModel(ModelCtor, config, app) { config = extend({}, config); config.dataSource = dataSource; - setSharedMethodSharedProperties(ModelCtor, app, config); - app.registry.configureModel(ModelCtor, config); + + setSharedMethodSharedProperties(ModelCtor, app, config); } function setSharedMethodSharedProperties(model, app, modelConfigs) { @@ -568,15 +568,38 @@ function setSharedMethodSharedProperties(model, app, modelConfigs) { // set sharedMethod.shared using the merged settings var sharedMethods = model.sharedClass.methods({includeDisabled: true}); + + // re-map glob style values to regular expressions + var tests = Object + .keys(settings) + .filter(function(setting) { + return settings.hasOwnProperty(setting) && setting.indexOf('*') >= 0; + }) + .map(function(setting) { + // Turn * into an testable regexp string + var glob = escapeRegExp(setting).replace(/\*/g, '(.)*'); + return {regex: new RegExp(glob), setting: settings[setting]}; + }) || []; sharedMethods.forEach(function(sharedMethod) { // use the specific setting if it exists - var hasSpecificSetting = settings.hasOwnProperty(sharedMethod.name); + var methodName = sharedMethod.isStatic ? sharedMethod.name : 'prototype.' + sharedMethod.name; + var hasSpecificSetting = settings.hasOwnProperty(methodName); if (hasSpecificSetting) { - sharedMethod.shared = settings[sharedMethod.name]; - } else { // otherwise, use the default setting if it exists - var hasDefaultSetting = settings.hasOwnProperty('*'); - if (hasDefaultSetting) - sharedMethod.shared = settings['*']; + if (settings[methodName] === false) { + sharedMethod.sharedClass.disableMethodByName(methodName); + } else { + sharedMethod.shared = true; + } + } else { + tests.forEach(function(glob) { + if (glob.regex.test(methodName)) { + if (glob.setting === false) { + sharedMethod.sharedClass.disableMethodByName(methodName); + } else { + sharedMethod.shared = true; + } + } + }); } }); } @@ -585,6 +608,11 @@ function clearHandlerCache(app) { app._handlers = undefined; } +// Sanitize all RegExp reserved characters except * for pattern gobbing +function escapeRegExp(str) { + return str.replace(/[\-\[\]\/\{\}\(\)\+\?\.\\\^\$\|]/g, '\\$&'); +} + /** * Listen for connections and update the configured port. * diff --git a/test/loopback.test.js b/test/loopback.test.js index a79e98f3..bc9154b4 100644 --- a/test/loopback.test.js +++ b/test/loopback.test.js @@ -739,4 +739,141 @@ describe('loopback', function() { }); } }); + + describe('Hiding shared methods', function() { + var app; + + beforeEach(setupLoopback); + + it('hides remote methods using fixed method names', function() { + var TestModel = app.registry.createModel(uniqueModelName); + app.model(TestModel, { + dataSource: null, + methods: { + staticMethod: { + isStatic: true, + http: {path: '/static'}, + }, + }, + options: { + remoting: { + sharedMethods: { + staticMethod: false, + }, + }, + }, + }); + + var publicMethods = getSharedMethods(TestModel); + + expect(publicMethods).not.to.include.members([ + 'staticMethod', + ]); + }); + + it('hides remote methods using a glob pattern', function() { + var TestModel = app.registry.createModel(uniqueModelName); + app.model(TestModel, { + dataSource: null, + methods: { + staticMethod: { + isStatic: true, + http: {path: '/static'}, + }, + instanceMethod: { + isStatic: false, + http: {path: '/instance'}, + }, + }, + options: { + remoting: { + sharedMethods: { + 'prototype.*': false, + }, + }, + }, + }); + + var publicMethods = getSharedMethods(TestModel); + + expect(publicMethods).to.include.members([ + 'staticMethod', + ]); + expect(publicMethods).not.to.include.members([ + 'instanceMethod', + ]); + }); + + it('hides all remote methods using *', function() { + var TestModel = app.registry.createModel(uniqueModelName); + app.model(TestModel, { + dataSource: null, + methods: { + staticMethod: { + isStatic: true, + http: {path: '/static'}, + }, + instanceMethod: { + isStatic: false, + http: {path: '/instance'}, + }, + }, + options: { + remoting: { + sharedMethods: { + '*': false, + }, + }, + }, + }); + + var publicMethods = getSharedMethods(TestModel); + + expect(publicMethods).to.be.empty(); + }); + + it('hides methods for related models using globs', function() { + var TestModel = app.registry.createModel(uniqueModelName); + var RelatedModel = app.registry.createModel(uniqueModelName); + app.dataSource('test', {connector: 'memory'}); + app.model(RelatedModel, {dataSource: 'test'}); + app.model(TestModel, { + dataSource: 'test', + relations: { + related: { + type: 'hasOne', + model: RelatedModel, + }, + }, + options: { + remoting: { + sharedMethods: { + '*__related': false, + }, + }, + }, + }); + + var publicMethods = getSharedMethods(TestModel); + + expect(publicMethods).to.not.include.members([ + 'prototype.__create__related', + ]); + }); + + function setupLoopback() { + app = loopback({localRegistry: true}); + } + + function getSharedMethods(Model) { + return Model.sharedClass + .methods() + .filter(function(m) { + return m.shared === true; + }) + .map(function(m) { + return m.stringName.replace(/^[^.]+\./, ''); // drop the class name + }); + } + }); });