Relationship data ActiveRecord > JSONAPI::Resources > Ember Data


#1

Hi, one for the rails peeps. I was hoping you might be able to help me out? How do you represent rails active record relationships in ember data via json-api? I’ve done a bit of reading around and I can’t seem to make ED acknowledge the correct related record.

I am creating a map application which is essentially just a load of markers (waypoint) with a load of polylines (route) drawn between them.

What I would like to achieve (which may be idiomatic to rails, please tell me if i’m doing it wrong) is to be able to treat waypoint.incomingRoute as the route model in the store that is representative of the route that is detailed in the relationships data in the jsonapi.

What I currently have is:

Rails models

class Waypoint < ApplicationRecord
  belongs_to :ride
  has_one :incoming_route, class_name: 'Route', foreign_key: 'waypoint_end_id'
  has_one :outgoing_route, class_name: 'Route', foreign_key: 'waypoint_start_id'
end

class Route < ApplicationRecord
  belongs_to :waypoint_start, class_name: 'Waypoint'
  belongs_to :waypoint_end, class_name: 'Waypoint'
  belongs_to :ride
end

Rails resources

class ApiV1::WaypointResource < JSONAPI::Resource
  has_one :incoming_route, class_name: 'Route', foreign_key: 'waypoint_end_id'
  has_one :outgoing_route, class_name: 'Route', foreign_key: 'waypoint_start_id'
  has_one :ride
  attributes :name, :latlng, :position
end

class ApiV1::RouteResource < JSONAPI::Resource
  has_one :ride
  attributes :state, :line
end

Ember Data models

# Waypoint:

export default DS.Model.extend({
  incomingRoute: DS.belongsTo('route'),
  outgoingRoute: DS.belongsTo('route'),
  ride: DS.belongsTo('ride'),
});

# Route:

export default DS.Model.extend({
  waypointStart: DS.belongsTo('waypoint'),
  waypointEnd: DS.belongsTo('waypoint'),
  ride: DS.belongsTo('ride'),
});

But when given the following payload from JSON::API:

{
  "data": {
    "id": "4",
    "type": "waypoints",
    "links": {
      "self": "http://localhost:3000/api-v1/waypoints/4"
    },
    "attributes": {
      "name": "Leeds Bradford Airport",
      "latlng": "POINT (53.8689904 -1.6660177)",
      "position": 4
    },
    "relationships": {
      "incoming-route": {
        "links": {
          "self": "http://localhost:3000/api-v1/waypoints/4/relationships/incoming-route",
          "related": "http://localhost:3000/api-v1/waypoints/4/incoming-route"
        }
      },
      "outgoing-route": {
        "links": {
          "self": "http://localhost:3000/api-v1/waypoints/4/relationships/outgoing-route",
          "related": "http://localhost:3000/api-v1/waypoints/4/outgoing-route"
        }
      },
      "ride": {
        "links": {
          "self": "http://localhost:3000/api-v1/waypoints/4/relationships/ride",
          "related": "http://localhost:3000/api-v1/waypoints/4/ride"
        }
      }
    }
  }
}

It doesn’t seem to be able to differentiate in the model which route it needs to use:

Error while processing route: ride Assertion Failed: You defined the 'incomingRoute' relationship on
(unknown mixin), but multiple possible inverse relationships of type (unknown mixin) were found on (unknown mixin).

How do I match up the outgoing-route and incoming-route referenced in the waypoint model with the correct route objects in Ember Data and also, same question for the associated waypointStart and waypointEnd associations on the route model?

Thanks in advance for any help.

Andy.


#2

So based purely on the error message I think the problem may be that you need to explicitly set relationship inverses on your Ember Data models. In your rails models you’re setting foreign key:

  has_one :incoming_route, class_name: 'Route', foreign_key: 'waypoint_end_id'
  has_one :outgoing_route, class_name: 'Route', foreign_key: 'waypoint_start_id'

e.g. incoming -> waypoint_end and outgoing -> waypoint_start, but in your Ember Data models you aren’t specifying that. Ember tries to resolve inverse relationships automatically but if it can’t it throws errors like that. Try something like this:

# Waypoint:

export default DS.Model.extend({
  incomingRoute: DS.belongsTo('route', { inverse: 'waypointEnd' }),
  outgoingRoute: DS.belongsTo('route', { inverse: 'waypointStart' }),
  ride: DS.belongsTo('ride'),
});

# Route:

export default DS.Model.extend({
  waypointStart: DS.belongsTo('waypoint', { inverse: 'outgoingRoute' }),
  waypointEnd: DS.belongsTo('waypoint', { inverse: 'incomingRoute' }),
  ride: DS.belongsTo('ride'),
});

That should make it explicit to Ember how those relationships are constructed. Again, Ember can do some of the inference by itself but in a situation like this there is ambiguity and you must specify inverses. Also worth mentioning that you can set inverses to ‘null’ if you don’t want Ember to treat it as a two-way relationship.


#3

w00p! My errors are gone and it seems to be behaving.

It’s not clear what you can actually send to inverse, I thought it was the model class name rather than an instance. I’ll continue with the dev and see if it allows me to do what i wanted.

Thanks for the quick response, that was really helpful.


#4

AFAIK it’s either always ‘null’ (for no inverse) or the relationship name (not the class). Technically the class wouldn’t be enough to disambiguate because you could have 10 relationships to the same class with different names and a single inverse on the other side that points to one of them, so you have to tell Ember which one it is by name.

Anyway, glad you got it working. Good luck! And definitely post here or in discord if you have any more questions!


#5

Thanks, I will. Discord moves a bit too quick so I’ll stick to here.