Skip to content

Commit 4de3aa7

Browse files
committed
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.
1 parent 668a9d0 commit 4de3aa7

File tree

2 files changed

+174
-0
lines changed

2 files changed

+174
-0
lines changed

lib/model.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,9 +424,39 @@ module.exports = function(registry) {
424424
options.isStatic = !m;
425425
name = options.isStatic ? name : m[1];
426426
}
427+
428+
if (options.accepts) {
429+
options = extend({}, options);
430+
options.accepts = setupOptionsArgs(options.accepts);
431+
}
432+
427433
this.sharedClass.defineMethod(name, options);
428434
};
429435

436+
function setupOptionsArgs(accepts) {
437+
if (!Array.isArray(accepts))
438+
accepts = [accepts];
439+
440+
return accepts.map(function(arg) {
441+
if (arg.http && arg.http === 'optionsFromRequest') {
442+
// deep clone to preserve the input value
443+
arg = extend({}, arg);
444+
arg.http = createOptionsViaModelMethod;
445+
}
446+
return arg;
447+
});
448+
}
449+
450+
function createOptionsViaModelMethod(ctx) {
451+
var EMPTY_OPTIONS = {};
452+
var ModelCtor = ctx.method && ctx.method.ctor;
453+
if (!ModelCtor)
454+
return EMPTY_OPTIONS;
455+
if (typeof ModelCtor.createOptionsFromRemotingContext !== 'function')
456+
return EMPTY_OPTIONS;
457+
return ModelCtor.createOptionsFromRemotingContext(ctx);
458+
}
459+
430460
/**
431461
* Disable remote invocation for the method with the given name.
432462
*
@@ -873,6 +903,46 @@ module.exports = function(registry) {
873903

874904
Model.ValidationError = require('loopback-datasource-juggler').ValidationError;
875905

906+
/**
907+
* Create "options" value to use when invoking model methods
908+
* via strong-remoting (e.g. REST).
909+
*
910+
* Example
911+
*
912+
* ```js
913+
* MyModel.myMethod = function(options, cb) {
914+
* // by default, options contains only one property "accessToken"
915+
* var accessToken = options && options.accessToken;
916+
* var userId = accessToken && accessToken.userId;
917+
* var message = 'Hello ' + (userId ? 'user #' + userId : 'anonymous');
918+
* cb(null, message);
919+
* });
920+
*
921+
* MyModel.remoteMethod('myMethod', {
922+
* accepts: {
923+
* arg: 'options',
924+
* type: 'object',
925+
* // "optionsFromRequest" is a loopback-specific HTTP mapping that
926+
* // calls Model's createOptionsFromRemotingContext
927+
* // to build the argument value
928+
* http: 'optionsFromRequest'
929+
* },
930+
* returns: {
931+
* arg: 'message',
932+
* type: 'string'
933+
* }
934+
* });
935+
* ```
936+
*
937+
* @param {Object} ctx A strong-remoting Context instance
938+
* @returns {Object} The value to pass to "options" argument.
939+
*/
940+
Model.createOptionsFromRemotingContext = function(ctx) {
941+
return {
942+
accessToken: ctx.req.accessToken,
943+
};
944+
};
945+
876946
// setup the initial model
877947
Model.setup();
878948

test/model.test.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -886,4 +886,108 @@ describe.onServer('Remote Methods', function() {
886886
// fails on time-out when not implemented correctly
887887
});
888888
});
889+
890+
describe('Model.createOptionsFromRemotingContext', function() {
891+
var app, TestModel, accessToken, userId, actualOptions;
892+
893+
before(setupAppAndRequest);
894+
before(createUserAndAccessToken);
895+
896+
it('sets empty options.accessToken for anonymous requests', function(done) {
897+
request(app).get('/TestModels/saveOptions')
898+
.expect(204, function(err) {
899+
if (err) return done(err);
900+
expect(actualOptions).to.eql({accessToken: null});
901+
done();
902+
});
903+
});
904+
905+
it('sets options.accessToken for authorized requests', function(done) {
906+
request(app).get('/TestModels/saveOptions')
907+
.set('Authorization', accessToken.id)
908+
.expect(204, function(err) {
909+
if (err) return done(err);
910+
expect(actualOptions).to.have.property('accessToken');
911+
expect(actualOptions.accessToken.toObject())
912+
.to.eql(accessToken.toObject());
913+
done();
914+
});
915+
});
916+
917+
it('allows "beforeRemote" hooks to contribute options', function(done) {
918+
TestModel.beforeRemote('saveOptions', function(ctx, unused, next) {
919+
ctx.args.options.hooked = true;
920+
next();
921+
});
922+
923+
request(app).get('/TestModels/saveOptions')
924+
.expect(204, function(err) {
925+
if (err) return done(err);
926+
expect(actualOptions).to.have.property('hooked', true);
927+
done();
928+
});
929+
});
930+
931+
it('allows apps to add options before remoting hooks', function(done) {
932+
TestModel.createOptionsFromRemotingContext = function(ctx) {
933+
return {hooks: []};
934+
};
935+
936+
TestModel.beforeRemote('saveOptions', function(ctx, unused, next) {
937+
ctx.args.options.hooks.push('beforeRemote');
938+
next();
939+
});
940+
941+
// In real apps, this code can live in a component or in a boot script
942+
app.remotes().phases
943+
.addBefore('invoke', 'options-from-request')
944+
.use(function(ctx, next) {
945+
ctx.args.options.hooks.push('custom');
946+
next();
947+
});
948+
949+
request(app).get('/TestModels/saveOptions')
950+
.expect(204, function(err) {
951+
if (err) return done(err);
952+
expect(actualOptions.hooks).to.eql(['custom', 'beforeRemote']);
953+
done();
954+
});
955+
});
956+
957+
function setupAppAndRequest() {
958+
app = loopback({localRegistry: true, loadBuiltinModels: true});
959+
960+
app.dataSource('db', {connector: 'memory'});
961+
962+
TestModel = app.registry.createModel('TestModel', {base: 'Model'});
963+
TestModel.saveOptions = function(options, cb) {
964+
actualOptions = options;
965+
cb();
966+
};
967+
968+
TestModel.remoteMethod('saveOptions', {
969+
accepts: {arg: 'options', type: 'object', http: 'optionsFromRequest'},
970+
http: {verb: 'GET', path: '/saveOptions'},
971+
});
972+
973+
app.model(TestModel, {dataSource: null});
974+
975+
app.enableAuth({dataSource: 'db'});
976+
977+
app.use(loopback.token());
978+
app.use(loopback.rest());
979+
}
980+
981+
function createUserAndAccessToken() {
982+
var CREDENTIALS = {email: '[email protected]', password: 'pass'};
983+
var User = app.registry.getModel('User');
984+
return User.create(CREDENTIALS)
985+
.then(function(u) {
986+
return User.login(CREDENTIALS);
987+
}).then(function(token) {
988+
accessToken = token;
989+
userId = token.userId;
990+
});
991+
}
992+
});
889993
});

0 commit comments

Comments
 (0)