diff --git a/.github/workflows/federation-compatibility.yml b/.github/workflows/federation-compatibility.yml index e9c74d2158..5ba4c34453 100644 --- a/.github/workflows/federation-compatibility.yml +++ b/.github/workflows/federation-compatibility.yml @@ -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 diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000000..b26da14c9b --- /dev/null +++ b/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)") + ) +``` diff --git a/docs/guides/federation.md b/docs/guides/federation.md index 55fbffdf6d..08e8f3d270 100644 --- a/docs/guides/federation.md +++ b/docs/guides/federation.md @@ -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. @@ -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: @@ -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 diff --git a/strawberry/federation/enum.py b/strawberry/federation/enum.py index e703ecdd61..21b9f5192c 100644 --- a/strawberry/federation/enum.py +++ b/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 @@ -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: ... @@ -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]: ... @@ -61,8 +76,11 @@ 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. @@ -70,13 +88,28 @@ def enum( 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) diff --git a/strawberry/federation/field.py b/strawberry/federation/field.py index 806e6ae8bb..be261f9c22 100644 --- a/strawberry/federation/field.py +++ b/strawberry/federation/field.py @@ -25,6 +25,7 @@ from strawberry.field import _RESOLVER_TYPE, StrawberryField from strawberry.permission import BasePermission + from .schema_directives import Override T = TypeVar("T") @@ -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, @@ -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, @@ -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, @@ -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, @@ -131,25 +144,47 @@ 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()) @@ -157,12 +192,6 @@ def field( 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, diff --git a/strawberry/federation/object_type.py b/strawberry/federation/object_type.py index c36b18d1e4..54d72a05c1 100644 --- a/strawberry/federation/object_type.py +++ b/strawberry/federation/object_type.py @@ -2,6 +2,7 @@ TYPE_CHECKING, Callable, Iterable, + List, Optional, Sequence, Type, @@ -31,19 +32,25 @@ def _impl_type( name: Optional[str] = None, description: Optional[str] = None, directives: Iterable[object] = (), + authenticated: bool = False, keys: Iterable[Union["Key", str]] = (), extend: bool = False, shareable: bool = False, inaccessible: bool = UNSET, + policy: Optional[List[List[str]]] = None, + requires_scopes: Optional[List[List[str]]] = None, tags: Iterable[str] = (), is_input: bool = False, is_interface: bool = False, is_interface_object: bool = False, ) -> T: from strawberry.federation.schema_directives import ( + Authenticated, Inaccessible, InterfaceObject, Key, + Policy, + RequiresScopes, Shareable, Tag, ) @@ -55,12 +62,21 @@ def _impl_type( for key in keys ) - if shareable: - directives.append(Shareable()) + if authenticated: + directives.append(Authenticated()) if inaccessible is not UNSET: directives.append(Inaccessible()) + if policy: + directives.append(Policy(policies=policy)) + + 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) @@ -89,10 +105,15 @@ def type( *, name: Optional[str] = None, description: Optional[str] = None, - keys: Iterable[Union["Key", str]] = (), + directives: Iterable[object] = (), + authenticated: bool = False, + extend: bool = False, inaccessible: bool = UNSET, + keys: Iterable[Union["Key", str]] = (), + policy: Optional[List[List[str]]] = None, + requires_scopes: Optional[List[List[str]]] = None, + shareable: bool = False, tags: Iterable[str] = (), - extend: bool = False, ) -> T: ... @@ -107,12 +128,15 @@ def type( *, name: Optional[str] = None, description: Optional[str] = None, - keys: Iterable[Union["Key", str]] = (), - inaccessible: bool = UNSET, - tags: Iterable[str] = (), + directives: Iterable[object] = (), + authenticated: bool = False, extend: bool = False, + inaccessible: bool = UNSET, + keys: Iterable[Union["Key", str]] = (), + policy: Optional[List[List[str]]] = None, + requires_scopes: Optional[List[List[str]]] = None, shareable: bool = False, - directives: Iterable[object] = (), + tags: Iterable[str] = (), ) -> Callable[[T], T]: ... @@ -122,22 +146,28 @@ def type( *, name: Optional[str] = None, description: Optional[str] = None, - keys: Iterable[Union["Key", str]] = (), - inaccessible: bool = UNSET, - tags: Iterable[str] = (), + directives: Iterable[object] = (), + authenticated: bool = False, extend: bool = False, + inaccessible: bool = UNSET, + keys: Iterable[Union["Key", str]] = (), + policy: Optional[List[List[str]]] = None, + requires_scopes: Optional[List[List[str]]] = None, shareable: bool = False, - directives: Iterable[object] = (), + tags: Iterable[str] = (), ): return _impl_type( cls, name=name, description=description, directives=directives, + authenticated=authenticated, keys=keys, extend=extend, - shareable=shareable, inaccessible=inaccessible, + policy=policy, + requires_scopes=requires_scopes, + shareable=shareable, tags=tags, ) @@ -208,10 +238,13 @@ def interface( *, name: Optional[str] = None, description: Optional[str] = None, - keys: Iterable[Union["Key", str]] = (), + directives: Iterable[object] = (), + authenticated: bool = False, inaccessible: bool = UNSET, + keys: Iterable[Union["Key", str]] = (), + policy: Optional[List[List[str]]] = None, + requires_scopes: Optional[List[List[str]]] = None, tags: Iterable[str] = (), - directives: Iterable[object] = (), ) -> T: ... @@ -226,10 +259,13 @@ def interface( *, name: Optional[str] = None, description: Optional[str] = None, - keys: Iterable[Union["Key", str]] = (), + directives: Iterable[object] = (), + authenticated: bool = False, inaccessible: bool = UNSET, + keys: Iterable[Union["Key", str]] = (), + policy: Optional[List[List[str]]] = None, + requires_scopes: Optional[List[List[str]]] = None, tags: Iterable[str] = (), - directives: Iterable[object] = (), ) -> Callable[[T], T]: ... @@ -239,20 +275,26 @@ def interface( *, name: Optional[str] = None, description: Optional[str] = None, - keys: Iterable[Union["Key", str]] = (), + directives: Iterable[object] = (), + authenticated: bool = False, inaccessible: bool = UNSET, + keys: Iterable[Union["Key", str]] = (), + policy: Optional[List[List[str]]] = None, + requires_scopes: Optional[List[List[str]]] = None, tags: Iterable[str] = (), - directives: Iterable[object] = (), ): return _impl_type( cls, name=name, description=description, directives=directives, + authenticated=authenticated, keys=keys, inaccessible=inaccessible, - is_interface=True, + policy=policy, + requires_scopes=requires_scopes, tags=tags, + is_interface=True, ) @@ -265,12 +307,15 @@ def interface( def interface_object( cls: T, *, - keys: Iterable[Union["Key", str]], name: Optional[str] = None, description: Optional[str] = None, + directives: Iterable[object] = (), + authenticated: bool = False, inaccessible: bool = UNSET, + keys: Iterable[Union["Key", str]] = (), + policy: Optional[List[List[str]]] = None, + requires_scopes: Optional[List[List[str]]] = None, tags: Iterable[str] = (), - directives: Iterable[object] = (), ) -> T: ... @@ -283,12 +328,15 @@ def interface_object( ) def interface_object( *, - keys: Iterable[Union["Key", str]], name: Optional[str] = None, description: Optional[str] = None, + directives: Iterable[object] = (), + authenticated: bool = False, inaccessible: bool = UNSET, + keys: Iterable[Union["Key", str]] = (), + policy: Optional[List[List[str]]] = None, + requires_scopes: Optional[List[List[str]]] = None, tags: Iterable[str] = (), - directives: Iterable[object] = (), ) -> Callable[[T], T]: ... @@ -296,21 +344,27 @@ def interface_object( def interface_object( cls: Optional[T] = None, *, - keys: Iterable[Union["Key", str]], name: Optional[str] = None, description: Optional[str] = None, + directives: Iterable[object] = (), + authenticated: bool = False, inaccessible: bool = UNSET, + keys: Iterable[Union["Key", str]] = (), + policy: Optional[List[List[str]]] = None, + requires_scopes: Optional[List[List[str]]] = None, tags: Iterable[str] = (), - directives: Iterable[object] = (), ): return _impl_type( cls, name=name, description=description, directives=directives, + authenticated=authenticated, keys=keys, inaccessible=inaccessible, + policy=policy, + requires_scopes=requires_scopes, + tags=tags, is_interface=False, is_interface_object=True, - tags=tags, ) diff --git a/strawberry/federation/scalar.py b/strawberry/federation/scalar.py index 551569c97e..dc0257b44f 100644 --- a/strawberry/federation/scalar.py +++ b/strawberry/federation/scalar.py @@ -3,6 +3,7 @@ Any, Callable, Iterable, + List, NewType, Optional, Type, @@ -34,7 +35,10 @@ def scalar( parse_value: Optional[Callable] = None, parse_literal: Optional[Callable] = 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[[_T], _T]: ... @@ -51,7 +55,10 @@ def scalar( parse_value: Optional[Callable] = None, parse_literal: Optional[Callable] = 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]] = (), ) -> _T: ... @@ -67,7 +74,10 @@ def scalar( parse_value: Optional[Callable] = None, parse_literal: Optional[Callable] = 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]] = (), ) -> Any: """Annotates a class or type as a GraphQL custom scalar. @@ -95,16 +105,31 @@ def scalar( >>> self.items = items """ - from strawberry.federation.schema_directives import Inaccessible, Tag + from strawberry.federation.schema_directives import ( + Authenticated, + Inaccessible, + Policy, + RequiresScopes, + Tag, + ) if parse_value is None: parse_value = cls 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) diff --git a/strawberry/federation/schema_directives.py b/strawberry/federation/schema_directives.py index ced7b3791b..6c69865fd3 100644 --- a/strawberry/federation/schema_directives.py +++ b/strawberry/federation/schema_directives.py @@ -5,13 +5,17 @@ from strawberry.schema_directive import Location, schema_directive from strawberry.unset import UNSET -from .types import FieldSet, LinkImport, LinkPurpose +from .types import ( + FieldSet, + LinkImport, + LinkPurpose, +) @dataclass class ImportedFrom: name: str - url: str = "https://specs.apollo.dev/federation/v2.3" + url: str = "https://specs.apollo.dev/federation/v2.7" class FederationDirective: @@ -23,7 +27,7 @@ class FederationDirective: ) class External(FederationDirective): imported_from: ClassVar[ImportedFrom] = ImportedFrom( - name="external", url="https://specs.apollo.dev/federation/v2.3" + name="external", url="https://specs.apollo.dev/federation/v2.7" ) @@ -33,7 +37,7 @@ class External(FederationDirective): class Requires(FederationDirective): fields: FieldSet imported_from: ClassVar[ImportedFrom] = ImportedFrom( - name="requires", url="https://specs.apollo.dev/federation/v2.3" + name="requires", url="https://specs.apollo.dev/federation/v2.7" ) @@ -43,7 +47,7 @@ class Requires(FederationDirective): class Provides(FederationDirective): fields: FieldSet imported_from: ClassVar[ImportedFrom] = ImportedFrom( - name="provides", url="https://specs.apollo.dev/federation/v2.3" + name="provides", url="https://specs.apollo.dev/federation/v2.7" ) @@ -57,7 +61,7 @@ class Key(FederationDirective): fields: FieldSet resolvable: Optional[bool] = True imported_from: ClassVar[ImportedFrom] = ImportedFrom( - name="key", url="https://specs.apollo.dev/federation/v2.3" + name="key", url="https://specs.apollo.dev/federation/v2.7" ) @@ -69,7 +73,7 @@ class Key(FederationDirective): ) class Shareable(FederationDirective): imported_from: ClassVar[ImportedFrom] = ImportedFrom( - name="shareable", url="https://specs.apollo.dev/federation/v2.3" + name="shareable", url="https://specs.apollo.dev/federation/v2.7" ) @@ -115,7 +119,7 @@ def __init__( class Tag(FederationDirective): name: str imported_from: ClassVar[ImportedFrom] = ImportedFrom( - name="tag", url="https://specs.apollo.dev/federation/v2.3" + name="tag", url="https://specs.apollo.dev/federation/v2.7" ) @@ -124,8 +128,9 @@ class Tag(FederationDirective): ) class Override(FederationDirective): override_from: str = directive_field(name="from") + label: Optional[str] = UNSET imported_from: ClassVar[ImportedFrom] = ImportedFrom( - name="override", url="https://specs.apollo.dev/federation/v2.3" + name="override", url="https://specs.apollo.dev/federation/v2.7" ) @@ -147,7 +152,7 @@ class Override(FederationDirective): ) class Inaccessible(FederationDirective): imported_from: ClassVar[ImportedFrom] = ImportedFrom( - name="inaccessible", url="https://specs.apollo.dev/federation/v2.3" + name="inaccessible", url="https://specs.apollo.dev/federation/v2.7" ) @@ -157,7 +162,7 @@ class Inaccessible(FederationDirective): class ComposeDirective(FederationDirective): name: str imported_from: ClassVar[ImportedFrom] = ImportedFrom( - name="composeDirective", url="https://specs.apollo.dev/federation/v2.3" + name="composeDirective", url="https://specs.apollo.dev/federation/v2.7" ) @@ -166,5 +171,58 @@ class ComposeDirective(FederationDirective): ) class InterfaceObject(FederationDirective): imported_from: ClassVar[ImportedFrom] = ImportedFrom( - name="interfaceObject", url="https://specs.apollo.dev/federation/v2.3" + name="interfaceObject", url="https://specs.apollo.dev/federation/v2.7" + ) + + +@schema_directive( + locations=[ + Location.FIELD_DEFINITION, + Location.OBJECT, + Location.INTERFACE, + Location.SCALAR, + Location.ENUM, + ], + name="authenticated", + print_definition=False, +) +class Authenticated(FederationDirective): + imported_from: ClassVar[ImportedFrom] = ImportedFrom( + name="authenticated", url="https://specs.apollo.dev/federation/v2.7" + ) + + +@schema_directive( + locations=[ + Location.FIELD_DEFINITION, + Location.OBJECT, + Location.INTERFACE, + Location.SCALAR, + Location.ENUM, + ], + name="requiresScopes", + print_definition=False, +) +class RequiresScopes(FederationDirective): + scopes: "List[List[str]]" + imported_from: ClassVar[ImportedFrom] = ImportedFrom( + name="requiresScopes", url="https://specs.apollo.dev/federation/v2.7" + ) + + +@schema_directive( + locations=[ + Location.FIELD_DEFINITION, + Location.OBJECT, + Location.INTERFACE, + Location.SCALAR, + Location.ENUM, + ], + name="policy", + print_definition=False, +) +class Policy(FederationDirective): + policies: "List[List[str]]" + imported_from: ClassVar[ImportedFrom] = ImportedFrom( + name="policy", url="https://specs.apollo.dev/federation/v2.7" ) diff --git a/tests/federation/printer/test_authenticated.py b/tests/federation/printer/test_authenticated.py new file mode 100644 index 0000000000..db9ef10a22 --- /dev/null +++ b/tests/federation/printer/test_authenticated.py @@ -0,0 +1,122 @@ +import textwrap +from enum import Enum +from typing import List +from typing_extensions import Annotated + +import strawberry + + +def test_field_authenticated_printed_correctly(): + @strawberry.federation.interface(authenticated=True) + class SomeInterface: + id: strawberry.ID + + @strawberry.federation.type(authenticated=True) + class Product(SomeInterface): + upc: str = strawberry.federation.field(authenticated=True) + + @strawberry.federation.type + class Query: + @strawberry.federation.field(authenticated=True) + def top_products( + self, first: Annotated[int, strawberry.federation.argument()] + ) -> List[Product]: + return [] + + schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) + + expected = """ + schema @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@authenticated"]) { + query: Query + } + + type Product implements SomeInterface @authenticated { + id: ID! + upc: String! @authenticated + } + + type Query { + _service: _Service! + topProducts(first: Int!): [Product!]! @authenticated + } + + interface SomeInterface @authenticated { + id: ID! + } + + scalar _Any + + type _Service { + sdl: String! + } + """ + + assert schema.as_str() == textwrap.dedent(expected).strip() + + +def test_field_authenticated_printed_correctly_on_scalar(): + @strawberry.federation.scalar(authenticated=True) + class SomeScalar(str): + __slots__ = () + + @strawberry.federation.type + class Query: + hello: SomeScalar + + schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) + + expected = """ + schema @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@authenticated"]) { + query: Query + } + + type Query { + _service: _Service! + hello: SomeScalar! + } + + scalar SomeScalar @authenticated + + scalar _Any + + type _Service { + sdl: String! + } + """ + + assert schema.as_str() == textwrap.dedent(expected).strip() + + +def test_field_authenticated_printed_correctly_on_enum(): + @strawberry.federation.enum(authenticated=True) + class SomeEnum(Enum): + A = "A" + + @strawberry.federation.type + class Query: + hello: SomeEnum + + schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) + + expected = """ + schema @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@authenticated"]) { + query: Query + } + + type Query { + _service: _Service! + hello: SomeEnum! + } + + enum SomeEnum @authenticated { + A + } + + scalar _Any + + type _Service { + sdl: String! + } + """ + + assert schema.as_str() == textwrap.dedent(expected).strip() diff --git a/tests/federation/printer/test_compose_directive.py b/tests/federation/printer/test_compose_directive.py index a9ce64979f..ddbc0463c9 100644 --- a/tests/federation/printer/test_compose_directive.py +++ b/tests/federation/printer/test_compose_directive.py @@ -37,7 +37,7 @@ class Query: directive @sensitive(reason: String!) on OBJECT - schema @composeDirective(name: "@cacheControl") @link(url: "https://directives.strawberry.rocks/cacheControl/v0.1", import: ["@cacheControl"]) @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@composeDirective", "@key", "@shareable"]) { + schema @composeDirective(name: "@cacheControl") @link(url: "https://directives.strawberry.rocks/cacheControl/v0.1", import: ["@cacheControl"]) @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@composeDirective", "@key", "@shareable"]) { query: Query } @@ -102,7 +102,7 @@ class Query: directive @sensitive(reason: String!) on OBJECT - schema @composeDirective(name: "@cacheControl") @link(url: "https://f.strawberry.rocks/cacheControl/v1.0", import: ["@cacheControl"]) @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@composeDirective", "@key", "@shareable"]) { + schema @composeDirective(name: "@cacheControl") @link(url: "https://f.strawberry.rocks/cacheControl/v1.0", import: ["@cacheControl"]) @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@composeDirective", "@key", "@shareable"]) { query: Query } diff --git a/tests/federation/printer/test_entities.py b/tests/federation/printer/test_entities.py index 9d7bf01c02..eb5866a849 100644 --- a/tests/federation/printer/test_entities.py +++ b/tests/federation/printer/test_entities.py @@ -33,7 +33,7 @@ def top_products(self, first: int) -> List[Product]: schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@external"]) { + schema @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@external"]) { query: Query } @@ -96,7 +96,7 @@ def top_products(self, first: int) -> List[Product]: schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@external", "@key"]) { + schema @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@external", "@key"]) { query: Query } diff --git a/tests/federation/printer/test_inaccessible.py b/tests/federation/printer/test_inaccessible.py index 43c21d3223..e8801d3e64 100644 --- a/tests/federation/printer/test_inaccessible.py +++ b/tests/federation/printer/test_inaccessible.py @@ -44,7 +44,7 @@ def top_products( ) expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@external", "@inaccessible", "@key"]) { + schema @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@external", "@inaccessible", "@key"]) { query: Query } @@ -107,7 +107,7 @@ def hello(self) -> str: ) expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@inaccessible"]) { + schema @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@inaccessible"]) { query: Query mutation: Mutation } @@ -144,7 +144,7 @@ class Query: ) expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@inaccessible"]) { + schema @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@inaccessible"]) { query: Query } @@ -180,7 +180,7 @@ class Query: ) expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@inaccessible"]) { + schema @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@inaccessible"]) { query: Query } @@ -218,7 +218,7 @@ class Query: ) expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@inaccessible"]) { + schema @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@inaccessible"]) { query: Query } @@ -259,7 +259,7 @@ class Query: schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@inaccessible"]) { + schema @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@inaccessible"]) { query: Query } diff --git a/tests/federation/printer/test_interface.py b/tests/federation/printer/test_interface.py index 7215e45fc3..00d2464052 100644 --- a/tests/federation/printer/test_interface.py +++ b/tests/federation/printer/test_interface.py @@ -22,7 +22,7 @@ def top_products(self, first: int) -> List[Product]: schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@external", "@key"]) { + schema @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@external", "@key"]) { query: Query } diff --git a/tests/federation/printer/test_interface_object.py b/tests/federation/printer/test_interface_object.py index a37f726421..07d7a7406b 100644 --- a/tests/federation/printer/test_interface_object.py +++ b/tests/federation/printer/test_interface_object.py @@ -13,7 +13,7 @@ class SomeInterface: ) expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@interfaceObject", "@key"]) { + schema @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@interfaceObject", "@key"]) { query: Query } diff --git a/tests/federation/printer/test_keys.py b/tests/federation/printer/test_keys.py index 81dd5de248..5cf411aa4c 100644 --- a/tests/federation/printer/test_keys.py +++ b/tests/federation/printer/test_keys.py @@ -96,7 +96,7 @@ def top_products(self, first: int) -> List[Product]: schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@external", "@key"]) { + schema @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@external", "@key"]) { query: Query } diff --git a/tests/federation/printer/test_link.py b/tests/federation/printer/test_link.py index 2b89062614..c4fe1dce2a 100644 --- a/tests/federation/printer/test_link.py +++ b/tests/federation/printer/test_link.py @@ -47,7 +47,7 @@ class Query: query=Query, schema_directives=[ Link( - url="https://specs.apollo.dev/federation/v2.3", + url="https://specs.apollo.dev/federation/v2.7", import_=[ "@key", "@requires", @@ -64,7 +64,7 @@ class Query: ) expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key", "@requires", "@provides", "@external", {name: "@tag", as: "@mytag"}, "@extends", "@shareable", "@inaccessible", "@override"]) { + schema @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@key", "@requires", "@provides", "@external", {name: "@tag", as: "@mytag"}, "@extends", "@shareable", "@inaccessible", "@override"]) { query: Query } @@ -95,7 +95,7 @@ class Query: schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"]) { + schema @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@key"]) { query: Query } @@ -139,7 +139,7 @@ class Query: ) expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"]) { + schema @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@key"]) { query: Query } @@ -184,7 +184,7 @@ class Query: ) expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@inaccessible"]) { + schema @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@inaccessible"]) { query: Query } @@ -224,7 +224,7 @@ class Query: schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key", "@tag"]) { + schema @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@key", "@tag"]) { query: Query } @@ -303,7 +303,7 @@ class Query: schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"]) { + schema @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@key"]) { query: Query } diff --git a/tests/federation/printer/test_override.py b/tests/federation/printer/test_override.py index eb01edcbbb..bec1f6f95a 100644 --- a/tests/federation/printer/test_override.py +++ b/tests/federation/printer/test_override.py @@ -4,6 +4,7 @@ from typing import List import strawberry +from strawberry.federation.schema_directives import Override def test_field_override_printed_correctly(): @@ -24,7 +25,7 @@ def top_products(self, first: int) -> List[Product]: schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@external", "@key", "@override"]) { + schema @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@external", "@key", "@override"]) { query: Query } @@ -53,3 +54,55 @@ def top_products(self, first: int) -> List[Product]: """ assert schema.as_str() == textwrap.dedent(expected).strip() + + +def test_field_override_label_printed_correctly(): + @strawberry.interface + class SomeInterface: + id: strawberry.ID + + @strawberry.federation.type(keys=["upc"], extend=True) + class Product(SomeInterface): + upc: str = strawberry.federation.field( + external=True, + override=Override(override_from="mySubGraph", label="percent(1)"), + ) + + @strawberry.federation.type + class Query: + @strawberry.field + def top_products(self, first: int) -> List[Product]: + return [] + + schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) + + expected = """ + schema @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@external", "@key", "@override"]) { + query: Query + } + + extend type Product implements SomeInterface @key(fields: "upc") { + id: ID! + upc: String! @external @override(from: "mySubGraph", label: "percent(1)") + } + + type Query { + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! + topProducts(first: Int!): [Product!]! + } + + interface SomeInterface { + id: ID! + } + + scalar _Any + + union _Entity = Product + + type _Service { + sdl: String! + } + """ + + assert schema.as_str() == textwrap.dedent(expected).strip() diff --git a/tests/federation/printer/test_policy.py b/tests/federation/printer/test_policy.py new file mode 100644 index 0000000000..05dae2eb11 --- /dev/null +++ b/tests/federation/printer/test_policy.py @@ -0,0 +1,132 @@ +import textwrap +from enum import Enum +from typing import List +from typing_extensions import Annotated + +import strawberry + + +def test_field_policy_printed_correctly(): + @strawberry.federation.interface( + policy=[["client", "poweruser"], ["admin"], ["productowner"]] + ) + class SomeInterface: + id: strawberry.ID + + @strawberry.federation.type( + policy=[["client", "poweruser"], ["admin"], ["productowner"]] + ) + class Product(SomeInterface): + upc: str = strawberry.federation.field(policy=[["productowner"]]) + + @strawberry.federation.type + class Query: + @strawberry.federation.field( + policy=[["client", "poweruser"], ["admin"], ["productowner"]] + ) + def top_products( + self, first: Annotated[int, strawberry.federation.argument()] + ) -> List[Product]: + return [] + + schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) + + expected = """ + schema @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@policy"]) { + query: Query + } + + type Product implements SomeInterface @policy(policies: [["client", "poweruser"], ["admin"], ["productowner"]]) { + id: ID! + upc: String! @policy(policies: [["productowner"]]) + } + + type Query { + _service: _Service! + topProducts(first: Int!): [Product!]! @policy(policies: [["client", "poweruser"], ["admin"], ["productowner"]]) + } + + interface SomeInterface @policy(policies: [["client", "poweruser"], ["admin"], ["productowner"]]) { + id: ID! + } + + scalar _Any + + type _Service { + sdl: String! + } + """ + + assert schema.as_str() == textwrap.dedent(expected).strip() + + +def test_field_policy_printed_correctly_on_scalar(): + @strawberry.federation.scalar( + policy=[["client", "poweruser"], ["admin"], ["productowner"]] + ) + class SomeScalar(str): + __slots__ = () + + @strawberry.federation.type + class Query: + hello: SomeScalar + + schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) + + expected = """ + schema @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@policy"]) { + query: Query + } + + type Query { + _service: _Service! + hello: SomeScalar! + } + + scalar SomeScalar @policy(policies: [["client", "poweruser"], ["admin"], ["productowner"]]) + + scalar _Any + + type _Service { + sdl: String! + } + """ + + assert schema.as_str() == textwrap.dedent(expected).strip() + + +def test_field_policy_printed_correctly_on_enum(): + @strawberry.federation.enum( + policy=[["client", "poweruser"], ["admin"], ["productowner"]] + ) + class SomeEnum(Enum): + A = "A" + + @strawberry.federation.type + class Query: + hello: SomeEnum + + schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) + + expected = """ + schema @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@policy"]) { + query: Query + } + + type Query { + _service: _Service! + hello: SomeEnum! + } + + enum SomeEnum @policy(policies: [["client", "poweruser"], ["admin"], ["productowner"]]) { + A + } + + scalar _Any + + type _Service { + sdl: String! + } + """ + + assert schema.as_str() == textwrap.dedent(expected).strip() diff --git a/tests/federation/printer/test_provides.py b/tests/federation/printer/test_provides.py index b067669ab2..a13130bc82 100644 --- a/tests/federation/printer/test_provides.py +++ b/tests/federation/printer/test_provides.py @@ -39,7 +39,7 @@ def top_products(self, first: int) -> List[Product]: ) expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@external", "@key", "@provides"]) { + schema @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@external", "@key", "@provides"]) { query: Query } @@ -111,7 +111,7 @@ def top_products(self, first: int) -> List[Product]: ) expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@external", "@key", "@provides"]) { + schema @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@external", "@key", "@provides"]) { query: Query } diff --git a/tests/federation/printer/test_requires.py b/tests/federation/printer/test_requires.py index 0e6edb93db..fef6011949 100644 --- a/tests/federation/printer/test_requires.py +++ b/tests/federation/printer/test_requires.py @@ -39,7 +39,7 @@ def top_products(self, first: int) -> List[Product]: schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@external", "@key", "@requires"]) { + schema @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@external", "@key", "@requires"]) { query: Query } diff --git a/tests/federation/printer/test_requires_scopes.py b/tests/federation/printer/test_requires_scopes.py new file mode 100644 index 0000000000..2c42eec0bf --- /dev/null +++ b/tests/federation/printer/test_requires_scopes.py @@ -0,0 +1,132 @@ +import textwrap +from enum import Enum +from typing import List +from typing_extensions import Annotated + +import strawberry + + +def test_field_requires_scopes_printed_correctly(): + @strawberry.federation.interface( + requires_scopes=[["client", "poweruser"], ["admin"], ["productowner"]] + ) + class SomeInterface: + id: strawberry.ID + + @strawberry.federation.type( + requires_scopes=[["client", "poweruser"], ["admin"], ["productowner"]] + ) + class Product(SomeInterface): + upc: str = strawberry.federation.field(requires_scopes=[["productowner"]]) + + @strawberry.federation.type + class Query: + @strawberry.federation.field( + requires_scopes=[["client", "poweruser"], ["admin"], ["productowner"]] + ) + def top_products( + self, first: Annotated[int, strawberry.federation.argument()] + ) -> List[Product]: + return [] + + schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) + + expected = """ + schema @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@requiresScopes"]) { + query: Query + } + + type Product implements SomeInterface @requiresScopes(scopes: [["client", "poweruser"], ["admin"], ["productowner"]]) { + id: ID! + upc: String! @requiresScopes(scopes: [["productowner"]]) + } + + type Query { + _service: _Service! + topProducts(first: Int!): [Product!]! @requiresScopes(scopes: [["client", "poweruser"], ["admin"], ["productowner"]]) + } + + interface SomeInterface @requiresScopes(scopes: [["client", "poweruser"], ["admin"], ["productowner"]]) { + id: ID! + } + + scalar _Any + + type _Service { + sdl: String! + } + """ + + assert schema.as_str() == textwrap.dedent(expected).strip() + + +def test_field_requires_scopes_printed_correctly_on_scalar(): + @strawberry.federation.scalar( + requires_scopes=[["client", "poweruser"], ["admin"], ["productowner"]] + ) + class SomeScalar(str): + __slots__ = () + + @strawberry.federation.type + class Query: + hello: SomeScalar + + schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) + + expected = """ + schema @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@requiresScopes"]) { + query: Query + } + + type Query { + _service: _Service! + hello: SomeScalar! + } + + scalar SomeScalar @requiresScopes(scopes: [["client", "poweruser"], ["admin"], ["productowner"]]) + + scalar _Any + + type _Service { + sdl: String! + } + """ + + assert schema.as_str() == textwrap.dedent(expected).strip() + + +def test_field_requires_scopes_printed_correctly_on_enum(): + @strawberry.federation.enum( + requires_scopes=[["client", "poweruser"], ["admin"], ["productowner"]] + ) + class SomeEnum(Enum): + A = "A" + + @strawberry.federation.type + class Query: + hello: SomeEnum + + schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) + + expected = """ + schema @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@requiresScopes"]) { + query: Query + } + + type Query { + _service: _Service! + hello: SomeEnum! + } + + enum SomeEnum @requiresScopes(scopes: [["client", "poweruser"], ["admin"], ["productowner"]]) { + A + } + + scalar _Any + + type _Service { + sdl: String! + } + """ + + assert schema.as_str() == textwrap.dedent(expected).strip() diff --git a/tests/federation/printer/test_shareable.py b/tests/federation/printer/test_shareable.py index ff636dc3dc..6212b8425b 100644 --- a/tests/federation/printer/test_shareable.py +++ b/tests/federation/printer/test_shareable.py @@ -24,7 +24,7 @@ def top_products(self, first: int) -> List[Product]: schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@external", "@key", "@shareable"]) { + schema @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@external", "@key", "@shareable"]) { query: Query } diff --git a/tests/federation/printer/test_tag.py b/tests/federation/printer/test_tag.py index 89446347f5..388c0adf93 100644 --- a/tests/federation/printer/test_tag.py +++ b/tests/federation/printer/test_tag.py @@ -28,7 +28,7 @@ def top_products( schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@external", "@tag"]) { + schema @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@external", "@tag"]) { query: Query } @@ -68,7 +68,7 @@ class Query: schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@tag"]) { + schema @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@tag"]) { query: Query } @@ -101,7 +101,7 @@ class Query: schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@tag"]) { + schema @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@tag"]) { query: Query } @@ -136,7 +136,7 @@ class Query: schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@tag"]) { + schema @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@tag"]) { query: Query } @@ -177,7 +177,7 @@ class Query: schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@tag"]) { + schema @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@tag"]) { query: Query } @@ -220,7 +220,7 @@ class Query: ) expected = """ - schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@tag"]) { + schema @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@tag"]) { query: Query }