Schema Awareness
Previously, on the "Normalized Caching" page we've seen how Graphcache
stores normalized data in its store and how it traverses GraphQL documents to do so. What we've seen
is that just using the GraphQL document for traversal, and the __typename
introspection field
Graphcache is able to build a normalized caching structure that keeps our application up-to-date
across API results, allows it to store data by entities and keys, and provides us configuration
options to write manual cache updates and local
resolvers.
While this is all possible without any information about a GraphQL API's schema, the schema
option
on cacheExchange
allows us to pass an introspected schema to Graphcache:
const introspectedSchema = { __schema: { queryType: { name: 'Query', }, mutationType: { name: 'Mutation', }, subscriptionType: { name: 'Subscription', }, },};
cacheExchange({ schema: introspectedSchema });
In GraphQL, APIs allow for the entire schema to be "introspected", which are special GraphQL queries that give us information on what the API supports. This information can either be retrieved from a GraphQL API directly or from the GraphQL.js Schema and contains a list of all types, the types' fields, scalars, and other information.
In Graphcache we can pass this schema information to enable several features that aren't enabled if we don't pass any information to this option:
- Fragments will be matched deterministically: A fragment can be written to be on an interface type
or multiple fragments can be spread for separate union'ed types in a selection set. In many cases,
if Graphcache doesn't have any schema information then it won't know what possible types a field
can return and may sometimes make a guess and issue a
warning. If we pass Graphcache a
schema
then it'll be able to match fragments deterministically. - A schema may have non-default names for its root types;
Query
,Mutation
, andSubscription
. The names can be changed by passingschema
information tocacheExchange
which is important if the root type appears elsewhere in the schema, e.g. if theQuery
can be accessed on aMutation
field's result. - We may write a lot of configuration for our
cacheExchange
but if we pass aschema
then it'll start checking whether any of the configuration options actually don't exist, maybe because we've typo'd them. This is a small detail but can make a large different in a longer configuration. - Lastly; a schema contains information on which fields are optional or required. When Graphcache has a schema it knows optional fields that may be left out, and it'll be able to generate "partial results".
Partial Results
As we navigate an app that uses Graphcache we may be in states where some of our data is already cached while some aren't. Graphcache normalizes data and stores it in tables for links and records for each entity, which means that sometimes it can maybe even execute a query against its cache that it hasn't sent to the API before.
On the "Local Resolvers" page we've seen how to write
resolvers that resolve entities without having to have seen a link from an API result before. If
Graphcache uses these resolvers and previously cached data we often run into situations where a
"partial result" could already be generated, which is what Graphcache does when it has schema
information.

Without a schema
and information on which fields are optional, Graphcache will consider a "partial
result" as a cache miss. If we don't have all the information for a query then we can't execute
it against the locally cached data after all. However, an API's schema contains information on which
fields are required and optional, and if our apps are typed with this schema and
TypeScript, can't we then use and handle these partial results before a request is sent to the API?
This is the idea behind "Schema Awareness" and "Partial Results". When Graphcache has schema
information it may give us partial results with the stale
flag
set while it fetches the full result from the API in the
background. This allows our apps to show some information while more is loading.
Getting your schema
But how do you get an introspected schema
? The process of introspecting a schema is running an
introspection query on the GraphQL API, which will give us our IntrospectionQuery
result. So an
introspection is just another query we can run against our GraphQL APIs or schemas.
As long as introspection
is turned on and permitted, we can download an introspection schema by
running a normal GraphQL query against the API and save the result in a JSON file.
import { getIntrospectionQuery } from 'graphql';import fetch from 'node-fetch'; // or your preferred request in Node.jsimport * as fs from 'fs';
fetch('http://localhost:3000/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ variables: {}, query: getIntrospectionQuery({ descriptions: false }), }),}) .then(result => result.json()) .then(({ data }) => { fs.writeFile('./schema.json', JSON.stringify(data), err => { if (err) { console.error('Writing failed:', err); return; } console.log('Schema written!'); }); });
Alternatively, if you're already using GraphQL Code Generator
you can use their @graphql-codegen/introspection
plugin to do the same automatically
against a local schema. Furthermore it's also possible to
execute
the introspection query directly
against your GraphQLSchema
.
Optimizing a schema
An IntrospectionQuery
JSON blob from a GraphQL API can without modification become quite large.
The shape of this data is { "__schema": ... }
and this schema data will contain information on
all directives, types, input objects, scalars, deprecation, enums, and more. This can quickly add up and one of the
largest schemas, the GitHub GraphQL API's schema, has an introspection size of about 1.1MB, or about
50KB gzipped.
However, we can use the @urql/introspection
package's minifyIntrospectionQuery
helper to reduce
the size of this introspection data. This helper strips out information on directives, scalars,
input types, deprecation, enums, and redundant fields to only leave information that Graphcache
actually requires.
In the example of the GitHub GraphQL API this reduces the introspected data to around 20kB gzipped, which is much more acceptable.
Installation & Setup
First, install the @urql/introspection
package:
yarn add @urql/introspection# ornpm install --save @urql/introspection
You'll then need to integrate it into your introspection script or in another place where it can optimise the introspection data. For this example, we'll just add it to the fetching script from above.
import { getIntrospectionQuery } from 'graphql';import fetch from 'node-fetch'; // or your preferred request in Node.jsimport * as fs from 'fs';
import { getIntrospectedSchema, minifyIntrospectionQuery } from '@urql/introspection';
fetch('http://localhost:3000/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ variables: {}, query: getIntrospectionQuery({ descriptions: false }), }),}) .then(result => result.json()) .then(({ data }) => { const minified = minifyIntrospectionQuery(getIntrospectedSchema(data)); fs.writeFileSync('./schema.json', JSON.stringify(minified)); });
The getIntrospectionSchema
doesn't only accept IntrospectionQuery
JSON data as inputs, but also
allows you to pass a JSON string, GraphQLSchema
, or GraphQL Schema SDL strings. It's a convenience
helper and not needed in the above example.
Integrating a schema
Once we have a schema that's already saved to a JSON file, we can load it and pass it to the
cacheExchange
's schema
option:
import schema from './schema.json';
const cache = cacheExchange({ schema });
It may be worth checking what your bundler or framework does when you import a JSON file. Typically
you can reduce the parsing time by making sure it's turned into a string and parsed using
JSON.parse