join v0.2
for defining supergraphs which join multiple subgraphs
Status | Draft |
Version | 0.2 |
This document defines a number of directives join
for describing joins between subgraph types in a supergraph.
This specification provides machinery to:
1How to read this document
This document uses RFC 2119 guidance regarding normative terms: MUST / MUST NOT / REQUIRED / SHALL / SHALL NOT / SHOULD / SHOULD NOT / RECOMMENDED / MAY / OPTIONAL.
1.1What this document isn't
This document specifies only the structure and semantics of supergraphs. It’s expected that a supergraph will generally be the output of a compilation process which composes subgraphs. The mechanics of that process are not specified normatively here. Conforming implementations may choose any approach they like, so long as the result conforms to the requirements of this document.
2Example: Photo Library
This section is non‐normative.
We’ll refer to this example of a photo library throughout the document:
schema
@core(feature: "https://specs.apollo.dev/core/v1.0")
@core(feature: "https://specs.apollo.dev/join/v1.0") {
query: Query
}
directive @core(feature: String!) repeatable on SCHEMA
directive @join__owner(graph: join__Graph!) on OBJECT
directive @join__type(
graph: join__Graph!
key: String!
) repeatable on OBJECT | INTERFACE
directive @join__field(
graph: join__Graph
requires: String
provides: String
) on FIELD_DEFINITION
directive @join__graph(name: String!, url: String!) on ENUM_VALUE
enum join__Graph {
AUTH @join__graph(name: "auth", url: "https://auth.api.com")
ALBUMS @join__graph(name: "albums", url: "https://albums.api.com")
IMAGES @join__graph(name: "images", url: "https://images.api.com")
}
type Query {
me: User @join__field(graph: AUTH)
images: [Image] @join__field(graph: IMAGES)
}
type User
@join__owner(graph: AUTH)
@join__type(graph: AUTH, key: "id")
@join__type(graph: ALBUMS, key: "id") {
id: ID! @join__field(graph: AUTH)
name: String @join__field(graph: AUTH)
albums: [Album!] @join__field(graph: ALBUMS)
}
type Album
@join__owner(graph: ALBUMS)
@join__type(graph: ALBUMS, key: "id") {
id: ID!
user: User
photos: [Image!]
}
type Image
@join__owner(graph: IMAGES)
@join__type(graph: ALBUMS, key: "url")
@join__type(graph: IMAGES, key: "url") {
url: Url @join__field(graph: IMAGES)
type: MimeType @join__field(graph: IMAGES)
albums: [Album!] @join__field(graph: ALBUMS)
}
scalar Url
scalar MimeType
The meaning of the @join__*
directives in this example is explored in the Directives section.
The example represents one way to compose three input schemas, based on federated composition. These schemas are provided for purposes of illustration only. This spec places no normative requirements on composer input. It does not require that subgraphs use federated composition directives, and it does not place any requirements on how the composer builds a supergraph, except to say that the resulting schema must be a valid supergraph document.
The auth subgraph provides the User
type and Query.me
.
type User @key(fields: "id") {
id: ID!
name: String
}
type Query {
me: User
}
The images subgraph provides the Image
type and URL
scalar.
type Image @key(fields: "url") {
url: Url
type: MimeType
}
type Query {
images: [Image]
}
extend type User {
favorite: Image
}
scalar Url
scalar MimeType
The albums subgraph provides the Album
type and extends User
and Image
with album information.
type Album @key(fields: "id") {
id: ID!
user: User
photos: [Image!]
}
extend type Image {
albums: [Album!]
}
extend type User {
albums: [Album!]
favorite: Album
}
3Actors
Producers generate supergraphs. This spec places requirements on supergraph producers.
Consumers consume supergraphs. This spec places requirements on supergraph consumers.
Composers (or compilers) are producers which compose subgraphs into a supergraph. This document places no particular requirements on the composition algorithm, except that it must produce a valid supergraph.
Routers are consumers which serve a composed schema as a GraphQL endpoint. This definition is non‐normative.
- Graph routers differ from standard GraphQL endpoints in that they are not expected to resolve fields or communicate with (non‐GraphQL) backend services on their own. Instead, graph routers receive GraphQL requests and service them by performing additional GraphQL requests. This spec provides guidance for implementing routers, but does not require particular implementations of query separation or dispatch, nor does it attempt to normatively separate routers from other supergraph consumers.
- Routers expose an API schema to clients that is created by transforming the supergraph schema (for example, the Graph enum and the directives described in this spec are removed from the API schema). The API schema is used to validate client operations and may be exposed to clients via introspection.
Endpoints are running servers which can resolve GraphQL queries against a schema. In this version of the spec, endpoints must be URLs, typically http/https URLs.
Subgraphs are GraphQL schemas which are composed to form a supergraph. Subgraph names and metadata are declared within the special Graph enum.
This spec does not place any requirements on subgraph schemas. Generally, they may be of any shape. In particular, subgraph schemas do not need to be supergraphs themselves or to follow this spec in any way; neither is it an error for them to do so. Composers MAY place additional requirements on subgraph schemas to aid in composition; composers SHOULD document any such requirements.
4Overview
This section is non‐normative. It describes the motivation behind the directives defined by this specification.
A supergraph schema describes a GraphQL schema that can be served by a router. The router does not contain logic to resolve any of the schema’s fields; instead, the supergraph schema contains join directives which tell the router which subgraph endpoint can resolve each field, as well as other information needed in order to construct subgraph operations.
Each supergraph schema contains a list of its included subgraphs. The Graph enum represents this list with an enum value for each subgraph. Each enum value is annotated with a @graph directive telling the router what endpoint can be used to reach the subgraph, and giving the subgraph a human‐readable name that can be used for purposes such as query plan visualization and server logs.
To resolve a field, the router needs to know to which subgraphs it can delegate the field’s resolution. One explicit way to indicate this in a supergraph schema is by annotating the field with a @field directive specifying which subgraph should be used to resolve that field. (There are other ways of indicating which subgraphs can resolve a field which will be described later.)
In order for the router to send an operation that resolves a given field on a parent object to a subgraph, the operation needs to first resolve the parent object itself. There are several ways to accomplish this, described below. The examples below include abbreviated versions of the supergraph schemas which do not include the schema
definition, directive definitions, or the Graph definition. This specification does not require the subgraph operations to be the same as those described in these examples; this is just intended to broadly describe the meanings of the directives.
4.1Root fields
If a field appears at the root of the overall operation (query or mutation), then it can be placed at the root of the subgraph operation.
# Supergraph schema
type Query {
fieldA: String @join__field(graph: A)
fieldAlsoFromA: String @join__field(graph: A)
fieldB: String @join__field(graph: B)
}
# Operation
{ fieldA fieldAlsoFromA fieldB }
# Generated subgraph operations
## On A:
{ fieldA fieldAlsoFromA }
## On B:
{ fieldB }
4.2Fields on the same subgraph as the parent operation
If a field’s parent field will be resolved by an operation on the same subgraph, then it can be resolved as part of the same operation, by putting it in a nested selection set on the parent field’s subgraph operation. Note that this example contains @type directives on an object type; this will be described later.
# Supergraph schema
type Query {
fieldA: X @join__field(graph: A)
}
type X @join__type(graph: A, key: "nestedFieldA") {
nestedFieldA: String @join__field(graph: A)
}
# Operation
{ fieldA { nestedFieldA } }
# Generated subgraph operations
## On A:
{ fieldA { nestedFieldA }}
4.3Fields provided by the parent field
Sometimes, a subgraph G may be capable of resolving a field that is ordinarily resolved in a different subgraph if the field’s parent object was resolved in G. Consider an example where the Product.priceCents: Int!
field is usually resolved by the Products subgraph, which knows the priceCents
for every Product
in your system. In the Marketing subgraph, there is a Query.todaysPromotion: Product!
field. While the Marketing subgraph cannot determine the priceCents
of every product in your system, it does know the priceCents
of the promoted products, and so the Marketing subgraph can resolve operations like { todaysPromotion { priceCents } }
.
When this is the case, you can include a provides
argument in the @field listing these “pre‐calculated” fields. The router can now resolve these fields in the “providing” subgraph instead of in the subgraph that would usually be used to resolve those fields.
# Supergraph schema
type Query {
todaysPromotion: Product! @join__field(graph: MARKETING, provides: "priceCents")
randomProduct: Product! @join__field(graph: PRODUCTS)
}
type Product @join__owner(graph: PRODUCTS) @join__type(graph: PRODUCTS, key: "id") {
id: ID! @join__field(graph: PRODUCTS)
priceCents: Int! @join__field(graph: PRODUCTS)
}
# Operation showing that `priceCents` is typically resolved on PRODUCTS
{ randomProduct { priceCents } }
# Generated subgraph operations
## On PRODUCTS
{ randomProduct { priceCents } }
# Operation showing that `provides` allows `priceCents` to be resolved on MARKETING
{ todaysPromotion { priceCents } }
# Generated subgraph operations
## On MARKETING
{ todaysPromotion { priceCents } }
4.4Fields on value types
Some types have the property that all of their fields can be resolved by any subgraph that can resolve a field returning that type. These types are called value types. (Imagine a type type T { x: Int, y: String }
where every resolver for a field of type T
actually produces an object like {x: 1, y: "z"}
, and the resolvers for the two fields on T
just unpack the values already in the object.)
# Supergraph schema
type Query {
fieldA: X @join__field(graph: A)
fieldB: X @join__field(graph: B)
}
type X {
anywhere: String
}
# Operation
{ fieldA { anywhere } }
# Generated subgraph operations
## On A
{ fieldA { anywhere } }
# Operation
{ fieldB { anywhere } }
# Generated subgraph operations
## On B
{ fieldB { anywhere } }
5Enums
5.1Graph
enum Graph
Enumerate subgraphs.
Documents MUST define a Graph enum. Each enum value describes a subgraph. Each enum value MUST have a @graph directive applied to it.
enum join__Graph {
AUTH @join__graph(name: "auth", url: "https://auth.api.com")
ALBUMS @join__graph(name: "albums", url: "https://albums.api.com")
IMAGES @join__graph(name: "images", url: "https://images.api.com")
}
The Graph enum is used as input to the @type and @field directives.
6Directives
6.1@graph
directive @graph(name: String!, url: String!) on ENUM_VALUE
Declare subgraph metadata on Graph enum values.
enum join__Graph {
AUTH @join__graph(name: "auth", url: "https://auth.api.com")
ALBUMS @join__graph(name: "albums", url: "https://albums.api.com")
IMAGES @join__graph(name: "images", url: "https://images.api.com")
}
The @graph directive MUST be applied to each enum value on Graph, and nowhere else. Each application of @graph MUST have a distinct value for the name
argument; this name is an arbitrary non‐empty string that can be used as a human‐readable identifier which may be used for purposes such as query plan visualization and server logs. The url
argument is an endpoint that can resolve GraphQL queries for the subgraph.
6.2@type
directive @type(
graph: Graph!,
key: FieldSet,
extension: Boolean = false,
resolvable: Boolean = true
) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
Declares an entity key for a type on a subgraph.
When this directive is placed on a type T
, it means that subgraph graph
MUST be able to:
- Resolve selections on objects of the given type that contain the field set in
key
- Use
Query._entities
to resolve representations of objects containing__typename: "T"
and the fields from the field set inkey
type Image
@join__owner(graph: IMAGES)
@join__type(graph: ALBUMS, key: "url")
@join__type(graph: IMAGES, key: "url") {
url: Url @join__field(graph: IMAGES)
type: MimeType @join__field(graph: IMAGES)
albums: [Album!] @join__field(graph: ALBUMS)
}
6.3@field
directive @field(
graph: Graph,
requires: FieldSet,
provides: FieldSet,
type: String,
external: Boolean,
override: String,
usedOverridden: Boolean
) on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
Specify the graph that can resolve the field.
The field’s parent type MUST be annotated with a @type with the same value of graph
as this directive, unless the parent type is a root operation type.
type User
@join__owner(graph: AUTH)
@join__type(graph: AUTH, key: "id")
@join__type(graph: ALBUMS, key: "id") {
id: ID! @join__field(graph: AUTH)
name: String @join__field(graph: AUTH)
albums: [Album!] @join__field(graph: ALBUMS)
}
type Album
@join__owner(graph: ALBUMS)
@join__type(graph: ALBUMS, key: "id") {
id: ID!
user: User
photos: [Image!]
}
type Image
@join__owner(graph: IMAGES)
@join__type(graph: ALBUMS, key: "url")
@join__type(graph: IMAGES, key: "url") {
url: Url @join__field(graph: IMAGES)
type: MimeType @join__field(graph: IMAGES)
albums: [Album!] @join__field(graph: ALBUMS)
}
Every field on a root operation type MUST be annotated with @field.
type Query {
me: User @join__field(graph: AUTH)
images: [Image] @join__field(graph: IMAGES)
}
The provides
argument specifies fields that can be resolved in operations run on subgraph graph
as a nested selection under this field, even if they ordinarily can only be resolved on other subgraphs.