Loopback: Wildcard remote hooks for relation methods

Created on 4 Nov 2014  路  22Comments  路  Source: strongloop/loopback

Example:
I make some requests :
POST /Books/{id}/sharedBooks
GET /Books/{id}/sharedBooks/{fk}
DELETE /Books/{id}/sharedBooks/{fk}

As my example you can see the code in SharedBook model like this:

// reset var defined during SV development
    SharedBook.beforeRemote('**', function (ctx, inst, next) {
      console.log('aaaaa');
      next();
    });

But i can't receive any text message from console log, how can i hook to beforeRemote in this case ?
Please help me, thanks much.

discussion stale

Most helpful comment

GET /Books/{id}/sharedBooks calls Books.__get__sharedBooks__(id, cb) under the hood. You should be able to install a before-remote hook via

 Book.beforeRemote('*.__get__sharedBooks__', function(ctx, inst, next) { ... });

I don't think there is a way for specifying a wildcard hook for relation methods.

All 22 comments

@bajtos do you have any experience in this case ?

GET /Books/{id}/sharedBooks calls Books.__get__sharedBooks__(id, cb) under the hood. You should be able to install a before-remote hook via

 Book.beforeRemote('*.__get__sharedBooks__', function(ctx, inst, next) { ... });

I don't think there is a way for specifying a wildcard hook for relation methods.

thanks, it works.

Does this mean, that in order to have a consistent API, if I have a remote hook to sharedBooks create, I have to create as many remote hooks as many relation I have to it?
First I create a sharedBooks.beforeRemote, then one hook for each relation, in case someone would like to create a sharedBook through it? - doesn't make too much sense.

Can this be fixed sometime in the future?

Does this mean, that in order to have a consistent API, if I have a remote hook to sharedBooks create, I have to create as many remote hooks as many relation I have to it?
First I create a sharedBooks.beforeRemote, then one hook for each relation, in case someone would like to create a sharedBook through it?

Yes, that's a correct assessment of the current implementation.

BTW have you tried using the recently added Operation Hooks instead of remoting hooks? I believe Operation Hooks are correctly invoked for relation methods too.

Can this be fixed sometime in the future?

Well, I'd like to see this fixed too, but the solution gets tricky pretty quickly.

Let's say you have "Product hasAndBelongsToMany Categories" and you want to create a remote hook for the "link" method, i.e. Product.__link__categories. What remote hook should be invoked on the Category model in this case?

Another example: "Product hasMany Categories" and making the request "GET /api/products/1/categories/2". Should we invoke remoting hooks for Category.find, Category.findById, or some other method?

I'll reopen this issue to keep the discussion going.

Operation hooks does seem to work correctly, and correct me if I'm wrong, but I didn't seem to find a way to access accessToken or session data inside observe. My exact usecase is that I want to have createdAt, createdBy, modifiedAt, modifiedBy properties, the createdBy & modifiedBy being belongTo relations to users. For this to work, I need the accessToken.

order.beforeRemote() works fine, and I can get the accessToken through ctx.req.accessToken.
order.observer("before save") works too, bc I can check ctx.isNewInstance and touch modified and created properties accordingly, but I don't have an accessToken in ctx.req, so in the end, I don't know who modifies/creates the model.

And I might set ACLs so you can't access /api/order/{order_id}, only /api/users/{my_user_id}/orders/{order_id}. I think this makes sense, and is the correct use if I don't want other people to access other's orders. Now I don't want to reimplement the same logic when an order is created from different REST endpoints - I don't care which endpoint was called to create an order. It may have been /api/orders /api/users/.../orders api/product/.../orders api/affiliate/.../orders or any other. And I don't want doubled code for each relation only to handle createdBy and modifiedBy.

I'm not very familiar with the link method, so I wouldn't know.

As for find and findById, I never was a big fan of findById, and beyond my emotional issues, I think if a function like this exists, I would say, it should only be a facade to find({where:{id:xxx}). The only usecase I know where it worth making a difference between find and findById is when someone wants to restrict listing everything, but would like to let individual records to be queried. I may be utterly simplifying here, but if that is the case, than it is a rare usecase, and should be handled in the .js as custom logic, and the findById should be handled as a oneliner interpreter for find() with where filter, then a query to /api/products/1/categories/2 would invoke Category.find and Product.find only. It's absolutely possible that I'm missing the point of findById, and if so pls enlighten me :)

@MikeSpock

Operation hooks does seem to work correctly, and correct me if I'm wrong, but I didn't seem to find a way to access accessToken or session data inside observe.

That's correct. I think we don't have a standardized way for passing accessToken to operation hooks and model methods yet. There are two possible solutions:

1) Use loopback.getCurrentContext().get('accessToken') inside your hook

2) Since most DAO methods accept an options argument now, you can add a before-remote hook to add the token to this argument and then pick it up via ctx.options in your hook

// IIRC, remote hooks are not inherited, you have to set it up on every model instance
// The easiest solution is to override setup() of LoopBack core Model.
var _setup = loopback.Model.setup;
loopback.Model.setup = function() {
  _setup.apply(this, arguments);
  this.beforeRemote('**', function(ctx, unused, cb) {
    ctx.args.options = ctx.args.options || {};
    ctx.args.options.accessToken = ctx.req.accessToken;
    cb();
  });
};

// now in your common/models/order.js
Order.observe('before save', function(ctx, next) {
  var token = ctx.options && ctx.options.accessToken;
  if (!token) return next(new Error('Authorization is required.'));
  // make your changes
  next();
});

Please note that I haven't tested the code, it may need few tweaks to get it working

I am cc-ing @raymondfeng and @ritch, they may be able to provide additional information on how to access the current accessToken and/or the current user from an operation hook and/or a model method.

@bajtos I have had the same exact thoughts recently. We should promote this somehow. Perhaps as a first class feature in remoting or core.

As far as this issue is concerned +1 to @bajtos response above.

Hi,

I have a quite similar problem. Lets say I have Product hasAndBelongsToMany Categories. What operation hook should I define if I like to trigger additional actions (send a push notification for example) if a new relation is made between these two models (PUT /Products/{id}/categories/rel/{fk} or PUT /Categories/{id}/products/rel/{fk})

Thanks,

David

Hey @tekand , just wondering have you found the solution to the problem above?? I am looking for a similar solution

@n2sandhu nope, still unsolved. :(

I am also looking for a way to observe when a new relation is created. Can't find anything in the docs.

I would like to do the same, a way to observe when a link is created.

@bajtos and/or @ritch - would you mind either answering @tekand's question here or moving the discussion by @tekand, @n2sandhu, @moklick and @jwebcat into another issue labeled as discussion or question if you don't think this is the right thread?

Nov 2015 to Jun 2016 is an awful long time ;)

Thanks for the poke, @pulkitsinghal.

@tekand @moklick @jwebcat please open a new issue to discuss this question. Let's keep the discussion here focused on remote hooks for relation methods.

When I put a remote hook on Favorite.afterRemote('find') I expect it will execute when I access /profiles/1/favorites.

Can we introduce this as a feature @bajtos?

When I put a remote hook on Favorite.afterRemote('find') I expect it will execute when I access /profiles/1/favorites.

Yes, that's what we all would expect.

Unfortunately, the way how relation methods are exposed via strong-remoting/REST makes it very difficult to implement such desired behaviour.

Can we introduce this as a feature

Sure, you are welcome to give it a try! See http://loopback.io/doc/en/contrib/index.html to get started.

Just be warned, this feature is tricky and complex to implement (at least I think so).

@bajtos where can find doc about relation model remote hooks , I did not find the format '*.__get__sharedBooks__'

@bajtos hey! Sorry to bubble up this issue again... but I read your comment https://github.com/strongloop/loopback/issues/737#issuecomment-61631204, and noticed that was back on 2014, sooo I was wondering if this is still the case? I tried it and didn't work at all with LB 3.

For now, I'm making custom endpoints so this is not super urgent but something I'd like to know how to do.

Thanks!!!

I read your comment #737 (comment), and noticed that was back on 2014, sooo I was wondering if this is still the case? I tried it and didn't work at all with LB 3.

Yes, this issue is fortunately still not addressed. Considering our focus on LB 4, I don't expect this issue to be fixed in LB 3.x ever 馃檨

My case is:
Endpoint: /Accounts/{id}/items
Goal: Do some data preprocessing before creating items
Direction: use beforeRemote function to model

  1. First I tried Accounts.beforeRemote('**', ... to dump the context, I found out loopback will display my relation method as Account.prototype.__create__items
  2. I did a quick check on loopback source code (https://github.com/strongloop/loopback/blob/0feda03d5b9534d4ce785e1de18c6788300c77af/lib/model.js) #214, found out that the code will build the function name "className + '.' + name"
  3. I change beforeRemote(functionName), i.e. functionName as "*.__create__items" OR "prototype.__create__items", it works.

Account.beforeRemote('prototype.__create__items', function(ctx, modelInstance, next){
//YOUR LOGIC HERE
next();
});

Was this page helpful?
0 / 5 - 0 ratings