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 | ||
---|---|---|
| ||
query {
todos: totara_todos_todos {
__typename
id
owner {
name
email
}
}
} |
And another query like
Code Block | ||
---|---|---|
| ||
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 | ||
---|---|---|
| ||
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 | ||
---|---|---|
| ||
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 | ||
---|---|---|
| ||
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).