Loopback: Best practise for accessing relations with 2 or more levels (nestRemoting) and managing access thorugh ACL properties

Created on 3 Dec 2015  路  15Comments  路  Source: strongloop/loopback

Hello,

I've got a rather basic question about related models, but can't find the right answer in the docs.

  • Let's assume we've got 3 models:
    user, group, file
  • many users have many groups (for example two users are related to the same group)
  • one group has many files
  • A user may only view and access groups he is related to
  • A user may only view and access files that are related to his groups.
  • A user may only create files related to his groups

Regarding users and groups, a simple relation and acl definitions are sufficient.

REST Examples:

  • When a user wants to see what groups he is related to he will access GET http://localhost:4000/api/users/1/groups
  • To view a group with id 2 he accesses GET http://localhost:4000/api/users/1/groups/2
    This works without any problems.

Now I would like to implement that a user is able to see which files are related to a specific group he is allowed to access.
First thing that came to my mind would be to provide a route like
GET http://localhost:4000/api/users/1/groups/2/files
or
POST http://localhost:4000/api/users/1/groups/2/file to create a new file

How can I create a route like this with loopback in a best practice way?

When defining a relation between group and file loopback generates the routes GET http://localhost:4000/api/groups/2/files etc.
But it does not create the relevant routes for /user/{userId}/groups/{groupId}/files in the user resource.

Thank you in advance,
Philipp

All 15 comments

Hey Philipp,

Thanks for asking this question; this is one of the new features which we have recently implemented; however the documentation is not ready yet. What you need to use is nestRemoting function, by which you can query nested models through Restfull APIs.

For example: lets say you have a Book model, which has many pages and chapters, each of which has many notes. Something similar to the following models:

var Book = app.model(
  'Book',
  { properties: { name: 'string' }, dataSource: 'db',
  plural: 'books' }
);
var Page = app.model(
  'Page',
  { properties: { name: 'string' }, dataSource: 'db',
  plural: 'pages' }
);
var Image = app.model(
  'Image',
  { properties: { name: 'string' }, dataSource: 'db',
  plural: 'images' }
);
var Note = app.model(
  'Note',
  { properties: { text: 'string' }, dataSource: 'db',
  plural: 'notes' }
);
var Chapter = app.model(
  'Chapter',
  { properties: { name: 'string' }, dataSource: 'db',
    plural: 'chapters' }
);
Book.hasMany(Page);
Book.hasMany(Chapter);
Page.hasMany(Note);
Chapter.hasMany(Note);

You can query one level in depth (via regular relationships), as illustrated with the following API endpoints:

/api/books/123/pages
Result Type: An array of pages data
Output: Queries pages of a specific book

And

/api/books/123/pages/456    
Result Type: An object of a page data   
Output: Queries a page data of a specific page under a specific book

However, in order to be able to query nested models more in depth and have them as API endpoints you need to define the nestRemoting functions:

Book.nestRemoting('pages');
Book.nestRemoting('chapters');
Image.nestRemoting('book');

The above lines enable you to have nested queries as follows:

/api/books/123/pages/456/notes  
Result Type:An array of notes objects   
Output: Queries all of the notes associated with a specific page under a specific book
/api/books/123/pages/456/notes/567  
Result Type:An object of a note data    
Output: Queries a specific note associated with a specific page under a specific boo

I hope this helps! The documentation would be ready soon. Also in the meantime I have seen one of the users has explained this functionality in their blog as well; this is the blog which may help you more.

Please let us know if this solves your problem and we can close the ticket. For more discussion I would encourage you to discuss in our Google community

Thanks!

Hi Amir,

thank you very much for your detailled and instant response!

Unfortunately, I am not yet able to get this running.

Here is my code:
https://github.com/philippdhh/GroupTravelMgrJs/tree/v0.0.2/common/models

I've got the model objects

  • AppUser (app-user.json, app-user.js)
  • Group (group.json, group.js)
  • GFile (gfile.json, gfile.js)

In app-user.json a hasMany relation to Group is defined:

  "relations": {
    "groups": {
      "type": "hasMany",
      "model": "Group",
      "foreignKey": "ownerId"
    }}

in groups.json a hasMany relation to GFile is defined:

  "relations": {
    "gfile": {
      "type": "hasMany",
      "model": "GFile",
      "foreignKey": "groupId"
 }}

May goal is to get routes like http://localhost/api/user/1/group/2/gfiles

Now I tried to use nestRemoting and added to app-user.js:

module.exports = function(AppUser) {
  AppUser.on('attached', function() {
    AppUser.nestRemoting('groups');
  });
};

and to group.js:

module.exports = function(Group) {
  Group.on('attached', function() {
    Group.nestRemoting('gfile');
});
}

Unfortunately when starting everything I get the following error:

C:\Users\Philipp\WebstormProjects\GroupTravelMgrJs\node_modules\loopback\lib\mod
el.js:818
      throw new Error('Relation `' + relationName + '` does not exist for mode
            ^
Error: Relation `groups` does not exist for model `AppUser`
    at Function.Model.nestRemoting (C:\Users\Philipp\WebstormProjects\GroupTrave
lMgrJs\node_modules\loopback\lib\model.js:818:13)
    at EventEmitter.<anonymous> (C:\Users\Philipp\WebstormProjects\GroupTravelMg
rJs\common\models\app-user.js:4:13)
    at EventEmitter.emit (events.js:129:20)
    at EventEmitter.app.model (C:\Users\Philipp\WebstormProjects\GroupTravelMgrJ
s\node_modules\loopback\lib\application.js:158:9)
    at C:\Users\Philipp\WebstormProjects\GroupTravelMgrJs\node_modules\loopback-
boot\lib\executor.js:174:9
    at Array.forEach (native)
    at setupModels (C:\Users\Philipp\WebstormProjects\GroupTravelMgrJs\node_modu
les\loopback-boot\lib\executor.js:170:23)
...

I've already tried many ways to spell groups (group, Group, groups, Groups, etc.) in the nestRemoting() call.

Do I maybe use nestRemoting() in the wrong context? Did I get the functionality totally wrong?
Could you maybe give me a hint where to put the nestRemoting() when working with json definition of models?

Thank you so much,
Philipp

@philippdhh to avoid any load-order related issues, I suggest to add all the nestRemoting calls in a separate boot file in server/boot.

This seems to work!

I have created a new file /server/boot/initNestRouting.js :

module.exports = function initNestRouting(app) {
  console.log('Initializing nestRemoting for models');
  app.models.AppUser.nestRemoting('groups');
  app.models.Group.nestRemoting('gfiles');
}

The route http://localhost:4000/api/AppUsers/1/groups/165/gfiles?access_token=xyz is not visible through api explorer but when calling it through REST client I now get a HTTP 401 AUTHORIZATION_REQUIRED instead of the 404 response I got until now.

So i guess I am almost done! :-)

What is missing is setting the correct ACL values.

Working with the first level nested relation I used the follwoing pattern in my app-user.json:

[...]  
"acls": [
    {
      "principalType": "ROLE",
      "principalId": "$owner",
      "permission": "ALLOW",
      "property": "__get__groups"
    },
    {
      "principalType": "ROLE",
      "principalId": "$owner",
      "permission": "ALLOW",
      "property": "__create__groups"
    }
[...]

I am not sure how to define access control for the next level.

  • In my example there is AppUser ID 1 that is $owner of Group ID 165.
  • There exists one GFile ID 99 that is related to Group ID 165.

I want AppUser 1 to be able to see that GFile ID 99 exists when he calls:http://localhost:4000/api/AppUsers/1/groups/165/gfiles?access_token=xyz

Where should I add the ACL? In AppUser Model? In Group Model?
Somewhere else in the Code?

Are there possible property definitions like "property": "__findById__groups__get__gfiles" (I have already tried this one which does not work)

Thank you very much. Guess this is the last step missing.

Best,
Philipp

Glad that it helped a bit. The easiest way to discover what the property (method) name actually is, would be to set a beforeRemote hook and echo something like ctx.method.name. I'm sure there's a debug option somewhere to do this too, but this beforeRemote trick might help you on your way.

It will apply to AppUser I think.

And now I got a 200 OK when calling http://localhost:4000/api/AppUsers/1/groups/165/gfiles?access_token=HgfBlY9OeLLcfKYXsX8U5znK4DBm28w4gVtGt9pxv1gwBxftCwn3Oz42U0r6Txu2

:-)

The property value I was looking for is "property": "__get__groups__gfiles"

@Amir-61 & @fabien : Thank you very much for your help! That not only solved the problem but I also learned a lot about the framework.

@philippdhh Glad that your problem is solved! :-)

@philippdhh happy to hear it worked!

@crandmck perhaps this should be added to the docs?

Hello. I try get more 2 levels nesting, for example /books/1/chapters/1/paragraphs/2/symbols/

Books have hasMany relation Chapters
Chapters have hasMany relation Paragraphs
... etc

I add app.models.Books.nestRemoting('chapters'); and /books/1/chapters/1/paragraphs/2/ work fine.
But if I add app.models.Chapters.nestRemoting('paragraphs'); /books/1/chapters/1/paragraphs/2/symbols/ don't work,
although chapters/1/paragraphs/2/symbols/ work fine.

I'm having the same issue. I need a structure like

  • house/{id}/rooms/{id}/lights/{id}/level

with the following relations:

House hasMany Room
Room hasMany Light

I can access

  • house/{id}/rooms/{id}

without any problem. However, when I try to access:

  • house/{id}/rooms/{id}/lights/

I get the following error:

{ error: { name: "Error", status: 404, message: "Shared class "House" has no method handling GET /1/rooms/1/lights", statusCode: 404, stack: "Error: Shared class "House" has no method handling GET /myLED/rooms/1/lights at restRemoteMethodNotFound (/home/andmaz/ceotDev/LoopBack/House/node_modules/strong-remoting/lib/rest-adapter.js:345:17) at Layer.handle [as handle_request] (/home/andmaz/ceotDev/LoopBack/House/node_modules/express/lib/router/layer.js:95:5) at trim_prefix (/home/andmaz/ceotDev/LoopBack/House/node_modules/express/lib/router/index.js:312:13) at /home/andmaz/ceotDev/LoopBack/House/node_modules/express/lib/router/index.js:280:7 at Function.process_params (/home/andmaz/ceotDev/LoopBack/House/node_modules/express/lib/router/index.js:330:12) at next (/home/andmaz/ceotDev/LoopBack/House/node_modules/express/lib/router/index.js:271:10) at Function.handle (/home/andmaz/ceotDev/LoopBack/House/node_modules/express/lib/router/index.js:176:3) at router (/home/andmaz/ceotDev/LoopBack/House/node_modules/express/lib/router/index.js:46:12) at Layer.handle [as handle_request] (/home/andmaz/ceotDev/LoopBack/House/node_modules/express/lib/router/layer.js:95:5) at trim_prefix (/home/andmaz/ceotDev/LoopBack/House/node_modules/express/lib/router/index.js:312:13) at /home/andmaz/ceotDev/LoopBack/House/node_modules/express/lib/router/index.js:280:7 at Function.process_params (/home/andmaz/ceotDev/LoopBack/House/node_modules/express/lib/router/index.js:330:12) at next (/home/andmaz/ceotDev/LoopBack/House/node_modules/express/lib/router/index.js:271:10) at jsonParser (/home/andmaz/ceotDev/LoopBack/House/node_modules/body-parser/lib/types/json.js:103:7) at Layer.handle [as handle_request] (/home/andmaz/ceotDev/LoopBack/House/node_modules/express/lib/router/layer.js:95:5) at trim_prefix (/home/andmaz/ceotDev/LoopBack/House/node_modules/express/lib/router/index.js:312:13)" } }

Is there any changes since this issue have been brought to light? Does the Loopback 3 solves this? Or the only way is to use the nestRemoting?

Hi guys, Anyone managed to do what @AndreMaz mentioned?

Hey @paulosuzart currently this can't be done automatically, check this https://github.com/strongloop/loopback/issues/2346#issuecomment-263583335 for more info.

The only way is creating the desired endpoints manually

@Amir-61 @fabien Is it true that dataSource: 'db' must be set in order to show nested models in api explorer? When I set it to be null, hasMany is not a function. Can you please confirm this? Thanks.

@shenghu I managed to get swagger to consider the nest remoting routes by causing the loopback explorer component to run after you set the nest remoting.
Instead of setting the explorer component in component-config.json you should do something like the following (in a boot script)

const explorer = require('loopback-component-explorer');

module.exports = function (app) {

  app.models.MyModel.nestRemoting('MyOtherModel');
  explorer(app, {mountPath: '/api/explorer'});
};
Was this page helpful?
0 / 5 - 0 ratings