Developing for the AJAX GraphQL API

Developing for the AJAX GraphQL API is very similar to developing for other endpoints. See Developing GraphQL APIs for Totara and Implementing GraphQL services 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:

query { todos: totara_todos_todos { __typename id owner { name email } } }

And another query like

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:

query { todos: totara_todos_todos { __typename id owner { __typename id name } } }

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

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:

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:

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).