Skip to content

Commit

Permalink
Support Federation Spec 2.7 (#3420)
Browse files Browse the repository at this point in the history
* added directives for apollo federation 2.6

* lint

* lint

* changed policies to policy to match spec

* documentation and release fine

* document what fed spec version is supported

* sourcery-ai feedback

* minor version

* pr feedback

* update federation-compatibility test version

* Update settings for apollo federation tests

* PR feedback: removed Federation__Policy and Federation__Scope to fix tests

* added override lable to bring in line with fed spec 2.7

---------

Co-authored-by: Patrick Arminio <patrick.arminio@gmail.com>
  • Loading branch information
TygerTaco and patrick91 committed Mar 27, 2024
1 parent ec4cf15 commit ac20ea8
Show file tree
Hide file tree
Showing 23 changed files with 766 additions and 105 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/federation-compatibility.yml
Expand Up @@ -28,20 +28,20 @@ jobs:
- uses: actions/setup-python@v4
id: setup-python
with:
python-version: "3.10"
python-version: "3.12"
cache: "poetry"
- run: poetry env use python3.10
- run: poetry env use python3.12
- run: poetry install

- name: export schema
run: poetry run strawberry export-schema schema:schema > schema.graphql
working-directory: federation-compatibility

- uses: apollographql/federation-subgraph-compatibility@v1
- uses: apollographql/federation-subgraph-compatibility@v2
with:
compose: 'federation-compatibility/docker-compose.yml'
schema: 'federation-compatibility/schema.graphql'
port: 4001
token: ${{ secrets.BOT_TOKEN }}
failOnWarning: true
failOnWarning: false
failOnRequired: true
19 changes: 19 additions & 0 deletions RELEASE.md
@@ -0,0 +1,19 @@
Release type: minor

This release adds support for Apollo Federation v2.7 which includes the `@authenticated`, `@requiresScopes`, `@policy` directives, as well as the `label` argument for `@override`.
As usual, we have first class support for them in the `strawberry.federation` namespace, here's an example:

```python
from strawberry.federation.schema_directives import Override


@strawberry.federation.type(
authenticated=True,
policy=[["client", "poweruser"], ["admin"]],
requires_scopes=[["client", "poweruser"], ["admin"]],
)
class Product:
upc: str = strawberry.federation.field(
override=Override(override_from="mySubGraph", label="percent(1)")
)
```
8 changes: 6 additions & 2 deletions docs/guides/federation.md
Expand Up @@ -326,7 +326,8 @@ Strawberry and Federation. The repo is available here:
## Federated schema directives

Strawberry provides implementations for
[Apollo federation-specific GraphQL directives](https://www.apollographql.com/docs/federation/federated-types/federated-directives/).
[Apollo federation-specific GraphQL directives](https://www.apollographql.com/docs/federation/federated-types/federated-directives/)
up to federation spec v2.7.

Some of these directives may not be necessary to directly include in your code,
and are accessed through other means.
Expand All @@ -348,6 +349,9 @@ Other directives you may need to specifically include when relevant.
- `@requires`
- `@shareable`
- `@tag`
- `@authenticated`
- `@requiresScopes`
- `@policy`

For example, adding the following directives:

Expand All @@ -372,7 +376,7 @@ Will result in the following GraphQL schema:
```graphql
schema
@link(
url: "https://specs.apollo.dev/federation/v2.3"
url: "https://specs.apollo.dev/federation/v2.7"
import: ["@key", "@inaccessible", "@shareable", "@tag"]
) {
query: Query
Expand Down
41 changes: 37 additions & 4 deletions strawberry/federation/enum.py
@@ -1,6 +1,15 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Any, Callable, Iterable, Optional, Union, overload
from typing import (
TYPE_CHECKING,
Any,
Callable,
Iterable,
List,
Optional,
Union,
overload,
)

from strawberry.enum import _process_enum
from strawberry.enum import enum_value as base_enum_value
Expand Down Expand Up @@ -36,7 +45,10 @@ def enum(
name: Optional[str] = None,
description: Optional[str] = None,
directives: Iterable[object] = (),
authenticated: bool = False,
inaccessible: bool = False,
policy: Optional[List[List[str]]] = None,
requires_scopes: Optional[List[List[str]]] = None,
tags: Optional[Iterable[str]] = (),
) -> EnumType:
...
Expand All @@ -49,7 +61,10 @@ def enum(
name: Optional[str] = None,
description: Optional[str] = None,
directives: Iterable[object] = (),
authenticated: bool = False,
inaccessible: bool = False,
policy: Optional[List[List[str]]] = None,
requires_scopes: Optional[List[List[str]]] = None,
tags: Optional[Iterable[str]] = (),
) -> Callable[[EnumType], EnumType]:
...
Expand All @@ -61,22 +76,40 @@ def enum(
name=None,
description=None,
directives=(),
inaccessible=False,
tags=(),
authenticated: bool = False,
inaccessible: bool = False,
policy: Optional[List[List[str]]] = None,
requires_scopes: Optional[List[List[str]]] = None,
tags: Optional[Iterable[str]] = (),
) -> Union[EnumType, Callable[[EnumType], EnumType]]:
"""Registers the enum in the GraphQL type system.
If name is passed, the name of the GraphQL type will be
the value passed of name instead of the Enum class name.
"""

from strawberry.federation.schema_directives import Inaccessible, Tag
from strawberry.federation.schema_directives import (
Authenticated,
Inaccessible,
Policy,
RequiresScopes,
Tag,
)

directives = list(directives)

if authenticated:
directives.append(Authenticated())

if inaccessible:
directives.append(Inaccessible())

if policy:
directives.append(Policy(policies=policy))

if requires_scopes:
directives.append(RequiresScopes(scopes=requires_scopes))

if tags:
directives.extend(Tag(name=tag) for tag in tags)

Expand Down
77 changes: 53 additions & 24 deletions strawberry/federation/field.py
Expand Up @@ -25,6 +25,7 @@
from strawberry.field import _RESOLVER_TYPE, StrawberryField
from strawberry.permission import BasePermission

from .schema_directives import Override

T = TypeVar("T")

Expand All @@ -36,13 +37,16 @@ def field(
name: Optional[str] = None,
is_subscription: bool = False,
description: Optional[str] = None,
authenticated: bool = False,
external: bool = False,
inaccessible: bool = False,
policy: Optional[List[List[str]]] = None,
provides: Optional[List[str]] = None,
override: Optional[Union[Override, str]] = None,
requires: Optional[List[str]] = None,
external: bool = False,
shareable: bool = False,
requires_scopes: Optional[List[List[str]]] = None,
tags: Optional[Iterable[str]] = (),
override: Optional[str] = None,
inaccessible: bool = False,
shareable: bool = False,
init: Literal[False] = False,
permission_classes: Optional[List[Type[BasePermission]]] = None,
deprecation_reason: Optional[str] = None,
Expand All @@ -61,13 +65,16 @@ def field(
name: Optional[str] = None,
is_subscription: bool = False,
description: Optional[str] = None,
authenticated: bool = False,
external: bool = False,
inaccessible: bool = False,
policy: Optional[List[List[str]]] = None,
provides: Optional[List[str]] = None,
override: Optional[Union[Override, str]] = None,
requires: Optional[List[str]] = None,
external: bool = False,
shareable: bool = False,
requires_scopes: Optional[List[List[str]]] = None,
tags: Optional[Iterable[str]] = (),
override: Optional[str] = None,
inaccessible: bool = False,
shareable: bool = False,
init: Literal[True] = True,
permission_classes: Optional[List[Type[BasePermission]]] = None,
deprecation_reason: Optional[str] = None,
Expand All @@ -87,13 +94,16 @@ def field(
name: Optional[str] = None,
is_subscription: bool = False,
description: Optional[str] = None,
authenticated: bool = False,
external: bool = False,
inaccessible: bool = False,
policy: Optional[List[List[str]]] = None,
provides: Optional[List[str]] = None,
override: Optional[Union[Override, str]] = None,
requires: Optional[List[str]] = None,
external: bool = False,
shareable: bool = False,
requires_scopes: Optional[List[List[str]]] = None,
tags: Optional[Iterable[str]] = (),
override: Optional[str] = None,
inaccessible: bool = False,
shareable: bool = False,
permission_classes: Optional[List[Type[BasePermission]]] = None,
deprecation_reason: Optional[str] = None,
default: Any = UNSET,
Expand All @@ -111,13 +121,16 @@ def field(
name: Optional[str] = None,
is_subscription: bool = False,
description: Optional[str] = None,
authenticated: bool = False,
external: bool = False,
inaccessible: bool = False,
policy: Optional[List[List[str]]] = None,
provides: Optional[List[str]] = None,
override: Optional[Union[Override, str]] = None,
requires: Optional[List[str]] = None,
external: bool = False,
shareable: bool = False,
requires_scopes: Optional[List[List[str]]] = None,
tags: Optional[Iterable[str]] = (),
override: Optional[str] = None,
inaccessible: bool = False,
shareable: bool = False,
permission_classes: Optional[List[Type[BasePermission]]] = None,
deprecation_reason: Optional[str] = None,
default: Any = dataclasses.MISSING,
Expand All @@ -131,38 +144,54 @@ def field(
init: Literal[True, False, None] = None,
) -> Any:
from .schema_directives import (
Authenticated,
External,
Inaccessible,
Override,
Policy,
Provides,
Requires,
RequiresScopes,
Shareable,
Tag,
)

directives = list(directives)

if authenticated:
directives.append(Authenticated())

if external:
directives.append(External())

if inaccessible:
directives.append(Inaccessible())

if override:
directives.append(
Override(override_from=override, label=UNSET)
if isinstance(override, str)
else override
)

if policy:
directives.append(Policy(policies=policy))

if provides:
directives.append(Provides(fields=" ".join(provides)))

if requires:
directives.append(Requires(fields=" ".join(requires)))

if external:
directives.append(External())
if requires_scopes:
directives.append(RequiresScopes(scopes=requires_scopes))

if shareable:
directives.append(Shareable())

if tags:
directives.extend(Tag(name=tag) for tag in tags)

if override:
directives.append(Override(override_from=override))

if inaccessible:
directives.append(Inaccessible())

return base_field( # type: ignore
resolver=resolver, # type: ignore
name=name,
Expand Down

0 comments on commit ac20ea8

Please sign in to comment.