Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

Developing for the AJAX GraphQL API is very similar to developing for other endpoints. See See Developing GraphQL APIs for Totara and  and Implementing GraphQL services for  for more details.

For more information about JavaScript API usage see the Tui front-end framework documentation.

Cache normalisation (Totara 19+)

In order to enable smoother usage with cache-normalising clients like Apollo 3, we have a lint rule that flags queries that could potentially result in missing data on the frontend.

“Normalising” here refers to combining entities that have the same __typename and id, so that when an entity receives new data, all queries using that entity will receive that new data also, without having to re-fetch from the server.

Cache-normalising clients like Apollo 3 will not merge the properties of non-normalised object fields by default, and instead overwrite the whole query. In practice, this means if you have a query like:

Code Block
languagegraphql
query {
  todos: totara_todos_todos {
    __typename
    id
    owner {
      name
      email
    }
  }
}

And another query like

Code Block
languagegraphql
query {
  todos: totara_todos_todos {
    __typename
    id
    owner {
      name
    }
  }
}

(notice the second query omits “email” from the owner field)

When the second query is fetched, the entity for the todo in the cache will be update with the new data. However, because owner is not a normalised field (doesn't have __typename and id), it will be replaced on the todo item, meaning the consumer of the first query will no longer have the email field on owner.

This happens because Apollo cannot tell if the owner object is the same one as before, or has changed. If the owner had changed and Apollo merged those two fields together, you would end up with the name from one user, and email from another, a state that is never possible with correct data from the server.

This will be flagged by the linting when running grunt (or npx grunt eslint:graphql).

There are a couple of ways to resolve this. The best way, wherever possible, is to add __typename and id to the field in both queries:

Code Block
languagegraphql
query {
  todos: totara_todos_todos {
    __typename
    id
    owner {
      __typename
      id
      name
    }
  }
}

However, sometimes that is not possible. Take the following query for example:

Code Block
query {
  todos: totara_todos_todos {
    __typename
    id
    metadata {
      course_id
    }
  }
}

Here, we have a metadata field. This doesn’t really represent a standalone entity, but is just a way of grouping extra data associated with the todo. We could give it a pretend ID (e.g. reuse the ID of the associated todo), but often that doesn’t really make sense.

Instead, we can tell both the lint tool and Apollo how the field should be handled. This is done by creating a js/internal/entry.js file in the Tui component, and adding "entry": "./js/internal/entry.js" to the tui.json. In this file we can tell Apollo how to handle the field by adding a type policy:

Code Block
languagejs
import { addTypePolicies } from 'tui/apollo_client';

addTypePolicies({
  totara_todos_todo: {
    fields: {
      metadata: { merge: true },
    },
  },
});

For a given field, you can set merge: true to tell Apollo to merge that field, or merge: false to replace it. You can read more about this in the Apollo documentation.

This will avoid Apollo printing a warning to the console, but we still need to let the linting on the backend know we are handling it. To do this we use a decoration:

Code Block
languagegraphql
type totara_todos_todo {
  id: core_id!
  owner: core_user!
  metadata: totara_todos_todo_metadata @cache(merge: true)
}

You can solve the lint error with either of these solutions, but a good rule of thumb is to use __typename and id where possible, and fall back to setting a type policy where that doesn’t make sense (such as for things that aren’t actually independent entities, like the metadata field on todos above).