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.
This commit is contained in:
Miroslav Bajtoš 2016-09-20 09:51:50 +02:00
parent 668a9d0ed6
commit 4de3aa77e3
2 changed files with 174 additions and 0 deletions

View File

@ -424,9 +424,39 @@ module.exports = function(registry) {
options.isStatic = !m; options.isStatic = !m;
name = options.isStatic ? name : m[1]; name = options.isStatic ? name : m[1];
} }
if (options.accepts) {
options = extend({}, options);
options.accepts = setupOptionsArgs(options.accepts);
}
this.sharedClass.defineMethod(name, options); 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. * 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; 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 // setup the initial model
Model.setup(); Model.setup();

View File

@ -886,4 +886,108 @@ describe.onServer('Remote Methods', function() {
// fails on time-out when not implemented correctly // 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;
});
}
});
}); });