Use contexts to share data
Share data along type hierarchies without overloading @keys
The @context
and @fromContext
directives are Enterprise features of the Apollo Router and require an organization with a GraphOS Enterprise plan. If your organization doesn't have an Enterprise plan, you can test it out by signing up for a free Enterprise trial.
In an entity, a nested object may have a dependency on an ancestor type and thus need access to that ancestor's field(s).
To enable a descendant to access an ancestor's field, you could add it as a @key
field to every entity along the chain of nested types, but that can become problematic. Deeply nested types can change the @key
fields of many entities. The added field may be irrelevant to the entities it's added to. Most importantly, overloading @key
fields often breaks the separation of concerns between different subgraphs.
The @context
and @fromContext
directives enable a subgraph to share fields without overloading @key
fields. You can use these directives to define one or more contexts in a subgraph. Contexts provide a way for a subgraph to share data between types of a nested-type hierarchy without overloading entity keys with extraneous fields. Contexts also preserve the separation of concerns between different subgraphs.
Using one context
As an example of a single context, the subgraph below tracks the last financial transaction made per user. The Transaction
type is a child of the User
type. Each transaction depends on the associated user's currency to calculate the transaction amount in the user's currency. That dependency—the currencyCode
argument of a Transaction
depending on the userCurrency { isoCode }
of a User
— is defined with a context. The @context
directive on User
sets the context, and @fromContext
on currencyCode
gets the contextual data.
scalar CurrencyCode;type Currency {id: ID!isoCode: CurrencyCode!}type User @key(fields: "id") @context(name: "userContext") {id: ID!lastTransaction: Transaction!userCurrency: Currency!}type Transaction @key(fields: "id") {id: ID!currency: Currency!amount: Int!amountInUserCurrency(currencyCode: CurrencyCode@fromContext(field: "$userContext { userCurrency { isoCode } }")): Int!}
ⓘ NOTE
An argument of @fromContext
doesn't appear in the API schema. Instead, it's populated automatically by the router.
In the example above, the argument currencyCode: CurrencyCode!
wouldn't appear in the API schema.
Using type conditions in @fromContext
In this example, note how the @fromContext
directive uses a series of type condition to select the desired field when accessing Child.prop1
. A type condition is not required if all possible contexts have a field present as is the case for Child.prop2
.
type Query {a: A!b: B!c: C!}type A @key(fields: "id") @context(name: "context_1"){id: ID!field: String!someField: String!child: Child!}type B @key(fields: "id") @context(name: "context_1"){id: ID!field: String!someField: String!child: Child!}type C @key(fields: "id") @context(name: "context_1") {id: ID!field: String!someOtherField: String!child: Child!}type Child @key(fields: "id") {id: ID!prop1(arg: String!@fromContext(field: "$context_1 ... on A { someField } ... on B { someField } ... on C { someOtherField }")): Int!prop2(arg: String!@fromContext(field: "$context_1 { field }")): Int!}
When the same contextual value is set in multiple places—as in the example with the Child.prop1
and Child.prop2
args
—the FieldValue
must resolve all types from each place into a single value that matches the parameter type.
ⓘ NOTE
Federation doesn't guarantee which context will be used if a field is reachable via multiple contexts.
Disambiguating contexts
When multiple ancestor entities in the type hierarchy could fulfill a set context, the nearest ancestor is chosen. For example, if both the parent and grandparent of a type can provide the value of a context, the parent is chosen because it's the closer ancestor.
In the following example, given nested types A
, B
, and C
, with C
referencing a context that either A
or B
could provide, C
uses the value from B
because it's a closer ancestor to C
than A
:
type Query {a: A!}type A @key(fields: "id") @context(name: "context_1") {id: ID!field: String!b: B!}type B @key(fields: "id") @context(name: "context_1") {id: ID!field: String!c: C!}type C @key(fields: "id") {id: ID!prop(arg: String! @fromContext(field: "$context_1 { field }")): Int!}
In a more complex graph, a field could be reachable via multiple paths, and a different field could be used to resolve the prop
depending on which path was used.
Referencing fields across subgraphs
The definition of context scopes can only exist in one subgraph schema. The @fromContext
directive can't reference a @context
defined in another subgraph. However, you can use contexts to share data across subgraphs using the @external
reference.
Reusing the Transaction
example, imagine a subgraph responsible for the User
and Currency
types:
scalar CurrencyCodetype Currency @shareable {id: ID!isoCode: CurrencyCode!}type User @key(fields: "id") {id: ID!userCurrency: Currency!}
If you want to reference those fields from another subgraph, you can use the @external
directive to pass data across subgraph boundaries:
scalar CurrencyCodetype Currency @shareable {id: ID!isoCode: CurrencyCode!}type User @key(fields: "id") @context(name: "userContext") {id: ID!# This is a reference to the field resolved elsewhereuserCurrency: Currency! @external# We add this field to our type herelastTransaction: Transaction!}type Transaction @key(fields: "id") {id: ID!currency: Currency!amount: Int!amountInUserCurrency(currencyCode: CurrencyCode@fromContext(field: "$userContext { userCurrency { isoCode } }")): Int!}