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:
parent
668a9d0ed6
commit
4de3aa77e3
70
lib/model.js
70
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();
|
||||
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue