We are using Apollo 2 in production and are very happy with it. Thanks for developing it! :) Now that Apollo 3 is out, we tried to upgrade and ran into some issues with caching.
Apollo 3 seems to believe that it is practical to manually specify merge strategies for every type and/or field (hundreds or thousands of lines of configuration) and also that this manual configuration cannot be checked at build time or start time, but instead fail eventually at runtime. To me, this sounds like an unacceptable combination. Given this, I set about writing code to inspect our schema and generate type policies. However, I ran into issues with Apollo not exposing sufficient information to build these policies.
I believe that a simple and general normalization strategy is sufficient for our use and for the vast majority of non-pathologic schemas that make the following assumptions:
- Most objects have
id
s that allow normalization - Objects that do not have
id
s are considered to be part of the parent object - Root objects are singletons and therefore do not need
id
s to be normalized
The algorithm is as follows:
- If the object has an
id
(or, as Apollo calls it,keyFields
), mergeincoming
(new object) intoexisting
(old object)- The merge function must treat the same fields with different arguments as different fields
- The merge function must treat the same fields with the same arguments but with different aliases as the same fields
- If the object lacks an
id
field, give it a syntheticid
and then go to step1
- For root objects, the synthetic
id
is the same as__typename
since they are singletons - For non-root objects, the synthetic
id
is derived from the parent object'sid
as well as the field name and arguments that returned the child object
- For root objects, the synthetic
- Replace
existing
(old field) withincoming
(new field)- Except when explicitly configured with a merging strategy to support pagination (Limit and Offset, Relay Connection (Cursor), etc.)
As an example, let's examine the following schema and query:
type Query {
# The current user
viewer: Viewer!
# Given a search string, find the associated `Location`
geocode(search: String!): Location
# A paginated list of all `Place`s in the system
places(limit: Int!, offset: Int!): [Place!]!
}
type Viewer {
name: String!
favoritePlace: Place
}
type Place {
id: ID!
name: String!
location: Location!
}
type Location {
latitude: Float!
longitude: Float!
}
# Query is a root type, so it is given a synthetic `id` of `"Query"`.
query ViewerAndPlaces {
# `Viewer` lacks `id`, so it is given a synthetic `id` based on the path from
# the nearest parent with an `id`. In this case the `id` is `"Query.viewer"`.
viewer {
name
# `Place` has an `id` field, so it doesn't need a synthetic `id`.
# Assuming `Place.id` is `"123"`, the global `id` could be `"Place:123"`
favoritePlace {
id
name
# `Location` lacks an `id` field, so it is given a synethic `id`.
# Assuming `Place.id` is `"123"`, the synthetic `id` for this `Location`
# could be `"Place:123.location"`.
location {
latitude
longitude
}
}
}
# `geocode` returns a `Location` and `Location` lacks `id`, so the synthetic
# `id` for this `Location` could be `"Query.geocode(search:'NYC')"`.
geocode(search: "NYC") {
latitude
longitude
}
# Paginated fields require special configuration but that can be done
# automatically by introspecting the schema and finding fields that
# take `limit` and `offset` arguments or return `Connection` types.
places(limit: 10, offset: 0) {
id
name
location {
latitude
longitude
}
}
}
Can Apollo 3 support this general normalization strategy? Apollo 2 seems to behave roughly like the algorithm outlined above. Any suggestions for making Apollo 3 behave more like Apollo 2 without manual and error-prone configuration of hundreds or thousands of types and fields?