Skip to content

Commit

Permalink
docs: context directives (#2975)
Browse files Browse the repository at this point in the history
Docs for `@context` and `@fromContext` directives.

---------

Co-authored-by: Maria Elisabeth Schreiber <[email protected]>
  • Loading branch information
shorgi and Meschreiber authored May 30, 2024
1 parent b48e856 commit 10737e2
Show file tree
Hide file tree
Showing 3 changed files with 332 additions and 1 deletion.
184 changes: 184 additions & 0 deletions docs/source/entities-advanced.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -909,3 +909,187 @@ const resolvers = {
A basic implementation of the `fetchProductByID` function might make a database call each time it's called. If we need to resolve `Product.price` for `N` different products, this results in `N` database calls. These calls are made in addition to the call made by the Reviews subgraph to fetch the initial list of reviews (and the `id` of each product). This is where the "N+1" problem gets its name. If not prevented, this problem can cause performance problems or even enable denial-of-service attacks.

This problem is not limited to reference resolvers. In fact, it can occur with any resolver that fetches from a data store. To handle this problem, we strongly recommend using [the dataloader pattern](https://github.com/graphql/dataloader). Nearly every GraphQL server library provides a dataloader implementation, and you should use it in every resolver. This is true even for resolvers that aren't for entities and that don't return a list. These resolvers can still cause N+1 issues via [batched requests](/technotes/TN0021-graph-security/#batched-requests).

<MinVersion version="2.8">

## Using contexts to share data along type hierarchies

</MinVersion>

Use the `@context` and `@fromContext` directives to set and get a _context_ to share data between types 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.

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.

### 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.

```graphql title="Example: using @context and @fromContext"
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.

</Note>

### Using multiple contexts

As an example using multiple contexts, the subgraph below has three entities using two contexts and a nested child entity referencing fields using those contexts:

```graphql title="Example: using multiple contexts"
type Query {
a: A!
b: B!
c: C!
}

type A @key(fields: "id") @context(name: "context_1") @context(name: "context_2") {
id: ID!
field: String!
child: Child!
}

type B @key(fields: "id") @context(name: "context_1") @context(name: "context_2") {
id: ID!
field: String!
child: Child!
}

type C @key(fields: "id") @context(name: "context_1") {
id: ID!
anotherField: String!
child: Child!
}

type Child @key(fields: "id") {
id: ID!
prop1(
arg: String! @fromContext(field: "$context_1 { field }")
): Int!
prop2(
arg: String!
@fromContext(field: "$context_2 ... A { field } ... B { field } ... C { anotherField }")
): Int!
}
```

When the same contextual value is set in multiple placesas 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.

</Note>

### 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`:

```graphql
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](#using-one-context), imagine a subgraph responsible for the `User` and `Currency` types:

```graphql
scalar CurrencyCode

type 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:

```graphql
scalar CurrencyCode

type Currency @shareable {
id: ID!
isoCode: CurrencyCode!
}

type User @key(fields: "id") @context(name: "userContext") {
id: ID!

# This is a reference to the field resolved elsewhere
userCurrency: Currency! @external

# We add this field to our type here
lastTransaction: Transaction!
}

type Transaction @key(fields: "id") {
id: ID!
currency: Currency!
amount: Int!
amountInUserCurrency(
currencyCode: CurrencyCode
@fromContext(field: "$userContext { userCurrency { isoCode } }")
): Int!
}
```
63 changes: 63 additions & 0 deletions docs/source/federated-types/federated-directives.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -913,3 +913,66 @@ If different subgraphs use different versions of a directive's corresponding spe

</tbody>
</table>

## Saving and referencing data with contexts

<MinVersion version="2.8">

### `@context`

</MinVersion>

The `@context` directive defines a named context from which a field of the annotated type can be passed to a receiver of the context. The receiver must be a field annotated with the `@fromContext` directive.

```graphql
directive @context(name: String!) on OBJECT | INTERFACE | UNION;
```

A `@context` directive must be applied to an object, interface, or union type. A type can be annotated with one or more `@context` directives.

Each `@context` must be defined with a name, and each `@context` name can be applied to multiple places within a subgraph. For example:

```graphql
type A @key(fields: "id") @context(name: "userContext") {
id: ID!
prop: String!
}

type B @key(fields: "id") @context(name: "userContext") {
id: ID!
prop: String!
}

type U @key(fields: "id") {
id: ID!
field (arg: String @fromContext(field: "$userContext { prop }")): String!
}
```

<MinVersion version="2.8">

### `@fromContext`

</MinVersion>

The `@fromContext` directive sets the context from which to receive the value of the annotated field. The context must have been defined with the `@context` directive.

```graphql
scalar FieldValue;

directive @fromContext(field: FieldValue!) on ARGUMENT_DEFINITION;
```

A `@fromContext` directive must be used as an argument on a field. Its field value—the `FieldValue` scalar—must contain the name of a defined context and a selection of a field from the context's type.

The selection syntax for `@fromContext` used in its `FieldValue` is similar to GraphQL field selection syntax, with some additional rules:
- The first element must be the name of a context defined by `@context` and prefixed with `$` (for example, `$myContext`). This is the only context that can be referenced by the annotated field.
- The `@skip` and `@include` directives must not be used.
- The second element must be a selection set that resolves to a single field.
- Top-level type conditions must not overlap with one another, so that the field can be resolved to a single value.
- All fields referenced in the `FieldValue` must be expressed within the current subgraph. If the fields are referenced across multiple subgraphs, they must be annotated with [`@external`](../entities-advanced/#referencing-fields-across-subgraphs) .
- The argument must be nullable. Because validation is done at the subgraph level, the referenced field may become nullable when merging subgraphs, such as when the field is nullable in one subgraph but not in another.

When the same contextual value is set in multiple places, the `FieldValue` must resolve all types from each place into a single value that matches the parameter type.

For examples using `@context` and `@fromContext`, see [Using contexts to share data along type hierarchies](../entities-advanced/#using-contexts-to-share-data-along-type-hierarchies).
86 changes: 85 additions & 1 deletion docs/source/federation-versions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,90 @@ For a comprehensive changelog for Apollo Federation and its associated libraries

- If you maintain a [subgraph-compatible library](./building-supergraphs/supported-subgraphs/), consult this article to stay current with recently added directives. All of these directive definitions are also listed in the [subgraph specification](./subgraph-spec/#subgraph-schema-additions).

## v2.8

<hr/>

<CodeColumns cols="3">

<div>

First release

**May 2024**

</div>

<div>

Available in GraphOS?

**No**

</div>

<div>

Minimum router version

**1.48.0**

</div>

</CodeColumns>

<hr/>

#### Directive changes

<table>
<thead>
<tr>
<th style={{ minWidth: 200 }}>Topic</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td>

##### `@context`

</td>
<td>

Introduced. [Learn more](./federated-types/federated-directives/#context).

```graphql
directive @context(name: String!) on OBJECT | INTERFACE | UNION;
```

</td>
</tr>

<tr>
<td>

##### `@fromContext`

</td>
<td>

Introduced. [Learn more](./federated-types/federated-directives/#fromcontext).

```graphql
scalar FieldValue;

directive @fromContext(field: FieldValue!) on ARGUMENT_DEFINITION;
```

</td>
</tr>

</tbody>
</table>

## v2.7

<hr/>
Expand All @@ -35,7 +119,7 @@ For a comprehensive changelog for Apollo Federation and its associated libraries

First release

February 2024
**February 2024**

</div>

Expand Down

0 comments on commit 10737e2

Please sign in to comment.