Loopback: Related models don't expose their remote methods.

Created on 8 Dec 2015  路  12Comments  路  Source: strongloop/loopback

I didn't find any way to expose the remote methods of a related model.

Example:

For a model 'Cat' I have defined a remote method, lets call it 'meow'.

So i can do:

GET /cats/{:id}/meow

The 'Cat' model belongsTo the 'User' model.

Now I would like to be able to do something like this:

GET /users/{:id}/cats/{:id}/meow

I already tried nestRemoting, which only works for nested 'blueprint' methods.

feature stale

Most helpful comment

Guys Any solution for that?

All 12 comments

hey @adrien-candiotti I am trying to reproduce your issue and could you explain blueprint method more clearly?

Sorry, i made a mistake in my explanation : it's not the default CRUD method (that I called 'blueprint' methods).
It's the relationship access method that appear with nestRemoting.
For example if in my example the Cat model belongsTo a Breed model, i can see the

GET /users/{:id}/cats/{:id}/breed

route in my explorer

What I don't see are the custom remote methods I added to the Cat model like

GET /users/{:id}/cats/{:id}/meow

hey @adrien-candiotti I reproduced your issue and yes the custom remote method doesn't work.
Thank you for noticing us this and I labeled it as a feature!

I tried nestRemoting and I believe this is built for multiple level extension, like if you have another model called food, which belongs to cat, then you can expose endpoint /api/users/{id}/cats/{id}/{food}.

And if you have to use /users/{:id}/cats/{:id}/meow as an endpoint, would you please take a little time to see our chat in this issue:
https://github.com/strongloop/loopback/issues/1859

Instead of asking for a nested remote method endpoint, the user in that issue is trying to get 2nd level custom scope work. And as a workaround, you can define a remote method in user and set path as /users/{:id}/cats/{:id}/meow

see solution details here

@loopback team : was this planned for a coming sprint?

Is there a workaround? In my case i have an resource object that has a relation of hasOne to a file model. The file model has a remoteMethod of upload.

/models/resources.json

{
  "name": "Resource",
  "base": "PersistedModel",
  "idInjection": true,
  "options": {
    "validateUpsert": true
  },
  "postgresql": {
    "schema": "public",
    "table": "resource"
  },
  "hidden": [
    "id"
  ],
  "mixins": {
    "Useredid": true,
    "TimeStamp": {
      "createdAt": "created_at",
      "updatedAt": "updated_at"
    }
  },
  "properties": {
    "categories": {
      "type": "string",
      "required": false,
      "length": null,
      "precision": null,
      "scale": null,
      "postgresql": {
        "columnName": "categories",
        "dataType": "text",
        "dataLength": null,
        "dataPrecision": null,
        "dataScale": null,
        "nullable": "YES"
      },
      "_selectable": true
    },
    "description": {
      "type": "string",
      "required": false,
      "length": null,
      "precision": null,
      "scale": null,
      "postgresql": {
        "columnName": "description",
        "dataType": "text",
        "dataLength": null,
        "dataPrecision": null,
        "dataScale": null,
        "nullable": "YES"
      },
      "_selectable": true
    },
    "features": {
      "type": [
        "string"
      ],
      "required": false,
      "length": null,
      "precision": null,
      "scale": null,
      "postgresql": {
        "columnName": "features",
        "dataType": "text",
        "dataLength": null,
        "dataPrecision": null,
        "dataScale": null,
        "nullable": "YES"
      },
      "_selectable": true
    },
    "name": {
      "type": "string",
      "required": true,
      "length": null,
      "precision": null,
      "scale": null,
      "postgresql": {
        "columnName": "name",
        "dataType": "character varying",
        "dataLength": 255,
        "dataPrecision": null,
        "dataScale": null,
        "nullable": "NO"
      },
      "_selectable": false
    },
    "tags": {
      "type": [
        "string"
      ],
      "required": false,
      "length": null,
      "precision": null,
      "scale": null,
      "postgresql": {
        "columnName": "tags",
        "dataType": "text",
        "dataLength": null,
        "dataPrecision": null,
        "dataScale": null,
        "nullable": "YES"
      },
      "_selectable": true
    },
    "url": {
      "type": "string",
      "required": false,
      "length": null,
      "precision": null,
      "scale": null,
      "postgresql": {
        "columnName": "url",
        "dataType": "text",
        "dataLength": null,
        "dataPrecision": null,
        "dataScale": null,
        "nullable": "YES"
      },
      "_selectable": true
    },
    "version": {
      "type": "string",
      "required": false,
      "length": null,
      "precision": null,
      "scale": null,
      "postgresql": {
        "columnName": "version",
        "dataType": "character varying",
        "dataLength": 50,
        "dataPrecision": null,
        "dataScale": null,
        "nullable": "YES"
      },
      "_selectable": true
    },
    "isVersioned": {
      "type": "boolean",
      "required": false,
      "length": null,
      "precision": null,
      "scale": null,
      "postgresql": {
        "columnName": "is_versioned",
        "dataType": "boolean",
        "dataLength": null,
        "dataPrecision": null,
        "dataScale": null,
        "nullable": "YES"
      },
      "_selectable": true
    },
    "longDescription": {
      "type": "string",
      "required": false,
      "length": null,
      "precision": null,
      "scale": null,
      "postgresql": {
        "columnName": "long_description",
        "dataType": "text",
        "dataLength": null,
        "dataPrecision": null,
        "dataScale": null,
        "nullable": "YES"
      },
      "_selectable": true
    },
    "redId": {
      "type": "string",
      "required": false,
      "length": null,
      "precision": null,
      "scale": null,
      "postgresql": {
        "columnName": "red_id",
        "dataType": "character varying",
        "dataLength": 10,
        "dataPrecision": null,
        "dataScale": null,
        "nullable": "YES"
      },
      "_selectable": false
    }
  },
  "validations": [],
  "relations": {
    "Contacts": {
      "type": "hasAndBelongsToMany",
      "model": "Contact"
    },
    "Logo": {
      "type": "hasOne",
      "model": "File",
      "foreignKey": "resource_id"
    }
  },
  "acls": [],
  "methods": {}
}

/models/files.json

{
  "name": "File",
  "base": "PersistedModel",
  "strict": false,
  "idInjection": true,
  "options": {
    "validateUpsert": true
  },
  "properties": {
    "name": {
      "type": "string"
    },
    "type": {
      "type": "string"
    },
    "url": {
      "type": "string",
      "required": false
    }
  },
  "validations": [],
  "relations": {
    "Resources": {
      "type": "belongsTo",
      "model": "Resource",
      "foreignKey": "resource_id"
    }
  },
  "acls": [],
  "methods": {}
}

/models/files.js

'use strict';
var CONTAINERS_URL = '/api/containers/';
module.exports = function(File) {
  File.upload = function (ctx,options,cb) {
        if(!options) options = {};
        ctx.req.params.container = 'common';
        File.app.models.Container.upload(ctx.req,ctx.result,options,function (err,fileObj) {
            if(err) {
                cb(err);
            } else {
              console.log(fileObj)
                var fileInfo = fileObj.files.file[0];
                File.create({
                    name: fileInfo.name,
                    type: fileInfo.type,
                    container: fileInfo.container,
                    url: CONTAINERS_URL+fileInfo.container+'/download/'+fileInfo.name
                },function (err,obj) {
                    if (err !== null) {
                        cb(err);
                    } else {
                        cb(null, obj);
                    }
                });
            }
        });
    };

    File.remoteMethod(
        'upload',
        {
            description: 'Uploads a file',
            accepts: [
                { arg: 'ctx', type: 'object', http: { source:'context' } },
                { arg: 'options', type: 'object', http:{ source: 'query'} }
            ],
            returns: {
                arg: 'fileObject', type: 'object', root: true
            },
            http: {verb: 'post'}
        }
    );
};

Guys Any solution for that?

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

This issue has been closed due to continued inactivity. Thank you for your understanding. If you believe this to be in error, please contact one of the code owners, listed in the CODEOWNERS file at the top-level of this repository.

I got a solution i need to share with all of you

nestRemoting function take an options json object which contain an attribute called filterMethod this method filter the model function to get only the defaults methods so i passed this attribute callback function with customization in the ( else if ) to check my remote method ( DoWhat ) and return it

server.models.Client.nestRemoting('units', {filterMethod: function(method, relation) {
        let regExp = /^__([^_]+)__([^_]+)$/;
        let matches = method.name.match(regExp);
        if (matches) {
            return '__' + matches[1] + '__' + relation.name + '__' + matches[2];
        } else if (method.name === 'DoWhat') {
            return method.name;
        }
    }});

I hope this solution help you

I'm having the exact same problem.
I have 2 models, user and car where: "user" can have many cars. I have remote method call car.services
[POST] /cars (works)
[POST] /cars/services (works)
[POST] /users/:id/cars (works)
[POST] /users/:id/cars/services (I cannot expose this end point via REST)

Can anyone assist, please?

After a long debugging session I found the solution -
In order for nest remoting to work, the methods in the nested model must specify the path in the json definition e.g.:

"http": {
   "verb": "get",
   "path": "/getDataToFill"
}

My full solution is as follows:

In a boot script:

const filterRegExp = /^__([^_]+)__([^_]+)$/;

const defaultFilterCallback = (method, relation) => {
  const matches = method.name.match(filterRegExp);
  if (matches) {
    return '__' + matches[1] + '__' + relation.name + '__' + matches[2];
  }
};
module.exports = function (app) {
  // 'bookings' is the relation name in the user model, not the name of the model
  app.models.User && app.models.Booking && app.models.User.nestRemoting('bookings', {
    filterMethod: function (method, relation) {
     // add default methods
      let matches = defaultFilterCallback(method, relation);
      if (matches) {
        return matches;
      }
       // add custom methods
      matches = method.name.indexOf('reserve') > -1 || method.name.indexOf('latest') > -1 || method.name.indexOf('applyCoupon') > -1 || method.name.indexOf('retrieve') > -1;
      if (matches) {
        return '__' + method.name + '__' + relation.name;
      }
    }
  });
}

Hope that helps

Still no built-in solution for this "issue" ?

Was this page helpful?
0 / 5 - 0 ratings