From d5b81c6dc2c401c8c2d4ad60e397f495b56a4b90 Mon Sep 17 00:00:00 2001 From: band Date: Thu, 14 Nov 2024 21:46:20 +0200 Subject: [PATCH 1/2] added ability to override federation version on export schema command --- RELEASE.md | 11 ++++ docs/guides/schema-export.md | 12 ++++ strawberry/cli/commands/export_schema.py | 12 ++++ strawberry/federation/schema_directives.py | 20 ++++++ tests/cli/test_export_schema.py | 41 ++++++++++++ .../sample_package/sample_module_federated.py | 65 +++++++++++++++++++ 6 files changed, 161 insertions(+) create mode 100644 RELEASE.md create mode 100644 tests/fixtures/sample_package/sample_module_federated.py diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000000..85aea7f31b --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,11 @@ +Release type: patch + +Added option for the export_schema command to override the output federation version. +ussage example: strawberry export-schema --federation-version=2.5 + +This change ONLY effect "strawberry export-schema" command and is fully backword-compatible without any logic change. + +Warning: Please use with caution!! + +If the schema define directives that are not supported by the specified version (in the override parameter) the schema +will still generate the output useing the value from the override, and may break at runtime. diff --git a/docs/guides/schema-export.md b/docs/guides/schema-export.md index 96e04f34eb..86c951680a 100644 --- a/docs/guides/schema-export.md +++ b/docs/guides/schema-export.md @@ -32,3 +32,15 @@ Alternatively, the `--output` option can be used: ```bash strawberry export-schema package.module:schema --output schema.graphql ``` + +You can override the output directive to have specific federation schema (e.g: +schema @link(url: "https://specs.apollo.dev/federation/v2.5"): + +```bash +strawberry export-schema package.module:schema --federation-version=2.5 --output schema.graphql +``` + +> [!WARNING] \ +> If the schema define directives that are not supported by the specified +> version (in the override parameter) the schema will still generate the output +> useing the value from the override, and may break at runtime. diff --git a/strawberry/cli/commands/export_schema.py b/strawberry/cli/commands/export_schema.py index 9592f5a488..effe7d621e 100644 --- a/strawberry/cli/commands/export_schema.py +++ b/strawberry/cli/commands/export_schema.py @@ -26,7 +26,19 @@ def export_schema( "-o", help="File to save the exported schema. If not provided, prints to console.", ), + federation_version: Path = typer.Option( + None, + "--federation-version", + "-e", + help=( + "Override the output federation schema version. please use with care!" + "schema may brake if it have directives that are not supported by the defined federation version." + "(for directive version compatibility please see: https://www.apollographql.com/docs/graphos/reference/federation/directives)" + ), + ), ) -> None: + if federation_version: + app.__setattr__("federation_version_override", federation_version) schema_symbol = load_schema(schema, app_dir) schema_text = print_schema(schema_symbol) diff --git a/strawberry/federation/schema_directives.py b/strawberry/federation/schema_directives.py index 0f9232d7f3..977ded8bcc 100644 --- a/strawberry/federation/schema_directives.py +++ b/strawberry/federation/schema_directives.py @@ -1,7 +1,9 @@ +import typing from dataclasses import dataclass from typing import ClassVar, List, Optional from strawberry import directive_field +from strawberry.cli import app from strawberry.schema_directive import Location, schema_directive from strawberry.types.unset import UNSET @@ -11,12 +13,30 @@ LinkPurpose, ) +FEDERATION_VERSION_BASE_URL = "https://specs.apollo.dev/federation/v" + @dataclass class ImportedFrom: name: str url: str = "https://specs.apollo.dev/federation/v2.7" + def __init__(self, **kwargs: typing.Dict[str, typing.Any]) -> None: + if hasattr(self, "__dataclass_fields__"): + args = self.__dataclass_fields__ + for key, value in kwargs.items(): + if key in args and type(value) is args.get(key).type: + self.__setattr__(key, value) + if ( + hasattr(app, "federation_version_override") + and type(value) is str + and str(value).startswith("https://specs.apollo.dev/federation/") + ): + self.__setattr__( + key, + f"{FEDERATION_VERSION_BASE_URL}{app.federation_version_override!s}", + ) + class FederationDirective: imported_from: ClassVar[ImportedFrom] diff --git a/tests/cli/test_export_schema.py b/tests/cli/test_export_schema.py index 3f1b342b74..d7ca452a01 100644 --- a/tests/cli/test_export_schema.py +++ b/tests/cli/test_export_schema.py @@ -19,6 +19,47 @@ def test_schema_export(cli_app: Typer, cli_runner: CliRunner): ) +def test_schema_export_with_federation_version_override( + cli_app: Typer, cli_runner: CliRunner +): + selector = "tests.fixtures.sample_package.sample_module_federated:schema" + result = cli_runner.invoke( + cli_app, ["export-schema", selector, "--federation-version=2.5"] + ) + assert result.exit_code == 0 + assert result.stdout == ( + 'schema @link(url: "https://specs.apollo.dev/federation/v2.5", import: ' + '["@key"]) {\n' + " query: Query\n" + "}\n" + "\n" + 'type Organization @key(fields: "id") {\n' + " id: ID!\n" + " name: String!\n" + " owner: User!\n" + "}\n" + "\n" + "type Query {\n" + " _entities(representations: [_Any!]!): [_Entity]!\n" + " _service: _Service!\n" + " organizations: [Organization!]!\n" + " organization(id: ID!): Organization\n" + "}\n" + "\n" + 'type User @key(fields: "id", resolvable: false) {\n' + " id: ID!\n" + "}\n" + "\n" + "scalar _Any\n" + "\n" + "union _Entity = Organization | User\n" + "\n" + "type _Service {\n" + " sdl: String!\n" + "}\n" + ) + + def test_default_schema_symbol_name(cli_app: Typer, cli_runner: CliRunner): selector = "tests.fixtures.sample_package.sample_module" result = cli_runner.invoke(cli_app, ["export-schema", selector]) diff --git a/tests/fixtures/sample_package/sample_module_federated.py b/tests/fixtures/sample_package/sample_module_federated.py new file mode 100644 index 0000000000..cd7f7a9e87 --- /dev/null +++ b/tests/fixtures/sample_package/sample_module_federated.py @@ -0,0 +1,65 @@ +import typing + +import strawberry +from strawberry.federation.schema_directives import Key + + +@strawberry.federation.type(directives=[Key(fields="id", resolvable=False)]) +class User: + id: strawberry.ID + + +async def get_user(root: "Organization", info: strawberry.Info) -> User: + return User(id=root._owner) + + +@strawberry.federation.type(keys=["id"]) +class Organization: + id: strawberry.ID + name: str + _owner: strawberry.Private[User] + owner: User = strawberry.field(resolver=get_user) + + @classmethod + async def resolve_reference( + cls, id: strawberry.ID + ) -> typing.Optional["Organization"]: + return await get_organization_by_id(id=id) + + +async def get_organizations(): + return [ + Organization( + id=strawberry.ID("org1"), + name="iotflow", + _owner="abcdert1", + ), + Organization( + id=strawberry.ID("org2"), + name="ibrag", + _owner="abdfr2", + ), + ] + + +async def get_organization_by_id(id: strawberry.ID) -> typing.Optional[Organization]: + for organization in await get_organizations(): + if organization.id == id: + return organization + return None + + +@strawberry.type +class Query: + organizations: typing.List[Organization] = strawberry.field( + resolver=get_organizations + ) + + @strawberry.field + async def organization(self, id: strawberry.ID) -> typing.Optional[Organization]: + return await get_organization_by_id(id) + + +schema = strawberry.federation.Schema( + query=Query, types=[Organization, User], enable_federation_2=True +) From cde4fa7679d4a287d920e6220eb465778a62c35c Mon Sep 17 00:00:00 2001 From: band Date: Thu, 14 Nov 2024 23:18:47 +0200 Subject: [PATCH 2/2] # fixed typeos # added negative and backward-compatibility tests --- RELEASE.md | 6 +- docs/guides/schema-export.md | 2 +- strawberry/cli/commands/export_schema.py | 5 +- tests/cli/test_export_schema.py | 70 +++++++++++++----------- 4 files changed, 44 insertions(+), 39 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index 85aea7f31b..4462f97824 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,11 +1,11 @@ Release type: patch Added option for the export_schema command to override the output federation version. -ussage example: strawberry export-schema --federation-version=2.5 +usage example: strawberry export-schema --federation-version=2.5 -This change ONLY effect "strawberry export-schema" command and is fully backword-compatible without any logic change. +This change ONLY affects "strawberry export-schema" command and is fully backward-compatible without any logic change. Warning: Please use with caution!! If the schema define directives that are not supported by the specified version (in the override parameter) the schema -will still generate the output useing the value from the override, and may break at runtime. +will still generate the output using the value from the override, and may break at runtime. diff --git a/docs/guides/schema-export.md b/docs/guides/schema-export.md index 86c951680a..6e03c92576 100644 --- a/docs/guides/schema-export.md +++ b/docs/guides/schema-export.md @@ -43,4 +43,4 @@ strawberry export-schema package.module:schema --federation-version=2.5 --output > [!WARNING] \ > If the schema define directives that are not supported by the specified > version (in the override parameter) the schema will still generate the output -> useing the value from the override, and may break at runtime. +> using the value from the override, and may break at runtime. diff --git a/strawberry/cli/commands/export_schema.py b/strawberry/cli/commands/export_schema.py index effe7d621e..1178a1f809 100644 --- a/strawberry/cli/commands/export_schema.py +++ b/strawberry/cli/commands/export_schema.py @@ -26,15 +26,16 @@ def export_schema( "-o", help="File to save the exported schema. If not provided, prints to console.", ), - federation_version: Path = typer.Option( + federation_version: float = typer.Option( None, "--federation-version", "-e", help=( "Override the output federation schema version. please use with care!" - "schema may brake if it have directives that are not supported by the defined federation version." + "schema may break if it have directives that are not supported by the defined federation version." "(for directive version compatibility please see: https://www.apollographql.com/docs/graphos/reference/federation/directives)" ), + min=1, ), ) -> None: if federation_version: diff --git a/tests/cli/test_export_schema.py b/tests/cli/test_export_schema.py index d7ca452a01..a2eb515a85 100644 --- a/tests/cli/test_export_schema.py +++ b/tests/cli/test_export_schema.py @@ -1,7 +1,25 @@ +import typing + +from click.testing import Result from typer import Typer from typer.testing import CliRunner +def check_generated_schema( + cli_app: Typer, + cli_runner: CliRunner, + schema_override: typing.Optional[float] = None, +) -> Result: + selector = "tests.fixtures.sample_package.sample_module_federated:schema" + args = [ + "export-schema", + selector, + ] + if schema_override: + args.append(f"--federation-version={schema_override!s}") + return cli_runner.invoke(cli_app, args) + + def test_schema_export(cli_app: Typer, cli_runner: CliRunner): selector = "tests.fixtures.sample_package.sample_module:schema" result = cli_runner.invoke(cli_app, ["export-schema", selector]) @@ -22,44 +40,30 @@ def test_schema_export(cli_app: Typer, cli_runner: CliRunner): def test_schema_export_with_federation_version_override( cli_app: Typer, cli_runner: CliRunner ): - selector = "tests.fixtures.sample_package.sample_module_federated:schema" - result = cli_runner.invoke( - cli_app, ["export-schema", selector, "--federation-version=2.5"] + result = check_generated_schema(cli_app, cli_runner, 2.5) + assert result.exit_code == 0 + assert result.stdout.startswith( + 'schema @link(url: "https://specs.apollo.dev/federation/v2.5"' ) + + +def test_schema_export_without_federation_version_override( + cli_app: Typer, cli_runner: CliRunner +): + result = check_generated_schema(cli_app, cli_runner) assert result.exit_code == 0 - assert result.stdout == ( - 'schema @link(url: "https://specs.apollo.dev/federation/v2.5", import: ' - '["@key"]) {\n' - " query: Query\n" - "}\n" - "\n" - 'type Organization @key(fields: "id") {\n' - " id: ID!\n" - " name: String!\n" - " owner: User!\n" - "}\n" - "\n" - "type Query {\n" - " _entities(representations: [_Any!]!): [_Entity]!\n" - " _service: _Service!\n" - " organizations: [Organization!]!\n" - " organization(id: ID!): Organization\n" - "}\n" - "\n" - 'type User @key(fields: "id", resolvable: false) {\n' - " id: ID!\n" - "}\n" - "\n" - "scalar _Any\n" - "\n" - "union _Entity = Organization | User\n" - "\n" - "type _Service {\n" - " sdl: String!\n" - "}\n" + assert result.stdout.startswith( + 'schema @link(url: "https://specs.apollo.dev/federation/v2.7"' ) +def test_invalid_schema_export_federation_version_override( + cli_app: Typer, cli_runner: CliRunner +): + result = check_generated_schema(cli_app, cli_runner, 0.2) + assert result.exit_code == 2 + + def test_default_schema_symbol_name(cli_app: Typer, cli_runner: CliRunner): selector = "tests.fixtures.sample_package.sample_module" result = cli_runner.invoke(cli_app, ["export-schema", selector])