-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Injecting Remote Context via Options #1495
Comments
@ritch - the initial chunk of code sits in |
A boot script works fine... The goal is to add this to core |
The mechanism of passing the context object via the However, I dislike the fact that we want to expose the full remoting context to all model methods. IMO, model methods should be as much transport (remoting-context) agnostic as possible. In my mind, here is the basic promise of LoopBack and strong-remoting: write your model methods once, then invoke them using whatever transport is most appropriate (REST RPC, JSON-RPC, WebSocket, MQTT, etc.). Once users start coupling their method implementation with remoting context details, it will be difficult to switch to a different transport. For example, I believe the websocket transport does not have the concept of We already had a similar discussion when implementing I am proposing to do the same here, for example: // usage - code-first
app.enableRemotingContextInjection();
// usage via loopback-boot
// server/config.json
{
port: 3000,
enableRemotingContextInjection: true,
// etc.
}
// implementation - provided by LoopBack
app.enableRemotingContextInjection = function() {
app.remotes().before('*.*', inject);
app.remotes().before('*.prototype.*', function(ctx, instance, next) {
inject(ctx, next);
});
// etc.
}; Thoughts? |
I think we should leave users the choice, either this or use the |
@ritch how would one set the accessToken or a user object? eg. When I create a middleware after loopback.token() to set things up, how would I get access to |
nvm, I understand that |
I'm finding that using the above sample code does not inject the Eg. I have a contractor model that hasMany contracts. The Contract.before('access', function () { }) hook is breaking when I try to access a contractor (ctx.options is undefined) but works fine when accessing the contract directly Example: Contractor.afterRemote('**', function (ctx, next) {
var Contract = Contractor.app.models.Contract
Contract.find(function (contracts) {
console.log(contracts)
})
}) Blows up with a 500 error when accessing contractor via explorer with the message: |
Just to summarize the error I'm seeing... Anytime I access a related model, whether it's using includes in the url or just running a MyRelatedModel.find, the before access hooks in the myRelatedModel instance context.options does not contain remoteCtx. |
This is a know issue of remoting hooks, see #737. If you are accessing your Contract model via Contractor model, then you need to configure context-injecting hook for Contractor too. |
You need to pass the Contractor.afterRemote('**', function (ctx, next) {
var Contract = Contractor.app.models.Contract
var filter = {};
var options = {remoteCtx: ctx};
// the example above was also missing the `err` argument
Contract.find(filter, options, function (err, contracts) {
console.log(contracts);
});
}); This highlights a downside of this approach: you must pass the |
This approach breaks |
Thanks @ritch most helpful info. I think i'm almost there. Small problem though: Via explorer: GET /contracts Contract.observe('access', function (ctx, next) {
//ctx has no access to remoting context
next();
}); I believe this is just the same issue you describe a fix for above except that I'm not sure (since I'm not directly calling Contract.find(filter, { remoteCtx: ctx }, function (err, contracts) {
}) then in: Contract.observe('access', function (ctx, next) {
//ctx.options.remoteCtx will be set
}); |
@digitalsadhu you need to inject the remoting context into options argument via a remoting hook. See the code in the issue description. function inject(ctx, next) {
var options = hasOptions(ctx.method.accepts) && (ctx.args.options || {});
if(options) {
options.remoteCtx = ctx;
ctx.args.options = options;
}
next();
}
app.remotes().before('*.*', inject);
app.remotes().before('*.prototype.*', function(ctx, instance, next) {
inject(ctx, next);
}); This code will inject remoting context to all models and all methods. If you prefer to inject the context only for Contract methods, then you can change the hook spec: app.remotes().before('Contract.*', inject);
app.remotes().before('Contract.prototype.*', function(ctx, instance, next) {
inject(ctx, next);
}); |
Thanks @bajtos I am doing that in a context.js boot script. Thats working fine in most cases. There are a couple places where its not working: Contract.observe('access') doesn't get ctx.options.remoteCtx where as Contract.observe('after save') does. I should also say, I've logged out the injection and it does happen before the access hook is called and all looks well, it just never makes it onto the access hook ctx object |
This works: Contract.observe('after save', function createHistory(ctx, next) {
var History = Contract.app.models.History;
var contract = ctx.instance;
History.create({
contractId: contract.id,
createdBy: ctx.options.remoteCtx.req.user.name,
snapshot: JSON.stringify(contract)
}, next);
}); This doesn't work: Contract.observe('access', function limitContractStaff(ctx, next) {
var user = ctx.options.remoteCtx.req.user;
//ctx.options === {}
if (user.group === roles.MANAGER)
ctx.query.where = merge(ctx.query.where || {}, { createdBy: user.name });
next();
}); |
NVM, pretty sure its just an overly complicated model with side effects I wasn't aware of. Sorry for the spam ;) |
@digitalsadhu OK :) OTOH, it is possible that your model method doesn't have |
Nope. I've tracked down the last of my issues and now have a working solution. Every case was just me not explicitly passing {remoteCtx:ctx} to model methods such as find or findById etc. once I had tracked down all of those everything worked. |
FWIW The reason I've jumped through hoops to get rid of getCurrentContext with this approach is that I've repeatedly run into issues with getCurrentContext returning null with various connectors or platforms. The latest I've discovered (and should file a bug) is that if you use the memory connector and give a file to add persistence. The use of async.queue to save changes to the json file seems to be the culprit (I did some investigation and removed the use of async.queue and the issue went away). I have a feeling you guys are already pretty aware of these types of issues and I suspect its mostly related to Anyway, my comment being, I think it be really great to see a simpler approach such as the one provided in this issue used. Thanks for all the help! |
Thanks for digging in this far...
Yes - |
Tip: app.remotes().before('*.prototype.*', function(ctx, instance, next) {
if (typeof instance === 'function') {
next = instance
}
inject(ctx, next);
}); As for some reason instance is the callback function and next is some other context like object. (Perhaps instance and next are reversed?) A bit odd but seems to do the trick. |
@ritch @bajtos The problem: This function is the culprit: app.remotes().methods().forEach(function(method) {
if(!hasOptions(method.accepts)) {
method.accepts.push({
arg: 'options',
type: 'object',
injectCtx: true
});
}
}); As soon as that code is commented out, problem goes away. Any ideas why this might be? Is there anyway I can work around this? (Other than reducing simultaneous queries to the server) |
@ritch @bajtos Ok figured it out. You can't just inject the entire httpcontext. It's just not performant. I am now injecting my user object directly and all is well again. function inject(ctx, next) {
var options = hasOptions(ctx.method.accepts) && (ctx.args.options || {});
if(options) {
options.user = ctx.req.user;
ctx.args.options = options;
}
next();
} @ritch One further thing to note is that the use of the accepts: [
{arg: 'options', type: 'object'},
{arg: 'options', type: 'object', injectCtx: true}
] Not a huge deal as in practice this only happens for the |
I have some ideas for solving the performance issues and keeping a clean separation between remote hooks and impls. |
Hi everyone, |
Hello, the patch allowing remote methods to receive remoting context via Remaining tasks:
Please note that I am still intending to land (and release) the solution proposed by @josieusa in strongloop/loopback-context#11 too, as it offers an important alternative to our "official" approach. |
Ouch, I pressed "comment" button too soon. The pull request for 2.x is waiting for CI right now (see #3048). |
which do you consider as the target approach ? |
@bajtos When this update is published, is there a possibility it will break 2.x projects using the workaround that already uses options? |
Propagating context via "options" argument is our recommended and supported approach. While it has downsides (e.g you have to manually add I don't know how reliable the new loopback-context version based on cls-hooked will be in practice. AFAIK, "continuation local storage" in Node.js is still relying on subtle tricks like pre-loading
That's a very good question, indeed. It really depends on how well your workaround handles existing The main reason for back-porting this new context propagation to 2.x is to allow the recently added access-token invalidation skip the access token of the current user making the request, which I consider as an important bug to be fixed (see #3034). I am happy to discuss how to minimise the impact of this back-ported change on existing LoopBack 2.x applications. Please move this discussion to #3048. |
📢 I have released this new feature in both 2.x ( |
The documentation at http://loopback.io/doc/en/lb3/Using-current-context.html was updated too (please allow few minutes for the changes to propagate to the website). I am calling this issue finally done 🎉 |
This is great work, thanks! One question though, any recommended path to upgrade from 2.29.1 to 2.37.0? I mean, are there any specific warnings or issues I should be aware of? Or is it enough to just change the package.json loopback version and then installing it? I know I have to clean up my code to stop using the previous context code, but is it as simple as removing it before the update, installing the new version, and then using the new features as described in the docs? Thanks again for solving this! |
@Saganus Yes, upgrading should be as easy as you described:
However, there may be community-maintained modules that may have issues with the new computed "options" argument, see e.g. mean-expert-official/loopback-sdk-builder#310. I recommend to run your test suite (including client tests) after enabling |
On node 8.9.1 with the MongoDB connector, getCurrentContext returns null if it is not set outside of the call to a model like:
However, it does work when you move it outside:
You also must use the bind option or it will return null as well. The documentation on loopback-context will have you pulling your hair out because it will not work as written. |
I have cross-posted the comment above to strongloop/loopback-context#21. I am going to lock down this issue to collaborators only, please open new issues if there is anything else to discuss. |
UPDATE 2016-12-22 We ended up implementing a different solution as described in #3023.
Tasks
master
branch - see Inject remoting context to options arg #3023 (released inloopback@3.2.0
)2.x
- see Inject remoting context to options arg [2.x] #3048 (released inloopback@2.37.0
)Original description for posterity
The example below allows you to inject the remote context object (from strong remoting) into all methods that accept an
options
argument.Why? So you can use the remote context in remote methods, operation hooks, connector implementation, etc.
This approach is specifically designed to allow you to do what is possible with
loopback.getCurrentContext()
but without the dependency oncls
. The injection approach is much simpler and should be quite a bit faster sincecls
adds some significant overhead.@bajtos @fabien @raymondfeng
The text was updated successfully, but these errors were encountered: