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;
|
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();
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue