Ember Data, JSONAPI, and known-to-be-empty relationships that are definitely not empty


#1

So I’ve got an app that throws this warning ALL THE TIME:

vendor.js:17426 WARNING: You pushed a record of type 'app-page-person' with 
a relationship 'page' configured as 'async: false'. You've included a link but no 
primary data, this may be an error in your payload. EmberData will treat this 
relationship as known-to-be-empty.

Here is my scenario and my expectation:

  1. My app is viewing a particular page. The page is in the store and loaded.
  2. On the page, I do a fetch to my JSONAPI endpoint to grab app-page-person records <url>/app-page-people?filter%5Bpage-id%5D=5344a64b-1394-4885-8f2b-ddd6e927025a&include=person.image
    • Note: I’m not using the built in ember-data fetching. All relationships are set to async: false and I’m doing the query on my own through store.query
  3. The resulting payload is pushed into the store. It includes the linkage data for the page without the full model, because I already have it
    ....
    {
      "id": "00fafeee-6c9d-4bd2-9108-eb5a7c11ca57",
      "type": "app-page-people",
      "links": {
        "self": "https://www...."
      },
      "attributes": { "sort-order": 34 },
      "relationships": {
        "page": {
          "links": {
            "self": "https://www.....",
            "related": "https://www....."
          },
          "data": {
            "type": "pages",
            "id": "5344a64b-1394-4885-8f2b-ddd6e927025a"
          }
        },
        "person": {
          "links": {
            "self": "https://www.....",
            "related": "https://www...."
          },
          "data": {
            "type": "people",
            "id": "00fafeee-6c9d-4bd2-9108-eb5a7c11ca57"
          }
        }
      }
    },
   ....
  1. I receive the above warning

My assumption

My expectation here is that since the page model is already in the store, things should be fine. I don’t understand why ember-data feels the need to warn every time.

Some more details

Page Model

export default DS.Model.extend({
  ...
  appPageProfiles: DS.hasMany('app-page-profile', { async: false, inverse: 'page' }),
  ...
})

AppPagePerson Model

export default DS.Model.extend({
  ...
  page: DS.belongsTo('page', { async: false }),
  ...
})

#2

Since the specs do not restrict the use of a related link along with a data section it is on ember-data to disambiguate what to do. In this case I believe ember-data is using the data linkage but also registering the links.related URL as the canonical source of truth. Since this is inherently an asynchronous operation designating the relationship as async: false conflicts with this assertion by the payload.

Therefor either the back end API is confused. Or your ember-data implementation needs to be taught that things are different for this resource.

import JSONAPISerializer from 'ember-data/serializers/json-api';

export default JSONAPISerializer.extend({
  extractRelationship(relationshipHash) {
    if (relationshipHash.links) {
      delete relationshipHash.links.related;
    }
    return this._super(...arguments);
  }
});

Here is a working example: https://ember-twiddle.com/3aad195eb666af34166d9689418731d5


#3

Hey @sukima, thanks for taking the time to explain this. It feels odd to me that ember-data thinks that the presence of links is an assertion that it relationship has to be async. It feels like the client is the one to determine, on it’s own, whether something should be sync or async, regardless of what the API comes back with. Maybe my expectations are wrong.

Regardless, I’ve been working on a grab-bag addon with ergonomic improvements to ember-data and JSONAPI stuff and I’ve added support for automatically removing the links key on payloads of relationships marked as sync.

I’ll add just the good parts below, but if you’re interested, you can see the the full implementation here: https://github.com/yapplabs/ember-data-utils/blob/master/addon/serializers/ember-data-utils-json-api.js

// app/serializers/application.js
export default DS.JSONAPISerializer.extend({
  extractRelationships(modelClass, resourceHash) {
    let relationships = this._super(modelClass, resourceHash);
    if (hasLinks(relationships)) {
      relationships = cleanupLinksForSyncRelationships(modelClass, relationships);
    }
    return relationships;
  }
});

function cleanupLinksForSyncRelationships(modelClass, relationshipHash) {
  for (let relationshipName in relationshipHash) {
    let { links } = relationshipHash[relationshipName];

    if (links && isRelationshipSync(modelClass, relationshipName)) {
      delete relationshipHash[relationshipName].links;
    }
  }

  return relationshipHash;
}

function hasLinks(relationships) {
  return Object.entries(relationships).some(rel => rel[1].links);
}

function isRelationshipSync(modelClass, relationshipName) {
  let relationship = modelClass.relationshipsByName.get(relationshipName);

  return !relationship.options.async;
}

#4

I don’t know if this barking up the wrong tree or not but if there are grievances on how ember-data is implemented or the assumptions it makes you can always open a discussion with the ember-data core team and/or open RFCs on the ember-data repository.

This is all open source anyway.


#5

That’s a good point. I still feel like I’m making sense of things, so I wouldn’t go so far as to call it a an official grievance. Once I have a better understanding and a chance to see this code shake things out a bit, I’ll circle back.

Thanks again for taking the time to explain. Much appreciated.


#6

We’ve noticed the same behavior in our applications as well. I’ve got two ways of fixing this…

I opened a PR on Ember Data today that will not overwrite a sync relationship if it has data already. This will prevent the known to be empty warnings you’re describing. I think the test case here is the best way to describe what I think the behavior should be: https://github.com/emberjs/data/pull/5880

As far as how to get this fixed today, I’ve also found that stripping out the links is the best way to deal with this in app code. Here’s a gist of how we strip out all links with the application serializer.

While it works, I don’t think link stripping is a good long term solution because links and sync relationships are pretty powerful. They let you access data without triggering network requests, but also let you lazily load data when you need it. My take is that this is the sweet spot of building data heavy Ember apps.


#7

Bless you, Ryan! That is a perfect solution. I’m not comfortable with the stripping, but the amount of warnings (with their accompanying console.traces) is killing dev performance.