Skip to content

Commit ee106e4

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 65a3a0b commit ee106e4

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
@@ -428,9 +428,39 @@ module.exports = function(registry) {
428428
if (options.isStatic === undefined) {
429429
options.isStatic = true;
430430
}
431+
432+
if (options.accepts) {
433+
options = extend({}, options);
434+
options.accepts = setupOptionsArgs(options.accepts);
435+
}
436+
431437
this.sharedClass.defineMethod(name, options);
432438
};
433439

440+
function setupOptionsArgs(accepts) {
441+
if (!Array.isArray(accepts))
442+
accepts = [accepts];
443+
444+
return accepts.map(function(arg) {
445+
if (arg.http && arg.http === 'optionsFromRequest') {
446+
// deep clone to preserve the input value
447+
arg = extend({}, arg);
448+
arg.http = createOptionsViaModelMethod;
449+
}
450+
return arg;
451+
});
452+
}
453+
454+
function createOptionsViaModelMethod(ctx) {
455+
var EMPTY_OPTIONS = {};
456+
var ModelCtor = ctx.method && ctx.method.ctor;
457+
if (!ModelCtor)
458+
return EMPTY_OPTIONS;
459+
if (typeof ModelCtor.createOptionsFromRemotingContext !== 'function')
460+
return EMPTY_OPTIONS;
461+
return ModelCtor.createOptionsFromRemotingContext(ctx);
462+
}
463+
434464
/**
435465
* Disable remote invocation for the method with the given name.
436466
*
@@ -869,6 +899,46 @@ module.exports = function(registry) {
869899

870900
Model.ValidationError = require('loopback-datasource-juggler').ValidationError;
871901

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

test/model.test.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -786,4 +786,108 @@ describe.onServer('Remote Methods', function() {
786786
// fails on time-out when not implemented correctly
787787
});
788788
});
789+
790+
describe('Model.createOptionsFromRemotingContext', function() {
791+
var app, TestModel, accessToken, userId, actualOptions;
792+
793+
before(setupAppAndRequest);
794+
before(createUserAndAccessToken);
795+
796+
it('sets empty options.accessToken for anonymous requests', function(done) {
797+
request(app).get('/TestModels/saveOptions')
798+
.expect(204, function(err) {
799+
if (err) return done(err);
800+
expect(actualOptions).to.eql({accessToken: null});
801+
done();
802+
});
803+
});
804+
805+
it('sets options.accessToken for authorized requests', function(done) {
806+
request(app).get('/TestModels/saveOptions')
807+
.set('Authorization', accessToken.id)
808+
.expect(204, function(err) {
809+
if (err) return done(err);
810+
expect(actualOptions).to.have.property('accessToken');
811+
expect(actualOptions.accessToken.toObject())
812+
.to.eql(accessToken.toObject());
813+
done();
814+
});
815+
});
816+
817+
it('allows "beforeRemote" hooks to contribute options', function(done) {
818+
TestModel.beforeRemote('saveOptions', function(ctx, unused, next) {
819+
ctx.args.options.hooked = true;
820+
next();
821+
});
822+
823+
request(app).get('/TestModels/saveOptions')
824+
.expect(204, function(err) {
825+
if (err) return done(err);
826+
expect(actualOptions).to.have.property('hooked', true);
827+
done();
828+
});
829+
});
830+
831+
it('allows apps to add options before remoting hooks', function(done) {
832+
TestModel.createOptionsFromRemotingContext = function(ctx) {
833+
return {hooks: []};
834+
};
835+
836+
TestModel.beforeRemote('saveOptions', function(ctx, unused, next) {
837+
ctx.args.options.hooks.push('beforeRemote');
838+
next();
839+
});
840+
841+
// In real apps, this code can live in a component or in a boot script
842+
app.remotes().phases
843+
.addBefore('invoke', 'options-from-request')
844+
.use(function(ctx, next) {
845+
ctx.args.options.hooks.push('custom');
846+
next();
847+
});
848+
849+
request(app).get('/TestModels/saveOptions')
850+
.expect(204, function(err) {
851+
if (err) return done(err);
852+
expect(actualOptions.hooks).to.eql(['custom', 'beforeRemote']);
853+
done();
854+
});
855+
});
856+
857+
function setupAppAndRequest() {
858+
app = loopback({localRegistry: true, loadBuiltinModels: true});
859+
860+
app.dataSource('db', {connector: 'memory'});
861+
862+
TestModel = app.registry.createModel('TestModel', {base: 'Model'});
863+
TestModel.saveOptions = function(options, cb) {
864+
actualOptions = options;
865+
cb();
866+
};
867+
868+
TestModel.remoteMethod('saveOptions', {
869+
accepts: {arg: 'options', type: 'object', http: 'optionsFromRequest'},
870+
http: {verb: 'GET', path: '/saveOptions'},
871+
});
872+
873+
app.model(TestModel, {dataSource: null});
874+
875+
app.enableAuth({dataSource: 'db'});
876+
877+
app.use(loopback.token());
878+
app.use(loopback.rest());
879+
}
880+
881+
function createUserAndAccessToken() {
882+
var CREDENTIALS = {email: '[email protected]', password: 'pass'};
883+
var User = app.registry.getModel('User');
884+
return User.create(CREDENTIALS)
885+
.then(function(u) {
886+
return User.login(CREDENTIALS);
887+
}).then(function(token) {
888+
accessToken = token;
889+
userId = token.userId;
890+
});
891+
}
892+
});
789893
});

0 commit comments

Comments
 (0)