-
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
Inject remoting context to options arg #3023
Conversation
One more thought: the community was able to come up with a solution for injecting the "options" argument even without support from LoopBack. Does it perhaps mean there is no need to change LoopBack core and we should be promoting community components instead? I personally think (hope?) that the solution offered in this pull request is better than the two existing alternatives (gist and remote-ctx), but then we need to consider also the costs of maintaining this feature as part of LoopBack core. |
@doublemarked responding to your #2762 (comment) from the earlier pull request:
I am concerned that if we have a single
I agree having to implement custom "options" properties in two places (in a middleware and then in an options builder) is cumbersome. OTOH, I am rather reluctant to introduce opinionated conventions that may not work for everyone. Instead, I am proposing to leave this outside of LoopBack core and allow 3rd party components to contribute this feature.
IMO, the "options" object should contain things that one may want to pass when calling the method directly too. For example, imagine that your User.login({ email: '[email protected]', password: 'pass' })
.then((token) => {
return Product.create({ name: 'Pen' }, { accessToken: token });
}); That's why I think it's ok to have OTOH, components and applications are free to come up with different conventions. |
"One more thought: the community was able to come up with a solution for injecting the "options" argument even without support from LoopBack. Does it perhaps mean there is no need to change LoopBack core and we should be promoting community components instead?" No - this is so fundamental for building even basic APIs that a proper solution needs to be part of the core. I, like many others in the discussion, have lost days to understanding and trying to resolve the issue. It came across really badly that this issue was open so long and that no-one on the Loopback team was providing a well document workaround. It makes a huge difference if there is official guidance on how to resolve the issue, vs. a community workaround that may conflict with a future official solution, may not work in all use-cases, and may come with zero support. We've created our own solution that doesn't directly match your new recommendations and may lead to another future refactor. Loopback tries to achieve way too much and a lot of it should be removed from the core roadmap and delegated to community efforts. But this is not one of those things - this is the basic stuff that's pretty much required for any API. Sorry to sound negative - I really appreciate your work and the direction you're providing @bajtos. But this one caused a lot of pain, remained as an open issue for way too long, and from the comments on the thread (which on their own take an hour to get through) clearly caused a lot of others a lot of pain too. Good luck with the outstanding work to get a proper solution rolled out :) |
to anyone interested in testing @bajtos proposal, i've set up a sandbox here @bajtos: testing results so far
thoughts
|
@PaddyMann thank you for a thoughtful comment! @ebarault This is awesome, I really appreciate you have set up a test app that's demonstrating what you need from this feature.
I am afraid I don't understand. Could you please be more specific? I believe that all operation hooks invoked by built-in data-access methods like The first part (reading options) is already tested in the newly added PersistedModel tests, see e.g. https://github.com/strongloop/loopback/pull/3023/files#diff-3963e5ddedf38e53684571a414bae3e4R207 Product.observe('access', function(ctx, next) {
actualOptions = ctx.options;
next();
}); Are you perhaps looking for a slightly different functionality? Could you please provide code snippets showing what would you like to accomplish?
Here are few options with the current proposal that I have in my mind:
If none of those way is good and easy to use, than I guess loopback can provide both a global and a per-model extension point, i.e. by changing Model.createOptionsFromRemotingContext = function(ctx) {
return loopback.createOptionsFromRemotingContext(ctx);
};
loopback.createOptionsFromRemotingContext = function(ctx) {
return {
accessToken: ctx.req.accessToken,
};
}; My concern is that if one of your models uses a mixin that modifies only this model's Thoughts? |
Good catch, it took me a while to spot it. I edited the description, I hope the code is fixed now. |
@bajtos : about
ok, i see it now... i was doing my operation hook test in a custom remote method. I just realized that your PR right now does not cover those. Could you confirm your intention is to make the solution available for custom remote method? about the fix for createOptionsFromRemotingContext extension, i was personally falling back to ctx.req, but yours work too :-)
Note: I updated my previous post so to avoid any further confusion. |
@ebarault Yes, the solution will be available for custom remote methods too, as long as the method correctly describes the "options" argument in remoting metadata. As far as I understand your test app (ebarault/loopback-sandbox@09b9a8c), you would like to see Note that your custom method When I change your custom method to call user.testInjectedOptions = function(id, ctx) {
return this.findById(id, {}, ctx).then(function(u) {
return {
accessToken: ctx.accessToken,
user: ctx.currentUser,
currentUserId: ctx.currentUserId,
canInjectCtxOptionsInCustomRoleResolver: ctx.canInjectCtxOptionsInCustomRoleResolver || false,
canReadCtxOptionsInObserveLoaded: ctx.canReadCtxOptionsInObserveLoaded || false,
canInjectCtxOptionsInObserveLoaded: ctx.canInjectCtxOptionsInObserveLoaded || false,
canReadCtxOptionsInBeforeRemote: ctx.canReadCtxOptionsInBeforeRemote || false,
};
});
}; I am little bit confused about what kind of remote methods would you like to see supported by this new context propagation. Could you please clarify? I am adding a new (sub)task to fix context propagation in the authentication layer to my checklist at the top. |
"I am little bit confused about what kind of remote methods would you like to see supported by this new context propagation. Could you please clarify?" I might have misunderstood, but if you're asking about why we'd want context available in the operation hooks, then a classic use-case is setting updatedBy/createdBy when saving. This should be achievable as a simple mixin but isn't unless you pass in the userId. |
@bajtos : so... yes, indeed you're right, i was a bit too hastly in my tests, my custom method was not triggering the operation hook at all, i was misguided by the logs from the first call to user model in the beforeRemoteMethod hook (enriching the ctx.options with currentUser). My bad. when propagating the ctx inside the custom method as you did with findById, all is good. As for the business requirements underneath this technical demand: generally speaking once you decide to offload a certain business logic outside the remote method themselves, directly inside model operation hooks, you expect to find similar information in those hooks whatever the remote method at the origin of the operation on the model. Generally speaking : it can be for write purposes as described by @PaddyMann, such as inserting information on who performed the change on data, it can also be for read purposes such as more precisely filtering the data returned according to the destinating user, which is complementary to access control. But is there anything more to be done here? your pr seems to solve this requirement. Yes, it's not direct since we need to propagate the ctx from the custom remote method to the built-in method invocation, but do we have another solution without introducing some kind of magic under the hook (though not easily documentable, maintainable, and so on)? About how to make a single definition of a one-for-all
as an interim solution, waiting for the more satisfying built-in models customization you refer to in option 3
About support of ctx.options in authorization stage (
|
Yes, I came to the same conclusion myself. Let's keep the part "Propagate options in the authentication layer" out of scope of this pull request.
It looks like there are ways how to get Besides that, I don't have any better solution to offer right now.
Sounds good!
Thank you for joining the discussion! I believe my proposal here does support the use case you have described. It would be great if you could write a small sample app as @ebarault did to verify that you can indeed achieve what you need. That way we have a working code that shows your exact requirements. My conclusion is that there are no major issues in my current proposal and we should move forward. I'll give few more days for the community to chime in and work on modifying the relation methods in the meantime. |
Based on the above discussion, I'm pretty certain the use-case I describe will work fine. Thanks! |
4cb056c
to
bb49271
Compare
I added context propagation to relation and scope methods. I have discovered a bug in juggler along the way, loopbackio/loopback-datasource-juggler#1197 is need to make CI of this pull request green. This patch is ready for final review. @ritch @raymondfeng @superkhau and/or anybody else who is interested, please take a look at the code changes and let me know if there are any changes/improvements to make. |
I added a "Caveat" section to pull request description:
|
I'm confused, isn't this a good thing that the option object is passed to Product.find when calling |
Ah, sorry if my wording is confusing. What I wanted to say: some users (like me for example 😄 ) may expect that Could you offer any suggestions on how to improve my note to make this more clear? |
@slnode test please |
].concat(accepts).concat([ | ||
{ | ||
arg: 'options', type: 'object', http: 'optionsFromRequest', | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
bd11f93
to
2e419f8
Compare
@bajtos I've been following along on the sidelines a bit - still haven't had the time to delve into this properly, but I applaud all the work done. Thanks! AFAIK this won't be part of LB v2 LTS? |
Awesome work - and for the explanation in another thread of why you recommend this approach vs the new cls-hooked method! Have a great Christmas :) |
Thanks for this solution! Although the options argument in the "createChangeStream" method is not used in the default implementation, could this solution also be implemented to the "createChangeStream" method so it can be used there in a custom implementation as well? |
@BramKleinhout Good catch, would you mind contributing this improvement yourself? |
…ntext to the options argument of the createChangeStream method.
@BramKleinhout lovely, thank you 👏 |
Released in |
I've now upgraded and implemented this - thanks again @bajtos A couple of minor notes on the docs at https://loopback.io/doc/en/lb3/Using-current-context.html#use-a-custom-strong-remoting-phase: "There are several ways to customize this value: - Override createOptionsFromRemotingContext in your model. - Use a “beforeRemote” hook. - Use a custom strong-remoting phase." should be styled over multiple lines The final example needs a little tinkering to work - |
@PaddyMann awesome, thank you for the feedback! We have already fixed the first docs issue you have described. Could you please send a pull request fixing the final example? See http://loopback.io/doc/en/contrib/ |
@bajtos Will do. Very short on time currently as on paternity leave but it's on my todo list. Just hit an edge case which I think isn't supported - custom validators. @horatu355 raised it in #1495 My use case: I need a validator to check that a given Am I right in thinking this isn't supported currently? Any recommendations on what to do if not? |
No worries, take your time and enjoy your paternity leave in the first place! Your child is a new-born only once in their live ;-)
I was not able to find the comment you are referring to :(
AFAICT, the method Also at least some DAO methods, e.g. So in theory,
Create a reproduction case - either using loopback-sandbox per our bug-reporting, or a failing unit-test that can be added to loopback and/or juggler. If you can contribute the fix, then that would be even better of course :) |
It looks like a bug and I've done my best to contribute a fix @bajtos - see loopbackio/loopback-datasource-juggler#1229 |
in model.js
only runs once even if there are multiple consecutive calls to different models within one request session. How can I propagate this ctx to all consecutive Model calls?
Or shall I pass options as an argument for every built in method? I mean I like have hundreds of findby calls for built in methods and I will need to rewrite them to pass the options? What is the purpose for all of this? Its then not a framework but a hand-made parameter passing game that php session users will just ask: why? And then after developers migrate their codes to adhere this bad pattern you will find out...ooops we have fixed currentContext so options messing is not needed anymore, roll back guys! |
Could I draw your attention to the bug report that I posted above? I have included a test repo and quite a bit of detail. In short, it seems that access control lists using the dynamic |
So its getting very interesting in loopback 3.3.0.
this yields no ctx.options in any observer
this yields an options object
this yielded an options object as well
this yielded an options object as well but for example
throws an error as I would expect I think this whole options injection should be better covered and clarified. |
@DaGaMs I see you have already submitted a pull request #3209 to fix the issue #3209 you have reported, thank you for that and let's keep further discussions in those two places. @barocsi thank you for reporting these problems. Please open a new issue and provide us with full steps how to reproduce the problem (e.g. how are your model relations set up?). It would be even better if you could contribute the changes needed to fix the problems you are encountering, I am happy to help you along the way. See http://loopback.io/doc/en/contrib/code-contrib.html to get started. |
I am going to lock the discussion here. Please open a new issue for any problems related to (or caused by) this patch. Feel free to mention my handle in issue description to bring it into my attention. |
Based on the feedback in #2762 and #1676, I would like to discuss a slightly improved proposal for Injecting Remote Context via Options.
Important points I am addressing:
options.accessToken
to override the access token set by the authorisation layer.loopback-swagger
currentUser
) may require async computation. This use case should be supported too.Because we are unlikely to get this new API right on the first try, and because experimenting with features is difficult in LoopBack core, I would like to ship a minimal infrastructure that will allow 3rd party components to experiment with different approaches for building the "options" object, as we are already seeing to happen:
Solution details
Methods accepting an
options
argument should declare this argument in their remoting metadata and use a special valueoptionsFromRequest
as thehttp
mapping. This way there is no magic happening inbeforeRemote
hooks and method authors are in full control of how theoptions
argument is (or is not) initialized.Under the hood, this "magic string" value is converted by
Model.remoteMethod
to a function for strong-remoting, i.e. strong-remoting receivesaccepts.http
as a function, which is already a well-supported use case.An example using a trimmed-down metadata of
PersistedModel.create
:At runtime,
optionsFromRequest
invokes a static Model method calledcreateOptionsFromRemotingContext
to build the argument value. Subclasses and mixins are free to override this method, e.g. to return a class instance as shown in https://gist.github.com/ebarault/fdc39c5d07cc40f08664256f9e00905a.In line with my goal of keeping this feature minimal, the built-in method returns options with a single property
accessToken
:Extending the "options" object
Override
createOptionsFromRemotingContext
in your Model:Use a "beforeRemote" hook
Use a custom strong-remoting phase that's run before any remoting hooks are triggered
Caveats
Relations methods are defined on "modelFrom", therefore they will invoke
createOptionsFromRemotingContext
on "modelFrom" too, despite the fact that under the hood a method on "modelTo" is called.For example, if we have a relation called
Category hasMany Product
, then a request to/categories/1/products
will callCategory.createOptionsFromRemotingContext
to build an options object that will be eventually passed toProduct.find
. This is a limitation of the current implementation of scope/relation methods, fixing it is out of scope of this pull request.Tasks
optionsFromContext
PersistedModel
methodsOut of scope:
options
in the authentication layer, see ebarault/loopback-sandbox@09b9a8cWhat's next
I would like to get more feedback on my proposal before moving forward.