silkapp/rest

Cannot access subresources in generated javascript client without top-level getter. (was: How to use nested api with javascript client?)

ryskajakub opened this issue · 19 comments

Hi,

it's not clear for me, how to use the javascript generated client with a nested resource. Calling the top-level is easy:

var api = new MyApi("/api");
api.TopLevel.list(function (x) {doSomething(x);});

How do I call the nested resource?
Let's say I have this schema /toplevel/<id>/resource/.
Calling that resource with curl, for example /api/v1.0.0/toplevel/5/resource/ works, but I don't know how to call it with the generated client. This code doesn't work:

var api = new MyAPi("/api");
var topLevel = api.TopLevel("/5");
topLevel.Resource.create(/* new json item */);

Any help appreciated :-)

Off the top of my head, I think it depends on how you defined your resource. If it has a named identifier, it should be something like api.TopLevel.byFoo(5).Resource.list(). If it has an unnamed identifier, it would be api.TopLevel(5).Resource.list(). Please let me know if that works.

@hesselink I use unnamedSingle and api.TopLevel(5) already throws an exception.

undefined is not a function
at return TopLevel.access(url, secureUrl, modifyRequest);

That's strange. I just tested with an unnamedSingle in our code base, and the code I gave seems to work. Can you share (parts of) your actual code? Also, what version of rest-gen did you use to generate the client?

Sure.
I use rest-gen-0.16.1.3,
It looks like the JQuery version does not affect it, because it fails before the request is made.
The Router looks like this

root `compose` ((route topLevelRes) `compose` (route nestedRes))

TopLevel schema and nested Schema respectively looks like this:

import Safe (readMay)
topLevelS = withListing () $ unnamedSingle readMay
nestedS = noListing $ named []

So, to summarize, my api is running at /api and this code doesn't work:

var myApi = new MyApi("/api")
myApi.TopLevel.list() // this is ok
myApi.TopLevel(5).Nested
Uncaught TypeError: undefined is not a function

Sorry for not coming back to you sooner. I changed one of the endpoints (Post) in rest-example to have an unnamed getter. The schema now looks like this:

  , R.schema = withListing () $ unnamedSingleRead ById

I could then run the following code from node, and get a result:

var Api = require("./out/api.js");
var api = new Api("http://localhost:3000");
api.Post(1).get().then(function (x) { console.log("got it", x); })

So that clears up the javascript API. The question is why it doesn't work for you. Perhaps you haven't defined a getter? The resource record should have a get property that is Just handler for a get function to be generated in the API.

If that's not it, I'm not sure what else to do. Perhaps you could provide a complete example that doesn't work, that I can compile and run?

Looking at your code again, I think myApi.TopLevel(5).get() should work, but I'm not sure what you expect after that, since Nested doesn't have a listing or a single getter, so there's nothing you can do with it. If you did have e.g. a listing,myApi.TopLevel(5).Nested.list() should work. On the example api that I changed, api.Post(1).Comment.list() works.

  1. In which commit is that? I have this one: cce989b - from today and the the schema field looks like this:
, R.schema = withListing () $ named [("id", singleRead ById), ("latest", single Latest)]
  1. In my setting, I want to do a POST request, by calling create on the TopLevel resource. So that listing-and-getterless schema should be fine. The problem is that I couldn't get over the TopLevel single resource identification yet = I couldn't get over the point, where code like myApi.TopLevel(5) wouldn't throw an exception. Sorry that my examples are kind of confusing :-/
  1. Sorry, that was unclear. I just changed it locally to test, I didn't commit anything. The one schema line is actually the whole diff, if you want to try it.

  2. Hmm, the TopLevel access should work as I wrote above. Perhaps it's because of the way you're composing the API: I just noticed you're associating compose differently than we do. I'd write root -/ route topLevelRes --/ route nestedRes, which associates as

(root `compose` route topLevelRes) `compose` route nestedRes

Could you see if that fixes the issue?

  1. OK, I'm going to try it.

  2. In the latest uploaded rest-api, the operators you mentioned are defined in this way:

    (-/) :: Router m s -> Router s t -> Router m s | infixl 4 |
    (--/) :: Router m s -> Router s t -> Router m s | infixl 5 |

My reasoning is that in root -/ route topLevelRes --/ route nestedRes the --/ operator has higher precedence than the -/ and so the implicit parens are like this: rootcompose(route topLevelRescomposeroute nestedRes).
Having the parens the other way cause the code to not typecheck for me.

  1. You're right, I was confused, sorry.
  1. Ok, so now I now more about what's going on. If I don't specify handler for Resource's get, then the access function is not generated on the client. Calling api.TopLevel(1) thus tries to call the access on TopLevel which throws an exception.

What is the reasoning behind that? Are you trying to verify that the resource with such id exist before allowing the user to call the nested apis?

In my opinion, the user should be allowed to access all the nested resources despite the fact that the get isn't generated, anyway he can do so, since the corresponding /toplevel/<id>/nested/ resource is accepting requests, it only cannot be done with the generated javascript code, but he must do it with handwritten jQuery.ajax for example.

Ah, so if I understand correctly, if you have a resource without a getter, you cannot access any subresources using the generated Javascript client? Is that correct? That does sound like a bug.

Yes, you can reproduce the behaviour on the rest-example.

(As an aside, I'm having trouble seeing how you would use this in the real world: if you do have a listing, then you might as well have a single getter. If you don't have a listing, how will a user know which resources exist?)

I was actually planning to add the the single getter in future, so knowing that adding the getter would fix the problem would be sufficient for me, that's right.

Great, then I'll leave this one open to fix it eventually. Thanks for helping figure this one out!

You also cannot remove a resources that doesn't have a getter.

What do you mean exactly? Remove what? The subresource or the resource itself? And in the API, or only in the generated javascript?

You cannot remove the resource itself with the generated JS. Haskell client is ok.

It generates e.g. Foo.prototype.remove which you can never construct without a getter. Updates are okay, they are Foo.saveBy....