From 4de3aa77e35a0c2f01f3a8f58bc935962bc9ae16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 20 Sep 2016 09:51:50 +0200 Subject: [PATCH] Implement new http arg mapping optionsFromRequest Define a new Model method "createOptionsFromRemotingContext" that allows models to define what "options" should be passed to methods invoked via strong-remoting (e.g. REST). Define a new http mapping `http: 'optionsFromRequest'` that invokes `Model.createOptionsFromRemotingContext` to build the value from remoting context. This should provide enough infrastructure for components and applications to implement their own ways of building the "options" object. --- lib/model.js | 70 ++++++++++++++++++++++++++++++ test/model.test.js | 104 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 174 insertions(+) diff --git a/lib/model.js b/lib/model.js index 85e0917b..77d5b6d9 100644 --- a/lib/model.js +++ b/lib/model.js @@ -424,9 +424,39 @@ module.exports = function(registry) { options.isStatic = !m; name = options.isStatic ? name : m[1]; } + + if (options.accepts) { + options = extend({}, options); + options.accepts = setupOptionsArgs(options.accepts); + } + this.sharedClass.defineMethod(name, options); }; + function setupOptionsArgs(accepts) { + if (!Array.isArray(accepts)) + accepts = [accepts]; + + return accepts.map(function(arg) { + if (arg.http && arg.http === 'optionsFromRequest') { + // deep clone to preserve the input value + arg = extend({}, arg); + arg.http = createOptionsViaModelMethod; + } + return arg; + }); + } + + function createOptionsViaModelMethod(ctx) { + var EMPTY_OPTIONS = {}; + var ModelCtor = ctx.method && ctx.method.ctor; + if (!ModelCtor) + return EMPTY_OPTIONS; + if (typeof ModelCtor.createOptionsFromRemotingContext !== 'function') + return EMPTY_OPTIONS; + return ModelCtor.createOptionsFromRemotingContext(ctx); + } + /** * Disable remote invocation for the method with the given name. * @@ -873,6 +903,46 @@ module.exports = function(registry) { Model.ValidationError = require('loopback-datasource-juggler').ValidationError; + /** + * Create "options" value to use when invoking model methods + * via strong-remoting (e.g. REST). + * + * Example + * + * ```js + * MyModel.myMethod = function(options, cb) { + * // by default, options contains only one property "accessToken" + * var accessToken = options && options.accessToken; + * var userId = accessToken && accessToken.userId; + * var message = 'Hello ' + (userId ? 'user #' + userId : 'anonymous'); + * cb(null, message); + * }); + * + * MyModel.remoteMethod('myMethod', { + * accepts: { + * arg: 'options', + * type: 'object', + * // "optionsFromRequest" is a loopback-specific HTTP mapping that + * // calls Model's createOptionsFromRemotingContext + * // to build the argument value + * http: 'optionsFromRequest' + * }, + * returns: { + * arg: 'message', + * type: 'string' + * } + * }); + * ``` + * + * @param {Object} ctx A strong-remoting Context instance + * @returns {Object} The value to pass to "options" argument. + */ + Model.createOptionsFromRemotingContext = function(ctx) { + return { + accessToken: ctx.req.accessToken, + }; + }; + // setup the initial model Model.setup(); diff --git a/test/model.test.js b/test/model.test.js index 07c663f0..04fdba11 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -886,4 +886,108 @@ describe.onServer('Remote Methods', function() { // fails on time-out when not implemented correctly }); }); + + describe('Model.createOptionsFromRemotingContext', function() { + var app, TestModel, accessToken, userId, actualOptions; + + before(setupAppAndRequest); + before(createUserAndAccessToken); + + it('sets empty options.accessToken for anonymous requests', function(done) { + request(app).get('/TestModels/saveOptions') + .expect(204, function(err) { + if (err) return done(err); + expect(actualOptions).to.eql({accessToken: null}); + done(); + }); + }); + + it('sets options.accessToken for authorized requests', function(done) { + request(app).get('/TestModels/saveOptions') + .set('Authorization', accessToken.id) + .expect(204, function(err) { + if (err) return done(err); + expect(actualOptions).to.have.property('accessToken'); + expect(actualOptions.accessToken.toObject()) + .to.eql(accessToken.toObject()); + done(); + }); + }); + + it('allows "beforeRemote" hooks to contribute options', function(done) { + TestModel.beforeRemote('saveOptions', function(ctx, unused, next) { + ctx.args.options.hooked = true; + next(); + }); + + request(app).get('/TestModels/saveOptions') + .expect(204, function(err) { + if (err) return done(err); + expect(actualOptions).to.have.property('hooked', true); + done(); + }); + }); + + it('allows apps to add options before remoting hooks', function(done) { + TestModel.createOptionsFromRemotingContext = function(ctx) { + return {hooks: []}; + }; + + TestModel.beforeRemote('saveOptions', function(ctx, unused, next) { + ctx.args.options.hooks.push('beforeRemote'); + next(); + }); + + // In real apps, this code can live in a component or in a boot script + app.remotes().phases + .addBefore('invoke', 'options-from-request') + .use(function(ctx, next) { + ctx.args.options.hooks.push('custom'); + next(); + }); + + request(app).get('/TestModels/saveOptions') + .expect(204, function(err) { + if (err) return done(err); + expect(actualOptions.hooks).to.eql(['custom', 'beforeRemote']); + done(); + }); + }); + + function setupAppAndRequest() { + app = loopback({localRegistry: true, loadBuiltinModels: true}); + + app.dataSource('db', {connector: 'memory'}); + + TestModel = app.registry.createModel('TestModel', {base: 'Model'}); + TestModel.saveOptions = function(options, cb) { + actualOptions = options; + cb(); + }; + + TestModel.remoteMethod('saveOptions', { + accepts: {arg: 'options', type: 'object', http: 'optionsFromRequest'}, + http: {verb: 'GET', path: '/saveOptions'}, + }); + + app.model(TestModel, {dataSource: null}); + + app.enableAuth({dataSource: 'db'}); + + app.use(loopback.token()); + app.use(loopback.rest()); + } + + function createUserAndAccessToken() { + var CREDENTIALS = {email: 'context@example.com', password: 'pass'}; + var User = app.registry.getModel('User'); + return User.create(CREDENTIALS) + .then(function(u) { + return User.login(CREDENTIALS); + }).then(function(token) { + accessToken = token; + userId = token.userId; + }); + } + }); });