How Ember knows which URL to hit in the backend?


#1

I can’t figure out what is the convention rule in the when querring in model hook in a route handler. How Ember knows which URL to hit in the following situation:

Here is my router definition:

#router.js
Router.map(function() {
 ...
  this.route('languages', { path: '/shops/:shop_id/languages'});
});

Here the link definition in a template:

#application.hbs
{{#link-to 'languages' currentUser.user.shop.id class="nav-link"}}...{{/link-to}}

Here the route handler:

#languages.js

export default Route.extend(AuthenticatedRouteMixin, {
  model(params) {
    return this.store.query('language', { shop: params.shop_id })
  }
});

I hoped that Ember to hit shops/:shop_id/languages URL, but it did /languages?shop=6449 instead. Can somebody explain me the role of language String in this.store.query('language', { shop: params.shop_id }), - I belived that Ember would just deserialize language model (defined in models/language.js). Thank you.


#2

Is there an easier solution other than to implement a proper adapter and override query method defined in JSONAPIAdapter or or urlForQuery from BuildURLMixin? Looks like I’ll have to override all the main URLs to create/edit/update a shop language because they use a different Ajax call (get, put, delete, etc.).


#3

Hi @belgoros,

I think you are making a little confusion, if I did not misunderstood your point, I will try to make it clear in the simplest way:

The router route’s path is used to specify an URL for the {{link-to}} helper, but it does have nothing to share with Ember Data.

If your backend implements json:api all you need to do is to implement the method DS.JSONAPIAdapter.urlForQuery as described here.

If your backend does not implement json:api you have, unfortunately, to handle all the communication yourself, that might be done by implementing an adapter yourself or to write it in bare javascript inside an external module and import it in your ember application.

I hope I did not misunderstand your question.


#4

Hi,

This was the source for a lot of confusion for me at first, mostly because I came from a Rails world where something like /shops/3/languages meant something to me w.r.t. the back end.

Since then I’ve come to realize that when you see a url like the one above in your browser’s address bar that you should view it as just a serialization of the app’s state, i.e. how to go about setting up models and templates in the browser’s window, as opposed to what is going to get sent to the server.

Something like this: this.store.query('comments', {post: params.post_id}) will result in what you saw above being sent to the server: GET api/comments?post=3, while something like this.store.findRecord('comment', params.comment_id) will result in GET api/comments/2.

The key thing to remember is that this.store.query(~~~) will always result in attaching query params to the url, while this.store.findRecord(~~~) will spit out what you were hoping for above, i.e. the params being inserted into the meat of the url.

I hope that answered your question.


#5

Thank you, guys, for your responses ! The solution I came to is the following:

  • I created a shop-language.js adapter:
export default ApplicationAdapter.extend({
  urlForQuery (query, modelName) {
    return `/shops/${query.shopId}/languages`;
  }
});

Then call query in languages.js route handler model hook:

export default Route.extend(AuthenticatedRouteMixin, {
  model(params) {
    return this.store.query('shop-language', { shopId: params.shopId});
  }
});

And the shop languages route is defined in routerlike that:

Router.map(function() {
  this.route('languages', { path: '/:shopId/languages'});
...
});

It seems to work, any other remarks are welcome !

Thank you!


#6

One other way to handle “nested” models like this is, if your shop model has a hasMany(‘language’) relationship, you can use “links” either in your backend response or add them in your shop serializer. Then when you fetch shops.get(‘languages’) it will fetch the languages async from the url that you specify in related “links”.

Essentially in JSON API links are a way of providing related data or paginated data URLs as part of your payload (instead of sideloading the data or providing ids). So while the normal resource structure is assumed to be “flat”, you can use links to fetch “nested” data like this.


#7

@dknutsen, Thank you for your response. Is there any example or docs on how to use the above solution ? Thank you. I’m using Rails as back-end API.


#8

One more question: I see that I have XHR request hit as follows:

http://localhost:4200/shops/613/languages?shopId=613

Is it possible to remove the query param in the end ?shopId=613, - it is already present in the URL followed by languages.


#9

@belgoros again this requires having a hasMany(‘languages’) relationship on the shops model, and if you’re using JSON API the specifics might be a little different (try looking at the JSON API spec) because we’re using the Rest Adapter/Rest Serializer, but this is what we do:

In our users serializer for example, we link the “nested” accounts like so:

// serializers/user.js
...
  normalize: function(typeClass, hash, prop){
    // add an 'accounts' link to support our 'hasMany'
    hash.links = {
      accounts: `/users/${hash.id}/accounts`,
    };
    return this._super(typeClass, hash);
  },
...

Then whenever we have a user model and want to fetch the accounts for it we can simply say user.get('accounts') and it fetches them from that URL


#10

Should I include the accounts collection when returning a User from the backend or Ember will fetch the User accounts using the provided URL: ``/users/${hash.id}/accounts` ?

In my case I have relations defined both in Rails and Ember like a Shop has_many Languages.

When getting shop languages like that:

#shop_serializer.js

export default DS.JSONAPISerializer.extend({
  normalize: function(typeClass, hash, prop){
    hash.links = {
      shopLanguages: `/shops/${hash.id}/languages`,
    };
    return this._super(typeClass, hash, prop);
  },
});

in the route handler:

# routes/shops/shop-languages.js
import Route from '@ember/routing/route';

export default Route.extend({
   model(params) {
    let shops = this.modelFor('shops');
    let shop = shops.findBy('id', params.shop_id);
    console.log('shops: ' + shops);
    console.log('shop: ' + shop);
    console.log(shop.get('shopLanguages'));
    return shop.get('shopLanguages');
  }
});

I get the following in the console:

XHR finished loading: GET "http://localhost:4200/shops"
shops: <DS.RecordArray:ember276>
shop: <draft-api-ember@model:shop::ember347:7>

but the url ``/shops/${hash.id}/languages` has never been hit. I checked the value of shops array and shop - all of them seem to be correct.


#11

The baskend is using Rails API with active_model_serializers gem, the response it is sending is 100% compatible json-api.