Keys from the schema
GraphQL only For plain HTTP, name keys in the Surrogate-Key header on your origin's response.
Your schema already describes which fields identify an object. The @surrogateKey directive lets
you state the rest there too: which fields form a key, which fixed keys a type or field carries,
and where the automatic key rules should stay out. GraphPilot reads it when it builds a
deployment, so the keys change with the schema and there is no second place to keep in sync.
If your schema already marks keys with another directive, such as @key from Apollo
Federation,
you can configure GraphPilot to read that one instead.
What purges together
Section titled “What purges together”A surrogate key decides which cached responses one purge removes together. Start from that question, and the place to write the directive follows:
| A purge should reach | Declare | Key |
|---|---|---|
| One object, wherever it appears | Nothing, or a key field: id: ID! @surrogateKey | Product:id:p01 |
| Objects that share values | @surrogateKey(of: "category.id") on the type | Product:category. |
| Every implementor or member at once | The key on the interface or union | Medium:genre:fiction |
| What one field of a parent returns | @surrogateKey(of: "id") on the field | Category:featured. |
| A group you name by hand | @surrogateKey(static: "catalogue") | catalogue |
Each directive defines exactly one key. That key can be based on a single field or on a
combination of fields. To give the same type more than one key, repeat the directive; it is
repeatable.
The first four are built from values in the response, so they are entity keys: a mutation whose
response carries the same key purges them automatically. A static
key is only ever purged when you name it.
One object
Section titled “One object”Automatic key fields
Section titled “Automatic key fields”A type with an id: ID! field is keyed Type:id:<id> without anything written in the schema,
by the two automatic rules for key scalars and key
names. Where one of them picks a field that identifies nothing, switch it off with
ignore.
A field as a key field
Section titled “A field as a key field”On a field that returns a scalar or an enum, the directive makes that field a key field of the
enclosing type. static is allowed there and attaches a fixed key that is included only when the
field is queried; of has no effect on such a field and must not be used.
type Product { id: ID! @surrogateKey # → Product:id:{id} name: String brand: String! @surrogateKey # → Product:brand:{brand}}Every field of a scalar or enum type
Section titled “Every field of a scalar or enum type”Annotating a scalar makes every field of that type a key field of the object it sits on. It does the same as listing the scalar in the configuration, but keeps it in your schema. An enum type works the same way.
scalar SKU @surrogateKey # Every field returning this type becomes a key field
type Product { sku: SKU! # → Product:sku:{sku} name: String category: Category!}
type Variant { parentSku: SKU! # → Variant:parentSku:{parentSku} color: String}enum StockStatus @surrogateKey { # Every field returning this type becomes a key field IN_STOCK SOLD_OUT}
type Product { stockStatus: StockStatus! # → Product:stockStatus:{stockStatus} name: String}
type Inventory { status: StockStatus! # → Inventory:status:{status} available: Int}Objects that share values
Section titled “Objects that share values”Composite keys
Section titled “Composite keys”A key built from several fields is a composite key: one key per combination of values. A type can carry single-field keys and composite keys side by side, one directive each:
# Single-field keystype Product { id: String! @surrogateKey # → Product:id:{id} sku: String! @surrogateKey # → Product:sku:{sku}}
# Composite keys (multiple fields)type PriceList @surrogateKey(of: "region currency") # → PriceList:currency:{currency}:region:{region} @surrogateKey(of: "channel region") # → PriceList:channel:{channel}:region:{region}{ region: String! currency: String! channel: String!}The fields of a composite key are sorted alphabetically by name, so region currency and
currency region produce the same key. When one of them is a list, the key multiplies out into
one key per combination: @surrogateKey(of: "brand tags") with brand: "acme" and
tags: ["new", "bestseller"] yields Product:brand:acme:tags:new and
Product:brand:acme:tags:bestseller.
Keys through a relation
Section titled “Keys through a relation”On an object type the directive needs at least one argument, of, static or both. A bare
@surrogateKey on an object does not know how to build a key, and none is created. of can
reach into a related type; selection set expansion
fetches those fields when the client did not select them.
type Product @surrogateKey( of: "brand category_id" # → Product:brand:{brand}:category_id:{category_id} ){ brand: String! category_id: ID! # → Product:category_id:{category_id}, automatic: an ID field is a key field}This key reaches products that share both brand and category, never products that share only one
of them. category_id is an ID, so the automatic rule for key scalars
also keys Product:category_id:{category_id} on its own; a foreign key of another scalar type gets
no such key. For a purge by brand alone or by category alone, declare those keys as well:
type Product @surrogateKey( of: "brand category_id" # → Product:brand:{brand}:category_id:{category_id} ){ brand: String! @surrogateKey # → Product:brand:{brand} category_id: ID! @surrogateKey # → Product:category_id:{category_id}}| Use case | Surrogate key |
|---|---|
| Products of a specific brand and category | Product:brand:{brand}:category_ |
| Products of a specific category | Product:category_ |
| Products of a specific brand | Product:brand:{brand} |
These examples use a foreign key field, category_id. Traversing the relation instead
(category.id) makes it clear that the key follows the relationship rather than a raw identifier,
and it needs no extra field on the type:
type Product @surrogateKey( of: "brand category.id" # → Product:brand:{brand}:category.id:{category.id} ){ brand: String! category: Category!}
type Category { id: String! name: String}The same with separate keys for the brand and the category:
type Product @surrogateKey( of: "brand category.id" # → Product:brand:{brand}:category.id:{category.id} ){ brand: String! @surrogateKey # → Product:brand:{brand} category: Category!}
type Category { id: String! @surrogateKey # → Category:id:{id} name: String}Every implementor or member at once
Section titled “Every implementor or member at once”A product catalog rarely has an interface or a union, so this section, and the cases further down that read a field returning one, use a media library instead: books, magazines and movies.
Interface
Section titled “Interface”On an interface the directive works as on an object type, and every implementor inherits the
keys, so you declare a key once for all of them. An implementor’s own directives add to the
inherited keys, and static on an interface gives all implementors a shared key.
An inherited key is named after the interface, with the implementor’s own name beside it:
Medium:genre:fiction and Book:genre:fiction for a book. Both are attached wherever the query
reaches the implementor: through a field of the interface type (media: [Medium!]), through a
field that returns the implementor itself (book: Book), or further down a selection. A purge by
either key therefore reaches every cached response that shows the object, whichever field the
query used.
Under a field of the interface type, a query that names no implementor in a fragment
({ media { title } }) gets the interface’s keys and their implementor twins. The keys an
implementor declares itself, or inherits from another interface or a union, are added where the
query names it: { media { ... on Book { title } } }.
The automatic keys are not inherited. An interface with an id: ID!
field is keyed Node:id:<id> where it is selected, and every implementor declares the same field
and carries User:id:<id> under its own name, so a User reached directly carries no Node:id
key.
interface Medium @surrogateKey( of: "author.id genre" # → Medium:author.id:{author.id}:genre:{genre} ){ genre: String! author: Author!}
# The keys below apply to a Book or Magazine wherever the query reaches it: through a field# of type Medium, or through a field returning Book or Magazine itself.type Book implements Medium { # → Book:author.id:{author.id}:genre:{genre} genre: String! author: Author!}
type Magazine implements Medium { # → Magazine:author.id:{author.id}:genre:{genre} genre: String! author: Author!}
type Author { id: String! name: String}Keys on the interface’s fields are inherited too, which gives a purge by genre, by author, or by both, across every implementor and on the interface itself:
interface Medium @surrogateKey( of: "author.id genre" # → Medium:author.id:{author.id}:genre:{genre} ){ genre: String! @surrogateKey # → Medium:genre:{genre} author: Author! @surrogateKey(of: "id") # → Medium:author.id:{author.id}}
# The keys below apply to a Book or Magazine wherever the query reaches it: through a field# of type Medium, or through a field returning Book or Magazine itself.type Book implements Medium { # → Book:author.id:{author.id}:genre:{genre} genre: String! # → Book:genre:{genre} author: Author! # → Book:author.id:{author.id}}
type Magazine implements Medium { # → Magazine:author.id:{author.id}:genre:{genre} genre: String! # → Magazine:genre:{genre} author: Author! # → Magazine:author.id:{author.id}}
type Author { id: String! name: String}| Use case | Surrogate key |
|---|---|
| Media of a specific author and genre | Medium:author. |
| Media of a specific author | Medium:author. |
| Media of a specific genre | Medium:genre:{genre} |
| Books of a specific author and genre | Book:author. |
| Books of a specific author | Book:author. |
| Books of a specific genre | Book:genre:{genre} |
| Magazines of a specific author and genre | Magazine:author. |
| Magazines of a specific author | Magazine:author. |
| Magazines of a specific genre | Magazine:genre:{genre} |
Through a field returning the implementor the field keys apply as well:
Medium.author @surrogateKey(of: "id") keys book { author } with Medium:author.id:{author.id}
and Book:author.id:{author.id}, exactly as it keys media { author }.
A type that also defines its own @surrogateKey gets both the inherited and its local keys. The
same holds for a union member.
On a union the directive is a shared contract across the members, with of checked per member.
Every member that has the fields the key names gets the key, each resolving to a scalar, an enum,
or a list of either; a member that lacks them gets none. The keys are named after the union, with
the member’s own name beside them, and they are attached wherever the query reaches the member.
Under a field of the union type that is the members the query names in a fragment:
{ contents { __typename } } carries none of them. A field that returns a member type itself
(book: Book) carries the union’s keys as well as the member’s own.
union Content @surrogateKey(of: "author.id genre") # → Content:author.id:{author.id}:genre:{genre}= | Book | Magazine | Movie
# The keys below apply to a member reached through a field of type Content and# named in a fragment of the query, or through a field returning the member itself.type Book { # → Book:author.id:{author.id}:genre:{genre} genre: String! author: Author!}
type Magazine { # → Magazine:author.id:{author.id}:genre:{genre} genre: String! author: Author!}
# No key is generated for this type# because the path `author.id` does not exist on `Movie`type Movie { genre: String! director: Author!}
type Author { id: String! name: String}A union has no fields of its own, so each further dimension, such as the director, is another directive with its own selection:
union Content @surrogateKey(of: "author.id genre") # → Content:author.id:{author.id}:genre:{genre} @surrogateKey(of: "director.id genre") # → Content:director.id:{director.id}:genre:{genre}= | Book | Magazine | Movie
# The keys below apply to a member reached through a field of type Content and# named in a fragment of the query, or through a field returning the member itself.type Book { # → Book:author.id:{author.id}:genre:{genre} genre: String! author: Author!}
type Magazine { # → Magazine:author.id:{author.id}:genre:{genre} genre: String! author: Author!}
type Movie { # → Movie:director.id:{director.id}:genre:{genre} genre: String! director: Author!}
type Author { id: String! name: String}One directive per selection gives a purge by author, by director, by genre, or a combination, across every member and on the union itself:
union Content @surrogateKey(of: "author.id genre") # → Content:author.id:{author.id}:genre:{genre} @surrogateKey(of: "author.id") # → Content:author.id:{author.id} @surrogateKey(of: "director.id genre") # → Content:director.id:{director.id}:genre:{genre} @surrogateKey(of: "director.id") # → Content:director.id:{director.id} @surrogateKey(of: "genre") # → Content:genre:{genre}= | Book | Magazine | Movie
# The keys below apply to a member reached through a field of type Content and# named in a fragment of the query, or through a field returning the member itself.type Book { # → Book:author.id:{author.id}:genre:{genre} genre: String! # → Book:genre:{genre} author: Author! # → Book:author.id:{author.id}}
type Magazine { # → Magazine:author.id:{author.id}:genre:{genre} genre: String! # → Magazine:genre:{genre} author: Author! # → Magazine:author.id:{author.id}}
type Movie { # → Movie:director.id:{director.id}:genre:{genre} genre: String! # → Movie:genre:{genre} director: Author! # → Movie:director.id:{director.id}}
type Author { id: String! name: String}| Use case | Surrogate key |
|---|---|
| Contents of a specific author and genre | Content:author. |
| Contents of a specific author | Content:author. |
| Contents of a specific director and genre | Content:director. |
| Contents of a specific director | Content:director. |
| Contents of a specific genre | Content:genre:{genre} |
| Books of a specific author and genre | Book:author. |
| Books of a specific author | Book:author. |
| Books of a specific genre | Book:genre:{genre} |
| Magazines of a specific author and genre | Magazine:author. |
| Magazines of a specific author | Magazine:author. |
| Magazines of a specific genre | Magazine:genre:{genre} |
| Movies of a specific director and genre | Movie:director. |
| Movies of a specific genre | Movie:genre:{genre} |
| Movies of a specific director | Movie:director. |
If of resolves for no member at all, the deployment build reports one schema error naming the
union.
Objects reached through a field
Section titled “Objects reached through a field”A key on a field that returns an object, an interface or a union names what that field returns,
from the parent’s side: @surrogateKey(of: "id") on Category.featured puts
Category:featured.id:p01 on every response that selected a category’s featured product p01. The field name is part of the key, so keys
from two fields of the same type never reach each other’s responses. Such a field needs of,
static or both; static attaches a fixed key that is included only when the field is queried.
type Category { id: ID! name: String featured: Product @surrogateKey(of: "id") # → Category:featured.id:{featured.id}}
type Product { id: ID! name: String}A composite key on such a field combines several subfields of the related type:
type Library { books: [Book!] @surrogateKey(of: "author.id genre") # → Library:books.author.id:{books.author.id}:books.genre:{books.genre}}
type Book { genre: String! author: Author!}
type Author { id: ID! name: String}On a field that returns an interface or a union, of is checked per member or implementor, as on
a union. Where members expose different leaf fields, repeat the directive with a
selection for each:
type Library { media: [Medium!] @surrogateKey(of: "author.id genre") # → Library:media.author.id:{media.author.id}:media.genre:{media.genre} contents: [Content!] @surrogateKey(of: "author.id genre") # → Library:contents.author.id:{contents.author.id}:contents.genre:{contents.genre} @surrogateKey(of: "director.id genre") # → Library:contents.director.id:{contents.director.id}:contents.genre:{contents.genre}}
union Content = Book | Magazine | Movie
interface Medium { genre: String! author: Author!}
type Book implements Medium { genre: String! author: Author!}
type Magazine implements Medium { genre: String! author: Author!}
type Movie { genre: String! @surrogateKey # → Movie:genre:{genre} director: Author!}
type Author { id: String! name: String}| Use case | Surrogate key |
|---|---|
| Library media of a specific author and genre | Library:media. |
| Library contents of a specific author and genre | Library:contents. |
| Library contents of a specific director and genre | Library:contents. |
| Movies of a specific genre | Movie:genre:{genre} |
A key on a field belongs to the enclosing type only. It gives no key to the field’s return types
(Book, Magazine, Movie). A key a member declares itself, such as Movie.genre, applies to
that member alongside the field’s keys, and to no other member.
A group you purge by hand
Section titled “A group you purge by hand”The static argument defines a fixed key. Use it to give a group of responses a shared handle you
can purge by hand, without introducing a new type. Whitespace separates keys, so you can
declare several keys on one directive or repeat the directive:
type Product @surrogateKey(static: "catalogue") { # → catalogue name: String!}type Category @surrogateKey(static: "catalogue") { # → catalogue name: String!}
# Define 3 static keys on the same type:type Promotion @surrogateKey(static: "catalogue") # → catalogue @surrogateKey(static: "promo banner"){ # → promo, banner (whitespace separates keys) title: String!}So static: "promo banner" declares two separate keys, promo and banner; there is
no way to put a space inside a single static key. Each key is then folded into
printable ASCII. Often a union or an interface is the more natural
handle.
On a scalar or enum
Section titled “On a scalar or enum”A static key on a leaf type attaches to any response that selected a field returning that type,
wherever the field sits.
scalar Money @surrogateKey(static: "ledger")
type Invoice { id: ID! total: Money!}A query for invoice { total } carries ledger; a query for invoice { id } does not.
On an OBJECT, INTERFACE or UNION, by contrast, static and of on the same directive
contribute independently. With the directive applied twice as above, a selection of
Profile.points carries loyalty and Profile:points:<value>.
On a union or interface member
Section titled “On a union or interface member”A static key on the union itself is added whenever the union is selected, and whenever a field
returning one of its members is. A static key on one of its members is added whenever the
query names that member in a fragment, or selects a field returning the member.
union Content @surrogateKey(static: "all-content") = NewsArticle | BlogArticletype BlogArticle @surrogateKey(static: "blogroll") { title: String! }A query naming both members carries all-content and blogroll, whether or not a blog article
came back. A query naming only ... on NewsArticle carries all-content and not blogroll,
whatever the response holds. The same rule applies to an interface and its implementors.
How values become keys
Section titled “How values become keys”How lists, null values and aliases turn into key values, and which directives the deployment
build refuses, is on how keys are built.
Switching the automatic rules off
Section titled “Switching the automatic rules off”The two automatic rules can pick a field that merely shares a key name or
type, and produce a key that identifies nothing: a key field holding rendered text, say. Such a
key still takes room in the key budget and
no purge will ever name it. ignore: true switches both rules off where the directive sits:
# No automatic key for any field of this typetype RenderedSnippet @surrogateKey(ignore: true) { id: ID! key: String}
# Product keeps Product:id:<id>, but not Product:key:<key>type Product { id: ID! key: String @surrogateKey(ignore: true)}An exclusion only switches off the automatic rules; it never removes a key you wrote. Where it
applies and how it meets the configuration’s ignored_types and ignored_fields is on the
directive reference.
Reading another directive
Section titled “Reading another directive”If your schema already marks keys with another directive, such as Apollo Federation’s @key,
GraphPilot can read that one instead:
[[surrogate_key.directives]]directive_name = "key" # the directive as it appears in your schema, without the @of_arg = "fields" # its argument that lists the key fieldsAn entry replaces the built-in @surrogateKey rather than adding to it. Every key of an entry,
including a marker directive with no arguments, is in the configuration
reference. The exact definition and
arguments of @surrogateKey are on the directive reference.