From ca4fac8278c0025863ae5ac95ce339ceb0a5fcc6 Mon Sep 17 00:00:00 2001 From: M Aswin Kishore <60577077+mak626@users.noreply.github.com> Date: Sat, 13 Jan 2024 21:57:26 +0530 Subject: [PATCH 01/19] feat: add support for federation-v2.3 using graphene-directives --- graphene_federation/__init__.py | 48 ++- .../appolo_versions/__init__.py | 41 ++ .../spec/federation-v1.0.graphql | 9 + .../spec/federation-v2.0.graphql | 30 ++ .../spec/federation-v2.1.graphql | 31 ++ .../spec/federation-v2.2.graphql | 31 ++ .../spec/federation-v2.3.graphql | 32 ++ graphene_federation/appolo_versions/v1_0.py | 76 ++++ graphene_federation/appolo_versions/v2_0.py | 143 +++++++ graphene_federation/appolo_versions/v2_1.py | 27 ++ graphene_federation/appolo_versions/v2_2.py | 24 ++ graphene_federation/appolo_versions/v2_3.py | 22 + .../appolo_versions/version.py | 9 + graphene_federation/compose_directive.py | 64 --- graphene_federation/directives/__init__.py | 10 + graphene_federation/directives/extends.py | 18 + graphene_federation/directives/external.py | 18 + .../directives/inaccessible.py | 18 + .../directives/interface_object.py | 14 + graphene_federation/directives/key.py | 15 + graphene_federation/directives/override.py | 16 + graphene_federation/directives/provides.py | 18 + graphene_federation/directives/requires.py | 39 ++ graphene_federation/directives/shareable.py | 19 + graphene_federation/directives/tag.py | 17 + graphene_federation/directives/utils.py | 20 + graphene_federation/entity.py | 94 ++--- graphene_federation/extend.py | 62 --- graphene_federation/external.py | 41 -- graphene_federation/federation_directive.py | 58 +++ graphene_federation/inaccessible.py | 46 --- graphene_federation/main.py | 147 ++++++- graphene_federation/override.py | 20 - graphene_federation/provides.py | 50 --- graphene_federation/requires.py | 31 -- graphene_federation/scalars/__init__.py | 5 + .../{types.py => scalars/_any.py} | 3 + graphene_federation/scalars/field_set_v1.py | 9 + graphene_federation/scalars/field_set_v2.py | 9 + graphene_federation/scalars/link_import.py | 59 +++ graphene_federation/scalars/link_purpose.py | 17 + .../schema_directives/__init__.py | 2 + .../schema_directives/compose_directive.py | 14 + .../schema_directives/link_directive.py | 39 ++ graphene_federation/service.py | 287 +------------ graphene_federation/shareable.py | 67 --- graphene_federation/tag.py | 20 - graphene_federation/tests/__init__.py | 0 .../tests/test_annotation_corner_cases.py | 391 ------------------ .../tests/test_annotation_corner_cases_v1.py | 388 ----------------- graphene_federation/tests/test_custom_enum.py | 57 --- graphene_federation/tests/test_entity.py | 1 - graphene_federation/tests/test_entity_v1.py | 1 - graphene_federation/tests/test_extend.py | 128 ------ graphene_federation/transform/__init__.py | 1 + .../transform/field_set_case_transform.py | 13 + graphene_federation/utils.py | 117 +----- graphene_federation/validators/__init__.py | 4 + graphene_federation/validators/extends.py | 26 ++ graphene_federation/validators/key.py | 27 ++ graphene_federation/validators/requires.py | 31 ++ graphene_federation/validators/utils.py | 120 ++++++ setup.py | 2 + ...t_annotate_object_with_meta_name_1.graphql | 25 ++ ...t_annotate_object_with_meta_name_2.graphql | 15 + ...otated_field_also_used_in_filter_1.graphql | 25 ++ ...otated_field_also_used_in_filter_2.graphql | 15 + .../test_camel_case_field_name_1.graphql | 23 ++ .../test_camel_case_field_name_2.graphql | 13 + ...ield_name_without_auto_camelcase_1.graphql | 23 ++ ...ield_name_without_auto_camelcase_2.graphql | 13 + .../test_similar_field_name_1.graphql | 33 ++ .../test_similar_field_name_2.graphql | 23 ++ ...t_annotate_object_with_meta_name_1.graphql | 22 + ...t_annotate_object_with_meta_name_2.graphql | 12 + ...otated_field_also_used_in_filter_1.graphql | 22 + ...otated_field_also_used_in_filter_2.graphql | 12 + .../test_camel_case_field_name_1.graphql | 20 + .../test_camel_case_field_name_2.graphql | 10 + ...ield_name_without_auto_camelcase_1.graphql | 20 + ...ield_name_without_auto_camelcase_2.graphql | 10 + .../test_similar_field_name_1.graphql | 30 ++ .../test_similar_field_name_2.graphql | 20 + .../test_custom_enum_1.graphql | 23 ++ .../test_custom_enum_2.graphql | 18 + tests/test_annotation_corner_cases.py | 127 ++++++ tests/test_annotation_corner_cases_v1.py | 127 ++++++ tests/test_custom_enum.py | 36 ++ tests/util.py | 42 ++ 89 files changed, 2110 insertions(+), 1845 deletions(-) create mode 100644 graphene_federation/appolo_versions/__init__.py create mode 100644 graphene_federation/appolo_versions/spec/federation-v1.0.graphql create mode 100644 graphene_federation/appolo_versions/spec/federation-v2.0.graphql create mode 100644 graphene_federation/appolo_versions/spec/federation-v2.1.graphql create mode 100644 graphene_federation/appolo_versions/spec/federation-v2.2.graphql create mode 100644 graphene_federation/appolo_versions/spec/federation-v2.3.graphql create mode 100644 graphene_federation/appolo_versions/v1_0.py create mode 100644 graphene_federation/appolo_versions/v2_0.py create mode 100644 graphene_federation/appolo_versions/v2_1.py create mode 100644 graphene_federation/appolo_versions/v2_2.py create mode 100644 graphene_federation/appolo_versions/v2_3.py create mode 100644 graphene_federation/appolo_versions/version.py delete mode 100644 graphene_federation/compose_directive.py create mode 100644 graphene_federation/directives/__init__.py create mode 100644 graphene_federation/directives/extends.py create mode 100644 graphene_federation/directives/external.py create mode 100644 graphene_federation/directives/inaccessible.py create mode 100644 graphene_federation/directives/interface_object.py create mode 100644 graphene_federation/directives/key.py create mode 100644 graphene_federation/directives/override.py create mode 100644 graphene_federation/directives/provides.py create mode 100644 graphene_federation/directives/requires.py create mode 100644 graphene_federation/directives/shareable.py create mode 100644 graphene_federation/directives/tag.py create mode 100644 graphene_federation/directives/utils.py delete mode 100644 graphene_federation/extend.py delete mode 100644 graphene_federation/external.py create mode 100644 graphene_federation/federation_directive.py delete mode 100644 graphene_federation/inaccessible.py delete mode 100644 graphene_federation/override.py delete mode 100644 graphene_federation/provides.py delete mode 100644 graphene_federation/requires.py create mode 100644 graphene_federation/scalars/__init__.py rename graphene_federation/{types.py => scalars/_any.py} (80%) create mode 100644 graphene_federation/scalars/field_set_v1.py create mode 100644 graphene_federation/scalars/field_set_v2.py create mode 100644 graphene_federation/scalars/link_import.py create mode 100644 graphene_federation/scalars/link_purpose.py create mode 100644 graphene_federation/schema_directives/__init__.py create mode 100644 graphene_federation/schema_directives/compose_directive.py create mode 100644 graphene_federation/schema_directives/link_directive.py delete mode 100644 graphene_federation/shareable.py delete mode 100644 graphene_federation/tag.py delete mode 100644 graphene_federation/tests/__init__.py delete mode 100644 graphene_federation/tests/test_annotation_corner_cases.py delete mode 100644 graphene_federation/tests/test_annotation_corner_cases_v1.py delete mode 100644 graphene_federation/tests/test_custom_enum.py delete mode 100644 graphene_federation/tests/test_entity.py delete mode 100644 graphene_federation/tests/test_entity_v1.py delete mode 100644 graphene_federation/tests/test_extend.py create mode 100644 graphene_federation/transform/__init__.py create mode 100644 graphene_federation/transform/field_set_case_transform.py create mode 100644 graphene_federation/validators/__init__.py create mode 100644 graphene_federation/validators/extends.py create mode 100644 graphene_federation/validators/key.py create mode 100644 graphene_federation/validators/requires.py create mode 100644 graphene_federation/validators/utils.py create mode 100644 tests/gql/test_annotation_corner_cases/test_annotate_object_with_meta_name_1.graphql create mode 100644 tests/gql/test_annotation_corner_cases/test_annotate_object_with_meta_name_2.graphql create mode 100644 tests/gql/test_annotation_corner_cases/test_annotated_field_also_used_in_filter_1.graphql create mode 100644 tests/gql/test_annotation_corner_cases/test_annotated_field_also_used_in_filter_2.graphql create mode 100644 tests/gql/test_annotation_corner_cases/test_camel_case_field_name_1.graphql create mode 100644 tests/gql/test_annotation_corner_cases/test_camel_case_field_name_2.graphql create mode 100644 tests/gql/test_annotation_corner_cases/test_camel_case_field_name_without_auto_camelcase_1.graphql create mode 100644 tests/gql/test_annotation_corner_cases/test_camel_case_field_name_without_auto_camelcase_2.graphql create mode 100644 tests/gql/test_annotation_corner_cases/test_similar_field_name_1.graphql create mode 100644 tests/gql/test_annotation_corner_cases/test_similar_field_name_2.graphql create mode 100644 tests/gql/test_annotation_corner_cases_v1/test_annotate_object_with_meta_name_1.graphql create mode 100644 tests/gql/test_annotation_corner_cases_v1/test_annotate_object_with_meta_name_2.graphql create mode 100644 tests/gql/test_annotation_corner_cases_v1/test_annotated_field_also_used_in_filter_1.graphql create mode 100644 tests/gql/test_annotation_corner_cases_v1/test_annotated_field_also_used_in_filter_2.graphql create mode 100644 tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_1.graphql create mode 100644 tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_2.graphql create mode 100644 tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_without_auto_camelcase_1.graphql create mode 100644 tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_without_auto_camelcase_2.graphql create mode 100644 tests/gql/test_annotation_corner_cases_v1/test_similar_field_name_1.graphql create mode 100644 tests/gql/test_annotation_corner_cases_v1/test_similar_field_name_2.graphql create mode 100644 tests/gql/test_custom_enum/test_custom_enum_1.graphql create mode 100644 tests/gql/test_custom_enum/test_custom_enum_2.graphql create mode 100644 tests/test_annotation_corner_cases.py create mode 100644 tests/test_annotation_corner_cases_v1.py create mode 100644 tests/test_custom_enum.py create mode 100644 tests/util.py diff --git a/graphene_federation/__init__.py b/graphene_federation/__init__.py index 73786d0..b35a0e6 100644 --- a/graphene_federation/__init__.py +++ b/graphene_federation/__init__.py @@ -1,10 +1,40 @@ +from graphene_directives import DirectiveLocation + +from .appolo_versions import FederationVersion, LATEST_VERSION +from .directives import ( + extends, + external, + inaccessible, + interface_object, + key, + override, + provides, + requires, + shareable, + tag, +) +from .federation_directive import FederationDirective from .main import build_schema -from .entity import key -from .extend import extend -from .external import external -from .requires import requires -from .shareable import shareable -from .inaccessible import inaccessible -from .provides import provides -from .override import override -from .compose_directive import mark_composable, is_composable +from .schema_directives import compose_directive, link_directive +from .service import get_sdl + +__all__ = [ + "FederationVersion", + "LATEST_VERSION", + "build_schema", + "FederationDirective", + "DirectiveLocation", + "extends", + "external", + "inaccessible", + "interface_object", + "key", + "override", + "provides", + "requires", + "shareable", + "tag", + "compose_directive", + "link_directive", + "get_sdl", +] diff --git a/graphene_federation/appolo_versions/__init__.py b/graphene_federation/appolo_versions/__init__.py new file mode 100644 index 0000000..b2d4156 --- /dev/null +++ b/graphene_federation/appolo_versions/__init__.py @@ -0,0 +1,41 @@ +from graphql import GraphQLDirective + +from .v1_0 import get_directives as get_directives_v1_0 +from .v2_0 import get_directives as get_directives_v2_0 +from .v2_1 import get_directives as get_directives_v2_1 +from .v2_2 import get_directives as get_directives_v2_2 +from .v2_3 import get_directives as get_directives_v2_3 +from .version import FederationVersion + +LATEST_VERSION = FederationVersion.VERSION_2_3 + + +def get_directives_based_on_version( + federation_version: FederationVersion, +) -> dict[str, GraphQLDirective]: + match federation_version: + case FederationVersion.VERSION_1_0: + return get_directives_v1_0() + case FederationVersion.VERSION_2_0: + return get_directives_v2_0() + case FederationVersion.VERSION_2_1: + return get_directives_v2_1() + case FederationVersion.VERSION_2_2: + return get_directives_v2_2() + case FederationVersion.VERSION_2_3: + return get_directives_v2_3() + case _: + return get_directives_v2_3() + + +def get_directive_from_name( + directive_name: str, federation_version: FederationVersion +) -> GraphQLDirective: + directive = get_directives_based_on_version(federation_version).get( + directive_name, None + ) + if directive is None: + raise ValueError( + f"@{directive_name} not supported on federation version {federation_version}" + ) + return directive diff --git a/graphene_federation/appolo_versions/spec/federation-v1.0.graphql b/graphene_federation/appolo_versions/spec/federation-v1.0.graphql new file mode 100644 index 0000000..19d492f --- /dev/null +++ b/graphene_federation/appolo_versions/spec/federation-v1.0.graphql @@ -0,0 +1,9 @@ +directive @key(fields: _FieldSet!) repeatable on OBJECT | INTERFACE +directive @requires(fields: _FieldSet!) on FIELD_DEFINITION +directive @provides(fields: _FieldSet!) on FIELD_DEFINITION +directive @external on FIELD_DEFINITION +scalar _FieldSet + +# this is an optional directive +# used in frameworks that don't natively support GraphQL extend syntax +directive @extends on OBJECT | INTERFACE diff --git a/graphene_federation/appolo_versions/spec/federation-v2.0.graphql b/graphene_federation/appolo_versions/spec/federation-v2.0.graphql new file mode 100644 index 0000000..494e97a --- /dev/null +++ b/graphene_federation/appolo_versions/spec/federation-v2.0.graphql @@ -0,0 +1,30 @@ +directive @key(fields: FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE +directive @requires(fields: FieldSet!) on FIELD_DEFINITION +directive @provides(fields: FieldSet!) on FIELD_DEFINITION +directive @external on OBJECT | FIELD_DEFINITION +directive @shareable on FIELD_DEFINITION | OBJECT +directive @extends on OBJECT | INTERFACE +directive @override(from: String!) on FIELD_DEFINITION +directive @inaccessible on + | FIELD_DEFINITION + | OBJECT + | INTERFACE + | UNION + | ENUM + | ENUM_VALUE + | SCALAR + | INPUT_OBJECT + | INPUT_FIELD_DEFINITION + | ARGUMENT_DEFINITION +directive @tag(name: String!) repeatable on + | FIELD_DEFINITION + | INTERFACE + | OBJECT + | UNION + | ARGUMENT_DEFINITION + | SCALAR + | ENUM + | ENUM_VALUE + | INPUT_OBJECT + | INPUT_FIELD_DEFINITION +scalar FieldSet \ No newline at end of file diff --git a/graphene_federation/appolo_versions/spec/federation-v2.1.graphql b/graphene_federation/appolo_versions/spec/federation-v2.1.graphql new file mode 100644 index 0000000..d07dfaf --- /dev/null +++ b/graphene_federation/appolo_versions/spec/federation-v2.1.graphql @@ -0,0 +1,31 @@ +directive @composeDirective(name: String!) repeatable on SCHEMA +directive @extends on OBJECT | INTERFACE +directive @external on OBJECT | FIELD_DEFINITION +directive @key(fields: FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE +directive @inaccessible on + | FIELD_DEFINITION + | OBJECT + | INTERFACE + | UNION + | ENUM + | ENUM_VALUE + | SCALAR + | INPUT_OBJECT + | INPUT_FIELD_DEFINITION + | ARGUMENT_DEFINITION +directive @override(from: String!) on FIELD_DEFINITION +directive @provides(fields: FieldSet!) on FIELD_DEFINITION +directive @requires(fields: FieldSet!) on FIELD_DEFINITION +directive @shareable on FIELD_DEFINITION | OBJECT +directive @tag(name: String!) repeatable on + | FIELD_DEFINITION + | INTERFACE + | OBJECT + | UNION + | ARGUMENT_DEFINITION + | SCALAR + | ENUM + | ENUM_VALUE + | INPUT_OBJECT + | INPUT_FIELD_DEFINITION +scalar FieldSet diff --git a/graphene_federation/appolo_versions/spec/federation-v2.2.graphql b/graphene_federation/appolo_versions/spec/federation-v2.2.graphql new file mode 100644 index 0000000..b75272b --- /dev/null +++ b/graphene_federation/appolo_versions/spec/federation-v2.2.graphql @@ -0,0 +1,31 @@ +directive @composeDirective(name: String!) repeatable on SCHEMA +directive @extends on OBJECT | INTERFACE +directive @external on OBJECT | FIELD_DEFINITION +directive @key(fields: FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE +directive @inaccessible on + | FIELD_DEFINITION + | OBJECT + | INTERFACE + | UNION + | ENUM + | ENUM_VALUE + | SCALAR + | INPUT_OBJECT + | INPUT_FIELD_DEFINITION + | ARGUMENT_DEFINITION +directive @override(from: String!) on FIELD_DEFINITION +directive @provides(fields: FieldSet!) on FIELD_DEFINITION +directive @requires(fields: FieldSet!) on FIELD_DEFINITION +directive @shareable repeatable on FIELD_DEFINITION | OBJECT +directive @tag(name: String!) repeatable on + | FIELD_DEFINITION + | INTERFACE + | OBJECT + | UNION + | ARGUMENT_DEFINITION + | SCALAR + | ENUM + | ENUM_VALUE + | INPUT_OBJECT + | INPUT_FIELD_DEFINITION +scalar FieldSet diff --git a/graphene_federation/appolo_versions/spec/federation-v2.3.graphql b/graphene_federation/appolo_versions/spec/federation-v2.3.graphql new file mode 100644 index 0000000..8bf0015 --- /dev/null +++ b/graphene_federation/appolo_versions/spec/federation-v2.3.graphql @@ -0,0 +1,32 @@ +directive @composeDirective(name: String!) repeatable on SCHEMA +directive @extends on OBJECT | INTERFACE +directive @external on OBJECT | FIELD_DEFINITION +directive @key(fields: FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE +directive @inaccessible on + | FIELD_DEFINITION + | OBJECT + | INTERFACE + | UNION + | ENUM + | ENUM_VALUE + | SCALAR + | INPUT_OBJECT + | INPUT_FIELD_DEFINITION + | ARGUMENT_DEFINITION +directive @interfaceObject on OBJECT +directive @override(from: String!) on FIELD_DEFINITION +directive @provides(fields: FieldSet!) on FIELD_DEFINITION +directive @requires(fields: FieldSet!) on FIELD_DEFINITION +directive @shareable repeatable on FIELD_DEFINITION | OBJECT +directive @tag(name: String!) repeatable on + | FIELD_DEFINITION + | INTERFACE + | OBJECT + | UNION + | ARGUMENT_DEFINITION + | SCALAR + | ENUM + | ENUM_VALUE + | INPUT_OBJECT + | INPUT_FIELD_DEFINITION +scalar FieldSet diff --git a/graphene_federation/appolo_versions/v1_0.py b/graphene_federation/appolo_versions/v1_0.py new file mode 100644 index 0000000..65079ab --- /dev/null +++ b/graphene_federation/appolo_versions/v1_0.py @@ -0,0 +1,76 @@ +from graphene_directives import CustomDirective, DirectiveLocation +from graphql import GraphQLArgument, GraphQLDirective, GraphQLNonNull + +from ..scalars import _FieldSet +from ..transform import field_set_case_transform +from ..validators import validate_key, validate_requires + +key_directive = CustomDirective( + name="key", + locations=[ + DirectiveLocation.OBJECT, + DirectiveLocation.INTERFACE, + ], + args={"fields": GraphQLArgument(GraphQLNonNull(_FieldSet))}, + description="Federation @key directive", + is_repeatable=True, + add_definition_to_schema=False, + non_field_validator=validate_key, + input_transform=field_set_case_transform, +) + +requires_directive = CustomDirective( + name="requires", + locations=[ + DirectiveLocation.FIELD_DEFINITION, + ], + args={"fields": GraphQLArgument(GraphQLNonNull(_FieldSet))}, + description="Federation @requires directive", + is_repeatable=True, + add_definition_to_schema=False, + field_validator=validate_requires, + input_transform=field_set_case_transform, +) + + +provides_directive = CustomDirective( + name="provides", + locations=[ + DirectiveLocation.FIELD_DEFINITION, + ], + args={"fields": GraphQLArgument(GraphQLNonNull(_FieldSet))}, + description="Federation @provides directive", + add_definition_to_schema=False, +) + +external_directive = CustomDirective( + name="external", + locations=[ + DirectiveLocation.FIELD_DEFINITION, + ], + description="Federation @external directive", + add_definition_to_schema=False, +) + +extends_directive = CustomDirective( + name="extends", + locations=[ + DirectiveLocation.OBJECT, + DirectiveLocation.INTERFACE, + ], + description="Federation @extends directive", + add_definition_to_schema=False, +) + + +def get_directives() -> dict[str, GraphQLDirective]: + return { + directive.name: directive + for directive in [ + key_directive, + requires_directive, + provides_directive, + external_directive, + extends_directive, + ] + } diff --git a/graphene_federation/appolo_versions/v2_0.py b/graphene_federation/appolo_versions/v2_0.py new file mode 100644 index 0000000..a703562 --- /dev/null +++ b/graphene_federation/appolo_versions/v2_0.py @@ -0,0 +1,143 @@ +from graphene_directives import CustomDirective, DirectiveLocation +from graphql import ( + GraphQLArgument, + GraphQLBoolean, + GraphQLDirective, + GraphQLNonNull, + GraphQLString, +) + +from .v1_0 import extends_directive +from ..scalars import FieldSet +from ..transform import field_set_case_transform +from ..validators import validate_key, validate_requires + +key_directive = CustomDirective( + name="key", + locations=[ + DirectiveLocation.OBJECT, + DirectiveLocation.INTERFACE, + ], + args={ + "fields": GraphQLArgument(GraphQLNonNull(FieldSet)), + # Changed + "resolvable": GraphQLArgument(GraphQLBoolean, default_value=True), + }, + description="Federation @key directive", + is_repeatable=True, + add_definition_to_schema=False, + non_field_validator=validate_key, + input_transform=field_set_case_transform, +) + +requires_directive = CustomDirective( + name="requires", + locations=[ + DirectiveLocation.FIELD_DEFINITION, + ], + args={"fields": GraphQLArgument(GraphQLNonNull(FieldSet))}, + description="Federation @requires directive", + is_repeatable=True, + add_definition_to_schema=False, + field_validator=validate_requires, + input_transform=field_set_case_transform, +) + + +provides_directive = CustomDirective( + name="provides", + locations=[ + DirectiveLocation.FIELD_DEFINITION, + ], + args={"fields": GraphQLArgument(GraphQLNonNull(FieldSet))}, + description="Federation @provides directive", + add_definition_to_schema=False, +) + + +external_directive = CustomDirective( + name="external", + locations=[ + DirectiveLocation.OBJECT, + DirectiveLocation.FIELD_DEFINITION, + ], + description="Federation @external directive", + add_definition_to_schema=False, +) + + +shareable_directive = CustomDirective( + name="shareable", + locations=[ + DirectiveLocation.FIELD_DEFINITION, + DirectiveLocation.OBJECT, + ], + description="Federation @shareable directive", + add_definition_to_schema=False, +) + + +override_directive = CustomDirective( + name="override", + locations=[ + DirectiveLocation.FIELD_DEFINITION, + ], + args={ + "from": GraphQLArgument(GraphQLNonNull(GraphQLString)), + }, + description="Federation @override directive", + add_definition_to_schema=False, +) + +inaccessible_directive = CustomDirective( + name="inaccessible", + locations=[ + DirectiveLocation.FIELD_DEFINITION, + DirectiveLocation.OBJECT, + DirectiveLocation.INTERFACE, + DirectiveLocation.UNION, + DirectiveLocation.ENUM, + DirectiveLocation.ENUM_VALUE, + DirectiveLocation.SCALAR, + DirectiveLocation.INPUT_OBJECT, + DirectiveLocation.INPUT_FIELD_DEFINITION, + DirectiveLocation.ARGUMENT_DEFINITION, + ], + description="Federation @inaccessible directive", + add_definition_to_schema=False, +) + +tag_directive = CustomDirective( + name="tag", + locations=[ + DirectiveLocation.FIELD_DEFINITION, + DirectiveLocation.INTERFACE, + DirectiveLocation.OBJECT, + DirectiveLocation.UNION, + DirectiveLocation.ARGUMENT_DEFINITION, + DirectiveLocation.SCALAR, + DirectiveLocation.ENUM, + DirectiveLocation.ENUM_VALUE, + DirectiveLocation.INPUT_OBJECT, + DirectiveLocation.INPUT_FIELD_DEFINITION, + ], + description="Federation @tag directive", + add_definition_to_schema=False, +) + + +def get_directives() -> dict[str, GraphQLDirective]: + return { + directive.name: directive + for directive in [ + key_directive, + requires_directive, + provides_directive, + external_directive, + shareable_directive, + extends_directive, + override_directive, + inaccessible_directive, + tag_directive, + ] + } diff --git a/graphene_federation/appolo_versions/v2_1.py b/graphene_federation/appolo_versions/v2_1.py new file mode 100644 index 0000000..18bc3f5 --- /dev/null +++ b/graphene_federation/appolo_versions/v2_1.py @@ -0,0 +1,27 @@ +from graphene_directives import CustomDirective, DirectiveLocation +from graphql import ( + GraphQLArgument, + GraphQLDirective, + GraphQLNonNull, + GraphQLString, +) + +from .v2_0 import get_directives as get_directives_v2_0 + +compose_directive = CustomDirective( + name="composeDirective", + locations=[ + DirectiveLocation.SCHEMA, + ], + args={ + "name": GraphQLArgument(GraphQLNonNull(GraphQLString)), + }, + description="Federation @composeDirective directive", + add_definition_to_schema=False, +) + + +def get_directives() -> dict[str, GraphQLDirective]: + directives = get_directives_v2_0() + directives.update({directive.name: directive for directive in [compose_directive]}) + return directives diff --git a/graphene_federation/appolo_versions/v2_2.py b/graphene_federation/appolo_versions/v2_2.py new file mode 100644 index 0000000..e657351 --- /dev/null +++ b/graphene_federation/appolo_versions/v2_2.py @@ -0,0 +1,24 @@ +from graphene_directives import CustomDirective, DirectiveLocation +from graphql import GraphQLDirective + +from .v2_0 import shareable_directive as sharable_directive_v2_0 +from .v2_1 import get_directives as get_directives_v2_1 + +shareable_directive = CustomDirective( + name="shareable", + locations=[ + DirectiveLocation.FIELD_DEFINITION, + DirectiveLocation.OBJECT, + ], + description="Federation @shareable directive", + add_definition_to_schema=False, + is_repeatable=True, # Changed +) + + +def get_directives() -> dict[str, GraphQLDirective]: + directives = get_directives_v2_1() + directives.update( + {directive.name: directive for directive in [sharable_directive_v2_0]} + ) + return directives diff --git a/graphene_federation/appolo_versions/v2_3.py b/graphene_federation/appolo_versions/v2_3.py new file mode 100644 index 0000000..e179c9a --- /dev/null +++ b/graphene_federation/appolo_versions/v2_3.py @@ -0,0 +1,22 @@ +from graphene_directives import CustomDirective, DirectiveLocation +from graphql import GraphQLDirective + +from .v2_2 import get_directives as get_directives_v2_2 + +interface_object_directive = CustomDirective( + name="interfaceObject", + locations=[ + DirectiveLocation.OBJECT, + ], + description="Federation @interfaceObject directive", + add_definition_to_schema=False, + is_repeatable=True, +) + + +def get_directives() -> dict[str, GraphQLDirective]: + directives = get_directives_v2_2() + directives.update( + {directive.name: directive for directive in [interface_object_directive]} + ) + return directives diff --git a/graphene_federation/appolo_versions/version.py b/graphene_federation/appolo_versions/version.py new file mode 100644 index 0000000..bd5d811 --- /dev/null +++ b/graphene_federation/appolo_versions/version.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class FederationVersion(Enum): + VERSION_1_0 = "1.0" + VERSION_2_0 = "2.0" + VERSION_2_1 = "2.1" + VERSION_2_2 = "2.2" + VERSION_2_3 = "2.3" diff --git a/graphene_federation/compose_directive.py b/graphene_federation/compose_directive.py deleted file mode 100644 index 824d697..0000000 --- a/graphene_federation/compose_directive.py +++ /dev/null @@ -1,64 +0,0 @@ -from typing import Optional - -from graphql import GraphQLDirective - - -def is_composable(directive: GraphQLDirective) -> bool: - """ - Checks if the directive will be composed to supergraph. - Validates the presence of _compose_import_url attribute - """ - return hasattr(directive, "_compose_import_url") - - -def mark_composable( - directive: GraphQLDirective, import_url: str, import_as: Optional[str] = None -) -> GraphQLDirective: - """ - Marks directive with _compose_import_url and _compose_import_as - Enables Identification of directives which are to be composed to supergraph - """ - setattr(directive, "_compose_import_url", import_url) - if import_as: - setattr(directive, "_compose_import_as", import_as) - return directive - - -def compose_directive_schema_extensions(directives: list[GraphQLDirective]): - """ - Generates schema extends string for ComposeDirective - """ - link_schema = "" - compose_directive_schema = "" - # Using dictionary to generate cleaner schema when multiple directives imports from same URL. - links: dict = {} - - for directive in directives: - # TODO: Replace with walrus operator when dropping Python 3.8 support - if hasattr(directive, "_compose_import_url"): - compose_import_url = getattr(directive, "_compose_import_url") - if hasattr(directive, "_compose_import_as"): - compose_import_as = getattr(directive, "_compose_import_as") - import_value = ( - f'{{ name: "@{directive.name}, as: "@{compose_import_as}" }}' - ) - imported_name = compose_import_as - else: - import_value = f'"@{directive.name}"' - imported_name = directive.name - - import_url = compose_import_url - - if links.get(import_url): - links[import_url] = links[import_url].append(import_value) - else: - links[import_url] = [import_value] - - compose_directive_schema += ( - f' @composeDirective(name: "@{imported_name}")\n' - ) - - for import_url in links: - link_schema += f' @link(url: "{import_url}", import: [{",".join(value for value in links[import_url])}])\n' - - return link_schema + compose_directive_schema diff --git a/graphene_federation/directives/__init__.py b/graphene_federation/directives/__init__.py new file mode 100644 index 0000000..e57b374 --- /dev/null +++ b/graphene_federation/directives/__init__.py @@ -0,0 +1,10 @@ +from .extends import extends +from .external import external +from .inaccessible import inaccessible +from .interface_object import interface_object +from .key import key +from .override import override +from .provides import provides +from .requires import requires +from .shareable import shareable +from .tag import tag diff --git a/graphene_federation/directives/extends.py b/graphene_federation/directives/extends.py new file mode 100644 index 0000000..c4ed831 --- /dev/null +++ b/graphene_federation/directives/extends.py @@ -0,0 +1,18 @@ +from typing import Any, Callable + +from graphene_directives import directive_decorator + +from ..appolo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name + + +def extends( + non_field: Any = None, + *, + federation_version: FederationVersion = LATEST_VERSION, +) -> Callable: + directive = get_directive_from_name("extends", federation_version) + + if non_field: + return directive_decorator(directive)(field=None)(non_field) + + return directive_decorator(directive) diff --git a/graphene_federation/directives/external.py b/graphene_federation/directives/external.py new file mode 100644 index 0000000..aa1eab8 --- /dev/null +++ b/graphene_federation/directives/external.py @@ -0,0 +1,18 @@ +from typing import Any, Callable + +from graphene_directives import directive_decorator + +from .utils import is_non_field +from ..appolo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name + + +def external( + field: Any = None, + *, + federation_version: FederationVersion = LATEST_VERSION, +) -> Callable: + directive = get_directive_from_name("external", federation_version) + decorator = directive_decorator(directive) + return ( + decorator(field=None)(field) if is_non_field(field) else decorator(field=field) + ) diff --git a/graphene_federation/directives/inaccessible.py b/graphene_federation/directives/inaccessible.py new file mode 100644 index 0000000..e7168d1 --- /dev/null +++ b/graphene_federation/directives/inaccessible.py @@ -0,0 +1,18 @@ +from typing import Any, Callable + +from graphene_directives import directive_decorator + +from .utils import is_non_field +from ..appolo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name + + +def inaccessible( + field: Any = None, + *, + federation_version: FederationVersion = LATEST_VERSION, +) -> Callable: + directive = get_directive_from_name("inaccessible", federation_version) + decorator = directive_decorator(directive) + return ( + decorator(field=None)(field) if is_non_field(field) else decorator(field=field) + ) diff --git a/graphene_federation/directives/interface_object.py b/graphene_federation/directives/interface_object.py new file mode 100644 index 0000000..c36bc66 --- /dev/null +++ b/graphene_federation/directives/interface_object.py @@ -0,0 +1,14 @@ +from typing import Callable + +from graphene_directives import directive_decorator + +from ..appolo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name + + +def interface_object( + federation_version: FederationVersion = LATEST_VERSION, +) -> Callable: + directive = get_directive_from_name( + "interfaceObject", federation_version=federation_version + ) + return directive_decorator(directive) diff --git a/graphene_federation/directives/key.py b/graphene_federation/directives/key.py new file mode 100644 index 0000000..677fd6f --- /dev/null +++ b/graphene_federation/directives/key.py @@ -0,0 +1,15 @@ +from typing import Callable + +from graphene_directives import directive_decorator + +from ..appolo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name + + +def key( + fields: str, + resolvable: bool = None, + *, + federation_version: FederationVersion = LATEST_VERSION, +) -> Callable: + directive = get_directive_from_name("key", federation_version) + return directive_decorator(directive)(fields=fields, resolvable=resolvable) diff --git a/graphene_federation/directives/override.py b/graphene_federation/directives/override.py new file mode 100644 index 0000000..a30eea3 --- /dev/null +++ b/graphene_federation/directives/override.py @@ -0,0 +1,16 @@ +from typing import Any, Callable + +from graphene_directives import directive_decorator + +from ..appolo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name + + +def override( + field: Any, + from_: str, + federation_version: FederationVersion = LATEST_VERSION, +) -> Callable: + directive = get_directive_from_name( + "override", federation_version=federation_version + ) + return directive_decorator(directive)(field=field, **{"from": from_}) diff --git a/graphene_federation/directives/provides.py b/graphene_federation/directives/provides.py new file mode 100644 index 0000000..243308a --- /dev/null +++ b/graphene_federation/directives/provides.py @@ -0,0 +1,18 @@ +from typing import Any, Callable, Union + +from graphene_directives import directive_decorator + +from ..appolo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name + + +def provides( + field: Any, + fields: Union[str, list[str]], + federation_version: FederationVersion = LATEST_VERSION, +) -> Callable: + directive = get_directive_from_name( + "provides", federation_version=federation_version + ) + return directive_decorator(directive)( + field=field, fields=fields if isinstance(fields, str) else " ".join(fields) + ) diff --git a/graphene_federation/directives/requires.py b/graphene_federation/directives/requires.py new file mode 100644 index 0000000..c2b14c8 --- /dev/null +++ b/graphene_federation/directives/requires.py @@ -0,0 +1,39 @@ +from typing import Any, Callable, Union + +from graphene_directives import directive_decorator + +from ..appolo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name +from ..validators import build_ast + + +def add_typename(fields: dict, level: int = 0) -> str: + new_fields = [] + if level != 0: + new_fields.append("__typename") + for field, value in fields.items(): + if "typename" in field.lower(): + continue + elif len(value) == 0: + new_fields.append(field) + else: + new_fields.extend([field, "{", add_typename(value, level + 1), "}"]) + + return " ".join(new_fields) + + +def requires( + field: Any, + fields: Union[str, list[str]], + federation_version: FederationVersion = LATEST_VERSION, +) -> Callable: + directive = get_directive_from_name("requires", federation_version) + fields = add_typename( + build_ast( + input_str=fields if isinstance(fields, str) else " ".join(fields), + valid_special_chars='_()"', + ) + ) + return directive_decorator(directive)( + field=field, + fields=fields, + ) diff --git a/graphene_federation/directives/shareable.py b/graphene_federation/directives/shareable.py new file mode 100644 index 0000000..2c2fa8e --- /dev/null +++ b/graphene_federation/directives/shareable.py @@ -0,0 +1,19 @@ +from typing import Any, Callable + +from graphene_directives import directive_decorator + +from .utils import is_non_field +from ..appolo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name + + +def shareable( + field: Any = None, + federation_version: FederationVersion = LATEST_VERSION, +) -> Callable: + directive = get_directive_from_name( + "shareable", federation_version=federation_version + ) + decorator = directive_decorator(directive) + return ( + decorator(field=None)(field) if is_non_field(field) else decorator(field=field) + ) diff --git a/graphene_federation/directives/tag.py b/graphene_federation/directives/tag.py new file mode 100644 index 0000000..566f53f --- /dev/null +++ b/graphene_federation/directives/tag.py @@ -0,0 +1,17 @@ +from typing import Any, Callable + +from graphene_directives import directive_decorator + +from .utils import is_non_field +from ..appolo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name + + +def tag( + field: Any = None, + federation_version: FederationVersion = LATEST_VERSION, +) -> Callable: + directive = get_directive_from_name("tag", federation_version=federation_version) + decorator = directive_decorator(directive) + return ( + decorator(field=None)(field) if is_non_field(field) else decorator(field=field) + ) diff --git a/graphene_federation/directives/utils.py b/graphene_federation/directives/utils.py new file mode 100644 index 0000000..5ccf8e4 --- /dev/null +++ b/graphene_federation/directives/utils.py @@ -0,0 +1,20 @@ +from typing import Any + +from graphene import Enum, InputObjectType, Interface, ObjectType, Scalar, Union + + +def is_non_field(field: Any): + try: + if ( + issubclass(field, ObjectType) + or issubclass(field, Interface) + or issubclass(field, InputObjectType) + or issubclass(field, Enum) + or issubclass(field, Union) + or issubclass(field, Scalar) + ): + return True + else: + return False + except TypeError: + return False diff --git a/graphene_federation/entity.py b/graphene_federation/entity.py index e99cff9..7c7adcc 100644 --- a/graphene_federation/entity.py +++ b/graphene_federation/entity.py @@ -1,27 +1,31 @@ from __future__ import annotations -import collections.abc -from typing import Any, Callable, Dict +from typing import Any, Callable +from typing import Dict, Type from graphene import Field, List, NonNull, ObjectType, Union -from graphene.types.schema import Schema +from graphene import Schema from graphene.types.schema import TypeMap +from graphene.utils.str_converters import to_camel_case +from graphene_directives.utils import has_non_field_attribute -from .types import _Any -from .utils import ( - check_fields_exist_on_type, - field_name_to_type_attribute, - is_valid_compound_key, -) +from .appolo_versions import LATEST_VERSION, get_directive_from_name +from .scalars import _Any -def update(d, u): - for k, v in u.items(): - if isinstance(v, collections.abc.Mapping): - d[k] = update(d.get(k, {}), v) - else: - d[k] = v - return d +def field_name_to_type_attribute(schema: Schema, model: Any) -> Callable[[str], str]: + """ + Create field name conversion method (from schema name to actual graphene_type attribute name). + """ + field_names = {} + if schema.auto_camelcase: + field_names = { + to_camel_case(attr_name): attr_name + for attr_name in getattr(model._meta, "fields", []) + } + return lambda schema_field_name: field_names.get( + schema_field_name, schema_field_name + ) def get_entities(schema: Schema) -> Dict[str, Any]: @@ -32,25 +36,27 @@ def get_entities(schema: Schema) -> Dict[str, Any]: """ type_map: TypeMap = schema.graphql_schema.type_map entities = {} + key_directive = get_directive_from_name("key", LATEST_VERSION) + extends_directive = get_directive_from_name("extends", LATEST_VERSION) for type_name, type_ in type_map.items(): if not hasattr(type_, "graphene_type"): continue - if getattr(type_.graphene_type, "_keys", None): - entities[type_name] = type_.graphene_type - - # Validation for compound keys - key_str = " ".join(type_.graphene_type._keys) - type_name = type_.graphene_type._meta.name - if "{" in key_str: # checking for subselection to identify compound key - assert is_valid_compound_key( - type_name, key_str, schema - ), f'Invalid compound key definition for type "{type_name}"' + + graphene_type = type_.graphene_type + is_entity = any( + [ + has_non_field_attribute(graphene_type, key_directive), + has_non_field_attribute(graphene_type, extends_directive), + ] + ) + if is_entity: + entities[type_name] = graphene_type return entities -def get_entity_cls(entities: Dict[str, Any]) -> Union: +def get_entity_cls(entities: Dict[str, Any]) -> Type[Union]: """ - Create _Entity type which is a union of all the entities types. + Create _Entity type which is a union of all the entity types. """ class _Entity(Union): @@ -136,40 +142,12 @@ def resolve_entities(self, info, representations, sub_field_resolution=False): resolver = getattr( model, "_%s__resolve_reference" % model.__name__, None ) or getattr(model, "_resolve_reference", None) + if resolver and not sub_field_resolution: model_instance = resolver(model_instance, info) entities.append(model_instance) + return entities return EntityQuery - - -def key(fields: str, resolvable: bool = True) -> Callable: - """ - Take as input a field that should be used as key for that entity. - See specification: https://www.apollographql.com/docs/federation/federation-spec/#key - """ - - def decorator(type_): - # Check the provided fields actually exist on the Type. - if " " not in fields: - assert ( - fields in type_._meta.fields - ), f'Field "{fields}" does not exist on type "{type_._meta.name}"' - if "{" not in fields: - # Skip valid fields check if the key is a compound key. The validation for compound keys - # is done on calling get_entities() - fields_set = set(fields.split(" ")) - assert check_fields_exist_on_type( - fields=fields_set, type_=type_ - ), f'Field "{fields}" does not exist on type "{type_._meta.name}"' - - keys = getattr(type_, "_keys", []) - keys.append(fields) - setattr(type_, "_keys", keys) - setattr(type_, "_resolvable", resolvable) - - return type_ - - return decorator diff --git a/graphene_federation/extend.py b/graphene_federation/extend.py deleted file mode 100644 index c6fb972..0000000 --- a/graphene_federation/extend.py +++ /dev/null @@ -1,62 +0,0 @@ -from typing import Any, Callable, Dict - -from graphene import Schema - -from graphene_federation.utils import check_fields_exist_on_type, is_valid_compound_key - - -def get_extended_types(schema: Schema) -> Dict[str, Any]: - """ - Find all the extended types from the schema. - They can be easily distinguished from the other type as - the `@extend` decorator adds a `_extended` attribute to them. - """ - extended_types = {} - for type_name, type_ in schema.graphql_schema.type_map.items(): - if not hasattr(type_, "graphene_type"): - continue - if getattr(type_.graphene_type, "_extended", False): - extended_types[type_name] = type_.graphene_type - - # Validation for compound keys - key_str = " ".join(type_.graphene_type._keys) - type_name = type_.graphene_type._meta.name - if "{" in key_str: # checking for subselection to identify compound key - assert is_valid_compound_key( - type_name, key_str, schema - ), f'Invalid compound key definition for type "{type_name}"' - return extended_types - - -def extend(fields: str) -> Callable: - """ - Decorator to use to extend a given type. - The field to extend must be provided as input as a string. - """ - - def decorator(type_): - assert not hasattr( - type_, "_keys" - ), "Can't extend type which is already extended or has @key" - # Check the provided fields actually exist on the Type. - - if "{" not in fields: # Check for compound keys - # Skip valid fields check if the key is a compound key. The validation for compound keys - # is done on calling get_extended_types() - fields_set = set(fields.split(" ")) - assert check_fields_exist_on_type( - fields=fields_set, type_=type_ - ), f'Field "{fields}" does not exist on type "{type_._meta.name}"' - - assert getattr(type_._meta, "description", None) is None, ( - f'Type "{type_.__name__}" has a non empty description and it is also marked with extend.' - "\nThey are mutually exclusive." - "\nSee https://github.com/graphql/graphql-js/issues/2385#issuecomment-577997521" - ) - # Set a `_keys` attribute so it will be registered as an entity - setattr(type_, "_keys", [fields]) - # Set a `_extended` attribute to be able to distinguish it from the other entities - setattr(type_, "_extended", True) - return type_ - - return decorator diff --git a/graphene_federation/external.py b/graphene_federation/external.py deleted file mode 100644 index 89b6713..0000000 --- a/graphene_federation/external.py +++ /dev/null @@ -1,41 +0,0 @@ -from graphene import Schema -from graphene.types.objecttype import ObjectTypeMeta - -from graphene_federation.utils import get_attributed_fields - - -def external(field): - """ - Mark a field as external. - """ - if isinstance(field, ObjectTypeMeta): - field._external_entity = True - else: - field._external = True - return field - - -def get_external_fields(schema: Schema) -> dict: - """ - Find all the extended types from the schema. - They can be easily distinguished from the other type as - the `@external` decorator adds a `_external` attribute to them. - """ - return get_attributed_fields(attribute="_external", schema=schema) - - -def get_external_object_types(schema: Schema) -> dict: - """ - Find all the extended object types from the schema. - They can be easily distinguished from the other type as - the `@external` decorator adds a `_external_entity` attribute to them. - """ - fields = {} - - for type_name, type_ in schema.graphql_schema.type_map.items(): - if hasattr(type_, "graphene_type") and hasattr( - type_.graphene_type, "_external_entity" - ): - fields[type_name] = type_ - - return fields diff --git a/graphene_federation/federation_directive.py b/graphene_federation/federation_directive.py new file mode 100644 index 0000000..73e1b1e --- /dev/null +++ b/graphene_federation/federation_directive.py @@ -0,0 +1,58 @@ +from typing import Any, Callable, Collection, Dict, Optional + +from graphene_directives import CustomDirective, DirectiveLocation, directive_decorator +from graphql import ( + DirectiveDefinitionNode, + GraphQLArgument, + GraphQLDirective, +) + + +class FederationDirective(GraphQLDirective): + def __init__( + self, + name: str, + spec_url: str, + locations: Collection[DirectiveLocation], + args: Optional[Dict[str, GraphQLArgument]] = None, + is_repeatable: bool = False, + description: Optional[str] = None, + extensions: Optional[Dict[str, Any]] = None, + ast_node: Optional[DirectiveDefinitionNode] = None, + add_to_schema_directives: bool = True, + ) -> None: + assert spec_url is not None, "FederationDirective requires spec_url" + self.spec_url = spec_url + self.add_to_schema_directives = add_to_schema_directives + + self.graphene_directive = CustomDirective( + name=name, + locations=locations, + args=args, + is_repeatable=is_repeatable, + description=description, + extensions=extensions, + ast_node=ast_node, + ) + parent_attributes = { + "name", + "locations", + "args", + "is_repeatable", + "description", + "extensions", + "ast_node", + } + parent_kwargs = {} + + # Copy attributes of graphene_directive + for k, v in self.graphene_directive.__dict__.items(): + if k not in parent_attributes: + setattr(self, k, v) + else: + parent_kwargs[k] = v + + super().__init__(**parent_kwargs) + + def decorator(self) -> Callable: + return directive_decorator(self.graphene_directive) diff --git a/graphene_federation/inaccessible.py b/graphene_federation/inaccessible.py deleted file mode 100644 index fffc44a..0000000 --- a/graphene_federation/inaccessible.py +++ /dev/null @@ -1,46 +0,0 @@ -from typing import Any, Dict, Optional - -from graphene import Schema - -from graphene_federation.utils import get_attributed_fields - - -def get_inaccessible_types(schema: Schema) -> Dict[str, Any]: - """ - Find all the inaccessible types from the schema. - They can be easily distinguished from the other type as - the `@inaccessible` decorator adds a `_inaccessible` attribute to them. - """ - inaccessible_types = {} - for type_name, type_ in schema.graphql_schema.type_map.items(): - if not hasattr(type_, "graphene_type"): - continue - if getattr(type_.graphene_type, "_inaccessible", False): - inaccessible_types[type_name] = type_.graphene_type - return inaccessible_types - - -def inaccessible(field: Optional[Any] = None) -> Any: - """ - Decorator to use to inaccessible a given type. - """ - - # noinspection PyProtectedMember,PyPep8Naming - def decorator(field_or_type): - # TODO Check the provided fields actually exist on the Type. - # Set a `_inaccessible` attribute to be able to distinguish it from the other entities - setattr(field_or_type, "_inaccessible", True) - return field_or_type - - if field: - return decorator(field) - return decorator - - -def get_inaccessible_fields(schema: Schema) -> dict: - """ - Find all the inacessible types from the schema. - They can be easily distinguished from the other type as - the `@inaccessible` decorator adds a `_inaccessible` attribute to them. - """ - return get_attributed_fields(attribute="_inaccessible", schema=schema) diff --git a/graphene_federation/main.py b/graphene_federation/main.py index 87ad5ca..c3f5a78 100644 --- a/graphene_federation/main.py +++ b/graphene_federation/main.py @@ -1,13 +1,30 @@ +from typing import Collection, Type, Union from typing import Optional -from graphene import Schema -from graphene import ObjectType +from graphene import ObjectType, PageInfo +from graphene_directives import ( + SchemaDirective, + build_schema as build_directive_schema, + directive_decorator, +) +from graphene_directives.schema import Schema +from . import FederationDirective +from .appolo_versions import ( + FederationVersion, + LATEST_VERSION, + get_directive_from_name, + get_directives_based_on_version, +) +from .appolo_versions.v2_1 import compose_directive as ComposeDirective from .entity import get_entity_query +from .schema_directives import compose_directive, link_directive from .service import get_service_query -def _get_query(schema: Schema, query_cls: Optional[ObjectType] = None) -> ObjectType: +def _get_query( + schema: Schema, query_cls: Optional[ObjectType] = None +) -> Type[ObjectType]: type_name = "Query" bases = [get_service_query(schema)] entity_cls = get_entity_query(schema) @@ -21,22 +38,112 @@ def _get_query(schema: Schema, query_cls: Optional[ObjectType] = None) -> Object def build_schema( - query: Optional[ObjectType] = None, - mutation: Optional[ObjectType] = None, - federation_version: Optional[float] = None, - enable_federation_2: bool = False, - schema: Optional[Schema] = None, - **kwargs + query: Union[ObjectType, Type[ObjectType]] = None, + mutation: Union[ObjectType, Type[ObjectType]] = None, + subscription: Union[ObjectType, Type[ObjectType]] = None, + types: Collection[Union[ObjectType, Type[ObjectType]]] = None, + directives: Union[Collection[FederationDirective], None] = None, + schema_directives: Collection[SchemaDirective] = None, + auto_camelcase: bool = True, + enable_federation_2: bool = True, + federation_version: FederationVersion = None, ) -> Schema: - schema = schema or Schema(query=query, mutation=mutation, **kwargs) - schema.auto_camelcase = kwargs.get("auto_camelcase", True) - schema.federation_version = float( - (federation_version or 2) if (enable_federation_2 or federation_version) else 1 + federation_version = ( + federation_version + if federation_version + else LATEST_VERSION + if enable_federation_2 + else FederationVersion.VERSION_1_0 ) - federation_query = _get_query(schema, schema.query) - # Use shallow copy to prevent recursion error - kwargs = schema.__dict__.copy() - kwargs.pop("query") - kwargs.pop("graphql_schema") - kwargs.pop("federation_version") - return type(schema)(query=federation_query, **kwargs) + enable_federation_2 = federation_version.value > FederationVersion.VERSION_1_0.value + + _types = list(types) if types is not None else [] + + _directives = get_directives_based_on_version(federation_version) + federation_directives = set(_directives.keys()) + if directives is not None: + _directives.update({directive.name: directive for directive in directives}) + + schema_args = { + "mutation": mutation, + "subscription": subscription, + "types": _types, + "directives": _directives.values(), + "auto_camelcase": auto_camelcase, + } + + schema: Schema = build_directive_schema(query=query, **schema_args) + + if "PageInfo" in schema.graphql_schema.type_map: + # PageInfo needs @sharable directive + try: + sharable = get_directive_from_name("shareable", federation_version) + + _types.append( + directive_decorator(target_directive=sharable)(field=None)(PageInfo) + ) + except ValueError: + # Federation Version does not support @sharable + pass + + _schema_directives = [] + directives_used = schema.get_directives_used() + if schema_directives or directives: + if not enable_federation_2: + raise ValueError( + f"Schema Directives & Directives are not supported on {federation_version=}. Use >=2.0 " + ) + + if ( + any( + schema_directive.target_directive == ComposeDirective + for schema_directive in schema_directives or [] + ) + or directives + ): + directives_used.append(ComposeDirective) + + if directives_used and enable_federation_2: + imports = [ + str(directive) + for directive in directives_used + if directive.name in federation_directives + ] + if imports: + _schema_directives.append( + link_directive( + url=f"https://specs.apollo.dev/federation/v{federation_version.value}", + import_=sorted(imports), + ) + ) + + if directives: + url__imports: dict[str, list[str]] = {} + for directive in directives: + assert isinstance( + directive, FederationDirective + ), "directives must be of instance FederationDirective" + + if not directive.add_to_schema_directives: + continue + + _imports = url__imports.get(directive.spec_url) + if _imports: + _imports.append(str(directive)) + else: + url__imports[directive.spec_url] = [str(directive)] + + for spec, imports in url__imports.items(): + _schema_directives.append(link_directive(url=spec, import_=sorted(imports))) + + for directive in directives: + if not directive.add_to_schema_directives: + continue + _schema_directives.append(compose_directive(name=str(directive))) + + if schema_directives: + _schema_directives.extend(list(schema_directives)) + + schema_args["schema_directives"] = _schema_directives if enable_federation_2 else [] + schema = build_directive_schema(query=query, **schema_args) + return build_directive_schema(query=_get_query(schema, schema.query), **schema_args) diff --git a/graphene_federation/override.py b/graphene_federation/override.py deleted file mode 100644 index fb8dac8..0000000 --- a/graphene_federation/override.py +++ /dev/null @@ -1,20 +0,0 @@ -from graphene import Schema - -from graphene_federation.utils import get_attributed_fields - - -def override(field, from_: str): - """ - Decorator to use to override a given type. - """ - field._override = from_ - return field - - -def get_override_fields(schema: Schema) -> dict: - """ - Find all the overridden types from the schema. - They can be easily distinguished from the other type as - the `@override` decorator adds a `_override` attribute to them. - """ - return get_attributed_fields(attribute="_override", schema=schema) diff --git a/graphene_federation/provides.py b/graphene_federation/provides.py deleted file mode 100644 index ee59bc7..0000000 --- a/graphene_federation/provides.py +++ /dev/null @@ -1,50 +0,0 @@ -from typing import Any, Union, Dict, List - -from graphene import Field -from graphene import Schema - -from graphene_federation.utils import get_attributed_fields - - -def get_provides_parent_types(schema: Schema) -> Dict[str, Any]: - """ - Find all the types for which a field is provided from the schema. - They can be easily distinguished from the other type as - the `@provides` decorator used on the type itself adds a `_provide_parent_type` attribute to them. - """ - provides_parent_types = {} - for type_name, type_ in schema.graphql_schema.type_map.items(): - if not hasattr(type_, "graphene_type"): - continue - if getattr(type_.graphene_type, "_provide_parent_type", False): - provides_parent_types[type_name] = type_.graphene_type - return provides_parent_types - - -def provides(field, fields: Union[str, List[str]] = None): - """ - - :param field: base type (when used as decorator) or field of base type - :param fields: - :return: - """ - if fields is None: # used as decorator on base type - if isinstance(field, Field): - raise ValueError("Please specify fields") - field._provide_parent_type = True - else: # used as wrapper over field - # TODO: We should validate the `fields` input to check it is actually existing fields but we - # don't have access here to the graphene type of the object it provides those fields for. - if isinstance(fields, str): - fields = fields.split() - field._provides = fields - return field - - -def get_provides_fields(schema: Schema) -> dict: - """ - Find all the extended types from the schema. - They can be easily distinguished from the other type as - the `@provides` decorator adds a `_provides` attribute to them. - """ - return get_attributed_fields(attribute="_provides", schema=schema) diff --git a/graphene_federation/requires.py b/graphene_federation/requires.py deleted file mode 100644 index e270e5b..0000000 --- a/graphene_federation/requires.py +++ /dev/null @@ -1,31 +0,0 @@ -from typing import List, Union - -from graphene import Schema - -from graphene_federation.utils import get_attributed_fields - - -def requires(field, fields: Union[str, List[str]]): - """ - Mark the required fields for a given field. - The input `fields` can be either a string or a list. - When it is a string we split at spaces to get the list of fields. - """ - # TODO: We should validate the `fields` input to check it is actually existing fields but we - # don't have access here to the parent graphene type. - if isinstance(fields, str): - fields = fields.split() - assert not hasattr( - field, "_requires" - ), "Can't chain `requires()` method calls on one field." - field._requires = fields - return field - - -def get_required_fields(schema: Schema) -> dict: - """ - Find all the extended types with required fields from the schema. - They can be easily distinguished from the other type as - the `@requires` decorator adds a `_requires` attribute to them. - """ - return get_attributed_fields(attribute="_requires", schema=schema) diff --git a/graphene_federation/scalars/__init__.py b/graphene_federation/scalars/__init__.py new file mode 100644 index 0000000..9fb3066 --- /dev/null +++ b/graphene_federation/scalars/__init__.py @@ -0,0 +1,5 @@ +from ._any import _Any +from .field_set_v1 import _FieldSet +from .field_set_v2 import FieldSet +from .link_import import link_import +from .link_purpose import link_purpose diff --git a/graphene_federation/types.py b/graphene_federation/scalars/_any.py similarity index 80% rename from graphene_federation/types.py rename to graphene_federation/scalars/_any.py index 65d7dea..63f526a 100644 --- a/graphene_federation/types.py +++ b/graphene_federation/scalars/_any.py @@ -2,7 +2,10 @@ class _Any(Scalar): + name = "_Any" __typename = String(required=True) + description = None + specified_by_url = None @staticmethod def serialize(dt): diff --git a/graphene_federation/scalars/field_set_v1.py b/graphene_federation/scalars/field_set_v1.py new file mode 100644 index 0000000..3bd6d5a --- /dev/null +++ b/graphene_federation/scalars/field_set_v1.py @@ -0,0 +1,9 @@ +from graphql import GraphQLString + +# _FieldSet = GraphQLScalarType(name="_FieldSet") + +""" +To avoid _FieldSet from coming into schema we are defining it as String +""" + +_FieldSet = GraphQLString diff --git a/graphene_federation/scalars/field_set_v2.py b/graphene_federation/scalars/field_set_v2.py new file mode 100644 index 0000000..7e26458 --- /dev/null +++ b/graphene_federation/scalars/field_set_v2.py @@ -0,0 +1,9 @@ +from graphql import GraphQLString + +# FieldSet = GraphQLScalarType(name="FieldSet") + +""" +To avoid FieldSet from coming into schema we are defining it as String +""" + +FieldSet = GraphQLString diff --git a/graphene_federation/scalars/link_import.py b/graphene_federation/scalars/link_import.py new file mode 100644 index 0000000..06c4717 --- /dev/null +++ b/graphene_federation/scalars/link_import.py @@ -0,0 +1,59 @@ +from math import isfinite +from typing import Any + +from graphql import ( + GraphQLError, + GraphQLScalarType, + StringValueNode, + ValueNode, + print_ast, +) +from graphql.pyutils import inspect + + +def _serialize_string(output_value: Any) -> str: + if isinstance(output_value, str): + return output_value + if isinstance(output_value, bool): + return "true" if output_value else "false" + if isinstance(output_value, int) or ( + isinstance(output_value, float) and isfinite(output_value) + ): + return str(output_value) + # do not serialize builtin types as strings, but allow serialization of custom + # types via their `__str__` method + if type(output_value).__module__ == "builtins": + raise GraphQLError( + "link__Import cannot represent value: " + inspect(output_value) + ) + + return str(output_value) + + +def _coerce_string(input_value: Any) -> str: + if not isinstance(input_value, str): + raise GraphQLError( + "link__Import cannot represent a non string value: " + inspect(input_value) + ) + return input_value + + +def _parse_string_literal(value_node: ValueNode, _variables: Any = None) -> str: + """Parse a string value node in the AST.""" + if not isinstance(value_node, StringValueNode): + raise GraphQLError( + "link__Import cannot represent a non string value: " + + print_ast(value_node), + value_node, + ) + return value_node.value + + +# Reference: https://www.apollographql.com/docs/federation/subgraph-spec/ + +link_import = GraphQLScalarType( + name="link__Import", + serialize=_serialize_string, + parse_value=_coerce_string, + parse_literal=_parse_string_literal, +) diff --git a/graphene_federation/scalars/link_purpose.py b/graphene_federation/scalars/link_purpose.py new file mode 100644 index 0000000..2aade95 --- /dev/null +++ b/graphene_federation/scalars/link_purpose.py @@ -0,0 +1,17 @@ +from graphql import GraphQLEnumType, GraphQLEnumValue + +# Reference: https://www.apollographql.com/docs/federation/subgraph-spec/ + +link_purpose = GraphQLEnumType( + name="link__Purpose", + values={ + "SECURITY": GraphQLEnumValue( + value="SECURITY", + description="`SECURITY` features provide metadata necessary to securely resolve fields.", + ), + "EXECUTION": GraphQLEnumValue( + value="EXECUTION", + description="`EXECUTION` features provide metadata necessary for operation execution.", + ), + }, +) diff --git a/graphene_federation/schema_directives/__init__.py b/graphene_federation/schema_directives/__init__.py new file mode 100644 index 0000000..7339a26 --- /dev/null +++ b/graphene_federation/schema_directives/__init__.py @@ -0,0 +1,2 @@ +from .compose_directive import compose_directive +from .link_directive import link_directive diff --git a/graphene_federation/schema_directives/compose_directive.py b/graphene_federation/schema_directives/compose_directive.py new file mode 100644 index 0000000..eea84b0 --- /dev/null +++ b/graphene_federation/schema_directives/compose_directive.py @@ -0,0 +1,14 @@ +from graphene_directives import SchemaDirective + +from ..appolo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name + + +def compose_directive( + name: str, + federation_version: FederationVersion = LATEST_VERSION, +) -> SchemaDirective: + directive = get_directive_from_name("composeDirective", federation_version) + return SchemaDirective( + target_directive=directive, + arguments={"name": name}, + ) diff --git a/graphene_federation/schema_directives/link_directive.py b/graphene_federation/schema_directives/link_directive.py new file mode 100644 index 0000000..fb0d2e8 --- /dev/null +++ b/graphene_federation/schema_directives/link_directive.py @@ -0,0 +1,39 @@ +from typing import Optional + +from graphene_directives import CustomDirective, DirectiveLocation, SchemaDirective +from graphql import ( + GraphQLArgument, + GraphQLList, + GraphQLNonNull, + GraphQLString, +) + +from ..scalars import link_import, link_purpose + +_link_directive = CustomDirective( + name="link", + locations=[ + DirectiveLocation.SCHEMA, + ], + args={ + "url": GraphQLArgument(GraphQLNonNull(GraphQLString)), + "as": GraphQLArgument(GraphQLString), + "for": GraphQLArgument(link_purpose), + "import": GraphQLArgument(GraphQLList(link_import)), + }, + description="Federation @link directive", + add_definition_to_schema=False, + is_repeatable=True, +) + + +def link_directive( + url: str, + as_: Optional[str] = None, + for_: Optional[str] = None, + import_: Optional[list[str]] = None, +) -> SchemaDirective: + return SchemaDirective( + target_directive=_link_directive, + arguments={"url": url, "as": as_, "for": for_, "import": import_}, + ) diff --git a/graphene_federation/service.py b/graphene_federation/service.py index 84dbc72..ab0b2c2 100644 --- a/graphene_federation/service.py +++ b/graphene_federation/service.py @@ -1,286 +1,21 @@ -import re -from typing import List +from graphene import Field, ObjectType, String +from graphene_directives.schema import Schema +from graphql.utilities.print_schema import print_scalar -from graphene.types.interface import InterfaceOptions -from graphene.types.union import UnionOptions -from graphql import GraphQLInterfaceType, GraphQLObjectType +from .scalars import FieldSet, _FieldSet -from .compose_directive import is_composable, compose_directive_schema_extensions -from .external import get_external_fields, get_external_object_types -from .inaccessible import get_inaccessible_types, get_inaccessible_fields -from .override import get_override_fields -from .requires import get_required_fields -from .shareable import get_shareable_types, get_shareable_fields -from graphql.utilities.print_schema import print_fields -from graphene import ObjectType, String, Field, Schema - -from .extend import get_extended_types -from .provides import get_provides_parent_types, get_provides_fields - -from .entity import get_entities -from .tag import get_tagged_fields -from .utils import field_name_to_type_attribute, type_attribute_to_field_name - - -class MonoFieldType: - """ - In order to be able to reuse the `print_fields` method to get a singular field - string definition, we need to define an object that has a `.fields` attribute. - """ - - def __init__(self, name, field): - self.fields = {name: field} - - -def convert_fields(schema: Schema, fields: List[str]) -> str: - get_field_name = type_attribute_to_field_name(schema) - return " ".join([get_field_name(field) for field in fields]) - - -def convert_fields_for_requires(schema: Schema, fields: List[str]) -> str: - """ - Adds __typename for resolving union,sub-field types - """ - get_field_name = type_attribute_to_field_name(schema) - new_fields = [] - for field in fields: - if "typename" not in field.lower(): # skip user defined typename - new_fields.append(get_field_name(field)) - if "{" in field: - new_fields.append("__typename") - - return " ".join(new_fields) - - -DECORATORS = { - "_external": lambda schema, fields: "@external", - "_requires": lambda schema, fields: f'@requires(fields: "{convert_fields_for_requires(schema, fields)}")', - "_provides": lambda schema, fields: f'@provides(fields: "{convert_fields(schema, fields)}")', - "_shareable": lambda schema, fields: "@shareable", - "_inaccessible": lambda schema, fields: "@inaccessible", - "_override": lambda schema, from_: f'@override(from: "{from_}")', - "_tag": lambda schema, name: f'@tag(name: "{name}")', -} - - -def field_to_string(field) -> str: - str_field = print_fields(field) - # Remove blocks added by `print_block` - block_match = re.match(r" \{\n(?P.*)\n\}", str_field, flags=re.DOTALL) - if block_match: - str_field = block_match.groups()[0] - return str_field - - -def add_entity_fields_decorators(entity, schema: Schema, string_schema: str) -> str: - """ - For a given entity, go through all its fields and see if any directive decorator need to be added. - The methods (from graphene-federation) marking fields that require some special treatment for federation add - corresponding attributes to the field itself. - Those attributes are listed in the `DECORATORS` variable as key and their respective value is the resolver that - returns what needs to be amended to the field declaration. - - This method simply go through the fields that need to be modified and replace them with their annotated version in the - schema string representation. - """ - entity_name = entity._meta.name - entity_type = schema.graphql_schema.get_type(entity_name) - str_fields = [] - get_model_attr = field_name_to_type_attribute(schema, entity) - for field_name, field in ( - entity_type.fields.items() if getattr(entity_type, "fields", None) else [] - ): - str_field = field_to_string(MonoFieldType(field_name, field)) - # Check if we need to annotate the field by checking if it has the decorator attribute set on the field. - f = getattr(entity, get_model_attr(field_name), None) - if f is not None: - for decorator, decorator_resolver in DECORATORS.items(): - decorator_value = getattr(f, decorator, None) - if decorator_value: - str_field += f" {decorator_resolver(schema, decorator_value)}" - str_fields.append(str_field) - str_fields_annotated = "\n".join(str_fields) - # Replace the original field declaration by the annotated one - if isinstance(entity_type, GraphQLObjectType) or isinstance( - entity_type, GraphQLInterfaceType - ): - str_fields_original = field_to_string(entity_type) - else: - str_fields_original = "" - pattern = re.compile( - r"(type\s%s\s[^\{]*)\{\s*%s\s*\}" - % (entity_name, re.escape(str_fields_original)) - ) - string_schema = pattern.sub(r"\g<1> {\n%s\n}" % str_fields_annotated, string_schema) - return string_schema - - -def get_sdl(schema: Schema) -> str: +def get_sdl(schema) -> str: """ Add all needed decorators to the string representation of the schema. """ string_schema = str(schema) + # Remove All Scalar definitions + for scalar in [_FieldSet, FieldSet]: + string_schema = string_schema.replace(print_scalar(scalar), "") - regex = r"schema \{(\w|\!|\s|\:)*\}" - pattern = re.compile(regex) - string_schema = pattern.sub(" ", string_schema) - - # Get various objects that need to be amended - extended_types = get_extended_types(schema) - provides_parent_types = get_provides_parent_types(schema) - provides_fields = get_provides_fields(schema) - entities = get_entities(schema) - required_fields = get_required_fields(schema) - external_fields = get_external_fields(schema) - external_object_types = get_external_object_types(schema) - override_fields = get_override_fields(schema) - - schema_extensions = [] - - if schema.federation_version >= 2: - shareable_types = get_shareable_types(schema) - inaccessible_types = get_inaccessible_types(schema) - shareable_fields = get_shareable_fields(schema) - tagged_fields = get_tagged_fields(schema) - inaccessible_fields = get_inaccessible_fields(schema) - - federation_spec_import = [] - - if extended_types or external_object_types: - federation_spec_import.append('"@extends"') - if external_fields: - federation_spec_import.append('"@external"') - if entities: - federation_spec_import.append('"@key"') - if override_fields: - federation_spec_import.append('"@override"') - if provides_parent_types or provides_fields: - federation_spec_import.append('"@provides"') - if required_fields: - federation_spec_import.append('"@requires"') - if inaccessible_types or inaccessible_fields: - federation_spec_import.append('"@inaccessible"') - if shareable_types or shareable_fields: - federation_spec_import.append('"@shareable"') - if tagged_fields: - federation_spec_import.append('"@tag"') - - if schema.federation_version >= 2.1 and hasattr(schema, "directives"): - preserved_directives = [ - directive for directive in schema.directives if is_composable(directive) - ] - if preserved_directives: - federation_spec_import.append('"@composeDirective"') - schema_extensions.append( - compose_directive_schema_extensions(preserved_directives) - ) - - schema_import = ", ".join(federation_spec_import) - schema_extensions = [ - f'@link(url: "https://specs.apollo.dev/federation/v{schema.federation_version}", import: [{schema_import}])' - ] + schema_extensions - - # Add fields directives (@external, @provides, @requires, @shareable, @inaccessible) - entities_ = ( - set(provides_parent_types.values()) - | set(extended_types.values()) - | set(entities.values()) - | set(required_fields.values()) - | set(provides_fields.values()) - ) - - if schema.federation_version >= 2: - entities_ = ( - entities_ - | set(shareable_types.values()) - | set(inaccessible_types.values()) - | set(inaccessible_fields.values()) - | set(shareable_fields.values()) - | set(tagged_fields.values()) - ) - for entity in entities_: - string_schema = add_entity_fields_decorators(entity, schema, string_schema) - - # Prepend `extend` keyword to the type definition of extended types - # noinspection DuplicatedCode - for entity_name, entity in extended_types.items(): - type_def = re.compile(rf"type {entity_name} ([^{{]*)") - repl_str = rf"extend type {entity_name} \1" - string_schema = type_def.sub(repl_str, string_schema) - - # Add entity keys declarations - get_field_name = type_attribute_to_field_name(schema) - for entity_name, entity in entities.items(): - type_def_re = rf"(type {entity_name} [^\{{]*)" + " " - - # resolvable argument of @key directive is true by default. If false, we add 'resolvable: false' to sdl. - if ( - schema.federation_version >= 2 - and hasattr(entity, "_resolvable") - and not entity._resolvable - ): - type_annotation = ( - ( - " ".join( - [ - f'@key(fields: "{get_field_name(key)}"' - for key in entity._keys - ] - ) - ) - + f", resolvable: {str(entity._resolvable).lower()})" - + " " - ) - else: - type_annotation = ( - " ".join( - [f'@key(fields: "{get_field_name(key)}")' for key in entity._keys] - ) - + " " - ) - repl_str = rf"\1{type_annotation}" - pattern = re.compile(type_def_re) - string_schema = pattern.sub(repl_str, string_schema) - - if schema.federation_version >= 2: - # Add `@external` keyword to the type definition of external object types - for object_type_name, _ in external_object_types.items(): - type_def = re.compile(rf"type {object_type_name} ([^{{]*)") - repl_str = rf"type {object_type_name} @external \1" - string_schema = type_def.sub(repl_str, string_schema) - - for type_name, type in shareable_types.items(): - # noinspection PyProtectedMember - if isinstance(type._meta, UnionOptions): - type_def_re = rf"(union {type_name})" - else: - type_def_re = rf"(type {type_name} [^\{{]*)" + " " - type_annotation = " @shareable" - repl_str = rf"\1{type_annotation} " - pattern = re.compile(type_def_re) - string_schema = pattern.sub(repl_str, string_schema) - - for type_name, type in inaccessible_types.items(): - # noinspection PyProtectedMember - if isinstance(type._meta, InterfaceOptions): - type_def_re = rf"(interface {type_name}[^\{{]*)" - elif isinstance(type._meta, UnionOptions): - type_def_re = rf"(union {type_name})" - else: - type_def_re = rf"(type {type_name} [^\{{]*)" + " " - type_annotation = " @inaccessible" - repl_str = rf"\1{type_annotation} " - pattern = re.compile(type_def_re) - string_schema = pattern.sub(repl_str, string_schema) - - if schema_extensions: - string_schema = ( - "extend schema\n " + "\n".join(schema_extensions) + "\n" + string_schema - ) - - return string_schema + return string_schema.strip() def get_service_query(schema: Schema): @@ -289,13 +24,13 @@ def get_service_query(schema: Schema): class _Service(ObjectType): sdl = String() - def resolve_sdl(parent, _): + def resolve_sdl(self, _) -> str: # noqa return sdl_str class ServiceQuery(ObjectType): _service = Field(_Service, name="_service", required=True) - def resolve__service(parent, info): + def resolve__service(self, info) -> _Service: # noqa return _Service() return ServiceQuery diff --git a/graphene_federation/shareable.py b/graphene_federation/shareable.py deleted file mode 100644 index 29634f6..0000000 --- a/graphene_federation/shareable.py +++ /dev/null @@ -1,67 +0,0 @@ -from typing import Any, Dict, Optional - -from graphene import Schema -from graphene.types.interface import InterfaceOptions - -from graphene_federation.utils import get_attributed_fields - - -def get_shareable_types(schema: Schema) -> Dict[str, Any]: - """ - Find all the extended types from the schema. - They can be easily distinguished from the other type as - the `@shareable` decorator adds a `_shareable` attribute to them. - """ - shareable_types = {} - for type_name, type_ in schema.graphql_schema.type_map.items(): - if not hasattr(type_, "graphene_type"): - continue - if type_name == "PageInfo" or getattr(type_.graphene_type, "_shareable", False): - shareable_types[type_name] = type_.graphene_type - return shareable_types - - -def shareable(field: Optional[Any] = None) -> Any: - """ - Decorator to use to shareable a given type. - """ - - # noinspection PyProtectedMember,PyPep8Naming - def decorator(type_): - assert not hasattr( - type_, "_keys" - ), "Can't extend type which is already extended or has @key" - assert not hasattr( - type_, "_keys" - ), "Can't extend type which is already extended or has @key" - # Check the provided fields actually exist on the Type. - assert getattr(type_._meta, "description", None) is None, ( - f'Type "{type_.__name__}" has a non empty description and it is also marked with extend.' - "\nThey are mutually exclusive." - "\nSee https://github.com/graphql/graphql-js/issues/2385#issuecomment-577997521" - ) - # Set a `_shareable` attribute to be able to distinguish it from the other entities - setattr(type_, "_shareable", True) - return type_ - - if field: - assert not isinstance(field._meta, InterfaceOptions), ( - "The @Shareable directive is about indicating when an object field " - "can be resolved by multiple subgraphs. As interface fields are not " - "directly resolved (their implementation is), @Shareable is not " - "meaningful on an interface field and is not allowed (at least since " - "federation 2.2; earlier versions of federation 2 mistakenly ignored " - "@Shareable on interface fields). " - ) - field._shareable = True - return field - return decorator - - -def get_shareable_fields(schema: Schema) -> dict: - """ - Find all the extended types from the schema. - They can be easily distinguished from the other type as - the `@shareable` decorator adds a `_shareable` attribute to them. - """ - return get_attributed_fields(attribute="_shareable", schema=schema) diff --git a/graphene_federation/tag.py b/graphene_federation/tag.py deleted file mode 100644 index 05f0fb3..0000000 --- a/graphene_federation/tag.py +++ /dev/null @@ -1,20 +0,0 @@ -from graphene import Schema - -from graphene_federation.utils import get_attributed_fields - - -def tag(field, name: str): - """ - Decorator to use to override a given type. - """ - field._tag = name - return field - - -def get_tagged_fields(schema: Schema) -> dict: - """ - Find all the extended types from the schema. - They can be easily distinguished from the other type as - the `@external` decorator adds a `_external` attribute to them. - """ - return get_attributed_fields(attribute="_tag", schema=schema) diff --git a/graphene_federation/tests/__init__.py b/graphene_federation/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/graphene_federation/tests/test_annotation_corner_cases.py b/graphene_federation/tests/test_annotation_corner_cases.py deleted file mode 100644 index 72aeea0..0000000 --- a/graphene_federation/tests/test_annotation_corner_cases.py +++ /dev/null @@ -1,391 +0,0 @@ -from textwrap import dedent - -from graphql import graphql_sync - -from graphene import ObjectType, ID, String, Field - -from graphene_federation import external, requires -from graphene_federation.entity import key -from graphene_federation.extend import extend -from graphene_federation.main import build_schema -from graphene_federation.utils import clean_schema - - -def test_similar_field_name(): - """ - Test annotation with fields that have similar names. - """ - - @extend("id") - class ChatUser(ObjectType): - uid = ID() - identified = ID() - id = external(ID()) - i_d = ID() - ID = ID() - - class ChatMessage(ObjectType): - id = ID(required=True) - user = Field(ChatUser) - - class ChatQuery(ObjectType): - message = Field(ChatMessage, id=ID(required=True)) - - chat_schema = build_schema(query=ChatQuery, enable_federation_2=True) - expected_result = dedent( - """ - schema { - query: ChatQuery - } - - type ChatQuery { - message(id: ID!): ChatMessage - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - } - - type ChatMessage { - id: ID! - user: ChatUser - } - - type ChatUser { - uid: ID - identified: ID - id: ID - iD: ID - ID: ID - } - - union _Entity = ChatUser - - scalar _Any - - type _Service { - sdl: String - } - """ - ) - assert clean_schema(chat_schema) == clean_schema(expected_result) - # Check the federation service schema definition language - query = """ - query { - _service { - sdl - } - } - """ - result = graphql_sync(chat_schema.graphql_schema, query) - assert not result.errors - expected_result = dedent( - """ - extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@extends", "@external", "@key"]) - - type ChatQuery { - message(id: ID!): ChatMessage - } - type ChatMessage { - id: ID! - user: ChatUser - } - extend type ChatUser @key(fields: "id") { - uid: ID - identified: ID - id: ID @external - iD: ID - ID: ID - } - """ - ) - assert clean_schema(result.data["_service"]["sdl"]) == clean_schema(expected_result) - - -def test_camel_case_field_name(): - """ - Test annotation with fields that have camel cases or snake case. - """ - - @extend("auto_camel") - class Camel(ObjectType): - auto_camel = external(String()) - forcedCamel = requires(String(), fields="auto_camel") - a_snake = String() - aCamel = String() - - class Query(ObjectType): - camel = Field(Camel) - - schema = build_schema(query=Query, enable_federation_2=True) - expected_result = dedent( - """ - type Query { - camel: Camel - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - } - - type Camel { - autoCamel: String - forcedCamel: String - aSnake: String - aCamel: String - } - - union _Entity = Camel - - scalar _Any - - type _Service { - sdl: String - } - """ - ) - assert clean_schema(schema) == clean_schema(expected_result) - # Check the federation service schema definition language - query = """ - query { - _service { - sdl - } - } - """ - result = graphql_sync(schema.graphql_schema, query) - assert not result.errors - expected_result = dedent( - """ - extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@extends", "@external", "@key", "@requires"]) - type Query { - camel: Camel - } - extend type Camel @key(fields: "autoCamel") { - autoCamel: String @external - forcedCamel: String @requires(fields: "autoCamel") - aSnake: String - aCamel: String - } - """ - ) - assert clean_schema(result.data["_service"]["sdl"]) == clean_schema(expected_result) - - -def test_camel_case_field_name_without_auto_camelcase(): - """ - Test annotation with fields that have camel cases or snake case but with the auto_camelcase disabled. - """ - - @extend("auto_camel") - class Camel(ObjectType): - auto_camel = external(String()) - forcedCamel = requires(String(), fields="auto_camel") - a_snake = String() - aCamel = String() - - class Query(ObjectType): - camel = Field(Camel) - - schema = build_schema(query=Query, auto_camelcase=False, enable_federation_2=True) - expected_result = dedent( - """ - type Query { - camel: Camel - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - } - - type Camel { - auto_camel: String - forcedCamel: String - a_snake: String - aCamel: String - } - - union _Entity = Camel - - scalar _Any - - type _Service { - sdl: String - } - """ - ) - assert clean_schema(schema) == clean_schema(expected_result) - # Check the federation service schema definition language - query = """ - query { - _service { - sdl - } - } - """ - result = graphql_sync(schema.graphql_schema, query) - assert not result.errors - expected_result = dedent( - """ - extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@extends", "@external", "@key", "@requires"]) - type Query { - camel: Camel - } - - extend type Camel @key(fields: "auto_camel") { - auto_camel: String @external - forcedCamel: String @requires(fields: "auto_camel") - a_snake: String - aCamel: String - } - """ - ) - assert clean_schema(result.data["_service"]["sdl"]) == clean_schema(expected_result) - - -def test_annotated_field_also_used_in_filter(): - """ - Test that when a field also used in filter needs to get annotated, it really annotates only the field. - See issue https://github.com/preply/graphene-federation/issues/50 - """ - - @key("id") - class B(ObjectType): - id = ID() - - @extend("id") - class A(ObjectType): - id = external(ID()) - b = Field(B, id=ID()) - - class Query(ObjectType): - a = Field(A) - - schema = build_schema(query=Query, enable_federation_2=True) - expected_result = dedent( - """ - type Query { - a: A - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - } - - type A { - id: ID - b(id: ID): B - } - - type B { - id: ID - } - - union _Entity = A | B - - scalar _Any - - type _Service { - sdl: String - } - """ - ) - assert clean_schema(schema) == clean_schema(expected_result) - # Check the federation service schema definition language - query = """ - query { - _service { - sdl - } - } - """ - result = graphql_sync(schema.graphql_schema, query) - assert not result.errors - expected_result = dedent( - """ - extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@extends", "@external", "@key"]) - type Query { - a: A - } - - extend type A @key(fields: "id") { - id: ID @external - b(id: ID): B - } - - type B @key(fields: "id") { - id: ID - } - """ - ) - assert clean_schema(result.data["_service"]["sdl"]) == clean_schema(expected_result) - - -def test_annotate_object_with_meta_name(): - @key("id") - class B(ObjectType): - class Meta: - name = "Potato" - - id = ID() - - @extend("id") - class A(ObjectType): - class Meta: - name = "Banana" - - id = external(ID()) - b = Field(B, id=ID()) - - class Query(ObjectType): - a = Field(A) - - schema = build_schema(query=Query, enable_federation_2=True) - expected_result = dedent( - """ - type Query { - a: Banana - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - } - - type Banana { - id: ID - b(id: ID): Potato - } - - type Potato { - id: ID - } - - union _Entity = Banana | Potato - - scalar _Any - - type _Service { - sdl: String - } - """ - ) - assert clean_schema(schema) == clean_schema(expected_result) - # Check the federation service schema definition language - query = """ - query { - _service { - sdl - } - } - """ - result = graphql_sync(schema.graphql_schema, query) - assert not result.errors - expected_result = dedent( - """ - extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@extends", "@external", "@key"]) - type Query { - a: Banana - } - - extend type Banana @key(fields: "id") { - id: ID @external - b(id: ID): Potato - } - - type Potato @key(fields: "id") { - id: ID - } - """ - ) - # assert compare_schema(result.data["_service"]["sdl"].strip(), expected_result) - assert clean_schema(result.data["_service"]["sdl"]) == clean_schema(expected_result) diff --git a/graphene_federation/tests/test_annotation_corner_cases_v1.py b/graphene_federation/tests/test_annotation_corner_cases_v1.py deleted file mode 100644 index 82ca7fc..0000000 --- a/graphene_federation/tests/test_annotation_corner_cases_v1.py +++ /dev/null @@ -1,388 +0,0 @@ -from textwrap import dedent - -from graphql import graphql_sync - -from graphene import ObjectType, ID, String, Field - -from graphene_federation.entity import key -from graphene_federation.extend import extend -from graphene_federation.external import external -from graphene_federation.requires import requires -from graphene_federation.main import build_schema -from graphene_federation.utils import clean_schema - - -def test_similar_field_name(): - """ - Test annotation with fields that have similar names. - """ - - @extend("id") - class ChatUser(ObjectType): - uid = ID() - identified = ID() - id = external(ID()) - i_d = ID() - ID = ID() - - class ChatMessage(ObjectType): - id = ID(required=True) - user = Field(ChatUser) - - class ChatQuery(ObjectType): - message = Field(ChatMessage, id=ID(required=True)) - - chat_schema = build_schema(query=ChatQuery) - expected_result = dedent( - """ - schema { - query: ChatQuery - } - - type ChatQuery { - message(id: ID!): ChatMessage - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - } - - type ChatMessage { - id: ID! - user: ChatUser - } - - type ChatUser { - uid: ID - identified: ID - id: ID - iD: ID - ID: ID - } - - union _Entity = ChatUser - - scalar _Any - - type _Service { - sdl: String - } - """ - ) - assert clean_schema(chat_schema) == clean_schema(expected_result) - # Check the federation service schema definition language - query = """ - query { - _service { - sdl - } - } - """ - result = graphql_sync(chat_schema.graphql_schema, query) - assert not result.errors - expected_result = dedent( - """ - type ChatQuery { - message(id: ID!): ChatMessage - } - - type ChatMessage { - id: ID! - user: ChatUser - } - - extend type ChatUser @key(fields: "id") { - uid: ID - identified: ID - id: ID @external - iD: ID - ID: ID - } - """ - ) - assert clean_schema(result.data["_service"]["sdl"]) == clean_schema(expected_result) - - -def test_camel_case_field_name(): - """ - Test annotation with fields that have camel cases or snake case. - """ - - @extend("auto_camel") - class Camel(ObjectType): - auto_camel = external(String()) - forcedCamel = requires(String(), fields="auto_camel") - a_snake = String() - aCamel = String() - - class Query(ObjectType): - camel = Field(Camel) - - schema = build_schema(query=Query) - expected_result = dedent( - """ - type Query { - camel: Camel - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - } - - type Camel { - autoCamel: String - forcedCamel: String - aSnake: String - aCamel: String - } - - union _Entity = Camel - - scalar _Any - - type _Service { - sdl: String - } - """ - ) - assert clean_schema(schema) == clean_schema(expected_result) - # Check the federation service schema definition language - query = """ - query { - _service { - sdl - } - } - """ - result = graphql_sync(schema.graphql_schema, query) - assert not result.errors - expected_result = dedent( - """ - type Query { - camel: Camel - } - - extend type Camel @key(fields: "autoCamel") { - autoCamel: String @external - forcedCamel: String @requires(fields: "autoCamel") - aSnake: String - aCamel: String - } - """ - ) - assert clean_schema(result.data["_service"]["sdl"]) == clean_schema(expected_result) - - -def test_camel_case_field_name_without_auto_camelcase(): - """ - Test annotation with fields that have camel cases or snake case but with the auto_camelcase disabled. - """ - - @extend("auto_camel") - class Camel(ObjectType): - auto_camel = external(String()) - forcedCamel = requires(String(), fields="auto_camel") - a_snake = String() - aCamel = String() - - class Query(ObjectType): - camel = Field(Camel) - - schema = build_schema(query=Query, auto_camelcase=False) - expected_result = dedent( - """ - type Query { - camel: Camel - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - } - - type Camel { - auto_camel: String - forcedCamel: String - a_snake: String - aCamel: String - } - - union _Entity = Camel - - scalar _Any - - type _Service { - sdl: String - } - """ - ) - assert clean_schema(schema) == clean_schema(expected_result) - # Check the federation service schema definition language - query = """ - query { - _service { - sdl - } - } - """ - result = graphql_sync(schema.graphql_schema, query) - assert not result.errors - expected_result = dedent( - """ - type Query { - camel: Camel - } - - extend type Camel @key(fields: "auto_camel") { - auto_camel: String @external - forcedCamel: String @requires(fields: "auto_camel") - a_snake: String - aCamel: String - } - """ - ) - assert clean_schema(result.data["_service"]["sdl"]) == clean_schema(expected_result) - - -def test_annotated_field_also_used_in_filter(): - """ - Test that when a field also used in filter needs to get annotated, it really annotates only the field. - See issue https://github.com/preply/graphene-federation/issues/50 - """ - - @key("id") - class B(ObjectType): - id = ID() - - @extend("id") - class A(ObjectType): - id = external(ID()) - b = Field(B, id=ID()) - - class Query(ObjectType): - a = Field(A) - - schema = build_schema(query=Query) - expected_result = dedent( - """ - type Query { - a: A - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - } - - type A { - id: ID - b(id: ID): B - } - - type B { - id: ID - } - - union _Entity = A | B - - scalar _Any - - type _Service { - sdl: String - } - """ - ) - assert clean_schema(schema) == clean_schema(expected_result) - # Check the federation service schema definition language - query = """ - query { - _service { - sdl - } - } - """ - result = graphql_sync(schema.graphql_schema, query) - assert not result.errors - expected_result = dedent( - """ - type Query { - a: A - } - - extend type A @key(fields: "id") { - id: ID @external - b(id: ID): B - } - - type B @key(fields: "id") { - id: ID - } - """ - ) - assert clean_schema(result.data["_service"]["sdl"]) == clean_schema(expected_result) - - -def test_annotate_object_with_meta_name(): - @key("id") - class B(ObjectType): - class Meta: - name = "Potato" - - id = ID() - - @extend("id") - class A(ObjectType): - class Meta: - name = "Banana" - - id = external(ID()) - b = Field(B, id=ID()) - - class Query(ObjectType): - a = Field(A) - - schema = build_schema(query=Query) - expected_result = dedent( - """ - type Query { - a: Banana - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - } - - type Banana { - id: ID - b(id: ID): Potato - } - - type Potato { - id: ID - } - - union _Entity = Banana | Potato - - scalar _Any - - type _Service { - sdl: String - } - """ - ) - assert clean_schema(schema) == clean_schema(expected_result) - # Check the federation service schema definition language - query = """ - query { - _service { - sdl - } - } - """ - result = graphql_sync(schema.graphql_schema, query) - assert not result.errors - expected_result = dedent( - """ - type Query { - a: Banana - } - - extend type Banana @key(fields: "id") { - id: ID @external - b(id: ID): Potato - } - - type Potato @key(fields: "id") { - id: ID - } - """ - ) - assert clean_schema(result.data["_service"]["sdl"]) == clean_schema(expected_result) diff --git a/graphene_federation/tests/test_custom_enum.py b/graphene_federation/tests/test_custom_enum.py deleted file mode 100644 index 933c559..0000000 --- a/graphene_federation/tests/test_custom_enum.py +++ /dev/null @@ -1,57 +0,0 @@ -from textwrap import dedent - -import graphene -from graphene import ObjectType -from graphql import graphql_sync - -from graphene_federation import build_schema, shareable, inaccessible -from graphene_federation.utils import clean_schema - - -def test_custom_enum(): - class Episode(graphene.Enum): - NEWHOPE = 4 - EMPIRE = 5 - JEDI = 6 - - @shareable - class TestCustomEnum(graphene.ObjectType): - test_shareable_scalar = shareable(Episode()) - test_inaccessible_scalar = inaccessible(Episode()) - - class Query(ObjectType): - test = Episode() - test2 = graphene.List(TestCustomEnum, required=True) - - schema = build_schema( - query=Query, enable_federation_2=True, types=(TestCustomEnum,) - ) - query = """ - query { - _service { - sdl - } - } - """ - result = graphql_sync(schema.graphql_schema, query) - expected_result = dedent( - """ - extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@inaccessible", "@shareable"]) - type TestCustomEnum @shareable { - testShareableScalar: Episode @shareable - testInaccessibleScalar: Episode @inaccessible - } - - enum Episode { - NEWHOPE - EMPIRE - JEDI - } - - type Query { - test: Episode - test2: [TestCustomEnum]! - } - """ - ) - assert clean_schema(result.data["_service"]["sdl"]) == clean_schema(expected_result) diff --git a/graphene_federation/tests/test_entity.py b/graphene_federation/tests/test_entity.py deleted file mode 100644 index fab55d2..0000000 --- a/graphene_federation/tests/test_entity.py +++ /dev/null @@ -1 +0,0 @@ -# test resolve_entities method diff --git a/graphene_federation/tests/test_entity_v1.py b/graphene_federation/tests/test_entity_v1.py deleted file mode 100644 index fab55d2..0000000 --- a/graphene_federation/tests/test_entity_v1.py +++ /dev/null @@ -1 +0,0 @@ -# test resolve_entities method diff --git a/graphene_federation/tests/test_extend.py b/graphene_federation/tests/test_extend.py deleted file mode 100644 index b34c884..0000000 --- a/graphene_federation/tests/test_extend.py +++ /dev/null @@ -1,128 +0,0 @@ -from textwrap import dedent - -import pytest - -from graphene import ObjectType, ID, String, Field -from graphql import graphql_sync - -from graphene_federation import build_schema, external, shareable -from graphene_federation.utils import clean_schema -from graphene_federation.extend import extend - - -def test_extend_non_existing_field_failure(): - """ - Test that using the key decorator and providing a field that does not exist fails. - """ - with pytest.raises(AssertionError) as err: - - @extend("potato") - class A(ObjectType): - id = ID() - - assert 'Field "potato" does not exist on type "A"' == str(err.value) - - -def test_multiple_extend_failure(): - """ - Test that the extend decorator can't be used more than once on a type. - """ - with pytest.raises(AssertionError) as err: - - @extend("id") - @extend("potato") - class A(ObjectType): - id = ID() - potato = String() - - assert "Can't extend type which is already extended or has @key" == str(err.value) - - -def test_extend_with_description_failure(): - """ - Test that adding a description to an extended type raises an error. - """ - with pytest.raises(AssertionError) as err: - - @extend("id") - class A(ObjectType): - class Meta: - description = "This is an object from here." - - id = ID() - - assert ( - 'Type "A" has a non empty description and it is also marked with extend.\nThey are mutually exclusive.' - in str(err.value) - ) - - -def test_extend_with_compound_primary_keys(): - @shareable - class Organization(ObjectType): - id = ID() - - @extend(fields="id organization {id }") - class User(ObjectType): - id = external(ID()) - organization = Field(Organization) - - class Query(ObjectType): - user = Field(User) - - schema = build_schema(query=Query, enable_federation_2=True) - expected_result = dedent( - """ - type Query { - user: User - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - } - - type User { - id: ID - organization: Organization - } - - type Organization { - id: ID - } - - union _Entity = User - - scalar _Any - - type _Service { - sdl: String - } - """ - ) - assert clean_schema(schema) == clean_schema(expected_result) - # Check the federation service schema definition language - query = """ - query { - _service { - sdl - } - } - """ - result = graphql_sync(schema.graphql_schema, query) - assert not result.errors - expected_result = dedent( - """ - extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@extends", "@external", "@key", "@shareable"]) - type Query { - user: User - } - - extend type User @key(fields: "id organization {id }") { - id: ID @external - organization: Organization - } - - type Organization @shareable { - id: ID - } - """ - ) - assert clean_schema(result.data["_service"]["sdl"]) == clean_schema(expected_result) diff --git a/graphene_federation/transform/__init__.py b/graphene_federation/transform/__init__.py new file mode 100644 index 0000000..b03eece --- /dev/null +++ b/graphene_federation/transform/__init__.py @@ -0,0 +1 @@ +from .field_set_case_transform import field_set_case_transform diff --git a/graphene_federation/transform/field_set_case_transform.py b/graphene_federation/transform/field_set_case_transform.py new file mode 100644 index 0000000..e6f9f2c --- /dev/null +++ b/graphene_federation/transform/field_set_case_transform.py @@ -0,0 +1,13 @@ +from graphene.utils.str_converters import to_camel_case +from graphene_directives import Schema + + +def field_set_case_transform(inputs: dict, schema: Schema) -> dict: + fields = inputs.get("fields") + if fields: + inputs["fields"] = ( + to_camel_case(fields).replace("_Typename", "__typename") + if schema.auto_camelcase + else fields + ) + return inputs diff --git a/graphene_federation/utils.py b/graphene_federation/utils.py index f374dde..8d026d9 100644 --- a/graphene_federation/utils.py +++ b/graphene_federation/utils.py @@ -1,119 +1,6 @@ import re -from typing import Any, Callable, List, Tuple - -from graphene import ObjectType, Schema -from graphene.types.definitions import GrapheneObjectType -from graphene.types.enum import EnumOptions -from graphene.types.scalars import ScalarOptions -from graphene.types.union import UnionOptions -from graphene.utils.str_converters import to_camel_case -from graphql import GraphQLEnumType, GraphQLNonNull, GraphQLScalarType, parse - - -def field_name_to_type_attribute(schema: Schema, model: Any) -> Callable[[str], str]: - """ - Create field name conversion method (from schema name to actual graphene_type attribute name). - """ - field_names = {} - if schema.auto_camelcase: - field_names = { - to_camel_case(attr_name): attr_name - for attr_name in getattr(model._meta, "fields", []) - } - return lambda schema_field_name: field_names.get( - schema_field_name, schema_field_name - ) - - -def type_attribute_to_field_name(schema: Schema) -> Callable[[str], str]: - """ - Create a conversion method to convert from graphene_type attribute name to the schema field name. - """ - if schema.auto_camelcase: - return lambda attr_name: to_camel_case(attr_name) - else: - return lambda attr_name: attr_name - - -def check_fields_exist_on_type(fields: set, type_: ObjectType): - return fields.issubset(set(type_._meta.fields)) - - -def is_valid_compound_key(type_name: str, key: str, schema: Schema): - key_document = parse(f"{{{key}}}") - - # List storing tuples of nodes in the key document with its parent types - key_nodes: List[Tuple[Any, GrapheneObjectType]] = [ - (key_document.definitions[0], schema.graphql_schema.type_map[type_name]) - ] - - while key_nodes: - selection_node, parent_object_type = key_nodes[0] - if isinstance(parent_object_type, GraphQLNonNull): - parent_type_fields = parent_object_type.of_type.fields - else: - parent_type_fields = parent_object_type.fields - for field in selection_node.selection_set.selections: - if schema.auto_camelcase: - field_name = to_camel_case(field.name.value) - else: - field_name = field.name.value - if field_name not in parent_type_fields: - # Field does not exist on parent - return False - - field_type = parent_type_fields[field_name].type - if field.selection_set: - # If the field has sub-selections, add it to node mappings to check for valid subfields - - if isinstance(field_type, GraphQLScalarType) or ( - isinstance(field_type, GraphQLNonNull) - and isinstance(field_type.of_type, GraphQLScalarType) - ): - # sub-selections are added to a scalar type, key is not valid - return False - - key_nodes.append((field, field_type)) - else: - # If there are no sub-selections for a field, it should be a scalar or enum - if not any( - [ - ( - isinstance(field_type, GraphQLScalarType) - or isinstance(field_type, GraphQLEnumType) - ), - ( - isinstance(field_type, GraphQLNonNull) - and ( - isinstance(field_type.of_type, GraphQLScalarType) - or isinstance(field_type.of_type, GraphQLEnumType) - ) - ), - ] - ): - return False - - key_nodes.pop(0) # Remove the current node as it is fully processed - - return True - - -def get_attributed_fields(attribute: str, schema: Schema): - fields = {} - for type_name, type_ in schema.graphql_schema.type_map.items(): - if ( - not hasattr(type_, "graphene_type") - or isinstance(type_.graphene_type._meta, UnionOptions) - or isinstance(type_.graphene_type._meta, ScalarOptions) - or isinstance(type_.graphene_type._meta, EnumOptions) - ): - continue - for field in list(type_.graphene_type._meta.fields): - if getattr(getattr(type_.graphene_type, field, None), attribute, False): - fields[type_name] = type_.graphene_type - continue - return fields def clean_schema(schema): - return re.sub(r"[ \n]+", " ", str(schema)).strip() + schema = re.sub(r"\s+", "", str(schema)) + return schema.strip() diff --git a/graphene_federation/validators/__init__.py b/graphene_federation/validators/__init__.py new file mode 100644 index 0000000..4dc7427 --- /dev/null +++ b/graphene_federation/validators/__init__.py @@ -0,0 +1,4 @@ +from .extends import validate_extends +from .key import validate_key +from .requires import validate_requires +from .utils import build_ast diff --git a/graphene_federation/validators/extends.py b/graphene_federation/validators/extends.py new file mode 100644 index 0000000..40a1b6f --- /dev/null +++ b/graphene_federation/validators/extends.py @@ -0,0 +1,26 @@ +from typing import Union + +from graphene import Field, Interface, ObjectType +from graphene_directives import Schema +from graphene_directives.utils import has_non_field_attribute + + +# todoo: remove +def validate_extends( + type_: Union[ObjectType, Interface, Field], _inputs: dict, _schema: Schema +) -> bool: + from ..appolo_versions import LATEST_VERSION, get_directive_from_name + + key = get_directive_from_name("key", LATEST_VERSION) + + assert not has_non_field_attribute( + type_=type_, target_directive=key + ), f"Can't extend type on {type_} which has @key" + + assert getattr(type_._meta, "description", None) is None, ( + f'Type "{type_.__name__}" has a non empty description and it is also marked with extend.' + "\nThey are mutually exclusive." + "\nSee https://github.com/graphql/graphql-js/issues/2385#issuecomment-577997521" + ) + + return True diff --git a/graphene_federation/validators/key.py b/graphene_federation/validators/key.py new file mode 100644 index 0000000..ff29ff1 --- /dev/null +++ b/graphene_federation/validators/key.py @@ -0,0 +1,27 @@ +from typing import Union + +from graphene import Field, Interface, ObjectType +from graphene_directives import Schema + +from .utils import build_ast, evaluate_ast, to_case + + +def validate_key( + type_: Union[ObjectType, Interface, Field], inputs: dict, schema: Schema +) -> bool: + ast_node = build_ast( + input_str=to_case(inputs.get("fields"), schema), valid_special_chars="_" + ) + errors = [] + evaluate_ast( + directive_name="key", + nodes=ast_node, + type_=type_, + ignore_fields=[], + errors=errors, + entity_types=schema.graphql_schema.type_map, + ) + if len(errors) != 0: + raise ValueError("\n".join(errors)) + + return True diff --git a/graphene_federation/validators/requires.py b/graphene_federation/validators/requires.py new file mode 100644 index 0000000..8c4eff9 --- /dev/null +++ b/graphene_federation/validators/requires.py @@ -0,0 +1,31 @@ +from typing import Union + +from graphene import Field, Interface, ObjectType +from graphene_directives import Schema + +from .utils import build_ast, evaluate_ast, to_case + + +def validate_requires( + parent_type: Union[ObjectType, Interface], + _field: Field, + inputs: dict, + schema: Schema, +) -> bool: + ast_node = build_ast( + input_str=to_case(inputs.get("fields"), schema), valid_special_chars='_()"' + ) + + errors = [] + evaluate_ast( + directive_name="requires", + nodes=ast_node, + type_=parent_type.graphene_type, + ignore_fields=["__typename", "_Typename"], + errors=errors, + entity_types=schema.graphql_schema.type_map, + ) + if len(errors) != 0: + raise ValueError("\n".join(errors)) + + return True diff --git a/graphene_federation/validators/utils.py b/graphene_federation/validators/utils.py new file mode 100644 index 0000000..03f02d0 --- /dev/null +++ b/graphene_federation/validators/utils.py @@ -0,0 +1,120 @@ +import re +from typing import Union + +from graphene import Field, Interface, NonNull, ObjectType +from graphene.types.definitions import GrapheneObjectType +from graphene.utils.str_converters import to_camel_case +from graphene_directives import Schema +from graphql import GraphQLField, GraphQLNonNull + + +def clean_combined_brackets(data, bracket_kind) -> list[str]: + cleaned = [] + + filtered = list(filter(lambda x: x != "", data.split(bracket_kind))) + + for index, field in enumerate(filtered): + if index % 2: + cleaned.extend([field, bracket_kind]) + else: + cleaned.append(field) + + if not len(filtered) % 2 == 0: + cleaned.append(bracket_kind) + + return cleaned + + +def build_ast(input_str: str, valid_special_chars: str) -> dict: + fields = input_str.split() + cleaned_fields = [] + pattern = rf"[^a-zA-Z{valid_special_chars}]+" + + for field in fields: + if "{" in field and field != "{": + cleaned_fields.extend(clean_combined_brackets(field, "{")) + elif "}" in field and field != "}": + cleaned_fields.extend(clean_combined_brackets(field, "}")) + elif field == "{" or field == "}": + cleaned_fields.append(field) + else: + cleaned_fields.append(re.sub(pattern, "", field)) + + parent = {} + field_stack = [] + field_level = [parent] + for index, field in enumerate(cleaned_fields): + if field == "{": + field_level.append(field_level[-1][field_stack[-1]]) + elif field == "}": + field_level.pop() + else: + field_stack.append(field) + field_level[-1][field] = {} + return parent + + +def check_fields_exist_on_type( + field: str, + type_: Union[ObjectType, Interface, Field, NonNull], + ignore_fields: list[str], + entity_types: dict[str, ObjectType], +) -> bool: + if field in ignore_fields: + return True + + if isinstance(type_, GraphQLField): + return check_fields_exist_on_type( + field, + type_.type, # noqa + ignore_fields, + entity_types, + ) + elif isinstance(type_, GraphQLNonNull): + return check_fields_exist_on_type( + field, type_.of_type, ignore_fields, entity_types + ) + elif isinstance(type_, GrapheneObjectType): + return field in type_.fields + elif issubclass(type_, ObjectType) or issubclass(type_, Interface): # noqa + return field in entity_types.get(type_._meta.name).fields # noqa + + return False + + +def evaluate_ast( + directive_name: str, + nodes: dict, + type_: ObjectType, + ignore_fields: list[str], + errors: list[str], + entity_types: dict[str, ObjectType], +) -> None: + for field, value in nodes.items(): + if not check_fields_exist_on_type( + field, + type_, + ignore_fields, + entity_types, + ): + errors.append( + f'@{directive_name}, field "{field}" does not exist on type "{type_}"' + ) # noqa + if len(value) != 0: + if hasattr(type_, "_meta"): + type_ = entity_types.get(type_._meta.name) # noqa + field_type = type_.fields[field] # noqa + else: + field_type = type_.fields[field] # noqa + evaluate_ast( + directive_name, + value, + field_type, + ignore_fields, + errors, + entity_types, + ) + + +def to_case(fields: str, schema: Schema) -> str: + return to_camel_case(fields) if schema.auto_camelcase else fields diff --git a/setup.py b/setup.py index 144e616..26df441 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,5 @@ import os + from setuptools import find_packages, setup @@ -35,6 +36,7 @@ def read(*rnames): install_requires=[ "graphene>=3.1", "graphql-core>=3.1", + "graphene-directives>=0.4.3", ], classifiers=[ "Development Status :: 5 - Production/Stable", diff --git a/tests/gql/test_annotation_corner_cases/test_annotate_object_with_meta_name_1.graphql b/tests/gql/test_annotation_corner_cases/test_annotate_object_with_meta_name_1.graphql new file mode 100644 index 0000000..efe26ec --- /dev/null +++ b/tests/gql/test_annotation_corner_cases/test_annotate_object_with_meta_name_1.graphql @@ -0,0 +1,25 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@extends", "@external", "@key"]) + +type Query { + a: Banana + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! +} + +type Banana @extends { + id: ID @external + b(id: ID ): Potato +} + +type Potato @key(fields: "id") { + id: ID +} + +union _Entity = Banana | Potato + +scalar _Any + +type _Service { + sdl: String +} \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases/test_annotate_object_with_meta_name_2.graphql b/tests/gql/test_annotation_corner_cases/test_annotate_object_with_meta_name_2.graphql new file mode 100644 index 0000000..3af4f57 --- /dev/null +++ b/tests/gql/test_annotation_corner_cases/test_annotate_object_with_meta_name_2.graphql @@ -0,0 +1,15 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@extends", "@external", "@key"]) + +type Query { + a: Banana +} + +type Banana @extends { + id: ID @external + b(id: ID ): Potato +} + +type Potato @key(fields: "id") { + id: ID +} \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases/test_annotated_field_also_used_in_filter_1.graphql b/tests/gql/test_annotation_corner_cases/test_annotated_field_also_used_in_filter_1.graphql new file mode 100644 index 0000000..8c2fde0 --- /dev/null +++ b/tests/gql/test_annotation_corner_cases/test_annotated_field_also_used_in_filter_1.graphql @@ -0,0 +1,25 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@extends", "@external", "@key"]) + +type Query { + a: A + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! +} + +type A @extends { + id: ID @external + b(id: ID ): B +} + +type B @key(fields: "id") { + id: ID +} + +union _Entity = A | B + +scalar _Any + +type _Service { + sdl: String +} \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases/test_annotated_field_also_used_in_filter_2.graphql b/tests/gql/test_annotation_corner_cases/test_annotated_field_also_used_in_filter_2.graphql new file mode 100644 index 0000000..30f8bbe --- /dev/null +++ b/tests/gql/test_annotation_corner_cases/test_annotated_field_also_used_in_filter_2.graphql @@ -0,0 +1,15 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@extends", "@external", "@key"]) + +type Query { + a: A +} + +type A @extends { + id: ID @external + b(id: ID ): B +} + +type B @key(fields: "id") { + id: ID +} \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_1.graphql b/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_1.graphql new file mode 100644 index 0000000..dad0a06 --- /dev/null +++ b/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_1.graphql @@ -0,0 +1,23 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@extends", "@external", "@key", "@requires"]) + +type Query { + camel: Camel + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! +} + +type Camel @key(fields: "autoCamel") @extends { + autoCamel: String @external + forcedCamel: String @requires(fields: "autoCamel") + aSnake: String + aCamel: String +} + +union _Entity = Camel + +scalar _Any + +type _Service { + sdl: String +} \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_2.graphql b/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_2.graphql new file mode 100644 index 0000000..c492c80 --- /dev/null +++ b/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_2.graphql @@ -0,0 +1,13 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@extends", "@external", "@key", "@requires"]) + +type Query { + camel: Camel +} + +type Camel @key(fields: "autoCamel") @extends { + autoCamel: String @external + forcedCamel: String @requires(fields: "autoCamel") + aSnake: String + aCamel: String +} \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_without_auto_camelcase_1.graphql b/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_without_auto_camelcase_1.graphql new file mode 100644 index 0000000..19c3729 --- /dev/null +++ b/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_without_auto_camelcase_1.graphql @@ -0,0 +1,23 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@extends", "@external", "@requires"]) + +type Query { + camel: Camel + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! +} + +type Camel @extends { + auto_camel: String @external + forcedCamel: String @requires(fields: "auto_camel") + a_snake: String + aCamel: String +} + +union _Entity = Camel + +scalar _Any + +type _Service { + sdl: String +} \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_without_auto_camelcase_2.graphql b/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_without_auto_camelcase_2.graphql new file mode 100644 index 0000000..c53e1d5 --- /dev/null +++ b/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_without_auto_camelcase_2.graphql @@ -0,0 +1,13 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@extends", "@external", "@requires"]) + +type Query { + camel: Camel +} + +type Camel @extends { + auto_camel: String @external + forcedCamel: String @requires(fields: "auto_camel") + a_snake: String + aCamel: String +} \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases/test_similar_field_name_1.graphql b/tests/gql/test_annotation_corner_cases/test_similar_field_name_1.graphql new file mode 100644 index 0000000..f7227c6 --- /dev/null +++ b/tests/gql/test_annotation_corner_cases/test_similar_field_name_1.graphql @@ -0,0 +1,33 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@extends", "@external", "@key"]) + +schema { + query: ChatQuery +} + +type ChatQuery { + message(id: ID!): ChatMessage + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! +} + +type ChatMessage { + id: ID! + user: ChatUser +} + +type ChatUser @key(fields: "id") @extends { + uid: ID + identified: ID + id: ID @external + iD: ID + ID: ID +} + +union _Entity = ChatUser + +scalar _Any + +type _Service { + sdl: String +} \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases/test_similar_field_name_2.graphql b/tests/gql/test_annotation_corner_cases/test_similar_field_name_2.graphql new file mode 100644 index 0000000..f3a6094 --- /dev/null +++ b/tests/gql/test_annotation_corner_cases/test_similar_field_name_2.graphql @@ -0,0 +1,23 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@extends", "@external", "@key"]) + +schema { + query: ChatQuery +} + +type ChatQuery { + message(id: ID!): ChatMessage +} + +type ChatMessage { + id: ID! + user: ChatUser +} + +type ChatUser @key(fields: "id") @extends { + uid: ID + identified: ID + id: ID @external + iD: ID + ID: ID +} \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases_v1/test_annotate_object_with_meta_name_1.graphql b/tests/gql/test_annotation_corner_cases_v1/test_annotate_object_with_meta_name_1.graphql new file mode 100644 index 0000000..b2d0d44 --- /dev/null +++ b/tests/gql/test_annotation_corner_cases_v1/test_annotate_object_with_meta_name_1.graphql @@ -0,0 +1,22 @@ +type Query { + a: Banana + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! +} + +type Banana @extends { + id: ID @external + b(id: ID ): Potato +} + +type Potato @key(fields: "id") { + id: ID +} + +union _Entity = Banana | Potato + +scalar _Any + +type _Service { + sdl: String +} \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases_v1/test_annotate_object_with_meta_name_2.graphql b/tests/gql/test_annotation_corner_cases_v1/test_annotate_object_with_meta_name_2.graphql new file mode 100644 index 0000000..24f6c7a --- /dev/null +++ b/tests/gql/test_annotation_corner_cases_v1/test_annotate_object_with_meta_name_2.graphql @@ -0,0 +1,12 @@ +type Query { + a: Banana +} + +type Banana @extends { + id: ID @external + b(id: ID ): Potato +} + +type Potato @key(fields: "id") { + id: ID +} \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases_v1/test_annotated_field_also_used_in_filter_1.graphql b/tests/gql/test_annotation_corner_cases_v1/test_annotated_field_also_used_in_filter_1.graphql new file mode 100644 index 0000000..6743c39 --- /dev/null +++ b/tests/gql/test_annotation_corner_cases_v1/test_annotated_field_also_used_in_filter_1.graphql @@ -0,0 +1,22 @@ +type Query { + a: A + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! +} + +type A @extends { + id: ID @external + b(id: ID ): B +} + +type B @key(fields: "id") { + id: ID +} + +union _Entity = A | B + +scalar _Any + +type _Service { + sdl: String +} \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases_v1/test_annotated_field_also_used_in_filter_2.graphql b/tests/gql/test_annotation_corner_cases_v1/test_annotated_field_also_used_in_filter_2.graphql new file mode 100644 index 0000000..f06d78a --- /dev/null +++ b/tests/gql/test_annotation_corner_cases_v1/test_annotated_field_also_used_in_filter_2.graphql @@ -0,0 +1,12 @@ +type Query { + a: A +} + +type A @extends { + id: ID @external + b(id: ID ): B +} + +type B @key(fields: "id") { + id: ID +} \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_1.graphql b/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_1.graphql new file mode 100644 index 0000000..75b54e6 --- /dev/null +++ b/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_1.graphql @@ -0,0 +1,20 @@ +type Query { + camel: Camel + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! +} + +type Camel @key(fields: "autoCamel") @extends { + autoCamel: String @external + forcedCamel: String @requires(fields: "autoCamel") + aSnake: String + aCamel: String +} + +union _Entity = Camel + +scalar _Any + +type _Service { + sdl: String +} \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_2.graphql b/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_2.graphql new file mode 100644 index 0000000..11e9b8d --- /dev/null +++ b/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_2.graphql @@ -0,0 +1,10 @@ +type Query { + camel: Camel +} + +type Camel @key(fields: "autoCamel") @extends { + autoCamel: String @external + forcedCamel: String @requires(fields: "autoCamel") + aSnake: String + aCamel: String +} \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_without_auto_camelcase_1.graphql b/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_without_auto_camelcase_1.graphql new file mode 100644 index 0000000..25aac2c --- /dev/null +++ b/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_without_auto_camelcase_1.graphql @@ -0,0 +1,20 @@ +type Query { + camel: Camel + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! +} + +type Camel @extends { + auto_camel: String @external + forcedCamel: String @requires(fields: "auto_camel") + a_snake: String + aCamel: String +} + +union _Entity = Camel + +scalar _Any + +type _Service { + sdl: String +} \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_without_auto_camelcase_2.graphql b/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_without_auto_camelcase_2.graphql new file mode 100644 index 0000000..095afd2 --- /dev/null +++ b/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_without_auto_camelcase_2.graphql @@ -0,0 +1,10 @@ +type Query { + camel: Camel +} + +type Camel @extends { + auto_camel: String @external + forcedCamel: String @requires(fields: "auto_camel") + a_snake: String + aCamel: String +} \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases_v1/test_similar_field_name_1.graphql b/tests/gql/test_annotation_corner_cases_v1/test_similar_field_name_1.graphql new file mode 100644 index 0000000..3531b4d --- /dev/null +++ b/tests/gql/test_annotation_corner_cases_v1/test_similar_field_name_1.graphql @@ -0,0 +1,30 @@ +schema { + query: ChatQuery +} + +type ChatQuery { + message(id: ID!): ChatMessage + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! +} + +type ChatMessage { + id: ID! + user: ChatUser +} + +type ChatUser @key(fields: "id") @extends { + uid: ID + identified: ID + id: ID @external + iD: ID + ID: ID +} + +union _Entity = ChatUser + +scalar _Any + +type _Service { + sdl: String +} \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases_v1/test_similar_field_name_2.graphql b/tests/gql/test_annotation_corner_cases_v1/test_similar_field_name_2.graphql new file mode 100644 index 0000000..7bb2baa --- /dev/null +++ b/tests/gql/test_annotation_corner_cases_v1/test_similar_field_name_2.graphql @@ -0,0 +1,20 @@ +schema { + query: ChatQuery +} + +type ChatQuery { + message(id: ID!): ChatMessage +} + +type ChatMessage { + id: ID! + user: ChatUser +} + +type ChatUser @key(fields: "id") @extends { + uid: ID + identified: ID + id: ID @external + iD: ID + ID: ID +} \ No newline at end of file diff --git a/tests/gql/test_custom_enum/test_custom_enum_1.graphql b/tests/gql/test_custom_enum/test_custom_enum_1.graphql new file mode 100644 index 0000000..4da9a1b --- /dev/null +++ b/tests/gql/test_custom_enum/test_custom_enum_1.graphql @@ -0,0 +1,23 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@inaccessible", "@shareable"]) + +type TestCustomEnum @shareable { + testShareableScalar: Episode @shareable + testInaccessibleScalar: Episode @inaccessible +} + +enum Episode @inaccessible { + NEWHOPE @inaccessible + EMPIRE + JEDI +} + +type Query { + test: Episode + test2: [TestCustomEnum]! + _service: _Service! +} + +type _Service { + sdl: String +} \ No newline at end of file diff --git a/tests/gql/test_custom_enum/test_custom_enum_2.graphql b/tests/gql/test_custom_enum/test_custom_enum_2.graphql new file mode 100644 index 0000000..30c6101 --- /dev/null +++ b/tests/gql/test_custom_enum/test_custom_enum_2.graphql @@ -0,0 +1,18 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@inaccessible", "@shareable"]) + +type TestCustomEnum @shareable { + testShareableScalar: Episode @shareable + testInaccessibleScalar: Episode @inaccessible +} + +enum Episode @inaccessible { + NEWHOPE @inaccessible + EMPIRE + JEDI +} + +type Query { + test: Episode + test2: [TestCustomEnum]! +} \ No newline at end of file diff --git a/tests/test_annotation_corner_cases.py b/tests/test_annotation_corner_cases.py new file mode 100644 index 0000000..b0078d7 --- /dev/null +++ b/tests/test_annotation_corner_cases.py @@ -0,0 +1,127 @@ +from pathlib import Path + +from graphene import Field, ID, ObjectType, String + +from graphene_federation import build_schema, extends, external, key, requires +from tests.util import file_handlers, sdl_query + +save_file, open_file = file_handlers(Path(__file__)) + + +def test_similar_field_name(): + """ + Test annotation with fields that have similar names. + """ + + @extends + @key("id") + class ChatUser(ObjectType): + uid = ID() + identified = ID() + id = external(ID()) + i_d = ID() + ID = ID() + + class ChatMessage(ObjectType): + id = ID(required=True) + user = Field(ChatUser) + + class ChatQuery(ObjectType): + message = Field(ChatMessage, id=ID(required=True)) + + schema = build_schema(query=ChatQuery, enable_federation_2=True) + + assert open_file("1") == str(schema) + assert open_file("2") == sdl_query(schema) + + +def test_camel_case_field_name(): + """ + Test annotation with fields that have camel cases or snake case. + """ + + @key("auto_camel") + @extends + class Camel(ObjectType): + auto_camel = external(String()) + forcedCamel = requires(String(), fields="auto_camel") + a_snake = String() + aCamel = String() + + class Query(ObjectType): + camel = Field(Camel) + + schema = build_schema(query=Query, enable_federation_2=True) + + assert open_file("1") == str(schema) + assert open_file("2") == sdl_query(schema) + + +def test_camel_case_field_name_without_auto_camelcase(): + """ + Test annotation with fields that have camel cases or snake case but with the auto_camelcase disabled. + """ + + @extends + class Camel(ObjectType): + auto_camel = external(String()) + forcedCamel = requires(String(), fields="auto_camel") + a_snake = String() + aCamel = String() + + class Query(ObjectType): + camel = Field(Camel) + + schema = build_schema(query=Query, auto_camelcase=False, enable_federation_2=True) + + assert open_file("1") == str(schema) + assert open_file("2") == sdl_query(schema) + + +def test_annotated_field_also_used_in_filter(): + """ + Test that when a field also used in filter needs to get annotated, it really annotates only the field. + See issue https://github.com/preply/graphene-federation/issues/50 + """ + + @key("id") + class B(ObjectType): + id = ID() + + @extends + class A(ObjectType): + id = external(ID()) + b = Field(B, id=ID()) + + class Query(ObjectType): + a = Field(A) + + schema = build_schema(query=Query, enable_federation_2=True) + + assert open_file("1") == str(schema) + assert open_file("2") == sdl_query(schema) + + +def test_annotate_object_with_meta_name(): + @key("id") + class B(ObjectType): + class Meta: + name = "Potato" + + id = ID() + + @extends + class A(ObjectType): + class Meta: + name = "Banana" + + id = external(ID()) + b = Field(B, id=ID()) + + class Query(ObjectType): + a = Field(A) + + schema = build_schema(query=Query, enable_federation_2=True) + + assert open_file("1") == str(schema) + assert open_file("2") == sdl_query(schema) diff --git a/tests/test_annotation_corner_cases_v1.py b/tests/test_annotation_corner_cases_v1.py new file mode 100644 index 0000000..6e50829 --- /dev/null +++ b/tests/test_annotation_corner_cases_v1.py @@ -0,0 +1,127 @@ +from pathlib import Path + +from graphene import Field, ID, ObjectType, String + +from graphene_federation import build_schema, extends, external, key, requires +from tests.util import file_handlers, sdl_query + +save_file, open_file = file_handlers(Path(__file__)) + + +def test_similar_field_name(): + """ + Test annotation with fields that have similar names. + """ + + @extends + @key("id") + class ChatUser(ObjectType): + uid = ID() + identified = ID() + id = external(ID()) + i_d = ID() + ID = ID() + + class ChatMessage(ObjectType): + id = ID(required=True) + user = Field(ChatUser) + + class ChatQuery(ObjectType): + message = Field(ChatMessage, id=ID(required=True)) + + schema = build_schema(query=ChatQuery, enable_federation_2=False) + + assert open_file("1") == str(schema) + assert open_file("2") == sdl_query(schema) + + +def test_camel_case_field_name(): + """ + Test annotation with fields that have camel cases or snake case. + """ + + @key("auto_camel") + @extends + class Camel(ObjectType): + auto_camel = external(String()) + forcedCamel = requires(String(), fields="auto_camel") + a_snake = String() + aCamel = String() + + class Query(ObjectType): + camel = Field(Camel) + + schema = build_schema(query=Query, enable_federation_2=False) + + assert open_file("1") == str(schema) + assert open_file("2") == sdl_query(schema) + + +def test_camel_case_field_name_without_auto_camelcase(): + """ + Test annotation with fields that have camel cases or snake case but with the auto_camelcase disabled. + """ + + @extends + class Camel(ObjectType): + auto_camel = external(String()) + forcedCamel = requires(String(), fields="auto_camel") + a_snake = String() + aCamel = String() + + class Query(ObjectType): + camel = Field(Camel) + + schema = build_schema(query=Query, auto_camelcase=False, enable_federation_2=False) + + assert open_file("1") == str(schema) + assert open_file("2") == sdl_query(schema) + + +def test_annotated_field_also_used_in_filter(): + """ + Test that when a field also used in filter needs to get annotated, it really annotates only the field. + See issue https://github.com/preply/graphene-federation/issues/50 + """ + + @key("id") + class B(ObjectType): + id = ID() + + @extends + class A(ObjectType): + id = external(ID()) + b = Field(B, id=ID()) + + class Query(ObjectType): + a = Field(A) + + schema = build_schema(query=Query, enable_federation_2=False) + + assert open_file("1") == str(schema) + assert open_file("2") == sdl_query(schema) + + +def test_annotate_object_with_meta_name(): + @key("id") + class B(ObjectType): + class Meta: + name = "Potato" + + id = ID() + + @extends + class A(ObjectType): + class Meta: + name = "Banana" + + id = external(ID()) + b = Field(B, id=ID()) + + class Query(ObjectType): + a = Field(A) + + schema = build_schema(query=Query, enable_federation_2=False) + + assert open_file("1") == str(schema) + assert open_file("2") == sdl_query(schema) diff --git a/tests/test_custom_enum.py b/tests/test_custom_enum.py new file mode 100644 index 0000000..ebb8bd8 --- /dev/null +++ b/tests/test_custom_enum.py @@ -0,0 +1,36 @@ +from pathlib import Path + +import graphene +from graphene import ObjectType + +from graphene_federation import build_schema +from graphene_federation import inaccessible, shareable +from tests.util import file_handlers, sdl_query + +save_file, open_file = file_handlers(Path(__file__)) + + +def test_custom_enum(): + @inaccessible + class Episode(graphene.Enum): + NEWHOPE = 4 + EMPIRE = 5 + JEDI = 6 + + inaccessible(Episode.NEWHOPE) + + @shareable + class TestCustomEnum(graphene.ObjectType): + test_shareable_scalar = shareable(Episode()) + test_inaccessible_scalar = inaccessible(Episode()) + + class Query(ObjectType): + test = Episode() + test2 = graphene.List(TestCustomEnum, required=True) + + schema = build_schema( + query=Query, enable_federation_2=True, types=(TestCustomEnum,) + ) + + assert open_file("1") == str(schema) + assert open_file("2") == sdl_query(schema) diff --git a/tests/util.py b/tests/util.py new file mode 100644 index 0000000..111e80f --- /dev/null +++ b/tests/util.py @@ -0,0 +1,42 @@ +import inspect +import os +from collections.abc import Callable +from pathlib import Path +from typing import Any + +from graphql import graphql_sync + + +def file_handlers( + path: Path, +) -> tuple[Callable[[Any, str], None], Callable[[str], str]]: + curr_dir = path.parent + file_name = path.name.replace(".py", "") + + try: + os.mkdir(f"{curr_dir}/gql/{file_name}") + except FileExistsError: + pass + + def save_file(data, extra_path: str = ""): + function_name = inspect.stack()[1].function + with open( + f"{curr_dir}/gql/{file_name}/{function_name}_{extra_path}.graphql", "w" + ) as f: + f.write(str(data)) + + def open_file(extra_path: str = ""): + function_name = inspect.stack()[1].function + with open( + f"{curr_dir}/gql/{file_name}/{function_name}_{extra_path}.graphql", "r" + ) as f: + return f.read() + + return save_file, open_file + + +def sdl_query(schema) -> str: + query = "query { _service { sdl } }" + result = graphql_sync(schema.graphql_schema, query) + assert not result.errors + return result.data["_service"]["sdl"] From 9210be676c0be32bcb41678f7776e7003175da30 Mon Sep 17 00:00:00 2001 From: M Aswin Kishore <60577077+mak626@users.noreply.github.com> Date: Sun, 14 Jan 2024 13:07:39 +0530 Subject: [PATCH 02/19] deprecate: extends --- graphene_federation/validators/__init__.py | 1 - graphene_federation/validators/extends.py | 26 ---------------------- 2 files changed, 27 deletions(-) delete mode 100644 graphene_federation/validators/extends.py diff --git a/graphene_federation/validators/__init__.py b/graphene_federation/validators/__init__.py index 4dc7427..da76627 100644 --- a/graphene_federation/validators/__init__.py +++ b/graphene_federation/validators/__init__.py @@ -1,4 +1,3 @@ -from .extends import validate_extends from .key import validate_key from .requires import validate_requires from .utils import build_ast diff --git a/graphene_federation/validators/extends.py b/graphene_federation/validators/extends.py deleted file mode 100644 index 40a1b6f..0000000 --- a/graphene_federation/validators/extends.py +++ /dev/null @@ -1,26 +0,0 @@ -from typing import Union - -from graphene import Field, Interface, ObjectType -from graphene_directives import Schema -from graphene_directives.utils import has_non_field_attribute - - -# todoo: remove -def validate_extends( - type_: Union[ObjectType, Interface, Field], _inputs: dict, _schema: Schema -) -> bool: - from ..appolo_versions import LATEST_VERSION, get_directive_from_name - - key = get_directive_from_name("key", LATEST_VERSION) - - assert not has_non_field_attribute( - type_=type_, target_directive=key - ), f"Can't extend type on {type_} which has @key" - - assert getattr(type_._meta, "description", None) is None, ( - f'Type "{type_.__name__}" has a non empty description and it is also marked with extend.' - "\nThey are mutually exclusive." - "\nSee https://github.com/graphql/graphql-js/issues/2385#issuecomment-577997521" - ) - - return True From 1cd59b84f3525f46d0e7cb546a0cded203be3379 Mon Sep 17 00:00:00 2001 From: M Aswin Kishore <60577077+mak626@users.noreply.github.com> Date: Sun, 14 Jan 2024 13:14:05 +0530 Subject: [PATCH 03/19] add: support upto 2.6 spec --- .github/workflows/tests.yml | 4 +- Makefile | 4 +- README.md | 24 +- examples/extend.py | 8 +- .../federation-v1.0.graphql | 0 .../federation-v2.0.graphql | 0 .../federation-v2.1.graphql | 0 .../federation-v2.2.graphql | 0 .../federation-v2.3.graphql | 0 federation_spec/federation-v2.4.graphql | 32 ++ federation_spec/federation-v2.5.graphql | 45 +++ federation_spec/federation-v2.6.graphql | 52 +++ graphene_federation/__init__.py | 8 +- .../apollo_versions/__init__.py | 49 +++ .../v1_0.py | 2 +- .../v2_0.py | 2 +- .../v2_1.py | 0 .../v2_2.py | 0 .../v2_3.py | 0 graphene_federation/apollo_versions/v2_4.py | 8 + graphene_federation/apollo_versions/v2_5.py | 50 +++ graphene_federation/apollo_versions/v2_6.py | 34 ++ .../version.py | 3 + .../appolo_versions/__init__.py | 41 --- graphene_federation/directives/__init__.py | 3 + .../directives/authenticated.py | 29 ++ graphene_federation/directives/extends.py | 30 +- graphene_federation/directives/external.py | 19 +- .../directives/inaccessible.py | 19 +- .../directives/interface_object.py | 32 +- graphene_federation/directives/key.py | 37 +- graphene_federation/directives/override.py | 30 +- graphene_federation/directives/policy.py | 26 ++ graphene_federation/directives/provides.py | 37 +- graphene_federation/directives/requires.py | 61 ++-- .../directives/requires_scopes.py | 28 ++ graphene_federation/directives/shareable.py | 20 +- graphene_federation/directives/tag.py | 20 +- graphene_federation/entity.py | 24 +- graphene_federation/main.py | 37 +- graphene_federation/scalars/__init__.py | 2 + .../scalars/federation_policy.py | 51 +++ graphene_federation/scalars/field_set_v1.py | 10 +- graphene_federation/scalars/field_set_v2.py | 10 +- graphene_federation/scalars/link_import.py | 2 +- graphene_federation/scalars/scope.py | 47 +++ .../schema_directives/compose_directive.py | 2 +- graphene_federation/service.py | 7 - graphene_federation/tests/test_extend_v1.py | 52 --- .../tests/test_inaccessible.py | 126 ------- graphene_federation/tests/test_key.py | 306 ---------------- graphene_federation/tests/test_key_v1.py | 83 ----- graphene_federation/tests/test_provides.py | 256 ------------- graphene_federation/tests/test_provides_v1.py | 251 ------------- graphene_federation/tests/test_requires.py | 230 ------------ graphene_federation/tests/test_requires_v1.py | 228 ------------ graphene_federation/tests/test_scalar.py | 61 ---- graphene_federation/tests/test_shareable.py | 135 ------- .../transform/field_set_case_transform.py | 9 +- graphene_federation/utils.py | 6 - graphene_federation/validators/__init__.py | 3 +- graphene_federation/validators/key.py | 8 +- graphene_federation/validators/requires.py | 15 +- graphene_federation/validators/utils.py | 344 ++++++++++++++---- integration_tests/service_a/src/schema.py | 13 +- integration_tests/service_c/src/schema.py | 8 +- integration_tests/service_d/src/schema.py | 7 +- integration_tests/tests/tests/test_main.py | 7 +- setup.py | 5 +- tests/__init__.py | 0 ...t_annotate_object_with_meta_name_1.graphql | 10 +- ...t_annotate_object_with_meta_name_2.graphql | 10 +- ...otated_field_also_used_in_filter_1.graphql | 10 +- ...otated_field_also_used_in_filter_2.graphql | 10 +- .../test_camel_case_field_name_1.graphql | 10 +- .../test_camel_case_field_name_2.graphql | 10 +- ...ield_name_without_auto_camelcase_1.graphql | 10 +- ...ield_name_without_auto_camelcase_2.graphql | 10 +- .../test_similar_field_name_1.graphql | 10 +- .../test_similar_field_name_2.graphql | 10 +- ...t_annotate_object_with_meta_name_1.graphql | 4 +- ...t_annotate_object_with_meta_name_2.graphql | 4 +- ...otated_field_also_used_in_filter_1.graphql | 4 +- ...otated_field_also_used_in_filter_2.graphql | 4 +- .../test_camel_case_field_name_1.graphql | 4 +- .../test_camel_case_field_name_2.graphql | 4 +- ...ield_name_without_auto_camelcase_1.graphql | 4 +- ...ield_name_without_auto_camelcase_2.graphql | 4 +- .../test_similar_field_name_1.graphql | 4 +- .../test_similar_field_name_2.graphql | 4 +- .../test_custom_enum_1.graphql | 10 +- .../test_custom_enum_2.graphql | 10 +- .../test_inaccessible_1.graphql | 22 ++ .../test_inaccessible_2.graphql | 17 + .../test_inaccessible_union_1.graphql | 34 ++ .../test_inaccessible_union_2.graphql | 29 ++ .../test_compound_primary_key_1.graphql | 31 ++ .../test_compound_primary_key_2.graphql | 21 ++ ..._compound_primary_key_with_depth_1.graphql | 37 ++ ..._compound_primary_key_with_depth_2.graphql | 27 ++ .../gql/test_key/test_multiple_keys_1.graphql | 27 ++ .../gql/test_key/test_multiple_keys_2.graphql | 17 + .../test_key_v1/test_multiple_keys_1.graphql | 20 + .../test_key_v1/test_multiple_keys_2.graphql | 10 + .../gql/test_provides/test_provides_1.graphql | 33 ++ .../gql/test_provides/test_provides_2.graphql | 23 ++ .../test_provides_multiple_fields_1.graphql | 33 ++ .../test_provides_multiple_fields_2.graphql | 23 ++ ...provides_multiple_fields_as_list_1.graphql | 33 ++ ...provides_multiple_fields_as_list_2.graphql | 23 ++ .../test_provides_v1/test_provides_1.graphql | 26 ++ .../test_provides_v1/test_provides_2.graphql | 16 + .../test_provides_multiple_fields_1.graphql | 26 ++ .../test_provides_multiple_fields_2.graphql | 16 + ...provides_multiple_fields_as_list_1.graphql | 26 ++ ...provides_multiple_fields_as_list_2.graphql | 16 + .../test_requires_multiple_fields_1.graphql | 29 ++ .../test_requires_multiple_fields_2.graphql | 19 + ...requires_multiple_fields_as_list_1.graphql | 29 ++ ...requires_multiple_fields_as_list_2.graphql | 19 + .../test_requires_with_input_1.graphql | 28 ++ .../test_requires_with_input_2.graphql | 18 + .../test_requires_multiple_fields_1.graphql | 22 ++ .../test_requires_multiple_fields_2.graphql | 12 + ...requires_multiple_fields_as_list_1.graphql | 22 ++ ...requires_multiple_fields_as_list_2.graphql | 12 + .../test_requires_with_input_1.graphql | 21 ++ .../test_requires_with_input_2.graphql | 11 + .../test_scalar/test_custom_scalar_1.graphql | 25 ++ .../test_scalar/test_custom_scalar_2.graphql | 20 + .../test_chat_schema_1.graphql | 37 ++ .../test_chat_schema_2.graphql | 27 ++ .../test_user_schema_1.graphql | 32 ++ .../test_user_schema_2.graphql | 22 ++ .../test_chat_schema_1.graphql | 30 ++ .../test_chat_schema_2.graphql | 20 + .../test_user_schema_1.graphql | 25 ++ .../test_user_schema_2.graphql | 15 + .../test_shareable/test_shareable_1.graphql | 22 ++ .../test_shareable/test_shareable_2.graphql | 17 + tests/test_extends.py | 36 ++ tests/test_inaccessible.py | 73 ++++ tests/test_key.py | 148 ++++++++ tests/test_key_v1.py | 40 ++ tests/test_provides.py | 83 +++++ tests/test_provides_v1.py | 83 +++++ tests/test_requires.py | 99 +++++ tests/test_requires_v1.py | 90 +++++ tests/test_scalar.py | 42 +++ .../tests => tests}/test_schema_annotation.py | 141 +------ .../test_schema_annotation_v1.py | 138 +------ tests/test_shareable.py | 83 +++++ 152 files changed, 3131 insertions(+), 2307 deletions(-) rename {graphene_federation/appolo_versions/spec => federation_spec}/federation-v1.0.graphql (100%) rename {graphene_federation/appolo_versions/spec => federation_spec}/federation-v2.0.graphql (100%) rename {graphene_federation/appolo_versions/spec => federation_spec}/federation-v2.1.graphql (100%) rename {graphene_federation/appolo_versions/spec => federation_spec}/federation-v2.2.graphql (100%) rename {graphene_federation/appolo_versions/spec => federation_spec}/federation-v2.3.graphql (100%) create mode 100644 federation_spec/federation-v2.4.graphql create mode 100644 federation_spec/federation-v2.5.graphql create mode 100644 federation_spec/federation-v2.6.graphql create mode 100644 graphene_federation/apollo_versions/__init__.py rename graphene_federation/{appolo_versions => apollo_versions}/v1_0.py (97%) rename graphene_federation/{appolo_versions => apollo_versions}/v2_0.py (98%) rename graphene_federation/{appolo_versions => apollo_versions}/v2_1.py (100%) rename graphene_federation/{appolo_versions => apollo_versions}/v2_2.py (100%) rename graphene_federation/{appolo_versions => apollo_versions}/v2_3.py (100%) create mode 100644 graphene_federation/apollo_versions/v2_4.py create mode 100644 graphene_federation/apollo_versions/v2_5.py create mode 100644 graphene_federation/apollo_versions/v2_6.py rename graphene_federation/{appolo_versions => apollo_versions}/version.py (70%) delete mode 100644 graphene_federation/appolo_versions/__init__.py create mode 100644 graphene_federation/directives/authenticated.py create mode 100644 graphene_federation/directives/policy.py create mode 100644 graphene_federation/directives/requires_scopes.py create mode 100644 graphene_federation/scalars/federation_policy.py create mode 100644 graphene_federation/scalars/scope.py delete mode 100644 graphene_federation/tests/test_extend_v1.py delete mode 100644 graphene_federation/tests/test_inaccessible.py delete mode 100644 graphene_federation/tests/test_key.py delete mode 100644 graphene_federation/tests/test_key_v1.py delete mode 100644 graphene_federation/tests/test_provides.py delete mode 100644 graphene_federation/tests/test_provides_v1.py delete mode 100644 graphene_federation/tests/test_requires.py delete mode 100644 graphene_federation/tests/test_requires_v1.py delete mode 100644 graphene_federation/tests/test_scalar.py delete mode 100644 graphene_federation/tests/test_shareable.py delete mode 100644 graphene_federation/utils.py create mode 100644 tests/__init__.py create mode 100644 tests/gql/test_inaccessible/test_inaccessible_1.graphql create mode 100644 tests/gql/test_inaccessible/test_inaccessible_2.graphql create mode 100644 tests/gql/test_inaccessible/test_inaccessible_union_1.graphql create mode 100644 tests/gql/test_inaccessible/test_inaccessible_union_2.graphql create mode 100644 tests/gql/test_key/test_compound_primary_key_1.graphql create mode 100644 tests/gql/test_key/test_compound_primary_key_2.graphql create mode 100644 tests/gql/test_key/test_compound_primary_key_with_depth_1.graphql create mode 100644 tests/gql/test_key/test_compound_primary_key_with_depth_2.graphql create mode 100644 tests/gql/test_key/test_multiple_keys_1.graphql create mode 100644 tests/gql/test_key/test_multiple_keys_2.graphql create mode 100644 tests/gql/test_key_v1/test_multiple_keys_1.graphql create mode 100644 tests/gql/test_key_v1/test_multiple_keys_2.graphql create mode 100644 tests/gql/test_provides/test_provides_1.graphql create mode 100644 tests/gql/test_provides/test_provides_2.graphql create mode 100644 tests/gql/test_provides/test_provides_multiple_fields_1.graphql create mode 100644 tests/gql/test_provides/test_provides_multiple_fields_2.graphql create mode 100644 tests/gql/test_provides/test_provides_multiple_fields_as_list_1.graphql create mode 100644 tests/gql/test_provides/test_provides_multiple_fields_as_list_2.graphql create mode 100644 tests/gql/test_provides_v1/test_provides_1.graphql create mode 100644 tests/gql/test_provides_v1/test_provides_2.graphql create mode 100644 tests/gql/test_provides_v1/test_provides_multiple_fields_1.graphql create mode 100644 tests/gql/test_provides_v1/test_provides_multiple_fields_2.graphql create mode 100644 tests/gql/test_provides_v1/test_provides_multiple_fields_as_list_1.graphql create mode 100644 tests/gql/test_provides_v1/test_provides_multiple_fields_as_list_2.graphql create mode 100644 tests/gql/test_requires/test_requires_multiple_fields_1.graphql create mode 100644 tests/gql/test_requires/test_requires_multiple_fields_2.graphql create mode 100644 tests/gql/test_requires/test_requires_multiple_fields_as_list_1.graphql create mode 100644 tests/gql/test_requires/test_requires_multiple_fields_as_list_2.graphql create mode 100644 tests/gql/test_requires/test_requires_with_input_1.graphql create mode 100644 tests/gql/test_requires/test_requires_with_input_2.graphql create mode 100644 tests/gql/test_requires_v1/test_requires_multiple_fields_1.graphql create mode 100644 tests/gql/test_requires_v1/test_requires_multiple_fields_2.graphql create mode 100644 tests/gql/test_requires_v1/test_requires_multiple_fields_as_list_1.graphql create mode 100644 tests/gql/test_requires_v1/test_requires_multiple_fields_as_list_2.graphql create mode 100644 tests/gql/test_requires_v1/test_requires_with_input_1.graphql create mode 100644 tests/gql/test_requires_v1/test_requires_with_input_2.graphql create mode 100644 tests/gql/test_scalar/test_custom_scalar_1.graphql create mode 100644 tests/gql/test_scalar/test_custom_scalar_2.graphql create mode 100644 tests/gql/test_schema_annotation/test_chat_schema_1.graphql create mode 100644 tests/gql/test_schema_annotation/test_chat_schema_2.graphql create mode 100644 tests/gql/test_schema_annotation/test_user_schema_1.graphql create mode 100644 tests/gql/test_schema_annotation/test_user_schema_2.graphql create mode 100644 tests/gql/test_schema_annotation_v1/test_chat_schema_1.graphql create mode 100644 tests/gql/test_schema_annotation_v1/test_chat_schema_2.graphql create mode 100644 tests/gql/test_schema_annotation_v1/test_user_schema_1.graphql create mode 100644 tests/gql/test_schema_annotation_v1/test_user_schema_2.graphql create mode 100644 tests/gql/test_shareable/test_shareable_1.graphql create mode 100644 tests/gql/test_shareable/test_shareable_2.graphql create mode 100644 tests/test_extends.py create mode 100644 tests/test_inaccessible.py create mode 100644 tests/test_key.py create mode 100644 tests/test_key_v1.py create mode 100644 tests/test_provides.py create mode 100644 tests/test_provides_v1.py create mode 100644 tests/test_requires.py create mode 100644 tests/test_requires_v1.py create mode 100644 tests/test_scalar.py rename {graphene_federation/tests => tests}/test_schema_annotation.py (51%) rename {graphene_federation/tests => tests}/test_schema_annotation_v1.py (52%) create mode 100644 tests/test_shareable.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 304fc2f..5e21ede 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,7 +8,7 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: ["3.9", "3.10"] + python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v1 @@ -21,7 +21,7 @@ jobs: python -m pip install --upgrade pip pip install -e ".[test]" - name: Run Unit Tests - run: py.test graphene_federation --cov=graphene_federation -vv + run: py.test tests --cov=graphene_federation -vv - name: Upload Coverage env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/Makefile b/Makefile index 8c2dd0a..bd8d30a 100644 --- a/Makefile +++ b/Makefile @@ -15,11 +15,11 @@ integration-tests: ## Run integration tests # ------------------------- dev-setup: ## Install development dependencies - docker-compose up -d && docker-compose exec graphene_federation bash + docker-compose up -d && docker-compose exec graphene_federation pip install -e ".[dev]" .PHONY: dev-setup tests: ## Run unit tests - docker-compose run graphene_federation py.test graphene_federation --cov=graphene_federation -vv + docker-compose run graphene_federation py.test tests --cov=graphene_federation -vv .PHONY: tests check-style: ## Run linting diff --git a/README.md b/README.md index eaf0a02..b2b40bf 100644 --- a/README.md +++ b/README.md @@ -30,12 +30,12 @@ At the moment it supports: * `sdl` (`_service` on field): enable to add schema in federation (as is) * `@key` decorator (entity support): enable to perform queries across service boundaries (you can have more than one key per type) -* `@extend`: extend remote types +* `@extends`: extend remote types * `external()`: mark a field as external * `requires()`: mark that field resolver requires other fields to be pre-fetched * `provides()`/`@provides`: annotate the expected returned fieldset from a field on a base type that is guaranteed to be selectable by the gateway. -Each type which is decorated with `@key` or `@extend` is added to the `_Entity` union. +Each type which is decorated with `@key` or `@extends` is added to the `_Entity` union. The [`__resolve_reference` method](https://www.apollographql.com/docs/federation/api/apollo-federation/#__resolvereference) can be defined for each type that is an entity. Note that since the notation with double underscores can be problematic in Python for model inheritance this resolver method can also be named `_resolve_reference` (the `__resolve_reference` method will take precedence if both are declared). @@ -57,7 +57,7 @@ It implements a federation schema for a basic e-commerce application over three First add an account service that expose a `User` type that can then be referenced in other services by its `id` field: ```python -from graphene import Field, ID, ObjectType, String +from graphene import Field, Int, ObjectType, String from graphene_federation import build_schema, key @key("id") @@ -81,7 +81,7 @@ schema = build_schema(query=Query) The product service exposes a `Product` type that can be used by other services via the `upc` field: ```python -from graphene import Argument, ID, Int, List, ObjectType, String +from graphene import Argument, Int, List, ObjectType, String from graphene_federation import build_schema, key @key("upc") @@ -108,10 +108,12 @@ It also has the ability to provide the username of the `User`. On top of that it adds to the `User`/`Product` types (that are both defined in other services) the ability to get their reviews. ```python -from graphene import Field, ID, Int, List, ObjectType, String -from graphene_federation import build_schema, extend, external, provides +from graphene import Field, Int, List, ObjectType, String -@extend("id") +from graphene_federation import build_schema, extends, external, key, provides + + +@key("id") class User(ObjectType): id = external(Int(required=True)) reviews = List(lambda: Review) @@ -122,21 +124,23 @@ class User(ObjectType): """ return [] -@extend("upc") + +@key("upc") class Product(ObjectType): upc = external(String(required=True)) reviews = List(lambda: Review) -# Note that both the base type and the field need to be decorated with `provides` (on the field itself you need to specify which fields get provided). -@provides + class Review(ObjectType): body = String() author = provides(Field(User), fields="username") product = Field(Product) + class Query(ObjectType): review = Field(Review) + schema = build_schema(query=Query) ``` diff --git a/examples/extend.py b/examples/extend.py index cdb3e1c..92562ae 100644 --- a/examples/extend.py +++ b/examples/extend.py @@ -1,8 +1,10 @@ import graphene -from graphene_federation import build_schema, extend, external +from graphene_federation import build_schema, extends, external, key -@extend(fields="id") + +@key("id") +@extends class Message(graphene.ObjectType): id = external(graphene.Int(required=True)) @@ -28,4 +30,4 @@ def resolve_file(self, **kwargs): """ result = schema.execute(query) print(result.data) -# {'sdl': 'type Query {\n message: Message\n}\n\nextend type Message @key(fields: "id") {\n id: Int! @external\n}'}} +# {'sdl': 'type Query {\n message: Message\n}\n\n type Message @key(fields: "id") @extends {\n id: Int! @external\n}'}} diff --git a/graphene_federation/appolo_versions/spec/federation-v1.0.graphql b/federation_spec/federation-v1.0.graphql similarity index 100% rename from graphene_federation/appolo_versions/spec/federation-v1.0.graphql rename to federation_spec/federation-v1.0.graphql diff --git a/graphene_federation/appolo_versions/spec/federation-v2.0.graphql b/federation_spec/federation-v2.0.graphql similarity index 100% rename from graphene_federation/appolo_versions/spec/federation-v2.0.graphql rename to federation_spec/federation-v2.0.graphql diff --git a/graphene_federation/appolo_versions/spec/federation-v2.1.graphql b/federation_spec/federation-v2.1.graphql similarity index 100% rename from graphene_federation/appolo_versions/spec/federation-v2.1.graphql rename to federation_spec/federation-v2.1.graphql diff --git a/graphene_federation/appolo_versions/spec/federation-v2.2.graphql b/federation_spec/federation-v2.2.graphql similarity index 100% rename from graphene_federation/appolo_versions/spec/federation-v2.2.graphql rename to federation_spec/federation-v2.2.graphql diff --git a/graphene_federation/appolo_versions/spec/federation-v2.3.graphql b/federation_spec/federation-v2.3.graphql similarity index 100% rename from graphene_federation/appolo_versions/spec/federation-v2.3.graphql rename to federation_spec/federation-v2.3.graphql diff --git a/federation_spec/federation-v2.4.graphql b/federation_spec/federation-v2.4.graphql new file mode 100644 index 0000000..8bf0015 --- /dev/null +++ b/federation_spec/federation-v2.4.graphql @@ -0,0 +1,32 @@ +directive @composeDirective(name: String!) repeatable on SCHEMA +directive @extends on OBJECT | INTERFACE +directive @external on OBJECT | FIELD_DEFINITION +directive @key(fields: FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE +directive @inaccessible on + | FIELD_DEFINITION + | OBJECT + | INTERFACE + | UNION + | ENUM + | ENUM_VALUE + | SCALAR + | INPUT_OBJECT + | INPUT_FIELD_DEFINITION + | ARGUMENT_DEFINITION +directive @interfaceObject on OBJECT +directive @override(from: String!) on FIELD_DEFINITION +directive @provides(fields: FieldSet!) on FIELD_DEFINITION +directive @requires(fields: FieldSet!) on FIELD_DEFINITION +directive @shareable repeatable on FIELD_DEFINITION | OBJECT +directive @tag(name: String!) repeatable on + | FIELD_DEFINITION + | INTERFACE + | OBJECT + | UNION + | ARGUMENT_DEFINITION + | SCALAR + | ENUM + | ENUM_VALUE + | INPUT_OBJECT + | INPUT_FIELD_DEFINITION +scalar FieldSet diff --git a/federation_spec/federation-v2.5.graphql b/federation_spec/federation-v2.5.graphql new file mode 100644 index 0000000..6089be2 --- /dev/null +++ b/federation_spec/federation-v2.5.graphql @@ -0,0 +1,45 @@ +directive @composeDirective(name: String!) repeatable on SCHEMA +directive @extends on OBJECT | INTERFACE +directive @external on OBJECT | FIELD_DEFINITION +directive @key(fields: FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE +directive @inaccessible on + | FIELD_DEFINITION + | OBJECT + | INTERFACE + | UNION + | ENUM + | ENUM_VALUE + | SCALAR + | INPUT_OBJECT + | INPUT_FIELD_DEFINITION + | ARGUMENT_DEFINITION +directive @interfaceObject on OBJECT +directive @override(from: String!) on FIELD_DEFINITION +directive @provides(fields: FieldSet!) on FIELD_DEFINITION +directive @requires(fields: FieldSet!) on FIELD_DEFINITION +directive @shareable repeatable on FIELD_DEFINITION | OBJECT +directive @tag(name: String!) repeatable on + | FIELD_DEFINITION + | INTERFACE + | OBJECT + | UNION + | ARGUMENT_DEFINITION + | SCALAR + | ENUM + | ENUM_VALUE + | INPUT_OBJECT + | INPUT_FIELD_DEFINITION +directive @authenticated on + FIELD_DEFINITION + | OBJECT + | INTERFACE + | SCALAR + | ENUM +directive @requiresScopes(scopes: [[Scope!]!]!) on + FIELD_DEFINITION + | OBJECT + | INTERFACE + | SCALAR + | ENUM +scalar Scope +scalar FieldSet diff --git a/federation_spec/federation-v2.6.graphql b/federation_spec/federation-v2.6.graphql new file mode 100644 index 0000000..0151c2d --- /dev/null +++ b/federation_spec/federation-v2.6.graphql @@ -0,0 +1,52 @@ +directive @composeDirective(name: String!) repeatable on SCHEMA +directive @extends on OBJECT | INTERFACE +directive @external on OBJECT | FIELD_DEFINITION +directive @key(fields: FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE +directive @inaccessible on + | FIELD_DEFINITION + | OBJECT + | INTERFACE + | UNION + | ENUM + | ENUM_VALUE + | SCALAR + | INPUT_OBJECT + | INPUT_FIELD_DEFINITION + | ARGUMENT_DEFINITION +directive @interfaceObject on OBJECT +directive @override(from: String!) on FIELD_DEFINITION +directive @provides(fields: FieldSet!) on FIELD_DEFINITION +directive @requires(fields: FieldSet!) on FIELD_DEFINITION +directive @shareable repeatable on FIELD_DEFINITION | OBJECT +directive @tag(name: String!) repeatable on + | FIELD_DEFINITION + | INTERFACE + | OBJECT + | UNION + | ARGUMENT_DEFINITION + | SCALAR + | ENUM + | ENUM_VALUE + | INPUT_OBJECT + | INPUT_FIELD_DEFINITION +directive @authenticated on + FIELD_DEFINITION + | OBJECT + | INTERFACE + | SCALAR + | ENUM +directive @requiresScopes(scopes: [[Scope!]!]!) on + FIELD_DEFINITION + | OBJECT + | INTERFACE + | SCALAR + | ENUM +directive @policy(policies: [[federation__Policy!]!]!) on + | FIELD_DEFINITION + | OBJECT + | INTERFACE + | SCALAR + | ENUM +scalar federation__Policy +scalar Scope +scalar FieldSet diff --git a/graphene_federation/__init__.py b/graphene_federation/__init__.py index b35a0e6..e4c45cc 100644 --- a/graphene_federation/__init__.py +++ b/graphene_federation/__init__.py @@ -1,15 +1,18 @@ from graphene_directives import DirectiveLocation -from .appolo_versions import FederationVersion, LATEST_VERSION +from .apollo_versions import FederationVersion, LATEST_VERSION from .directives import ( + authenticated, extends, external, inaccessible, interface_object, key, override, + policy, provides, requires, + requires_scope, shareable, tag, ) @@ -24,6 +27,7 @@ "build_schema", "FederationDirective", "DirectiveLocation", + "authenticated", "extends", "external", "inaccessible", @@ -31,7 +35,9 @@ "key", "override", "provides", + "policy", "requires", + "requires_scope", "shareable", "tag", "compose_directive", diff --git a/graphene_federation/apollo_versions/__init__.py b/graphene_federation/apollo_versions/__init__.py new file mode 100644 index 0000000..23d2c80 --- /dev/null +++ b/graphene_federation/apollo_versions/__init__.py @@ -0,0 +1,49 @@ +from graphql import GraphQLDirective + +from .v1_0 import get_directives as get_directives_v1_0 +from .v2_0 import get_directives as get_directives_v2_0 +from .v2_1 import get_directives as get_directives_v2_1 +from .v2_2 import get_directives as get_directives_v2_2 +from .v2_3 import get_directives as get_directives_v2_3 +from .v2_4 import get_directives as get_directives_v2_4 +from .v2_5 import get_directives as get_directives_v2_5 +from .v2_6 import get_directives as get_directives_v2_6 +from .version import FederationVersion + +LATEST_VERSION = FederationVersion.VERSION_2_6 + + +def get_directives_based_on_version( + federation_version: FederationVersion, +) -> dict[str, GraphQLDirective]: + if federation_version == FederationVersion.VERSION_1_0: + return get_directives_v1_0() + if federation_version == FederationVersion.VERSION_2_0: + return get_directives_v2_0() + if federation_version == FederationVersion.VERSION_2_1: + return get_directives_v2_1() + if federation_version == FederationVersion.VERSION_2_2: + return get_directives_v2_2() + if federation_version == FederationVersion.VERSION_2_3: + return get_directives_v2_3() + if federation_version == FederationVersion.VERSION_2_4: + return get_directives_v2_4() + if federation_version == FederationVersion.VERSION_2_5: + return get_directives_v2_5() + if federation_version == FederationVersion.VERSION_2_6: + return get_directives_v2_6() + + return get_directives_v2_6() + + +def get_directive_from_name( + directive_name: str, federation_version: FederationVersion +) -> GraphQLDirective: + directive = get_directives_based_on_version(federation_version).get( + directive_name, None + ) + if directive is None: + raise ValueError( + f"@{directive_name} not supported on federation version {federation_version}" + ) + return directive diff --git a/graphene_federation/appolo_versions/v1_0.py b/graphene_federation/apollo_versions/v1_0.py similarity index 97% rename from graphene_federation/appolo_versions/v1_0.py rename to graphene_federation/apollo_versions/v1_0.py index 65079ab..9d974f8 100644 --- a/graphene_federation/appolo_versions/v1_0.py +++ b/graphene_federation/apollo_versions/v1_0.py @@ -26,7 +26,6 @@ ], args={"fields": GraphQLArgument(GraphQLNonNull(_FieldSet))}, description="Federation @requires directive", - is_repeatable=True, add_definition_to_schema=False, field_validator=validate_requires, input_transform=field_set_case_transform, @@ -41,6 +40,7 @@ args={"fields": GraphQLArgument(GraphQLNonNull(_FieldSet))}, description="Federation @provides directive", add_definition_to_schema=False, + input_transform=field_set_case_transform, ) external_directive = CustomDirective( diff --git a/graphene_federation/appolo_versions/v2_0.py b/graphene_federation/apollo_versions/v2_0.py similarity index 98% rename from graphene_federation/appolo_versions/v2_0.py rename to graphene_federation/apollo_versions/v2_0.py index a703562..3609b7c 100644 --- a/graphene_federation/appolo_versions/v2_0.py +++ b/graphene_federation/apollo_versions/v2_0.py @@ -37,7 +37,6 @@ ], args={"fields": GraphQLArgument(GraphQLNonNull(FieldSet))}, description="Federation @requires directive", - is_repeatable=True, add_definition_to_schema=False, field_validator=validate_requires, input_transform=field_set_case_transform, @@ -52,6 +51,7 @@ args={"fields": GraphQLArgument(GraphQLNonNull(FieldSet))}, description="Federation @provides directive", add_definition_to_schema=False, + input_transform=field_set_case_transform, ) diff --git a/graphene_federation/appolo_versions/v2_1.py b/graphene_federation/apollo_versions/v2_1.py similarity index 100% rename from graphene_federation/appolo_versions/v2_1.py rename to graphene_federation/apollo_versions/v2_1.py diff --git a/graphene_federation/appolo_versions/v2_2.py b/graphene_federation/apollo_versions/v2_2.py similarity index 100% rename from graphene_federation/appolo_versions/v2_2.py rename to graphene_federation/apollo_versions/v2_2.py diff --git a/graphene_federation/appolo_versions/v2_3.py b/graphene_federation/apollo_versions/v2_3.py similarity index 100% rename from graphene_federation/appolo_versions/v2_3.py rename to graphene_federation/apollo_versions/v2_3.py diff --git a/graphene_federation/apollo_versions/v2_4.py b/graphene_federation/apollo_versions/v2_4.py new file mode 100644 index 0000000..194f22c --- /dev/null +++ b/graphene_federation/apollo_versions/v2_4.py @@ -0,0 +1,8 @@ +from graphql import GraphQLDirective + +from .v2_3 import get_directives as get_directives_v2_3 + + +# No Change, Added Subscription Support +def get_directives() -> dict[str, GraphQLDirective]: + return get_directives_v2_3() diff --git a/graphene_federation/apollo_versions/v2_5.py b/graphene_federation/apollo_versions/v2_5.py new file mode 100644 index 0000000..efe9d8e --- /dev/null +++ b/graphene_federation/apollo_versions/v2_5.py @@ -0,0 +1,50 @@ +from graphene_directives import CustomDirective, DirectiveLocation +from graphql import GraphQLArgument, GraphQLDirective, GraphQLList, GraphQLNonNull + +from .v2_4 import get_directives as get_directives_v2_4 +from ..scalars import Scope + +authenticated_directive = CustomDirective( + name="authenticated", + locations=[ + DirectiveLocation.FIELD_DEFINITION, + DirectiveLocation.OBJECT, + DirectiveLocation.INTERFACE, + DirectiveLocation.SCALAR, + DirectiveLocation.ENUM, + ], + description="Federation @authenticated directive", + add_definition_to_schema=False, +) + +requires_scope_directive = CustomDirective( + name="requiresScopes", + locations=[ + DirectiveLocation.FIELD_DEFINITION, + DirectiveLocation.OBJECT, + DirectiveLocation.INTERFACE, + DirectiveLocation.SCALAR, + DirectiveLocation.ENUM, + ], + args={ + "scopes": GraphQLArgument( + GraphQLNonNull( + GraphQLList(GraphQLNonNull(GraphQLList(GraphQLNonNull(Scope)))) + ) + ), + }, + description="Federation @requiresScopes directive", + add_definition_to_schema=False, +) + + +# No Change, Added Subscription Support +def get_directives() -> dict[str, GraphQLDirective]: + directives = get_directives_v2_4() + directives.update( + { + directive.name: directive + for directive in [authenticated_directive, requires_scope_directive] + } + ) + return directives diff --git a/graphene_federation/apollo_versions/v2_6.py b/graphene_federation/apollo_versions/v2_6.py new file mode 100644 index 0000000..3375292 --- /dev/null +++ b/graphene_federation/apollo_versions/v2_6.py @@ -0,0 +1,34 @@ +from graphene_directives import CustomDirective, DirectiveLocation +from graphql import GraphQLArgument, GraphQLDirective, GraphQLList, GraphQLNonNull + +from .v2_5 import get_directives as get_directives_v2_5 +from ..scalars import FederationPolicy + +policy_directive = CustomDirective( + name="policy", + locations=[ + DirectiveLocation.FIELD_DEFINITION, + DirectiveLocation.OBJECT, + DirectiveLocation.INTERFACE, + DirectiveLocation.SCALAR, + DirectiveLocation.ENUM, + ], + args={ + "policies": GraphQLArgument( + GraphQLNonNull( + GraphQLList( + GraphQLNonNull(GraphQLList(GraphQLNonNull(FederationPolicy))) + ) + ) + ), + }, + description="Federation @policy directive", + add_definition_to_schema=False, +) + + +# No Change, Added Subscription Support +def get_directives() -> dict[str, GraphQLDirective]: + directives = get_directives_v2_5() + directives.update({directive.name: directive for directive in [policy_directive]}) + return directives diff --git a/graphene_federation/appolo_versions/version.py b/graphene_federation/apollo_versions/version.py similarity index 70% rename from graphene_federation/appolo_versions/version.py rename to graphene_federation/apollo_versions/version.py index bd5d811..ce2c0e7 100644 --- a/graphene_federation/appolo_versions/version.py +++ b/graphene_federation/apollo_versions/version.py @@ -7,3 +7,6 @@ class FederationVersion(Enum): VERSION_2_1 = "2.1" VERSION_2_2 = "2.2" VERSION_2_3 = "2.3" + VERSION_2_4 = "2.4" + VERSION_2_5 = "2.5" + VERSION_2_6 = "2.6" diff --git a/graphene_federation/appolo_versions/__init__.py b/graphene_federation/appolo_versions/__init__.py deleted file mode 100644 index b2d4156..0000000 --- a/graphene_federation/appolo_versions/__init__.py +++ /dev/null @@ -1,41 +0,0 @@ -from graphql import GraphQLDirective - -from .v1_0 import get_directives as get_directives_v1_0 -from .v2_0 import get_directives as get_directives_v2_0 -from .v2_1 import get_directives as get_directives_v2_1 -from .v2_2 import get_directives as get_directives_v2_2 -from .v2_3 import get_directives as get_directives_v2_3 -from .version import FederationVersion - -LATEST_VERSION = FederationVersion.VERSION_2_3 - - -def get_directives_based_on_version( - federation_version: FederationVersion, -) -> dict[str, GraphQLDirective]: - match federation_version: - case FederationVersion.VERSION_1_0: - return get_directives_v1_0() - case FederationVersion.VERSION_2_0: - return get_directives_v2_0() - case FederationVersion.VERSION_2_1: - return get_directives_v2_1() - case FederationVersion.VERSION_2_2: - return get_directives_v2_2() - case FederationVersion.VERSION_2_3: - return get_directives_v2_3() - case _: - return get_directives_v2_3() - - -def get_directive_from_name( - directive_name: str, federation_version: FederationVersion -) -> GraphQLDirective: - directive = get_directives_based_on_version(federation_version).get( - directive_name, None - ) - if directive is None: - raise ValueError( - f"@{directive_name} not supported on federation version {federation_version}" - ) - return directive diff --git a/graphene_federation/directives/__init__.py b/graphene_federation/directives/__init__.py index e57b374..c07e952 100644 --- a/graphene_federation/directives/__init__.py +++ b/graphene_federation/directives/__init__.py @@ -1,10 +1,13 @@ +from .authenticated import authenticated from .extends import extends from .external import external from .inaccessible import inaccessible from .interface_object import interface_object from .key import key from .override import override +from .policy import policy from .provides import provides from .requires import requires +from .requires_scopes import requires_scope from .shareable import shareable from .tag import tag diff --git a/graphene_federation/directives/authenticated.py b/graphene_federation/directives/authenticated.py new file mode 100644 index 0000000..1d828f4 --- /dev/null +++ b/graphene_federation/directives/authenticated.py @@ -0,0 +1,29 @@ +from typing import Callable + +from graphene_directives import directive_decorator + +from .utils import is_non_field +from ..apollo_versions import ( + FederationVersion, + LATEST_VERSION, + get_directive_from_name, +) + + +def authenticated( + graphene_type=None, + *, + federation_version: FederationVersion = LATEST_VERSION, +) -> Callable: + directive = get_directive_from_name("authenticated", federation_version) + decorator = directive_decorator(directive) + + def wrapper(field_or_type): + if is_non_field(field_or_type): + return decorator(field=None)(field_or_type) + return decorator(field=field_or_type) + + if graphene_type: + return wrapper(graphene_type) + + return wrapper diff --git a/graphene_federation/directives/extends.py b/graphene_federation/directives/extends.py index c4ed831..4e02645 100644 --- a/graphene_federation/directives/extends.py +++ b/graphene_federation/directives/extends.py @@ -1,18 +1,36 @@ -from typing import Any, Callable +from typing import Callable from graphene_directives import directive_decorator -from ..appolo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name +from .utils import is_non_field +from ..apollo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name def extends( - non_field: Any = None, + graphene_type=None, *, federation_version: FederationVersion = LATEST_VERSION, ) -> Callable: directive = get_directive_from_name("extends", federation_version) + decorator = directive_decorator(directive) - if non_field: - return directive_decorator(directive)(field=None)(non_field) + def wrapper(field_or_type): + if is_non_field(field_or_type): + return decorator(field=None)(field_or_type) + raise TypeError( + "\n".join( + [ + f"\nInvalid Usage of {directive}.", + "Must be applied on a class of ObjectType|InterfaceType", + "Example:", + f"{directive}", + "class Product(graphene.ObjectType)", + "\t...", + ] + ) + ) - return directive_decorator(directive) + if graphene_type: + return wrapper(graphene_type) + + return wrapper diff --git a/graphene_federation/directives/external.py b/graphene_federation/directives/external.py index aa1eab8..3e182d0 100644 --- a/graphene_federation/directives/external.py +++ b/graphene_federation/directives/external.py @@ -1,18 +1,25 @@ -from typing import Any, Callable +from typing import Callable from graphene_directives import directive_decorator from .utils import is_non_field -from ..appolo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name +from ..apollo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name def external( - field: Any = None, + graphene_type=None, *, federation_version: FederationVersion = LATEST_VERSION, ) -> Callable: directive = get_directive_from_name("external", federation_version) decorator = directive_decorator(directive) - return ( - decorator(field=None)(field) if is_non_field(field) else decorator(field=field) - ) + + def wrapper(field_or_type): + if is_non_field(field_or_type): + return decorator(field=None)(field_or_type) + return decorator(field=field_or_type) + + if graphene_type: + return wrapper(graphene_type) + + return wrapper diff --git a/graphene_federation/directives/inaccessible.py b/graphene_federation/directives/inaccessible.py index e7168d1..eb80126 100644 --- a/graphene_federation/directives/inaccessible.py +++ b/graphene_federation/directives/inaccessible.py @@ -1,18 +1,25 @@ -from typing import Any, Callable +from typing import Callable from graphene_directives import directive_decorator from .utils import is_non_field -from ..appolo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name +from ..apollo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name def inaccessible( - field: Any = None, + graphene_type=None, *, federation_version: FederationVersion = LATEST_VERSION, ) -> Callable: directive = get_directive_from_name("inaccessible", federation_version) decorator = directive_decorator(directive) - return ( - decorator(field=None)(field) if is_non_field(field) else decorator(field=field) - ) + + def wrapper(field_or_type): + if is_non_field(field_or_type): + return decorator(field=None)(field_or_type) + return decorator(field=field_or_type) + + if graphene_type: + return wrapper(graphene_type) + + return wrapper diff --git a/graphene_federation/directives/interface_object.py b/graphene_federation/directives/interface_object.py index c36bc66..76eecd0 100644 --- a/graphene_federation/directives/interface_object.py +++ b/graphene_federation/directives/interface_object.py @@ -2,13 +2,35 @@ from graphene_directives import directive_decorator -from ..appolo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name +from .utils import is_non_field +from ..apollo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name def interface_object( + graphene_type=None, + *, federation_version: FederationVersion = LATEST_VERSION, ) -> Callable: - directive = get_directive_from_name( - "interfaceObject", federation_version=federation_version - ) - return directive_decorator(directive) + directive = get_directive_from_name("interfaceObject", federation_version) + decorator = directive_decorator(directive) + + def wrapper(field_or_type): + if is_non_field(field_or_type): + return decorator(field=None)(field_or_type) + raise TypeError( + "\n".join( + [ + f"\nInvalid Usage of {directive}.", + "Must be applied on a class of ObjectType", + "Example:", + f"{directive}", + "class Product(graphene.ObjectType)", + "\t...", + ] + ) + ) + + if graphene_type: + return wrapper(graphene_type) + + return wrapper diff --git a/graphene_federation/directives/key.py b/graphene_federation/directives/key.py index 677fd6f..89297fe 100644 --- a/graphene_federation/directives/key.py +++ b/graphene_federation/directives/key.py @@ -1,15 +1,42 @@ -from typing import Callable +from typing import Any, Union from graphene_directives import directive_decorator -from ..appolo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name +from .utils import is_non_field +from ..apollo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name +from ..validators import ast_to_str, build_ast def key( - fields: str, + fields: Union[str, list[str]], resolvable: bool = None, *, federation_version: FederationVersion = LATEST_VERSION, -) -> Callable: +) -> Any: directive = get_directive_from_name("key", federation_version) - return directive_decorator(directive)(fields=fields, resolvable=resolvable) + decorator = directive_decorator(directive) + fields = ast_to_str( + build_ast( + fields if isinstance(fields, str) else " ".join(fields), + ) + ) + + def wrapper(field_or_type): + if is_non_field(field_or_type): + return decorator(field=None, fields=fields, resolvable=resolvable)( + field_or_type + ) + raise TypeError( + "\n".join( + [ + f"\nInvalid Usage of {directive}.", + "Must be applied on a class of ObjectType|InterfaceType", + "Example:", + f"{directive}", + "class Product(graphene.ObjectType)", + "\t...", + ] + ) + ) + + return wrapper diff --git a/graphene_federation/directives/override.py b/graphene_federation/directives/override.py index a30eea3..aaa13e8 100644 --- a/graphene_federation/directives/override.py +++ b/graphene_federation/directives/override.py @@ -1,16 +1,38 @@ -from typing import Any, Callable +from typing import Callable from graphene_directives import directive_decorator -from ..appolo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name +from .utils import is_non_field +from ..apollo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name def override( - field: Any, + graphene_type, from_: str, + *, federation_version: FederationVersion = LATEST_VERSION, ) -> Callable: directive = get_directive_from_name( "override", federation_version=federation_version ) - return directive_decorator(directive)(field=field, **{"from": from_}) + decorator = directive_decorator(directive) + + def wrapper(field_or_type): + if is_non_field(field_or_type): + raise TypeError( + "\n".join( + [ + f"\nInvalid Usage of {directive}.", + "Must be applied on a field level", + "Example:", + "class Product(graphene.ObjectType)", + '\tname = override(graphene.Int(),from="Products")', + ] + ) + ) + return decorator(field=field_or_type, **{"from": from_}) + + if graphene_type: + return wrapper(graphene_type) + + return wrapper diff --git a/graphene_federation/directives/policy.py b/graphene_federation/directives/policy.py new file mode 100644 index 0000000..b7fef8b --- /dev/null +++ b/graphene_federation/directives/policy.py @@ -0,0 +1,26 @@ +from typing import Callable + +from graphene_directives import directive_decorator + +from .utils import is_non_field +from ..apollo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name + + +def policy( + graphene_type=None, + *, + policies: list[list[str]], + federation_version: FederationVersion = LATEST_VERSION, +) -> Callable: + directive = get_directive_from_name("policy", federation_version=federation_version) + decorator = directive_decorator(directive) + + def wrapper(field_or_type): + if is_non_field(field_or_type): + return decorator(field=None, policies=policies)(field_or_type) + return decorator(field=field_or_type, policies=policies) + + if graphene_type: + return wrapper(graphene_type) + + return wrapper diff --git a/graphene_federation/directives/provides.py b/graphene_federation/directives/provides.py index 243308a..85dee57 100644 --- a/graphene_federation/directives/provides.py +++ b/graphene_federation/directives/provides.py @@ -1,18 +1,45 @@ -from typing import Any, Callable, Union +from typing import Callable +from typing import Union from graphene_directives import directive_decorator -from ..appolo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name +from .utils import is_non_field +from ..apollo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name +from ..validators import ast_to_str, build_ast def provides( - field: Any, + graphene_type, fields: Union[str, list[str]], + *, federation_version: FederationVersion = LATEST_VERSION, ) -> Callable: directive = get_directive_from_name( "provides", federation_version=federation_version ) - return directive_decorator(directive)( - field=field, fields=fields if isinstance(fields, str) else " ".join(fields) + decorator = directive_decorator(directive) + fields = ast_to_str( + build_ast( + fields if isinstance(fields, str) else " ".join(fields), + ) ) + + def wrapper(field_or_type): + if is_non_field(field_or_type): + raise TypeError( + "\n".join( + [ + f"\nInvalid Usage of {directive}.", + "Must be applied on a field level", + "Example:", + "class Product(graphene.ObjectType)", + '\torders = provides(graphene.List(Order),fields="id")', + ] + ) + ) + return decorator(field=field_or_type, fields=fields) + + if graphene_type: + return wrapper(graphene_type) + + return wrapper diff --git a/graphene_federation/directives/requires.py b/graphene_federation/directives/requires.py index c2b14c8..f865a13 100644 --- a/graphene_federation/directives/requires.py +++ b/graphene_federation/directives/requires.py @@ -1,39 +1,46 @@ -from typing import Any, Callable, Union +from typing import Callable +from typing import Union from graphene_directives import directive_decorator -from ..appolo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name -from ..validators import build_ast - - -def add_typename(fields: dict, level: int = 0) -> str: - new_fields = [] - if level != 0: - new_fields.append("__typename") - for field, value in fields.items(): - if "typename" in field.lower(): - continue - elif len(value) == 0: - new_fields.append(field) - else: - new_fields.extend([field, "{", add_typename(value, level + 1), "}"]) - - return " ".join(new_fields) +from .utils import is_non_field +from ..apollo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name +from ..validators import ast_to_str, build_ast def requires( - field: Any, + graphene_type, fields: Union[str, list[str]], + *, federation_version: FederationVersion = LATEST_VERSION, ) -> Callable: directive = get_directive_from_name("requires", federation_version) - fields = add_typename( + decorator = directive_decorator(directive) + fields = ast_to_str( build_ast( - input_str=fields if isinstance(fields, str) else " ".join(fields), - valid_special_chars='_()"', - ) - ) - return directive_decorator(directive)( - field=field, - fields=fields, + fields if isinstance(fields, str) else " ".join(fields), + ), + add_type_name=True, # When resolvers receive the data, it will be type-casted as __typename info is added ) + + def wrapper(field_or_type): + if is_non_field(field_or_type): + raise TypeError( + "\n".join( + [ + f"\nInvalid Usage of {directive}.", + "Must be applied on a field level", + "Example:", + "class Product(graphene.ObjectType)", + "\tid = graphene.ID()", + "\torders = graphene.List(Order)" + '\torder_count = requires(graphene.Int(),fields="id orders { id }")', + ] + ) + ) + return decorator(field=field_or_type, fields=fields) + + if graphene_type: + return wrapper(graphene_type) + + return wrapper diff --git a/graphene_federation/directives/requires_scopes.py b/graphene_federation/directives/requires_scopes.py new file mode 100644 index 0000000..572d2e9 --- /dev/null +++ b/graphene_federation/directives/requires_scopes.py @@ -0,0 +1,28 @@ +from typing import Callable + +from graphene_directives import directive_decorator + +from .utils import is_non_field +from ..apollo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name + + +def requires_scope( + graphene_type=None, + *, + scopes: list[list[str]], + federation_version: FederationVersion = LATEST_VERSION, +) -> Callable: + directive = get_directive_from_name( + "requiresScopes", federation_version=federation_version + ) + decorator = directive_decorator(directive) + + def wrapper(field_or_type): + if is_non_field(field_or_type): + return decorator(field=None, scopes=scopes)(field_or_type) + return decorator(field=field_or_type, scopes=scopes) + + if graphene_type: + return wrapper(graphene_type) + + return wrapper diff --git a/graphene_federation/directives/shareable.py b/graphene_federation/directives/shareable.py index 2c2fa8e..51a6ce2 100644 --- a/graphene_federation/directives/shareable.py +++ b/graphene_federation/directives/shareable.py @@ -1,19 +1,27 @@ -from typing import Any, Callable +from typing import Callable from graphene_directives import directive_decorator from .utils import is_non_field -from ..appolo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name +from ..apollo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name def shareable( - field: Any = None, + graphene_type=None, + *, federation_version: FederationVersion = LATEST_VERSION, ) -> Callable: directive = get_directive_from_name( "shareable", federation_version=federation_version ) decorator = directive_decorator(directive) - return ( - decorator(field=None)(field) if is_non_field(field) else decorator(field=field) - ) + + def wrapper(field_or_type): + if is_non_field(field_or_type): + return decorator(field=None)(field_or_type) + return decorator(field=field_or_type) + + if graphene_type: + return wrapper(graphene_type) + + return wrapper diff --git a/graphene_federation/directives/tag.py b/graphene_federation/directives/tag.py index 566f53f..11d33ca 100644 --- a/graphene_federation/directives/tag.py +++ b/graphene_federation/directives/tag.py @@ -1,17 +1,25 @@ -from typing import Any, Callable +from typing import Callable from graphene_directives import directive_decorator from .utils import is_non_field -from ..appolo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name +from ..apollo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name def tag( - field: Any = None, + graphene_type=None, + *, federation_version: FederationVersion = LATEST_VERSION, ) -> Callable: directive = get_directive_from_name("tag", federation_version=federation_version) decorator = directive_decorator(directive) - return ( - decorator(field=None)(field) if is_non_field(field) else decorator(field=field) - ) + + def wrapper(field_or_type): + if is_non_field(field_or_type): + return decorator(field=None)(field_or_type) + return decorator(field=field_or_type) + + if graphene_type: + return wrapper(graphene_type) + + return wrapper diff --git a/graphene_federation/entity.py b/graphene_federation/entity.py index 7c7adcc..cf03cbc 100644 --- a/graphene_federation/entity.py +++ b/graphene_federation/entity.py @@ -1,33 +1,17 @@ from __future__ import annotations -from typing import Any, Callable +from typing import Any from typing import Dict, Type from graphene import Field, List, NonNull, ObjectType, Union -from graphene import Schema from graphene.types.schema import TypeMap -from graphene.utils.str_converters import to_camel_case +from graphene_directives import Schema from graphene_directives.utils import has_non_field_attribute -from .appolo_versions import LATEST_VERSION, get_directive_from_name +from .apollo_versions import LATEST_VERSION, get_directive_from_name from .scalars import _Any -def field_name_to_type_attribute(schema: Schema, model: Any) -> Callable[[str], str]: - """ - Create field name conversion method (from schema name to actual graphene_type attribute name). - """ - field_names = {} - if schema.auto_camelcase: - field_names = { - to_camel_case(attr_name): attr_name - for attr_name in getattr(model._meta, "fields", []) - } - return lambda schema_field_name: field_names.get( - schema_field_name, schema_field_name - ) - - def get_entities(schema: Schema) -> Dict[str, Any]: """ Find all the entities from the type map. @@ -92,7 +76,7 @@ def resolve_entities(self, info, representations, sub_field_resolution=False): model_arguments = representation.copy() model_arguments.pop("__typename") if schema.auto_camelcase: - get_model_attr = field_name_to_type_attribute(schema, model) + get_model_attr = schema.field_name_to_type_attribute(model) model_arguments = { get_model_attr(k): v for k, v in model_arguments.items() } diff --git a/graphene_federation/main.py b/graphene_federation/main.py index c3f5a78..0e9b705 100644 --- a/graphene_federation/main.py +++ b/graphene_federation/main.py @@ -10,13 +10,13 @@ from graphene_directives.schema import Schema from . import FederationDirective -from .appolo_versions import ( +from .apollo_versions import ( FederationVersion, LATEST_VERSION, get_directive_from_name, get_directives_based_on_version, ) -from .appolo_versions.v2_1 import compose_directive as ComposeDirective +from .apollo_versions.v2_1 import compose_directive as ComposeDirective from .entity import get_entity_query from .schema_directives import compose_directive, link_directive from .service import get_service_query @@ -34,7 +34,7 @@ def _get_query( type_name = query_cls.__name__ bases.append(query_cls) federated_query_cls = type(type_name, tuple(bases), {}) - return federated_query_cls + return federated_query_cls # noqa def build_schema( @@ -43,11 +43,39 @@ def build_schema( subscription: Union[ObjectType, Type[ObjectType]] = None, types: Collection[Union[ObjectType, Type[ObjectType]]] = None, directives: Union[Collection[FederationDirective], None] = None, + include_graphql_spec_directives: bool = True, schema_directives: Collection[SchemaDirective] = None, auto_camelcase: bool = True, - enable_federation_2: bool = True, + enable_federation_2: bool = False, federation_version: FederationVersion = None, ) -> Schema: + """ + Build Schema. + + Args: + query (Type[ObjectType]): Root query *ObjectType*. Describes entry point for fields to *read* + data in your Schema. + mutation (Optional[Type[ObjectType]]): Root mutation *ObjectType*. Describes entry point for + fields to *create, update or delete* data in your API. + subscription (Optional[Type[ObjectType]]): Root subscription *ObjectType*. Describes entry point + for fields to receive continuous updates. + types (Optional[Collection[Type[ObjectType]]]): List of any types to include in schema that + may not be introspected through root types. + directives (List[GraphQLDirective], optional): List of custom directives to include in the + GraphQL schema. + auto_camelcase (bool): Fieldnames will be transformed in Schema's TypeMap from snake_case + to camelCase (preferred by GraphQL standard). Default True. + schema_directives (Collection[SchemaDirective]): Directives that can be defined at DIRECTIVE_LOCATION.SCHEMA + with their argument values. + include_graphql_spec_directives (bool): Includes directives defined by GraphQL spec (@include, @skip, + @deprecated, @specifiedBy) + enable_federation_2 (bool): Whether to enable federation 2 directives (default False) + federation_version (FederationVersion): Specify the version explicit (default LATEST_VERSION) + + In case both enable_federation_2 and federation_version are specified, federation_version is given + higher priority + """ + federation_version = ( federation_version if federation_version @@ -70,6 +98,7 @@ def build_schema( "types": _types, "directives": _directives.values(), "auto_camelcase": auto_camelcase, + "include_graphql_spec_directives": include_graphql_spec_directives, } schema: Schema = build_directive_schema(query=query, **schema_args) diff --git a/graphene_federation/scalars/__init__.py b/graphene_federation/scalars/__init__.py index 9fb3066..bccde34 100644 --- a/graphene_federation/scalars/__init__.py +++ b/graphene_federation/scalars/__init__.py @@ -1,5 +1,7 @@ from ._any import _Any +from .federation_policy import FederationPolicy from .field_set_v1 import _FieldSet from .field_set_v2 import FieldSet from .link_import import link_import from .link_purpose import link_purpose +from .scope import Scope diff --git a/graphene_federation/scalars/federation_policy.py b/graphene_federation/scalars/federation_policy.py new file mode 100644 index 0000000..c456036 --- /dev/null +++ b/graphene_federation/scalars/federation_policy.py @@ -0,0 +1,51 @@ +from typing import Any + +from graphql import ( + GraphQLError, + GraphQLScalarType, + StringValueNode, + ValueNode, + print_ast, +) +from graphql.pyutils import inspect + + +def _serialize_string(output_value: Any) -> str: + if isinstance(output_value, str): + return output_value + # do not serialize builtin types as strings, but allow serialization of custom + # types via their `__str__` method + if type(output_value).__module__ == "builtins": + raise GraphQLError( + "federation__Policy cannot represent value: " + inspect(output_value) + ) + + return str(output_value) + + +def _coerce_string(input_value: Any) -> str: + if not isinstance(input_value, str): + raise GraphQLError( + "federation__Policy cannot represent a non string value: " + + inspect(input_value) + ) + return input_value + + +def _parse_string_literal(value_node: ValueNode, _variables: Any = None) -> str: + """Parse a string value node in the AST.""" + if not isinstance(value_node, StringValueNode): + raise GraphQLError( + "federation__Policy cannot represent a non string value: " + + print_ast(value_node), + value_node, + ) + return value_node.value + + +FederationPolicy = GraphQLScalarType( + name="federation__Policy", + serialize=_serialize_string, + parse_value=_coerce_string, + parse_literal=_parse_string_literal, +) diff --git a/graphene_federation/scalars/field_set_v1.py b/graphene_federation/scalars/field_set_v1.py index 3bd6d5a..747ca08 100644 --- a/graphene_federation/scalars/field_set_v1.py +++ b/graphene_federation/scalars/field_set_v1.py @@ -1,9 +1,3 @@ -from graphql import GraphQLString +from graphql import GraphQLScalarType -# _FieldSet = GraphQLScalarType(name="_FieldSet") - -""" -To avoid _FieldSet from coming into schema we are defining it as String -""" - -_FieldSet = GraphQLString +_FieldSet = GraphQLScalarType(name="_FieldSet") diff --git a/graphene_federation/scalars/field_set_v2.py b/graphene_federation/scalars/field_set_v2.py index 7e26458..a2a2939 100644 --- a/graphene_federation/scalars/field_set_v2.py +++ b/graphene_federation/scalars/field_set_v2.py @@ -1,9 +1,3 @@ -from graphql import GraphQLString +from graphql import GraphQLScalarType -# FieldSet = GraphQLScalarType(name="FieldSet") - -""" -To avoid FieldSet from coming into schema we are defining it as String -""" - -FieldSet = GraphQLString +FieldSet = GraphQLScalarType(name="FieldSet") diff --git a/graphene_federation/scalars/link_import.py b/graphene_federation/scalars/link_import.py index 06c4717..86faa8e 100644 --- a/graphene_federation/scalars/link_import.py +++ b/graphene_federation/scalars/link_import.py @@ -1,4 +1,3 @@ -from math import isfinite from typing import Any from graphql import ( @@ -9,6 +8,7 @@ print_ast, ) from graphql.pyutils import inspect +from math import isfinite def _serialize_string(output_value: Any) -> str: diff --git a/graphene_federation/scalars/scope.py b/graphene_federation/scalars/scope.py new file mode 100644 index 0000000..dbaf26f --- /dev/null +++ b/graphene_federation/scalars/scope.py @@ -0,0 +1,47 @@ +from typing import Any + +from graphql import ( + GraphQLError, + GraphQLScalarType, + StringValueNode, + ValueNode, + print_ast, +) +from graphql.pyutils import inspect + + +def _serialize_string(output_value: Any) -> str: + if isinstance(output_value, str): + return output_value + # do not serialize builtin types as strings, but allow serialization of custom + # types via their `__str__` method + if type(output_value).__module__ == "builtins": + raise GraphQLError("Scope cannot represent value: " + inspect(output_value)) + + return str(output_value) + + +def _coerce_string(input_value: Any) -> str: + if not isinstance(input_value, str): + raise GraphQLError( + "Scope cannot represent a non string value: " + inspect(input_value) + ) + return input_value + + +def _parse_string_literal(value_node: ValueNode, _variables: Any = None) -> str: + """Parse a string value node in the AST.""" + if not isinstance(value_node, StringValueNode): + raise GraphQLError( + "Scope cannot represent a non string value: " + print_ast(value_node), + value_node, + ) + return value_node.value + + +Scope = GraphQLScalarType( + name="Scope", + serialize=_serialize_string, + parse_value=_coerce_string, + parse_literal=_parse_string_literal, +) diff --git a/graphene_federation/schema_directives/compose_directive.py b/graphene_federation/schema_directives/compose_directive.py index eea84b0..9a87d16 100644 --- a/graphene_federation/schema_directives/compose_directive.py +++ b/graphene_federation/schema_directives/compose_directive.py @@ -1,6 +1,6 @@ from graphene_directives import SchemaDirective -from ..appolo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name +from ..apollo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name def compose_directive( diff --git a/graphene_federation/service.py b/graphene_federation/service.py index ab0b2c2..dbd0db6 100644 --- a/graphene_federation/service.py +++ b/graphene_federation/service.py @@ -1,8 +1,5 @@ from graphene import Field, ObjectType, String from graphene_directives.schema import Schema -from graphql.utilities.print_schema import print_scalar - -from .scalars import FieldSet, _FieldSet def get_sdl(schema) -> str: @@ -11,10 +8,6 @@ def get_sdl(schema) -> str: """ string_schema = str(schema) - # Remove All Scalar definitions - for scalar in [_FieldSet, FieldSet]: - string_schema = string_schema.replace(print_scalar(scalar), "") - return string_schema.strip() diff --git a/graphene_federation/tests/test_extend_v1.py b/graphene_federation/tests/test_extend_v1.py deleted file mode 100644 index 9999b0a..0000000 --- a/graphene_federation/tests/test_extend_v1.py +++ /dev/null @@ -1,52 +0,0 @@ -import pytest - -from graphene import ObjectType, ID, String - -from ..extend import extend - - -def test_extend_non_existing_field_failure(): - """ - Test that using the key decorator and providing a field that does not exist fails. - """ - with pytest.raises(AssertionError) as err: - - @extend("potato") - class A(ObjectType): - id = ID() - - assert 'Field "potato" does not exist on type "A"' == str(err.value) - - -def test_multiple_extend_failure(): - """ - Test that the extend decorator can't be used more than once on a type. - """ - with pytest.raises(AssertionError) as err: - - @extend("id") - @extend("potato") - class A(ObjectType): - id = ID() - potato = String() - - assert "Can't extend type which is already extended or has @key" == str(err.value) - - -def test_extend_with_description_failure(): - """ - Test that adding a description to an extended type raises an error. - """ - with pytest.raises(AssertionError) as err: - - @extend("id") - class A(ObjectType): - class Meta: - description = "This is an object from here." - - id = ID() - - assert ( - 'Type "A" has a non empty description and it is also marked with extend.\nThey are mutually exclusive.' - in str(err.value) - ) diff --git a/graphene_federation/tests/test_inaccessible.py b/graphene_federation/tests/test_inaccessible.py deleted file mode 100644 index 171c94b..0000000 --- a/graphene_federation/tests/test_inaccessible.py +++ /dev/null @@ -1,126 +0,0 @@ -from textwrap import dedent - -import graphene -from graphene import ObjectType -from graphql import graphql_sync - -from graphene_federation import inaccessible, build_schema -from graphene_federation.utils import clean_schema - - -def test_inaccessible_interface(): - @inaccessible - class ReviewInterface(graphene.Interface): - interfaced_body = graphene.String(required=True) - - @inaccessible - class Review(graphene.ObjectType): - class Meta: - interfaces = (ReviewInterface,) - - id = inaccessible(graphene.Int(required=True)) - body = graphene.String(required=True) - - class Query(ObjectType): - in_stock_count = graphene.Int(required=True) - - build_schema(query=Query, enable_federation_2=True, types=(ReviewInterface, Review)) - - -def test_inaccessible(): - @inaccessible - class Position(graphene.ObjectType): - x = graphene.Int(required=True) - y = inaccessible(graphene.Int(required=True)) - - class Query(ObjectType): - in_stock_count = graphene.Int(required=True) - - schema = build_schema(query=Query, enable_federation_2=True, types=(Position,)) - # Check the federation service schema definition language - query = """ - query { - _service { - sdl - } - } - """ - result = graphql_sync(schema.graphql_schema, query) - assert not result.errors - expected_result = dedent( - """ - extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@inaccessible"]) - type Position @inaccessible { - x: Int! - y: Int! @inaccessible - } - - type Query { - inStockCount: Int! - } - """ - ) - assert clean_schema(result.data["_service"]["sdl"]) == clean_schema(expected_result) - - -def test_inaccessible_union(): - @inaccessible - class Human(graphene.ObjectType): - name = graphene.String() - born_in = graphene.String() - - @inaccessible - class Droid(graphene.ObjectType): - name = inaccessible(graphene.String()) - primary_function = graphene.String() - - @inaccessible - class Starship(graphene.ObjectType): - name = graphene.String() - length = inaccessible(graphene.Int()) - - @inaccessible - class SearchResult(graphene.Union): - class Meta: - types = (Human, Droid, Starship) - - class Query(ObjectType): - in_stock_count = graphene.Int(required=True) - - schema = build_schema(query=Query, enable_federation_2=True, types=(SearchResult,)) - # Check the federation service schema definition language - query = """ - query { - _service { - sdl - } - } - """ - result = graphql_sync(schema.graphql_schema, query) - assert not result.errors - expected_schema = dedent( - """ - extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@inaccessible"]) - union SearchResult @inaccessible = Human | Droid | Starship - - type Human @inaccessible { - name: String - bornIn: String - } - - type Droid @inaccessible { - name: String @inaccessible - primaryFunction: String - } - - type Starship @inaccessible { - name: String - length: Int @inaccessible - } - - type Query { - inStockCount: Int! - } - """ - ) - assert clean_schema(result.data["_service"]["sdl"]) == clean_schema(expected_schema) diff --git a/graphene_federation/tests/test_key.py b/graphene_federation/tests/test_key.py deleted file mode 100644 index 5f09c95..0000000 --- a/graphene_federation/tests/test_key.py +++ /dev/null @@ -1,306 +0,0 @@ -import pytest -from textwrap import dedent - -from graphql import graphql_sync - -from graphene import ObjectType, ID, String, Field - -from graphene_federation.entity import key -from graphene_federation.main import build_schema -from graphene_federation.utils import clean_schema - - -def test_multiple_keys(): - @key("identifier") - @key("email") - class User(ObjectType): - identifier = ID() - email = String() - - class Query(ObjectType): - user = Field(User) - - schema = build_schema(query=Query, enable_federation_2=True) - expected_result = dedent( - """ - type Query { - user: User - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - } - - type User { - identifier: ID - email: String - } - - union _Entity = User - - scalar _Any - - type _Service { - sdl: String - } - """ - ) - assert clean_schema(schema) == clean_schema(expected_result) - # Check the federation service schema definition language - query = """ - query { - _service { - sdl - } - } - """ - result = graphql_sync(schema.graphql_schema, query) - assert not result.errors - expected_result = dedent( - """ - extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"]) - type Query { - user: User - } - - type User @key(fields: "email") @key(fields: "identifier") { - identifier: ID - email: String - } - """ - ) - assert clean_schema(result.data["_service"]["sdl"]) == clean_schema(expected_result) - - -def test_key_non_existing_field_failure(): - """ - Test that using the key decorator and providing a field that does not exist fails. - """ - with pytest.raises(AssertionError) as err: - - @key("potato") - class A(ObjectType): - id = ID() - - assert 'Field "potato" does not exist on type "A"' == str(err.value) - - -def test_compound_primary_key(): - class Organization(ObjectType): - registration_number = ID() - - @key("id organization { registration_number }") - class User(ObjectType): - id = ID() - organization = Field(Organization) - - class Query(ObjectType): - user = Field(User) - - schema = build_schema(query=Query, enable_federation_2=True) - expected_result = dedent( - """ - type Query { - user: User - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - } - - type User { - id: ID - organization: Organization - } - - type Organization { - registrationNumber: ID - } - - union _Entity = User - - scalar _Any - - type _Service { - sdl: String - } - """ - ) - assert clean_schema(schema) == clean_schema(expected_result) - # Check the federation service schema definition language - query = """ - query { - _service { - sdl - } - } - """ - result = graphql_sync(schema.graphql_schema, query) - assert not result.errors - expected_result = dedent( - """ - extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"]) - type Query { - user: User - } - - type User @key(fields: "id organization { registrationNumber }") { - id: ID - organization: Organization - } - - type Organization { - registrationNumber: ID - } - """ - ) - assert clean_schema(result.data["_service"]["sdl"]) == clean_schema(expected_result) - - -def test_compound_primary_key_with_depth(): - class BusinessUnit(ObjectType): - id = ID() - name = String() - - class Organization(ObjectType): - registration_number = ID() - business_unit = Field(BusinessUnit) - - @key("id organization { business_unit {id name}}") - class User(ObjectType): - id = ID() - organization = Field(Organization) - - class Query(ObjectType): - user = Field(User) - - schema = build_schema(query=Query, enable_federation_2=True) - expected_result = dedent( - """ - type Query { - user: User - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - } - - type User { - id: ID - organization: Organization - } - - type Organization { - registrationNumber: ID - businessUnit: BusinessUnit - } - - type BusinessUnit { - id: ID - name: String - } - - union _Entity = User - - scalar _Any - - type _Service { - sdl: String - } - """ - ) - assert clean_schema(schema) == clean_schema(expected_result) - # Check the federation service schema definition language - query = """ - query { - _service { - sdl - } - } - """ - result = graphql_sync(schema.graphql_schema, query) - assert not result.errors - expected_result = dedent( - """ - extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"]) - type Query { - user: User - } - - type User @key(fields: "id organization { businessUnit {id name}}") { - id: ID - organization: Organization - } - - type Organization { - registrationNumber: ID - businessUnit: BusinessUnit - } - - type BusinessUnit { - id: ID - name: String - } - """ - ) - assert clean_schema(result.data["_service"]["sdl"]) == clean_schema(expected_result) - - -def test_invalid_compound_primary_key_failures(): - class BusinessUnit(ObjectType): - id = ID() - name = String() - - class Organization(ObjectType): - registration_number = ID() - bu = Field(BusinessUnit) - - @key("id name organization { registration_number }") - class User(ObjectType): - id = ID() - organization = Field(Organization) - - class Query(ObjectType): - user = Field(User) - - with pytest.raises(AssertionError) as err: - # Field name absent on User ObjectType - build_schema(query=Query, enable_federation_2=True) - - assert 'Invalid compound key definition for type "User"' == str(err.value) - - @key("id organization { name }") - class User(ObjectType): - id = ID() - organization = Field(Organization) - - class Query(ObjectType): - user = Field(User) - - with pytest.raises(AssertionError) as err: - # Presence of invalid field in organization field key - build_schema(query=Query, enable_federation_2=True) - - assert 'Invalid compound key definition for type "User"' == str(err.value) - - @key("id organization { bu }") - class User(ObjectType): - id = ID() - organization = Field(Organization) - - class Query(ObjectType): - user = Field(User) - - with pytest.raises(AssertionError) as err: - # Presence of BusinessUnit in the key without subselection - build_schema(query=Query, enable_federation_2=True) - - assert 'Invalid compound key definition for type "User"' == str(err.value) - - @key("id organization { bu {name { field }} }") - class User(ObjectType): - id = ID() - organization = Field(Organization) - - class Query(ObjectType): - user = Field(User) - - with pytest.raises(AssertionError) as err: - # Presence of subselection for the scalar 'name' field - build_schema(query=Query, enable_federation_2=True) - - assert 'Invalid compound key definition for type "User"' == str(err.value) diff --git a/graphene_federation/tests/test_key_v1.py b/graphene_federation/tests/test_key_v1.py deleted file mode 100644 index a25f81b..0000000 --- a/graphene_federation/tests/test_key_v1.py +++ /dev/null @@ -1,83 +0,0 @@ -from textwrap import dedent - -import pytest - -from graphql import graphql_sync - -from graphene import ObjectType, ID, String, Field - -from graphene_federation.entity import key -from graphene_federation.main import build_schema -from graphene_federation.utils import clean_schema - - -def test_multiple_keys(): - @key("identifier") - @key("email") - class User(ObjectType): - identifier = ID() - email = String() - - class Query(ObjectType): - user = Field(User) - - schema = build_schema(query=Query) - expected_result = dedent( - """ - type Query { - user: User - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - } - - type User { - identifier: ID - email: String - } - - union _Entity = User - - scalar _Any - - type _Service { - sdl: String - } - """ - ) - assert clean_schema(schema) == clean_schema(expected_result) - # Check the federation service schema definition language - query = """ - query { - _service { - sdl - } - } - """ - result = graphql_sync(schema.graphql_schema, query) - assert not result.errors - expected_result = dedent( - """ - type Query { - user: User - } - - type User @key(fields: "email") @key(fields: "identifier") { - identifier: ID - email: String - } - """ - ) - assert clean_schema(result.data["_service"]["sdl"]) == clean_schema(expected_result) - - -def test_key_non_existing_field_failure(): - """ - Test that using the key decorator and providing a field that does not exist fails. - """ - with pytest.raises(AssertionError) as err: - - @key("potato") - class A(ObjectType): - id = ID() - - assert 'Field "potato" does not exist on type "A"' == str(err.value) diff --git a/graphene_federation/tests/test_provides.py b/graphene_federation/tests/test_provides.py deleted file mode 100644 index 021f6c3..0000000 --- a/graphene_federation/tests/test_provides.py +++ /dev/null @@ -1,256 +0,0 @@ -from textwrap import dedent - -from graphql import graphql_sync - -from graphene import Field, Int, ObjectType, String - -from graphene_federation import external -from graphene_federation.provides import provides -from graphene_federation.main import build_schema -from graphene_federation.extend import extend -from graphene_federation.utils import clean_schema - - -def test_provides(): - """ - https://www.apollographql.com/docs/federation/entities/#resolving-another-services-field-advanced - """ - - @extend("sku") - class Product(ObjectType): - sku = external(String(required=True)) - name = external(String()) - weight = external(Int()) - - @provides - class InStockCount(ObjectType): - product = provides(Field(Product, required=True), fields="name") - quantity = Int(required=True) - - class Query(ObjectType): - in_stock_count = Field(InStockCount) - - schema = build_schema(query=Query, enable_federation_2=True) - expected_result = dedent( - """ - type Query { - inStockCount: InStockCount - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - } - - type InStockCount { - product: Product! - quantity: Int! - } - - type Product { - sku: String! - name: String - weight: Int - } - - union _Entity = Product - - scalar _Any - - type _Service { - sdl: String - } - """ - ) - assert clean_schema(schema) == clean_schema(expected_result) - - # Check the federation service schema definition language - query = """ - query { - _service { - sdl - } - } - """ - result = graphql_sync(schema.graphql_schema, query) - assert not result.errors - expected_result = dedent( - """ - extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@extends", "@external", "@key", "@provides"]) - type Query { - inStockCount: InStockCount - } - - type InStockCount { - product: Product! @provides(fields: "name") - quantity: Int! - } - - extend type Product @key(fields: "sku") { - sku: String! @external - name: String @external - weight: Int @external - } - """ - ) - assert clean_schema(result.data["_service"]["sdl"]) == clean_schema(expected_result) - - -def test_provides_multiple_fields(): - """ - https://www.apollographql.com/docs/federation/entities/#resolving-another-services-field-advanced - """ - - @extend("sku") - class Product(ObjectType): - sku = external(String(required=True)) - name = external(String()) - weight = external(Int()) - - @provides - class InStockCount(ObjectType): - product = provides(Field(Product, required=True), fields="name weight") - quantity = Int(required=True) - - class Query(ObjectType): - in_stock_count = Field(InStockCount) - - schema = build_schema(query=Query, enable_federation_2=True) - expected_result = dedent( - """ - type Query { - inStockCount: InStockCount - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - } - - type InStockCount { - product: Product! - quantity: Int! - } - - type Product { - sku: String! - name: String - weight: Int - } - - union _Entity = Product - - scalar _Any - - type _Service { - sdl: String - } - """ - ) - assert clean_schema(schema) == clean_schema(expected_result) - # Check the federation service schema definition language - query = """ - query { - _service { - sdl - } - } - """ - result = graphql_sync(schema.graphql_schema, query) - assert not result.errors - expected_result = dedent( - """ - extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@extends", "@external", "@key", "@provides"]) - type Query { - inStockCount: InStockCount - } - - type InStockCount { - product: Product! @provides(fields: "name weight") - quantity: Int! - } - - extend type Product @key(fields: "sku") { - sku: String! @external - name: String @external - weight: Int @external - } - """ - ) - assert clean_schema(result.data["_service"]["sdl"]) == clean_schema(expected_result) - - -def test_provides_multiple_fields_as_list(): - """ - https://www.apollographql.com/docs/federation/entities/#resolving-another-services-field-advanced - """ - - @extend("sku") - class Product(ObjectType): - sku = external(String(required=True)) - name = external(String()) - weight = external(Int()) - - @provides - class InStockCount(ObjectType): - product = provides(Field(Product, required=True), fields=["name", "weight"]) - quantity = Int(required=True) - - class Query(ObjectType): - in_stock_count = Field(InStockCount) - - schema = build_schema(query=Query, enable_federation_2=True) - expected_result = dedent( - """ - type Query { - inStockCount: InStockCount - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - } - - type InStockCount { - product: Product! - quantity: Int! - } - - type Product { - sku: String! - name: String - weight: Int - } - - union _Entity = Product - - scalar _Any - - type _Service { - sdl: String - } - """ - ) - assert clean_schema(schema) == clean_schema(expected_result) - - # Check the federation service schema definition language - query = """ - query { - _service { - sdl - } - } - """ - result = graphql_sync(schema.graphql_schema, query) - assert not result.errors - expected_result = dedent( - """ - extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@extends", "@external", "@key", "@provides"]) - type Query { - inStockCount: InStockCount - } - - type InStockCount { - product: Product! @provides(fields: "name weight") - quantity: Int! - } - - extend type Product @key(fields: "sku") { - sku: String! @external - name: String @external - weight: Int @external - } - """ - ) - assert clean_schema(result.data["_service"]["sdl"]) == clean_schema(expected_result) diff --git a/graphene_federation/tests/test_provides_v1.py b/graphene_federation/tests/test_provides_v1.py deleted file mode 100644 index c59649b..0000000 --- a/graphene_federation/tests/test_provides_v1.py +++ /dev/null @@ -1,251 +0,0 @@ -from textwrap import dedent - -from graphql import graphql_sync - -from graphene import Field, Int, ObjectType, String - -from graphene_federation.provides import provides -from graphene_federation.main import build_schema -from graphene_federation.extend import extend -from graphene_federation.external import external -from graphene_federation.utils import clean_schema - - -def test_provides(): - """ - https://www.apollographql.com/docs/federation/entities/#resolving-another-services-field-advanced - """ - - @extend("sku") - class Product(ObjectType): - sku = external(String(required=True)) - name = external(String()) - weight = external(Int()) - - @provides - class InStockCount(ObjectType): - product = provides(Field(Product, required=True), fields="name") - quantity = Int(required=True) - - class Query(ObjectType): - in_stock_count = Field(InStockCount) - - schema = build_schema(query=Query) - expected_result = dedent( - """ - type Query { - inStockCount: InStockCount - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - } - - type InStockCount { - product: Product! - quantity: Int! - } - - type Product { - sku: String! - name: String - weight: Int - } - - union _Entity = Product - - scalar _Any - - type _Service { - sdl: String - } - """ - ) - assert clean_schema(schema) == clean_schema(expected_result) - # Check the federation service schema definition language - query = """ - query { - _service { - sdl - } - } - """ - result = graphql_sync(schema.graphql_schema, query) - assert not result.errors - expected_result = dedent( - """ - type Query { - inStockCount: InStockCount - } - - type InStockCount { - product: Product! @provides(fields: "name") - quantity: Int! - } - - extend type Product @key(fields: "sku") { - sku: String! @external - name: String @external - weight: Int @external - } - """ - ) - assert clean_schema(result.data["_service"]["sdl"]) == clean_schema(expected_result) - - -def test_provides_multiple_fields(): - """ - https://www.apollographql.com/docs/federation/entities/#resolving-another-services-field-advanced - """ - - @extend("sku") - class Product(ObjectType): - sku = external(String(required=True)) - name = external(String()) - weight = external(Int()) - - @provides - class InStockCount(ObjectType): - product = provides(Field(Product, required=True), fields="name weight") - quantity = Int(required=True) - - class Query(ObjectType): - in_stock_count = Field(InStockCount) - - schema = build_schema(query=Query) - expected_result = dedent( - """ - type Query { - inStockCount: InStockCount - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - } - - type InStockCount { - product: Product! - quantity: Int! - } - - type Product { - sku: String! - name: String - weight: Int - } - - union _Entity = Product - - scalar _Any - - type _Service { - sdl: String - } - """ - ) - assert clean_schema(schema) == clean_schema(expected_result) - # Check the federation service schema definition language - query = """ - query { - _service { - sdl - } - } - """ - result = graphql_sync(schema.graphql_schema, query) - assert not result.errors - expected_result = dedent( - """ - type Query { - inStockCount: InStockCount - } - - type InStockCount { - product: Product! @provides(fields: "name weight") - quantity: Int! - } - - extend type Product @key(fields: "sku") { - sku: String! @external - name: String @external - weight: Int @external - } - """ - ) - assert clean_schema(result.data["_service"]["sdl"]) == clean_schema(expected_result) - - -def test_provides_multiple_fields_as_list(): - """ - https://www.apollographql.com/docs/federation/entities/#resolving-another-services-field-advanced - """ - - @extend("sku") - class Product(ObjectType): - sku = external(String(required=True)) - name = external(String()) - weight = external(Int()) - - @provides - class InStockCount(ObjectType): - product = provides(Field(Product, required=True), fields=["name", "weight"]) - quantity = Int(required=True) - - class Query(ObjectType): - in_stock_count = Field(InStockCount) - - schema = build_schema(query=Query) - expected_result = dedent( - """ - type Query { - inStockCount: InStockCount - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - } - - type InStockCount { - product: Product! - quantity: Int! - } - - type Product { - sku: String! - name: String - weight: Int - } - - union _Entity = Product - - scalar _Any - - type _Service { - sdl: String - } - """ - ) - assert clean_schema(schema) == clean_schema(expected_result) - # Check the federation service schema definition language - query = """ - query { - _service { - sdl - } - } - """ - result = graphql_sync(schema.graphql_schema, query) - assert not result.errors - expected_result = dedent( - """ - type Query { - inStockCount: InStockCount - } - - type InStockCount { - product: Product! @provides(fields: "name weight") - quantity: Int! - } - - extend type Product @key(fields: "sku") { - sku: String! @external - name: String @external - weight: Int @external - } - """ - ) - assert clean_schema(result.data["_service"]["sdl"]) == clean_schema(expected_result) diff --git a/graphene_federation/tests/test_requires.py b/graphene_federation/tests/test_requires.py deleted file mode 100644 index 469c95d..0000000 --- a/graphene_federation/tests/test_requires.py +++ /dev/null @@ -1,230 +0,0 @@ -from textwrap import dedent - -import pytest - -from graphql import graphql_sync - -from graphene import Field, ID, Int, ObjectType, String - -from graphene_federation import external, requires -from graphene_federation.extend import extend -from graphene_federation.main import build_schema -from graphene_federation.utils import clean_schema - - -def test_chain_requires_failure(): - """ - Check that we can't nest call the requires method on a field. - """ - with pytest.raises(AssertionError) as err: - - @extend("id") - class A(ObjectType): - id = external(ID()) - something = requires(requires(String(), fields="id"), fields="id") - - assert "Can't chain `requires()` method calls on one field." == str(err.value) - - -def test_requires_multiple_fields(): - """ - Check that requires can take more than one field as input. - """ - - @extend("sku") - class Product(ObjectType): - sku = external(ID()) - size = external(Int()) - weight = external(Int()) - shipping_estimate = requires(String(), fields="size weight") - - class Query(ObjectType): - product = Field(Product) - - schema = build_schema(query=Query, enable_federation_2=True) - expected_result = dedent( - """ - type Query { - product: Product - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - } - - type Product { - sku: ID - size: Int - weight: Int - shippingEstimate: String - } - - union _Entity = Product - - scalar _Any - - type _Service { - sdl: String - } - """ - ) - assert clean_schema(schema) == clean_schema(expected_result) - # Check the federation service schema definition language - query = """ - query { - _service { - sdl - } - } - """ - result = graphql_sync(schema.graphql_schema, query) - assert not result.errors - expected_result = dedent( - """ - extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@extends", "@external", "@key", "@requires"]) - type Query { - product: Product - } - - extend type Product @key(fields: "sku") { - sku: ID @external - size: Int @external - weight: Int @external - shippingEstimate: String @requires(fields: "size weight") - } - """ - ) - assert clean_schema(result.data["_service"]["sdl"]) == clean_schema(expected_result) - - -def test_requires_multiple_fields_as_list(): - """ - Check that requires can take more than one field as input. - """ - - @extend("sku") - class Product(ObjectType): - sku = external(ID()) - size = external(Int()) - weight = external(Int()) - shipping_estimate = requires(String(), fields=["size", "weight"]) - - class Query(ObjectType): - product = Field(Product) - - schema = build_schema(query=Query, enable_federation_2=True) - expected_result = dedent( - """ - type Query { - product: Product - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - } - - type Product { - sku: ID - size: Int - weight: Int - shippingEstimate: String - } - - union _Entity = Product - - scalar _Any - - type _Service { - sdl: String - } - """ - ) - assert clean_schema(schema) == clean_schema(expected_result) - # Check the federation service schema definition language - query = """ - query { - _service { - sdl - } - } - """ - result = graphql_sync(schema.graphql_schema, query) - assert not result.errors - expected_result = dedent( - """ - extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@extends", "@external", "@key", "@requires"]) - type Query { - product: Product - } - - extend type Product @key(fields: "sku") { - sku: ID @external - size: Int @external - weight: Int @external - shippingEstimate: String @requires(fields: "size weight") - } - """ - ) - assert clean_schema(result.data["_service"]["sdl"]) == clean_schema(expected_result) - - -def test_requires_with_input(): - """ - Test checking that the issue https://github.com/preply/graphene-federation/pull/47 is resolved. - """ - - @extend("id") - class Acme(ObjectType): - id = external(ID(required=True)) - age = external(Int()) - foo = requires(Field(String, someInput=String()), fields="age") - - class Query(ObjectType): - acme = Field(Acme) - - schema = build_schema(query=Query, enable_federation_2=True) - expected_result = dedent( - """ - type Query { - acme: Acme - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - } - - type Acme { - id: ID! - age: Int - foo(someInput: String): String - } - - union _Entity = Acme - - scalar _Any - - type _Service { - sdl: String - } - """ - ) - assert clean_schema(schema) == clean_schema(expected_result) - # Check the federation service schema definition language - query = """ - query { - _service { - sdl - } - } - """ - result = graphql_sync(schema.graphql_schema, query) - assert not result.errors - expected_result = dedent( - """ - extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@extends", "@external", "@key", "@requires"]) - type Query { - acme: Acme - } - - extend type Acme @key(fields: "id") { - id: ID! @external - age: Int @external - foo(someInput: String): String @requires(fields: "age") - } - """ - ) - assert clean_schema(result.data["_service"]["sdl"]) == clean_schema(expected_result) diff --git a/graphene_federation/tests/test_requires_v1.py b/graphene_federation/tests/test_requires_v1.py deleted file mode 100644 index 3505473..0000000 --- a/graphene_federation/tests/test_requires_v1.py +++ /dev/null @@ -1,228 +0,0 @@ -from textwrap import dedent - -import pytest - -from graphql import graphql_sync - -from graphene import Field, ID, Int, ObjectType, String - -from graphene_federation.extend import extend -from graphene_federation.external import external -from graphene_federation.requires import requires -from graphene_federation.main import build_schema -from graphene_federation.utils import clean_schema - - -def test_chain_requires_failure(): - """ - Check that we can't nest call the requires method on a field. - """ - with pytest.raises(AssertionError) as err: - - @extend("id") - class A(ObjectType): - id = external(ID()) - something = requires(requires(String(), fields="id"), fields="id") - - assert "Can't chain `requires()` method calls on one field." == str(err.value) - - -def test_requires_multiple_fields(): - """ - Check that requires can take more than one field as input. - """ - - @extend("sku") - class Product(ObjectType): - sku = external(ID()) - size = external(Int()) - weight = external(Int()) - shipping_estimate = requires(String(), fields="size weight") - - class Query(ObjectType): - product = Field(Product) - - schema = build_schema(query=Query) - expected_result = dedent( - """ - type Query { - product: Product - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - } - - type Product { - sku: ID - size: Int - weight: Int - shippingEstimate: String - } - - union _Entity = Product - - scalar _Any - - type _Service { - sdl: String - } - """ - ) - assert clean_schema(schema) == clean_schema(expected_result) - # Check the federation service schema definition language - query = """ - query { - _service { - sdl - } - } - """ - result = graphql_sync(schema.graphql_schema, query) - assert not result.errors - expected_result = dedent( - """ - type Query { - product: Product - } - - extend type Product @key(fields: "sku") { - sku: ID @external - size: Int @external - weight: Int @external - shippingEstimate: String @requires(fields: "size weight") - } - """ - ) - assert clean_schema(result.data["_service"]["sdl"]) == clean_schema(expected_result) - - -def test_requires_multiple_fields_as_list(): - """ - Check that requires can take more than one field as input. - """ - - @extend("sku") - class Product(ObjectType): - sku = external(ID()) - size = external(Int()) - weight = external(Int()) - shipping_estimate = requires(String(), fields=["size", "weight"]) - - class Query(ObjectType): - product = Field(Product) - - schema = build_schema(query=Query) - expected_result = dedent( - """ - type Query { - product: Product - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - } - - type Product { - sku: ID - size: Int - weight: Int - shippingEstimate: String - } - - union _Entity = Product - - scalar _Any - - type _Service { - sdl: String - } - """ - ) - assert clean_schema(schema) == clean_schema(expected_result) - # Check the federation service schema definition language - query = """ - query { - _service { - sdl - } - } - """ - result = graphql_sync(schema.graphql_schema, query) - assert not result.errors - expected_result = dedent( - """ - type Query { - product: Product - } - - extend type Product @key(fields: "sku") { - sku: ID @external - size: Int @external - weight: Int @external - shippingEstimate: String @requires(fields: "size weight") - } - """ - ) - assert clean_schema(result.data["_service"]["sdl"]) == clean_schema(expected_result) - - -def test_requires_with_input(): - """ - Test checking that the issue https://github.com/preply/graphene-federation/pull/47 is resolved. - """ - - @extend("id") - class Acme(ObjectType): - id = external(ID(required=True)) - age = external(Int()) - foo = requires(Field(String, someInput=String()), fields="age") - - class Query(ObjectType): - acme = Field(Acme) - - schema = build_schema(query=Query) - expected_result = dedent( - """ - type Query { - acme: Acme - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - } - - type Acme { - id: ID! - age: Int - foo(someInput: String): String - } - - union _Entity = Acme - - scalar _Any - - type _Service { - sdl: String - } - """ - ) - assert clean_schema(schema) == clean_schema(expected_result) - # Check the federation service schema definition language - query = """ - query { - _service { - sdl - } - } - """ - result = graphql_sync(schema.graphql_schema, query) - assert not result.errors - expected_result = dedent( - """ - type Query { - acme: Acme - } - - extend type Acme @key(fields: "id") { - id: ID! @external - age: Int @external - foo(someInput: String): String @requires(fields: "age") - } - """ - ) - assert clean_schema(result.data["_service"]["sdl"]) == clean_schema(expected_result) diff --git a/graphene_federation/tests/test_scalar.py b/graphene_federation/tests/test_scalar.py deleted file mode 100644 index e80a2d0..0000000 --- a/graphene_federation/tests/test_scalar.py +++ /dev/null @@ -1,61 +0,0 @@ -from textwrap import dedent -from typing import Any - -import graphene -from graphene import Scalar, String, ObjectType -from graphql import graphql_sync - -from graphene_federation import build_schema, shareable, inaccessible -from graphene_federation.utils import clean_schema - - -def test_custom_scalar(): - class AddressScalar(Scalar): - base = String - - @staticmethod - def coerce_address(value: Any): - ... - - serialize = coerce_address - parse_value = coerce_address - - @staticmethod - def parse_literal(ast): - ... - - @shareable - class TestScalar(graphene.ObjectType): - test_shareable_scalar = shareable(String(x=AddressScalar())) - test_inaccessible_scalar = inaccessible(String(x=AddressScalar())) - - class Query(ObjectType): - test = String(x=AddressScalar()) - test2 = graphene.List(AddressScalar, required=True) - - schema = build_schema(query=Query, enable_federation_2=True, types=(TestScalar,)) - query = """ - query { - _service { - sdl - } - } - """ - result = graphql_sync(schema.graphql_schema, query) - expected_result = dedent( - """ - extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@inaccessible", "@shareable"]) - type TestScalar @shareable { - testShareableScalar(x: AddressScalar): String @shareable - testInaccessibleScalar(x: AddressScalar): String @inaccessible - } - - scalar AddressScalar - - type Query { - test(x: AddressScalar): String - test2: [AddressScalar]! - } - """ - ) - assert clean_schema(result.data["_service"]["sdl"]) == clean_schema(expected_result) diff --git a/graphene_federation/tests/test_shareable.py b/graphene_federation/tests/test_shareable.py deleted file mode 100644 index 9d28b5a..0000000 --- a/graphene_federation/tests/test_shareable.py +++ /dev/null @@ -1,135 +0,0 @@ -from textwrap import dedent - -import graphene -import pytest -from graphene import ObjectType -from graphql import graphql_sync - -from graphene_federation import shareable, build_schema -from graphene_federation.utils import clean_schema - - -@pytest.mark.xfail( - reason="The @Shareable directive is about indicating when an object field " - "can be resolved by multiple subgraphs. As interface fields are not " - "directly resolved (their implementation is), @Shareable is not " - "meaningful on an interface field and is not allowed (at least since " - "federation 2.2; earlier versions of federation 2 mistakenly ignored " - "@Shareable on interface fields)." -) -def test_shareable_interface_failures(): - @shareable - class ReviewInterface(graphene.Interface): - interfaced_body = graphene.String(required=True) - - @shareable - class Review(graphene.ObjectType): - class Meta: - interfaces = (ReviewInterface,) - - id = shareable(graphene.Int(required=True)) - body = graphene.String(required=True) - - class Query(ObjectType): - in_stock_count = graphene.Int(required=True) - - build_schema(query=Query, enable_federation_2=True, types=(ReviewInterface, Review)) - - -def test_shareable(): - @shareable - class Position(graphene.ObjectType): - x = graphene.Int(required=True) - y = shareable(graphene.Int(required=True)) - - class Query(ObjectType): - in_stock_count = graphene.Int(required=True) - - schema = build_schema(query=Query, enable_federation_2=True, types=(Position,)) - # Check the federation service schema definition language - query = """ - query { - _service { - sdl - } - } - """ - result = graphql_sync(schema.graphql_schema, query) - assert not result.errors - expected_result = dedent( - """ - extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@shareable"]) - type Position @shareable { - x: Int! - y: Int! @shareable - } - - type Query { - inStockCount: Int! - } - """ - ) - assert clean_schema(result.data["_service"]["sdl"]) == clean_schema(expected_result) - - -def test_shareable_union(): - @shareable - class Human(graphene.ObjectType): - name = graphene.String() - born_in = graphene.String() - - @shareable - class Droid(graphene.ObjectType): - name = shareable(graphene.String()) - primary_function = graphene.String() - - @shareable - class Starship(graphene.ObjectType): - name = graphene.String() - length = shareable(graphene.Int()) - - @shareable - class SearchResult(graphene.Union): - class Meta: - types = (Human, Droid, Starship) - - class Query(ObjectType): - in_stock_count = graphene.Int(required=True) - - schema = build_schema(query=Query, enable_federation_2=True, types=(SearchResult,)) - # Check the federation service schema definition language - query = """ - query { - _service { - sdl - } - } - """ - result = graphql_sync(schema.graphql_schema, query) - assert not result.errors - expected_result = dedent( - """ - extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@shareable"]) - union SearchResult @shareable = Human | Droid | Starship - - type Human @shareable { - name: String - bornIn: String - } - - type Droid @shareable { - name: String @shareable - primaryFunction: String - } - - type Starship @shareable { - name: String - length: Int @shareable - } - - type Query { - inStockCount: Int! - } - """ - ) - assert clean_schema(result.data["_service"]["sdl"]) == clean_schema(expected_result) diff --git a/graphene_federation/transform/field_set_case_transform.py b/graphene_federation/transform/field_set_case_transform.py index e6f9f2c..9232f43 100644 --- a/graphene_federation/transform/field_set_case_transform.py +++ b/graphene_federation/transform/field_set_case_transform.py @@ -1,13 +1,14 @@ -from graphene.utils.str_converters import to_camel_case from graphene_directives import Schema +from ..validators import InternalNamespace, to_case + def field_set_case_transform(inputs: dict, schema: Schema) -> dict: fields = inputs.get("fields") if fields: inputs["fields"] = ( - to_camel_case(fields).replace("_Typename", "__typename") - if schema.auto_camelcase - else fields + to_case(fields, schema) + .replace(InternalNamespace.UNION.value, "... on") + .replace(InternalNamespace.ARG.value, "") ) return inputs diff --git a/graphene_federation/utils.py b/graphene_federation/utils.py deleted file mode 100644 index 8d026d9..0000000 --- a/graphene_federation/utils.py +++ /dev/null @@ -1,6 +0,0 @@ -import re - - -def clean_schema(schema): - schema = re.sub(r"\s+", "", str(schema)) - return schema.strip() diff --git a/graphene_federation/validators/__init__.py b/graphene_federation/validators/__init__.py index da76627..ebf7b54 100644 --- a/graphene_federation/validators/__init__.py +++ b/graphene_federation/validators/__init__.py @@ -1,3 +1,4 @@ from .key import validate_key from .requires import validate_requires -from .utils import build_ast +from .utils import InternalNamespace +from .utils import ast_to_str, build_ast, to_case diff --git a/graphene_federation/validators/key.py b/graphene_federation/validators/key.py index ff29ff1..0d359be 100644 --- a/graphene_federation/validators/key.py +++ b/graphene_federation/validators/key.py @@ -9,10 +9,8 @@ def validate_key( type_: Union[ObjectType, Interface, Field], inputs: dict, schema: Schema ) -> bool: - ast_node = build_ast( - input_str=to_case(inputs.get("fields"), schema), valid_special_chars="_" - ) - errors = [] + errors: list[str] = [] + ast_node = build_ast(input_str=to_case(inputs.get("fields"), schema)) evaluate_ast( directive_name="key", nodes=ast_node, @@ -21,7 +19,7 @@ def validate_key( errors=errors, entity_types=schema.graphql_schema.type_map, ) - if len(errors) != 0: + if errors: raise ValueError("\n".join(errors)) return True diff --git a/graphene_federation/validators/requires.py b/graphene_federation/validators/requires.py index 8c4eff9..2984d9e 100644 --- a/graphene_federation/validators/requires.py +++ b/graphene_federation/validators/requires.py @@ -3,7 +3,7 @@ from graphene import Field, Interface, ObjectType from graphene_directives import Schema -from .utils import build_ast, evaluate_ast, to_case +from .utils import InternalNamespace, build_ast, evaluate_ast, to_case def validate_requires( @@ -12,20 +12,17 @@ def validate_requires( inputs: dict, schema: Schema, ) -> bool: - ast_node = build_ast( - input_str=to_case(inputs.get("fields"), schema), valid_special_chars='_()"' - ) - - errors = [] + errors: list[str] = [] + ast_node = build_ast(to_case(inputs.get("fields"), schema)) evaluate_ast( directive_name="requires", nodes=ast_node, - type_=parent_type.graphene_type, - ignore_fields=["__typename", "_Typename"], + type_=parent_type, + ignore_fields=["__typename", InternalNamespace.UNION.value], errors=errors, entity_types=schema.graphql_schema.type_map, ) - if len(errors) != 0: + if errors: raise ValueError("\n".join(errors)) return True diff --git a/graphene_federation/validators/utils.py b/graphene_federation/validators/utils.py index 03f02d0..24a079a 100644 --- a/graphene_federation/validators/utils.py +++ b/graphene_federation/validators/utils.py @@ -1,57 +1,28 @@ -import re +from enum import Enum from typing import Union from graphene import Field, Interface, NonNull, ObjectType -from graphene.types.definitions import GrapheneObjectType +from graphene.types.definitions import ( + GrapheneEnumType, + GrapheneInterfaceType, + GrapheneObjectType, + GrapheneScalarType, + GrapheneUnionType, +) from graphene.utils.str_converters import to_camel_case from graphene_directives import Schema -from graphql import GraphQLField, GraphQLNonNull +from graphql import ( + GraphQLField, + GraphQLList, + GraphQLNonNull, + GraphQLScalarType, + GraphQLType, +) -def clean_combined_brackets(data, bracket_kind) -> list[str]: - cleaned = [] - - filtered = list(filter(lambda x: x != "", data.split(bracket_kind))) - - for index, field in enumerate(filtered): - if index % 2: - cleaned.extend([field, bracket_kind]) - else: - cleaned.append(field) - - if not len(filtered) % 2 == 0: - cleaned.append(bracket_kind) - - return cleaned - - -def build_ast(input_str: str, valid_special_chars: str) -> dict: - fields = input_str.split() - cleaned_fields = [] - pattern = rf"[^a-zA-Z{valid_special_chars}]+" - - for field in fields: - if "{" in field and field != "{": - cleaned_fields.extend(clean_combined_brackets(field, "{")) - elif "}" in field and field != "}": - cleaned_fields.extend(clean_combined_brackets(field, "}")) - elif field == "{" or field == "}": - cleaned_fields.append(field) - else: - cleaned_fields.append(re.sub(pattern, "", field)) - - parent = {} - field_stack = [] - field_level = [parent] - for index, field in enumerate(cleaned_fields): - if field == "{": - field_level.append(field_level[-1][field_stack[-1]]) - elif field == "}": - field_level.pop() - else: - field_stack.append(field) - field_level[-1][field] = {} - return parent +class InternalNamespace(Enum): + UNION = "__union__" + ARG = "__arg__" def check_fields_exist_on_type( @@ -59,8 +30,10 @@ def check_fields_exist_on_type( type_: Union[ObjectType, Interface, Field, NonNull], ignore_fields: list[str], entity_types: dict[str, ObjectType], -) -> bool: - if field in ignore_fields: +) -> Union[GraphQLType, GraphQLField, bool]: + if field in ignore_fields or field.startswith( + "__arg__" # todo handle argument validations + ): return True if isinstance(type_, GraphQLField): @@ -70,18 +43,161 @@ def check_fields_exist_on_type( ignore_fields, entity_types, ) - elif isinstance(type_, GraphQLNonNull): + if isinstance(type_, GraphQLNonNull): return check_fields_exist_on_type( field, type_.of_type, ignore_fields, entity_types ) - elif isinstance(type_, GrapheneObjectType): - return field in type_.fields - elif issubclass(type_, ObjectType) or issubclass(type_, Interface): # noqa - return field in entity_types.get(type_._meta.name).fields # noqa + if isinstance(type_, GrapheneObjectType): + if field in type_.fields: + return type_.fields[field] + if isinstance(type_, GraphQLList): + return check_fields_exist_on_type( + field, type_.of_type, ignore_fields, entity_types + ) + if isinstance(type_, GrapheneUnionType): + for union_type in type_.types: + if union_type.name.lower() == field.lower(): + return union_type + try: + if issubclass(type_, ObjectType) or issubclass(type_, Interface): # noqa + entity_fields = entity_types.get(type_._meta.name) # noqa + if entity_fields is not None: + entity_fields = entity_fields.fields # noqa + if field in entity_fields: + return entity_fields[field] + except TypeError: + return False return False +def get_type_for_field( + type_, +) -> tuple[ + Union[ + GrapheneObjectType, + GrapheneInterfaceType, + GrapheneUnionType, + GrapheneScalarType, + GrapheneEnumType, + ], + bool, +]: + """ + Returns the type,is_selectable + """ + if isinstance(type_, GraphQLField): + return get_type_for_field(type_.type) + if isinstance(type_, GraphQLNonNull): + return get_type_for_field(type_.of_type) + if isinstance(type_, GraphQLList): + return get_type_for_field(type_.of_type) + if ( + isinstance(type_, GrapheneObjectType) + or isinstance(type_, GrapheneInterfaceType) + or isinstance(type_, GrapheneUnionType) + ): + return type_, True + if isinstance(type_, GraphQLScalarType) or isinstance(type_, GrapheneEnumType): + return type_, False + + raise NotImplementedError("get_type_for_field", type_) + + +"""" +AST FUNCTIONS + +For FieldSet Parsing +""" + + +def _tokenize(input_string): + input_string = input_string.strip() + tokens = [] + current_token = "" + open_braces_count = 0 + + if input_string.startswith("{"): + raise ValueError("@requires cannot start with {") + + index = 0 + while index < len(input_string): + char = input_string[index] + if char.isalnum(): + current_token += char + elif char == "{": + if current_token: + tokens.append(current_token) + tokens.append(char) + current_token = "" + open_braces_count += 1 + elif char == "}": + if current_token: + tokens.append(current_token) + tokens.append(char) + current_token = "" + open_braces_count -= 1 + elif char == ",": + if current_token: + tokens.append(current_token) + current_token = "" + elif char == "_": + current_token += char + elif char == "(": + tokens.append(current_token) + current_token = f"{char}" + index += 1 + mismatched_parenthesis = True + while index < len(input_string): + char = input_string[index] + if char.isalnum() or char == ",": + current_token += char + elif char.isspace(): + index += 1 + continue + elif char == ":": + current_token += ": " + elif char == ")": + current_token += char + mismatched_parenthesis = False + tokens.append( + ", ".join(current_token.split(",")).replace("(", "__arg__(") + ) + current_token = "" + break + else: + ValueError( + f"@requires({input_string}) has unknown character {char} at argument {current_token}" + ) + index += 1 + if mismatched_parenthesis: + raise ValueError( + f"@requires({input_string}) has mismatched parenthesis" + ) + elif char == ")": + raise ValueError(f"@requires({input_string}) has mismatched parenthesis") + elif char.isspace(): + if current_token == "on": + tokens.append("__union__") + elif current_token: + tokens.append(current_token) + current_token = "" + else: + if current_token: + tokens.append(current_token) + current_token = "" + + index += 1 + + if current_token: + tokens.append(current_token) + + if open_braces_count != 0: + raise ValueError(f"@requires({input_string}) has mismatched brackets") + + return tokens + + def evaluate_ast( directive_name: str, nodes: dict, @@ -90,22 +206,43 @@ def evaluate_ast( errors: list[str], entity_types: dict[str, ObjectType], ) -> None: - for field, value in nodes.items(): - if not check_fields_exist_on_type( - field, + for field_name, value in nodes.items(): + field_type = check_fields_exist_on_type( + field_name, type_, ignore_fields, entity_types, - ): + ) + has_selections = len(value) != 0 + + if not field_type: + errors.append( + f'@{directive_name}, field "{field_name}" does not exist on type "{type_}"' + ) + continue + + field_type, is_selectable = ( + get_type_for_field(field_type) + if not isinstance(field_type, bool) + else ( + field_type, + False, + ) + ) + + if is_selectable and not has_selections: + errors.append( + f'@{directive_name}, type {type_}, field "{field_name}" needs sub selections.' + ) + continue + + if not is_selectable and has_selections: errors.append( - f'@{directive_name}, field "{field}" does not exist on type "{type_}"' - ) # noqa + f'@{directive_name}, type {type_}, field "{field_name}" cannot have sub selections.' + ) + continue + if len(value) != 0: - if hasattr(type_, "_meta"): - type_ = entity_types.get(type_._meta.name) # noqa - field_type = type_.fields[field] # noqa - else: - field_type = type_.fields[field] # noqa evaluate_ast( directive_name, value, @@ -116,5 +253,78 @@ def evaluate_ast( ) -def to_case(fields: str, schema: Schema) -> str: - return to_camel_case(fields) if schema.auto_camelcase else fields +def build_ast(input_str: str) -> dict: + cleaned_fields = _tokenize(input_str) + + parent: dict[str, dict] = {} + field_stack: list[str] = [] + field_level = [parent] + for index, field in enumerate(cleaned_fields): + if field == "{": + field_level.append(field_level[-1][field_stack[-1]]) + elif field == "}": + field_level.pop() + else: + field_stack.append(field) + field_level[-1][field] = {} + return parent + + +def ast_to_str(fields: dict, add_type_name: bool = False, level: int = 0) -> str: + new_fields = [] + union_type = False + if level != 0 and add_type_name: + new_fields.append("__typename") + for field, value in fields.items(): + if "typename" in field.lower(): + continue + if "__union__" in field.lower(): + union_type = True + elif len(value) == 0: + new_fields.append(field) + else: + inner_fields = [ + field, + "{", + ast_to_str(value, add_type_name, level + 1), + "}", + ] + if union_type: + inner_fields.insert(0, "... on") + new_fields.extend(inner_fields) + + return " ".join(new_fields) + + +"""" +String Helpers + +For Schema Field Casing Parsing +""" + + +def to_case(fields: Union[str, None], schema: Schema) -> str: + if not fields: + return "" + + skip_next = False + + if schema.auto_camelcase: + data_fields = [] + for field in fields.split(): + if field == InternalNamespace.UNION.value: + data_fields.append(field) + skip_next = True + elif field == "__typename": + data_fields.append(field) + elif field.startswith(InternalNamespace.ARG.value): + data_fields.append(field) + else: + if skip_next: + data_fields.append(field) + skip_next = False + else: + data_fields.append(to_camel_case(field)) + return " ".join(data_fields) + + return fields diff --git a/integration_tests/service_a/src/schema.py b/integration_tests/service_a/src/schema.py index 60b9870..0988bb1 100644 --- a/integration_tests/service_a/src/schema.py +++ b/integration_tests/service_a/src/schema.py @@ -1,18 +1,20 @@ -from graphene import ObjectType, String, Int, List, NonNull, Field, Interface +from graphene import Field, Int, Interface, List, NonNull, ObjectType, String -from graphene_federation import build_schema, extend, external +from graphene_federation import build_schema, extends, external, key class DecoratedText(Interface): color = Int(required=True) -@extend(fields="id") +@key(fields="id") +@extends class FileNode(ObjectType): id = external(Int(required=True)) -@extend(fields="id") +@key(fields="id") +@extends class FunnyText(ObjectType): class Meta: interfaces = (DecoratedText,) @@ -37,7 +39,8 @@ def resolve_color(self, info, **kwargs): return self.id + 2 -@extend(fields="primaryEmail") +@key(fields="primaryEmail") +@extends class User(ObjectType): primaryEmail = external(String()) diff --git a/integration_tests/service_c/src/schema.py b/integration_tests/service_c/src/schema.py index ecf4489..c7cf06e 100644 --- a/integration_tests/service_c/src/schema.py +++ b/integration_tests/service_c/src/schema.py @@ -1,9 +1,10 @@ -from graphene import ObjectType, String, Int, List, NonNull, Field +from graphene import Field, Int, List, NonNull, ObjectType, String -from graphene_federation import build_schema, extend, external, requires, key, provides +from graphene_federation import build_schema, extends, external, key, provides, requires -@extend(fields="id") +@key(fields="id") +@extends class User(ObjectType): id = external(Int(required=True)) primary_email = external(String()) @@ -27,7 +28,6 @@ def __resolve_reference(self, info, **kwargs): return Article(id=self.id, text=f"text_{self.id}") -@provides class ArticleThatProvideAuthorAge(ObjectType): """ should not contain other graphene-federation decorators to proper test test-case diff --git a/integration_tests/service_d/src/schema.py b/integration_tests/service_d/src/schema.py index 2371047..0ddd882 100644 --- a/integration_tests/service_d/src/schema.py +++ b/integration_tests/service_d/src/schema.py @@ -1,6 +1,6 @@ -from graphene import ObjectType, Int, Field +from graphene import Field, Int, ObjectType -from graphene_federation import build_schema, extend, external +from graphene_federation import build_schema, extends, external, key """ Alphabet order - matters @@ -9,7 +9,8 @@ """ -@extend(fields="id") +@key(fields="id") +@extends class Article(ObjectType): id = external(Int(required=True)) diff --git a/integration_tests/tests/tests/test_main.py b/integration_tests/tests/tests/test_main.py index 491059c..ebc771d 100644 --- a/integration_tests/tests/tests/test_main.py +++ b/integration_tests/tests/tests/test_main.py @@ -1,4 +1,5 @@ import json + import requests @@ -70,9 +71,7 @@ def test_external_types(): assert { "id": 1001, "primaryEmail": "frank@frank.com", - } == posts[ - 3 - ]["author"] + } == posts[3]["author"] assert articles == [ { @@ -127,7 +126,7 @@ def test_multiple_key_decorators_apply_multiple_key_annotations(): def test_avoid_duplication_of_key_decorator(): sdl = fetch_sdl("service_a") - assert 'extend type FileNode @key(fields: "id") {' in sdl + assert 'type FileNode @key(fields: "id") @extends {' in sdl def test_requires(): diff --git a/setup.py b/setup.py index 26df441..789a330 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ def read(*rnames): ] dev_require = [ - "black==22.3.0", + "black==23.12.1", "flake8==4.0.1", "mypy==0.961", ] + tests_require @@ -36,7 +36,7 @@ def read(*rnames): install_requires=[ "graphene>=3.1", "graphql-core>=3.1", - "graphene-directives>=0.4.3", + "graphene-directives>=0.4.6", ], classifiers=[ "Development Status :: 5 - Production/Stable", @@ -48,6 +48,7 @@ def read(*rnames): "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ], extras_require={ "test": tests_require, diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/gql/test_annotation_corner_cases/test_annotate_object_with_meta_name_1.graphql b/tests/gql/test_annotation_corner_cases/test_annotate_object_with_meta_name_1.graphql index efe26ec..02c9630 100644 --- a/tests/gql/test_annotation_corner_cases/test_annotate_object_with_meta_name_1.graphql +++ b/tests/gql/test_annotation_corner_cases/test_annotate_object_with_meta_name_1.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@extends", "@external", "@key"]) + @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@extends", "@external", "@key"]) type Query { a: Banana @@ -22,4 +22,10 @@ scalar _Any type _Service { sdl: String -} \ No newline at end of file +} + +scalar FieldSet + +scalar Scope + +scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases/test_annotate_object_with_meta_name_2.graphql b/tests/gql/test_annotation_corner_cases/test_annotate_object_with_meta_name_2.graphql index 3af4f57..7d18cc1 100644 --- a/tests/gql/test_annotation_corner_cases/test_annotate_object_with_meta_name_2.graphql +++ b/tests/gql/test_annotation_corner_cases/test_annotate_object_with_meta_name_2.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@extends", "@external", "@key"]) + @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@extends", "@external", "@key"]) type Query { a: Banana @@ -12,4 +12,10 @@ type Banana @extends { type Potato @key(fields: "id") { id: ID -} \ No newline at end of file +} + +scalar FieldSet + +scalar Scope + +scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases/test_annotated_field_also_used_in_filter_1.graphql b/tests/gql/test_annotation_corner_cases/test_annotated_field_also_used_in_filter_1.graphql index 8c2fde0..c33e145 100644 --- a/tests/gql/test_annotation_corner_cases/test_annotated_field_also_used_in_filter_1.graphql +++ b/tests/gql/test_annotation_corner_cases/test_annotated_field_also_used_in_filter_1.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@extends", "@external", "@key"]) + @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@extends", "@external", "@key"]) type Query { a: A @@ -22,4 +22,10 @@ scalar _Any type _Service { sdl: String -} \ No newline at end of file +} + +scalar FieldSet + +scalar Scope + +scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases/test_annotated_field_also_used_in_filter_2.graphql b/tests/gql/test_annotation_corner_cases/test_annotated_field_also_used_in_filter_2.graphql index 30f8bbe..bca01c5 100644 --- a/tests/gql/test_annotation_corner_cases/test_annotated_field_also_used_in_filter_2.graphql +++ b/tests/gql/test_annotation_corner_cases/test_annotated_field_also_used_in_filter_2.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@extends", "@external", "@key"]) + @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@extends", "@external", "@key"]) type Query { a: A @@ -12,4 +12,10 @@ type A @extends { type B @key(fields: "id") { id: ID -} \ No newline at end of file +} + +scalar FieldSet + +scalar Scope + +scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_1.graphql b/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_1.graphql index dad0a06..a8f41d7 100644 --- a/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_1.graphql +++ b/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_1.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@extends", "@external", "@key", "@requires"]) + @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@extends", "@external", "@key", "@requires"]) type Query { camel: Camel @@ -20,4 +20,10 @@ scalar _Any type _Service { sdl: String -} \ No newline at end of file +} + +scalar FieldSet + +scalar Scope + +scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_2.graphql b/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_2.graphql index c492c80..3e56148 100644 --- a/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_2.graphql +++ b/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_2.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@extends", "@external", "@key", "@requires"]) + @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@extends", "@external", "@key", "@requires"]) type Query { camel: Camel @@ -10,4 +10,10 @@ type Camel @key(fields: "autoCamel") @extends { forcedCamel: String @requires(fields: "autoCamel") aSnake: String aCamel: String -} \ No newline at end of file +} + +scalar FieldSet + +scalar Scope + +scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_without_auto_camelcase_1.graphql b/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_without_auto_camelcase_1.graphql index 19c3729..4475a5a 100644 --- a/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_without_auto_camelcase_1.graphql +++ b/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_without_auto_camelcase_1.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@extends", "@external", "@requires"]) + @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@extends", "@external", "@requires"]) type Query { camel: Camel @@ -20,4 +20,10 @@ scalar _Any type _Service { sdl: String -} \ No newline at end of file +} + +scalar FieldSet + +scalar Scope + +scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_without_auto_camelcase_2.graphql b/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_without_auto_camelcase_2.graphql index c53e1d5..2599e85 100644 --- a/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_without_auto_camelcase_2.graphql +++ b/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_without_auto_camelcase_2.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@extends", "@external", "@requires"]) + @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@extends", "@external", "@requires"]) type Query { camel: Camel @@ -10,4 +10,10 @@ type Camel @extends { forcedCamel: String @requires(fields: "auto_camel") a_snake: String aCamel: String -} \ No newline at end of file +} + +scalar FieldSet + +scalar Scope + +scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases/test_similar_field_name_1.graphql b/tests/gql/test_annotation_corner_cases/test_similar_field_name_1.graphql index f7227c6..74001b6 100644 --- a/tests/gql/test_annotation_corner_cases/test_similar_field_name_1.graphql +++ b/tests/gql/test_annotation_corner_cases/test_similar_field_name_1.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@extends", "@external", "@key"]) + @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@extends", "@external", "@key"]) schema { query: ChatQuery @@ -30,4 +30,10 @@ scalar _Any type _Service { sdl: String -} \ No newline at end of file +} + +scalar FieldSet + +scalar Scope + +scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases/test_similar_field_name_2.graphql b/tests/gql/test_annotation_corner_cases/test_similar_field_name_2.graphql index f3a6094..0a81854 100644 --- a/tests/gql/test_annotation_corner_cases/test_similar_field_name_2.graphql +++ b/tests/gql/test_annotation_corner_cases/test_similar_field_name_2.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@extends", "@external", "@key"]) + @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@extends", "@external", "@key"]) schema { query: ChatQuery @@ -20,4 +20,10 @@ type ChatUser @key(fields: "id") @extends { id: ID @external iD: ID ID: ID -} \ No newline at end of file +} + +scalar FieldSet + +scalar Scope + +scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases_v1/test_annotate_object_with_meta_name_1.graphql b/tests/gql/test_annotation_corner_cases_v1/test_annotate_object_with_meta_name_1.graphql index b2d0d44..33ea1a2 100644 --- a/tests/gql/test_annotation_corner_cases_v1/test_annotate_object_with_meta_name_1.graphql +++ b/tests/gql/test_annotation_corner_cases_v1/test_annotate_object_with_meta_name_1.graphql @@ -19,4 +19,6 @@ scalar _Any type _Service { sdl: String -} \ No newline at end of file +} + +scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases_v1/test_annotate_object_with_meta_name_2.graphql b/tests/gql/test_annotation_corner_cases_v1/test_annotate_object_with_meta_name_2.graphql index 24f6c7a..231213d 100644 --- a/tests/gql/test_annotation_corner_cases_v1/test_annotate_object_with_meta_name_2.graphql +++ b/tests/gql/test_annotation_corner_cases_v1/test_annotate_object_with_meta_name_2.graphql @@ -9,4 +9,6 @@ type Banana @extends { type Potato @key(fields: "id") { id: ID -} \ No newline at end of file +} + +scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases_v1/test_annotated_field_also_used_in_filter_1.graphql b/tests/gql/test_annotation_corner_cases_v1/test_annotated_field_also_used_in_filter_1.graphql index 6743c39..721113e 100644 --- a/tests/gql/test_annotation_corner_cases_v1/test_annotated_field_also_used_in_filter_1.graphql +++ b/tests/gql/test_annotation_corner_cases_v1/test_annotated_field_also_used_in_filter_1.graphql @@ -19,4 +19,6 @@ scalar _Any type _Service { sdl: String -} \ No newline at end of file +} + +scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases_v1/test_annotated_field_also_used_in_filter_2.graphql b/tests/gql/test_annotation_corner_cases_v1/test_annotated_field_also_used_in_filter_2.graphql index f06d78a..04f332e 100644 --- a/tests/gql/test_annotation_corner_cases_v1/test_annotated_field_also_used_in_filter_2.graphql +++ b/tests/gql/test_annotation_corner_cases_v1/test_annotated_field_also_used_in_filter_2.graphql @@ -9,4 +9,6 @@ type A @extends { type B @key(fields: "id") { id: ID -} \ No newline at end of file +} + +scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_1.graphql b/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_1.graphql index 75b54e6..0f68074 100644 --- a/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_1.graphql +++ b/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_1.graphql @@ -17,4 +17,6 @@ scalar _Any type _Service { sdl: String -} \ No newline at end of file +} + +scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_2.graphql b/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_2.graphql index 11e9b8d..7722ee0 100644 --- a/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_2.graphql +++ b/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_2.graphql @@ -7,4 +7,6 @@ type Camel @key(fields: "autoCamel") @extends { forcedCamel: String @requires(fields: "autoCamel") aSnake: String aCamel: String -} \ No newline at end of file +} + +scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_without_auto_camelcase_1.graphql b/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_without_auto_camelcase_1.graphql index 25aac2c..bf84d73 100644 --- a/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_without_auto_camelcase_1.graphql +++ b/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_without_auto_camelcase_1.graphql @@ -17,4 +17,6 @@ scalar _Any type _Service { sdl: String -} \ No newline at end of file +} + +scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_without_auto_camelcase_2.graphql b/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_without_auto_camelcase_2.graphql index 095afd2..059614d 100644 --- a/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_without_auto_camelcase_2.graphql +++ b/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_without_auto_camelcase_2.graphql @@ -7,4 +7,6 @@ type Camel @extends { forcedCamel: String @requires(fields: "auto_camel") a_snake: String aCamel: String -} \ No newline at end of file +} + +scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases_v1/test_similar_field_name_1.graphql b/tests/gql/test_annotation_corner_cases_v1/test_similar_field_name_1.graphql index 3531b4d..2cbfefc 100644 --- a/tests/gql/test_annotation_corner_cases_v1/test_similar_field_name_1.graphql +++ b/tests/gql/test_annotation_corner_cases_v1/test_similar_field_name_1.graphql @@ -27,4 +27,6 @@ scalar _Any type _Service { sdl: String -} \ No newline at end of file +} + +scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases_v1/test_similar_field_name_2.graphql b/tests/gql/test_annotation_corner_cases_v1/test_similar_field_name_2.graphql index 7bb2baa..2a39f29 100644 --- a/tests/gql/test_annotation_corner_cases_v1/test_similar_field_name_2.graphql +++ b/tests/gql/test_annotation_corner_cases_v1/test_similar_field_name_2.graphql @@ -17,4 +17,6 @@ type ChatUser @key(fields: "id") @extends { id: ID @external iD: ID ID: ID -} \ No newline at end of file +} + +scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_custom_enum/test_custom_enum_1.graphql b/tests/gql/test_custom_enum/test_custom_enum_1.graphql index 4da9a1b..69b38f9 100644 --- a/tests/gql/test_custom_enum/test_custom_enum_1.graphql +++ b/tests/gql/test_custom_enum/test_custom_enum_1.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@inaccessible", "@shareable"]) + @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@inaccessible", "@shareable"]) type TestCustomEnum @shareable { testShareableScalar: Episode @shareable @@ -20,4 +20,10 @@ type Query { type _Service { sdl: String -} \ No newline at end of file +} + +scalar FieldSet + +scalar Scope + +scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_custom_enum/test_custom_enum_2.graphql b/tests/gql/test_custom_enum/test_custom_enum_2.graphql index 30c6101..2eeaf13 100644 --- a/tests/gql/test_custom_enum/test_custom_enum_2.graphql +++ b/tests/gql/test_custom_enum/test_custom_enum_2.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@inaccessible", "@shareable"]) + @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@inaccessible", "@shareable"]) type TestCustomEnum @shareable { testShareableScalar: Episode @shareable @@ -15,4 +15,10 @@ enum Episode @inaccessible { type Query { test: Episode test2: [TestCustomEnum]! -} \ No newline at end of file +} + +scalar FieldSet + +scalar Scope + +scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_inaccessible/test_inaccessible_1.graphql b/tests/gql/test_inaccessible/test_inaccessible_1.graphql new file mode 100644 index 0000000..7148fb5 --- /dev/null +++ b/tests/gql/test_inaccessible/test_inaccessible_1.graphql @@ -0,0 +1,22 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@inaccessible"]) + +type Position @inaccessible { + x: Int! + y: Int! @inaccessible +} + +type Query { + inStockCount: Int! + _service: _Service! +} + +type _Service { + sdl: String +} + +scalar FieldSet + +scalar Scope + +scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_inaccessible/test_inaccessible_2.graphql b/tests/gql/test_inaccessible/test_inaccessible_2.graphql new file mode 100644 index 0000000..e80e6a9 --- /dev/null +++ b/tests/gql/test_inaccessible/test_inaccessible_2.graphql @@ -0,0 +1,17 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@inaccessible"]) + +type Position @inaccessible { + x: Int! + y: Int! @inaccessible +} + +type Query { + inStockCount: Int! +} + +scalar FieldSet + +scalar Scope + +scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_inaccessible/test_inaccessible_union_1.graphql b/tests/gql/test_inaccessible/test_inaccessible_union_1.graphql new file mode 100644 index 0000000..604651d --- /dev/null +++ b/tests/gql/test_inaccessible/test_inaccessible_union_1.graphql @@ -0,0 +1,34 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@inaccessible"]) + +union SearchResult @inaccessible = Human | Droid | Starship + +type Human @inaccessible { + name: String + bornIn: String +} + +type Droid @inaccessible { + name: String @inaccessible + primaryFunction: String +} + +type Starship @inaccessible { + name: String + length: Int @inaccessible +} + +type Query { + inStockCount: Int! + _service: _Service! +} + +type _Service { + sdl: String +} + +scalar FieldSet + +scalar Scope + +scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_inaccessible/test_inaccessible_union_2.graphql b/tests/gql/test_inaccessible/test_inaccessible_union_2.graphql new file mode 100644 index 0000000..2061b12 --- /dev/null +++ b/tests/gql/test_inaccessible/test_inaccessible_union_2.graphql @@ -0,0 +1,29 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@inaccessible"]) + +union SearchResult @inaccessible = Human | Droid | Starship + +type Human @inaccessible { + name: String + bornIn: String +} + +type Droid @inaccessible { + name: String @inaccessible + primaryFunction: String +} + +type Starship @inaccessible { + name: String + length: Int @inaccessible +} + +type Query { + inStockCount: Int! +} + +scalar FieldSet + +scalar Scope + +scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_key/test_compound_primary_key_1.graphql b/tests/gql/test_key/test_compound_primary_key_1.graphql new file mode 100644 index 0000000..f96641d --- /dev/null +++ b/tests/gql/test_key/test_compound_primary_key_1.graphql @@ -0,0 +1,31 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@key"]) + +type Query { + user: User + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! +} + +type User @key(fields: "id organization { registrationNumber }") { + id: ID + organization: Organization +} + +type Organization { + registrationNumber: ID +} + +union _Entity = User + +scalar _Any + +type _Service { + sdl: String +} + +scalar FieldSet + +scalar Scope + +scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_key/test_compound_primary_key_2.graphql b/tests/gql/test_key/test_compound_primary_key_2.graphql new file mode 100644 index 0000000..c0fe521 --- /dev/null +++ b/tests/gql/test_key/test_compound_primary_key_2.graphql @@ -0,0 +1,21 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@key"]) + +type Query { + user: User +} + +type User @key(fields: "id organization { registrationNumber }") { + id: ID + organization: Organization +} + +type Organization { + registrationNumber: ID +} + +scalar FieldSet + +scalar Scope + +scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_key/test_compound_primary_key_with_depth_1.graphql b/tests/gql/test_key/test_compound_primary_key_with_depth_1.graphql new file mode 100644 index 0000000..712c0dc --- /dev/null +++ b/tests/gql/test_key/test_compound_primary_key_with_depth_1.graphql @@ -0,0 +1,37 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@key"]) + +type Query { + user: User + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! +} + +type User @key(fields: "id organization { businessUnit { id name } }") { + id: ID + organization: Organization +} + +type Organization { + registrationNumber: ID + businessUnit: BusinessUnit +} + +type BusinessUnit { + id: ID + name: String +} + +union _Entity = User + +scalar _Any + +type _Service { + sdl: String +} + +scalar FieldSet + +scalar Scope + +scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_key/test_compound_primary_key_with_depth_2.graphql b/tests/gql/test_key/test_compound_primary_key_with_depth_2.graphql new file mode 100644 index 0000000..7703429 --- /dev/null +++ b/tests/gql/test_key/test_compound_primary_key_with_depth_2.graphql @@ -0,0 +1,27 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@key"]) + +type Query { + user: User +} + +type User @key(fields: "id organization { businessUnit { id name } }") { + id: ID + organization: Organization +} + +type Organization { + registrationNumber: ID + businessUnit: BusinessUnit +} + +type BusinessUnit { + id: ID + name: String +} + +scalar FieldSet + +scalar Scope + +scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_key/test_multiple_keys_1.graphql b/tests/gql/test_key/test_multiple_keys_1.graphql new file mode 100644 index 0000000..f3bc99c --- /dev/null +++ b/tests/gql/test_key/test_multiple_keys_1.graphql @@ -0,0 +1,27 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@key"]) + +type Query { + user: User + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! +} + +type User @key(fields: "email") @key(fields: "identifier") { + identifier: ID + email: String +} + +union _Entity = User + +scalar _Any + +type _Service { + sdl: String +} + +scalar FieldSet + +scalar Scope + +scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_key/test_multiple_keys_2.graphql b/tests/gql/test_key/test_multiple_keys_2.graphql new file mode 100644 index 0000000..640a505 --- /dev/null +++ b/tests/gql/test_key/test_multiple_keys_2.graphql @@ -0,0 +1,17 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@key"]) + +type Query { + user: User +} + +type User @key(fields: "email") @key(fields: "identifier") { + identifier: ID + email: String +} + +scalar FieldSet + +scalar Scope + +scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_key_v1/test_multiple_keys_1.graphql b/tests/gql/test_key_v1/test_multiple_keys_1.graphql new file mode 100644 index 0000000..d25d8ef --- /dev/null +++ b/tests/gql/test_key_v1/test_multiple_keys_1.graphql @@ -0,0 +1,20 @@ +type Query { + user: User + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! +} + +type User @key(fields: "email") @key(fields: "identifier") { + identifier: ID + email: String +} + +union _Entity = User + +scalar _Any + +type _Service { + sdl: String +} + +scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_key_v1/test_multiple_keys_2.graphql b/tests/gql/test_key_v1/test_multiple_keys_2.graphql new file mode 100644 index 0000000..66f2ef3 --- /dev/null +++ b/tests/gql/test_key_v1/test_multiple_keys_2.graphql @@ -0,0 +1,10 @@ +type Query { + user: User +} + +type User @key(fields: "email") @key(fields: "identifier") { + identifier: ID + email: String +} + +scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_provides/test_provides_1.graphql b/tests/gql/test_provides/test_provides_1.graphql new file mode 100644 index 0000000..4ca98f8 --- /dev/null +++ b/tests/gql/test_provides/test_provides_1.graphql @@ -0,0 +1,33 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@external", "@key", "@provides"]) + +type Query { + inStockCount: InStockCount + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! +} + +type InStockCount { + product: Product! @provides(fields: "name") + quantity: Int! +} + +type Product @key(fields: "sku") { + sku: String! @external + name: String @external + weight: Int @external +} + +union _Entity = Product + +scalar _Any + +type _Service { + sdl: String +} + +scalar FieldSet + +scalar Scope + +scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_provides/test_provides_2.graphql b/tests/gql/test_provides/test_provides_2.graphql new file mode 100644 index 0000000..8d7805b --- /dev/null +++ b/tests/gql/test_provides/test_provides_2.graphql @@ -0,0 +1,23 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@external", "@key", "@provides"]) + +type Query { + inStockCount: InStockCount +} + +type InStockCount { + product: Product! @provides(fields: "name") + quantity: Int! +} + +type Product @key(fields: "sku") { + sku: String! @external + name: String @external + weight: Int @external +} + +scalar FieldSet + +scalar Scope + +scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_provides/test_provides_multiple_fields_1.graphql b/tests/gql/test_provides/test_provides_multiple_fields_1.graphql new file mode 100644 index 0000000..3279f2d --- /dev/null +++ b/tests/gql/test_provides/test_provides_multiple_fields_1.graphql @@ -0,0 +1,33 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@external", "@key", "@provides"]) + +type Query { + inStockCount: InStockCount + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! +} + +type InStockCount { + product: Product! @provides(fields: "name weight") + quantity: Int! +} + +type Product @key(fields: "sku") { + sku: String! @external + name: String @external + weight: Int @external +} + +union _Entity = Product + +scalar _Any + +type _Service { + sdl: String +} + +scalar FieldSet + +scalar Scope + +scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_provides/test_provides_multiple_fields_2.graphql b/tests/gql/test_provides/test_provides_multiple_fields_2.graphql new file mode 100644 index 0000000..78241f2 --- /dev/null +++ b/tests/gql/test_provides/test_provides_multiple_fields_2.graphql @@ -0,0 +1,23 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@external", "@key", "@provides"]) + +type Query { + inStockCount: InStockCount +} + +type InStockCount { + product: Product! @provides(fields: "name weight") + quantity: Int! +} + +type Product @key(fields: "sku") { + sku: String! @external + name: String @external + weight: Int @external +} + +scalar FieldSet + +scalar Scope + +scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_provides/test_provides_multiple_fields_as_list_1.graphql b/tests/gql/test_provides/test_provides_multiple_fields_as_list_1.graphql new file mode 100644 index 0000000..3e91464 --- /dev/null +++ b/tests/gql/test_provides/test_provides_multiple_fields_as_list_1.graphql @@ -0,0 +1,33 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@extends", "@external", "@key", "@provides"]) + +type Query { + inStockCount: InStockCount + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! +} + +type InStockCount { + product: Product! @provides(fields: "name weight") + quantity: Int! +} + +type Product @key(fields: "sku") @extends { + sku: String! @external + name: String @external + weight: Int @external +} + +union _Entity = Product + +scalar _Any + +type _Service { + sdl: String +} + +scalar FieldSet + +scalar Scope + +scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_provides/test_provides_multiple_fields_as_list_2.graphql b/tests/gql/test_provides/test_provides_multiple_fields_as_list_2.graphql new file mode 100644 index 0000000..f5edd9a --- /dev/null +++ b/tests/gql/test_provides/test_provides_multiple_fields_as_list_2.graphql @@ -0,0 +1,23 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@extends", "@external", "@key", "@provides"]) + +type Query { + inStockCount: InStockCount +} + +type InStockCount { + product: Product! @provides(fields: "name weight") + quantity: Int! +} + +type Product @key(fields: "sku") @extends { + sku: String! @external + name: String @external + weight: Int @external +} + +scalar FieldSet + +scalar Scope + +scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_provides_v1/test_provides_1.graphql b/tests/gql/test_provides_v1/test_provides_1.graphql new file mode 100644 index 0000000..97743bb --- /dev/null +++ b/tests/gql/test_provides_v1/test_provides_1.graphql @@ -0,0 +1,26 @@ +type Query { + inStockCount: InStockCount + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! +} + +type InStockCount { + product: Product! @provides(fields: "name") + quantity: Int! +} + +type Product @key(fields: "sku") { + sku: String! @external + name: String @external + weight: Int @external +} + +union _Entity = Product + +scalar _Any + +type _Service { + sdl: String +} + +scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_provides_v1/test_provides_2.graphql b/tests/gql/test_provides_v1/test_provides_2.graphql new file mode 100644 index 0000000..1945c6c --- /dev/null +++ b/tests/gql/test_provides_v1/test_provides_2.graphql @@ -0,0 +1,16 @@ +type Query { + inStockCount: InStockCount +} + +type InStockCount { + product: Product! @provides(fields: "name") + quantity: Int! +} + +type Product @key(fields: "sku") { + sku: String! @external + name: String @external + weight: Int @external +} + +scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_provides_v1/test_provides_multiple_fields_1.graphql b/tests/gql/test_provides_v1/test_provides_multiple_fields_1.graphql new file mode 100644 index 0000000..4c05924 --- /dev/null +++ b/tests/gql/test_provides_v1/test_provides_multiple_fields_1.graphql @@ -0,0 +1,26 @@ +type Query { + inStockCount: InStockCount + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! +} + +type InStockCount { + product: Product! @provides(fields: "name weight") + quantity: Int! +} + +type Product @key(fields: "sku") { + sku: String! @external + name: String @external + weight: Int @external +} + +union _Entity = Product + +scalar _Any + +type _Service { + sdl: String +} + +scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_provides_v1/test_provides_multiple_fields_2.graphql b/tests/gql/test_provides_v1/test_provides_multiple_fields_2.graphql new file mode 100644 index 0000000..c40ea48 --- /dev/null +++ b/tests/gql/test_provides_v1/test_provides_multiple_fields_2.graphql @@ -0,0 +1,16 @@ +type Query { + inStockCount: InStockCount +} + +type InStockCount { + product: Product! @provides(fields: "name weight") + quantity: Int! +} + +type Product @key(fields: "sku") { + sku: String! @external + name: String @external + weight: Int @external +} + +scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_provides_v1/test_provides_multiple_fields_as_list_1.graphql b/tests/gql/test_provides_v1/test_provides_multiple_fields_as_list_1.graphql new file mode 100644 index 0000000..17c52ea --- /dev/null +++ b/tests/gql/test_provides_v1/test_provides_multiple_fields_as_list_1.graphql @@ -0,0 +1,26 @@ +type Query { + inStockCount: InStockCount + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! +} + +type InStockCount { + product: Product! @provides(fields: "name weight") + quantity: Int! +} + +type Product @key(fields: "sku") @extends { + sku: String! @external + name: String @external + weight: Int @external +} + +union _Entity = Product + +scalar _Any + +type _Service { + sdl: String +} + +scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_provides_v1/test_provides_multiple_fields_as_list_2.graphql b/tests/gql/test_provides_v1/test_provides_multiple_fields_as_list_2.graphql new file mode 100644 index 0000000..a83c7c2 --- /dev/null +++ b/tests/gql/test_provides_v1/test_provides_multiple_fields_as_list_2.graphql @@ -0,0 +1,16 @@ +type Query { + inStockCount: InStockCount +} + +type InStockCount { + product: Product! @provides(fields: "name weight") + quantity: Int! +} + +type Product @key(fields: "sku") @extends { + sku: String! @external + name: String @external + weight: Int @external +} + +scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_requires/test_requires_multiple_fields_1.graphql b/tests/gql/test_requires/test_requires_multiple_fields_1.graphql new file mode 100644 index 0000000..7862db1 --- /dev/null +++ b/tests/gql/test_requires/test_requires_multiple_fields_1.graphql @@ -0,0 +1,29 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@extends", "@external", "@key", "@requires"]) + +type Query { + product: Product + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! +} + +type Product @key(fields: "sku") @extends { + sku: ID @external + size: Int @external + weight: Int @external + shippingEstimate: String @requires(fields: "size weight") +} + +union _Entity = Product + +scalar _Any + +type _Service { + sdl: String +} + +scalar FieldSet + +scalar Scope + +scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_requires/test_requires_multiple_fields_2.graphql b/tests/gql/test_requires/test_requires_multiple_fields_2.graphql new file mode 100644 index 0000000..b95534c --- /dev/null +++ b/tests/gql/test_requires/test_requires_multiple_fields_2.graphql @@ -0,0 +1,19 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@extends", "@external", "@key", "@requires"]) + +type Query { + product: Product +} + +type Product @key(fields: "sku") @extends { + sku: ID @external + size: Int @external + weight: Int @external + shippingEstimate: String @requires(fields: "size weight") +} + +scalar FieldSet + +scalar Scope + +scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_requires/test_requires_multiple_fields_as_list_1.graphql b/tests/gql/test_requires/test_requires_multiple_fields_as_list_1.graphql new file mode 100644 index 0000000..7862db1 --- /dev/null +++ b/tests/gql/test_requires/test_requires_multiple_fields_as_list_1.graphql @@ -0,0 +1,29 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@extends", "@external", "@key", "@requires"]) + +type Query { + product: Product + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! +} + +type Product @key(fields: "sku") @extends { + sku: ID @external + size: Int @external + weight: Int @external + shippingEstimate: String @requires(fields: "size weight") +} + +union _Entity = Product + +scalar _Any + +type _Service { + sdl: String +} + +scalar FieldSet + +scalar Scope + +scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_requires/test_requires_multiple_fields_as_list_2.graphql b/tests/gql/test_requires/test_requires_multiple_fields_as_list_2.graphql new file mode 100644 index 0000000..b95534c --- /dev/null +++ b/tests/gql/test_requires/test_requires_multiple_fields_as_list_2.graphql @@ -0,0 +1,19 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@extends", "@external", "@key", "@requires"]) + +type Query { + product: Product +} + +type Product @key(fields: "sku") @extends { + sku: ID @external + size: Int @external + weight: Int @external + shippingEstimate: String @requires(fields: "size weight") +} + +scalar FieldSet + +scalar Scope + +scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_requires/test_requires_with_input_1.graphql b/tests/gql/test_requires/test_requires_with_input_1.graphql new file mode 100644 index 0000000..29e0273 --- /dev/null +++ b/tests/gql/test_requires/test_requires_with_input_1.graphql @@ -0,0 +1,28 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@extends", "@external", "@key", "@requires"]) + +type Query { + acme: Acme + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! +} + +type Acme @key(fields: "id") @extends { + id: ID! @external + age: Int @external + foo(someInput: String ): String @requires(fields: "age") +} + +union _Entity = Acme + +scalar _Any + +type _Service { + sdl: String +} + +scalar FieldSet + +scalar Scope + +scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_requires/test_requires_with_input_2.graphql b/tests/gql/test_requires/test_requires_with_input_2.graphql new file mode 100644 index 0000000..5438141 --- /dev/null +++ b/tests/gql/test_requires/test_requires_with_input_2.graphql @@ -0,0 +1,18 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@extends", "@external", "@key", "@requires"]) + +type Query { + acme: Acme +} + +type Acme @key(fields: "id") @extends { + id: ID! @external + age: Int @external + foo(someInput: String ): String @requires(fields: "age") +} + +scalar FieldSet + +scalar Scope + +scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_requires_v1/test_requires_multiple_fields_1.graphql b/tests/gql/test_requires_v1/test_requires_multiple_fields_1.graphql new file mode 100644 index 0000000..93fdc28 --- /dev/null +++ b/tests/gql/test_requires_v1/test_requires_multiple_fields_1.graphql @@ -0,0 +1,22 @@ +type Query { + product: Product + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! +} + +type Product @key(fields: "sku") @extends { + sku: ID @external + size: Int @external + weight: Int @external + shippingEstimate: String @requires(fields: "size weight") +} + +union _Entity = Product + +scalar _Any + +type _Service { + sdl: String +} + +scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_requires_v1/test_requires_multiple_fields_2.graphql b/tests/gql/test_requires_v1/test_requires_multiple_fields_2.graphql new file mode 100644 index 0000000..aa1ac97 --- /dev/null +++ b/tests/gql/test_requires_v1/test_requires_multiple_fields_2.graphql @@ -0,0 +1,12 @@ +type Query { + product: Product +} + +type Product @key(fields: "sku") @extends { + sku: ID @external + size: Int @external + weight: Int @external + shippingEstimate: String @requires(fields: "size weight") +} + +scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_requires_v1/test_requires_multiple_fields_as_list_1.graphql b/tests/gql/test_requires_v1/test_requires_multiple_fields_as_list_1.graphql new file mode 100644 index 0000000..93fdc28 --- /dev/null +++ b/tests/gql/test_requires_v1/test_requires_multiple_fields_as_list_1.graphql @@ -0,0 +1,22 @@ +type Query { + product: Product + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! +} + +type Product @key(fields: "sku") @extends { + sku: ID @external + size: Int @external + weight: Int @external + shippingEstimate: String @requires(fields: "size weight") +} + +union _Entity = Product + +scalar _Any + +type _Service { + sdl: String +} + +scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_requires_v1/test_requires_multiple_fields_as_list_2.graphql b/tests/gql/test_requires_v1/test_requires_multiple_fields_as_list_2.graphql new file mode 100644 index 0000000..aa1ac97 --- /dev/null +++ b/tests/gql/test_requires_v1/test_requires_multiple_fields_as_list_2.graphql @@ -0,0 +1,12 @@ +type Query { + product: Product +} + +type Product @key(fields: "sku") @extends { + sku: ID @external + size: Int @external + weight: Int @external + shippingEstimate: String @requires(fields: "size weight") +} + +scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_requires_v1/test_requires_with_input_1.graphql b/tests/gql/test_requires_v1/test_requires_with_input_1.graphql new file mode 100644 index 0000000..b99e1a1 --- /dev/null +++ b/tests/gql/test_requires_v1/test_requires_with_input_1.graphql @@ -0,0 +1,21 @@ +type Query { + acme: Acme + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! +} + +type Acme @key(fields: "id") @extends { + id: ID! @external + age: Int @external + foo(someInput: String ): String @requires(fields: "age") +} + +union _Entity = Acme + +scalar _Any + +type _Service { + sdl: String +} + +scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_requires_v1/test_requires_with_input_2.graphql b/tests/gql/test_requires_v1/test_requires_with_input_2.graphql new file mode 100644 index 0000000..dad747a --- /dev/null +++ b/tests/gql/test_requires_v1/test_requires_with_input_2.graphql @@ -0,0 +1,11 @@ +type Query { + acme: Acme +} + +type Acme @key(fields: "id") @extends { + id: ID! @external + age: Int @external + foo(someInput: String ): String @requires(fields: "age") +} + +scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_scalar/test_custom_scalar_1.graphql b/tests/gql/test_scalar/test_custom_scalar_1.graphql new file mode 100644 index 0000000..6207da1 --- /dev/null +++ b/tests/gql/test_scalar/test_custom_scalar_1.graphql @@ -0,0 +1,25 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@inaccessible", "@shareable"]) + +type TestScalar @shareable { + testShareableScalar(x: AddressScalar): String @shareable + testInaccessibleScalar(x: AddressScalar): String @inaccessible +} + +scalar AddressScalar + +type Query { + test(x: AddressScalar): String + test2: [AddressScalar]! + _service: _Service! +} + +type _Service { + sdl: String +} + +scalar FieldSet + +scalar Scope + +scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_scalar/test_custom_scalar_2.graphql b/tests/gql/test_scalar/test_custom_scalar_2.graphql new file mode 100644 index 0000000..85df77e --- /dev/null +++ b/tests/gql/test_scalar/test_custom_scalar_2.graphql @@ -0,0 +1,20 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@inaccessible", "@shareable"]) + +type TestScalar @shareable { + testShareableScalar(x: AddressScalar): String @shareable + testInaccessibleScalar(x: AddressScalar): String @inaccessible +} + +scalar AddressScalar + +type Query { + test(x: AddressScalar): String + test2: [AddressScalar]! +} + +scalar FieldSet + +scalar Scope + +scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_schema_annotation/test_chat_schema_1.graphql b/tests/gql/test_schema_annotation/test_chat_schema_1.graphql new file mode 100644 index 0000000..ae44804 --- /dev/null +++ b/tests/gql/test_schema_annotation/test_chat_schema_1.graphql @@ -0,0 +1,37 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@extends", "@external", "@key"]) + +schema { + query: ChatQuery +} + +type ChatQuery { + message(id: ID!): ChatMessage + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! +} + +type ChatMessage { + id: ID! + text: String + userId: ID + user: ChatUser! +} + +type ChatUser @key(fields: "userId") @extends { + userId: ID! @external +} + +union _Entity = ChatUser + +scalar _Any + +type _Service { + sdl: String +} + +scalar FieldSet + +scalar Scope + +scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_schema_annotation/test_chat_schema_2.graphql b/tests/gql/test_schema_annotation/test_chat_schema_2.graphql new file mode 100644 index 0000000..9524a41 --- /dev/null +++ b/tests/gql/test_schema_annotation/test_chat_schema_2.graphql @@ -0,0 +1,27 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@extends", "@external", "@key"]) + +schema { + query: ChatQuery +} + +type ChatQuery { + message(id: ID!): ChatMessage +} + +type ChatMessage { + id: ID! + text: String + userId: ID + user: ChatUser! +} + +type ChatUser @key(fields: "userId") @extends { + userId: ID! @external +} + +scalar FieldSet + +scalar Scope + +scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_schema_annotation/test_user_schema_1.graphql b/tests/gql/test_schema_annotation/test_user_schema_1.graphql new file mode 100644 index 0000000..c94d20d --- /dev/null +++ b/tests/gql/test_schema_annotation/test_user_schema_1.graphql @@ -0,0 +1,32 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@key"]) + +schema { + query: UserQuery +} + +type UserQuery { + user(userId: ID!): User + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! +} + +type User @key(fields: "email") @key(fields: "userId") { + userId: ID! + email: String! + name: String +} + +union _Entity = User + +scalar _Any + +type _Service { + sdl: String +} + +scalar FieldSet + +scalar Scope + +scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_schema_annotation/test_user_schema_2.graphql b/tests/gql/test_schema_annotation/test_user_schema_2.graphql new file mode 100644 index 0000000..40434d8 --- /dev/null +++ b/tests/gql/test_schema_annotation/test_user_schema_2.graphql @@ -0,0 +1,22 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@key"]) + +schema { + query: UserQuery +} + +type UserQuery { + user(userId: ID!): User +} + +type User @key(fields: "email") @key(fields: "userId") { + userId: ID! + email: String! + name: String +} + +scalar FieldSet + +scalar Scope + +scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_schema_annotation_v1/test_chat_schema_1.graphql b/tests/gql/test_schema_annotation_v1/test_chat_schema_1.graphql new file mode 100644 index 0000000..5b58979 --- /dev/null +++ b/tests/gql/test_schema_annotation_v1/test_chat_schema_1.graphql @@ -0,0 +1,30 @@ +schema { + query: ChatQuery +} + +type ChatQuery { + message(id: ID!): ChatMessage + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! +} + +type ChatMessage { + id: ID! + text: String + userId: ID + user: ChatUser! +} + +type ChatUser @key(fields: "userId") @extends { + userId: ID! @external +} + +union _Entity = ChatUser + +scalar _Any + +type _Service { + sdl: String +} + +scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_schema_annotation_v1/test_chat_schema_2.graphql b/tests/gql/test_schema_annotation_v1/test_chat_schema_2.graphql new file mode 100644 index 0000000..bd4504a --- /dev/null +++ b/tests/gql/test_schema_annotation_v1/test_chat_schema_2.graphql @@ -0,0 +1,20 @@ +schema { + query: ChatQuery +} + +type ChatQuery { + message(id: ID!): ChatMessage +} + +type ChatMessage { + id: ID! + text: String + userId: ID + user: ChatUser! +} + +type ChatUser @key(fields: "userId") @extends { + userId: ID! @external +} + +scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_schema_annotation_v1/test_user_schema_1.graphql b/tests/gql/test_schema_annotation_v1/test_user_schema_1.graphql new file mode 100644 index 0000000..eea42b4 --- /dev/null +++ b/tests/gql/test_schema_annotation_v1/test_user_schema_1.graphql @@ -0,0 +1,25 @@ +schema { + query: UserQuery +} + +type UserQuery { + user(userId: ID!): User + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! +} + +type User @key(fields: "email") @key(fields: "userId") { + userId: ID! + email: String! + name: String +} + +union _Entity = User + +scalar _Any + +type _Service { + sdl: String +} + +scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_schema_annotation_v1/test_user_schema_2.graphql b/tests/gql/test_schema_annotation_v1/test_user_schema_2.graphql new file mode 100644 index 0000000..2b091d4 --- /dev/null +++ b/tests/gql/test_schema_annotation_v1/test_user_schema_2.graphql @@ -0,0 +1,15 @@ +schema { + query: UserQuery +} + +type UserQuery { + user(userId: ID!): User +} + +type User @key(fields: "email") @key(fields: "userId") { + userId: ID! + email: String! + name: String +} + +scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_shareable/test_shareable_1.graphql b/tests/gql/test_shareable/test_shareable_1.graphql new file mode 100644 index 0000000..c709fdc --- /dev/null +++ b/tests/gql/test_shareable/test_shareable_1.graphql @@ -0,0 +1,22 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@shareable"]) + +type Position @shareable { + x: Int! + y: Int! @shareable +} + +type Query { + inStockCount: Int! + _service: _Service! +} + +type _Service { + sdl: String +} + +scalar FieldSet + +scalar Scope + +scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_shareable/test_shareable_2.graphql b/tests/gql/test_shareable/test_shareable_2.graphql new file mode 100644 index 0000000..82759c6 --- /dev/null +++ b/tests/gql/test_shareable/test_shareable_2.graphql @@ -0,0 +1,17 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@shareable"]) + +type Position @shareable { + x: Int! + y: Int! @shareable +} + +type Query { + inStockCount: Int! +} + +scalar FieldSet + +scalar Scope + +scalar federation__Policy \ No newline at end of file diff --git a/tests/test_extends.py b/tests/test_extends.py new file mode 100644 index 0000000..43824c2 --- /dev/null +++ b/tests/test_extends.py @@ -0,0 +1,36 @@ +import pytest +from graphene import ID, ObjectType, String +from graphene_directives import DirectiveValidationError + +from graphene_federation import build_schema, extends, key + + +def test_extend_non_existing_field_failure(): + """ + Test that using the key decorator and providing a field that does not exist fails. + """ + with pytest.raises(ValueError) as err: + + @key("potato") + @extends + class A(ObjectType): + id = ID() + + build_schema(types=(A,)) + + assert str(err.value) == '@key, field "potato" does not exist on type "A"' + + +def test_multiple_extend_failure(): + """ + Test that the extend decorator can't be used more than once on a type. + """ + with pytest.raises(DirectiveValidationError) as err: + + @extends + @extends + class A(ObjectType): + id = ID() + potato = String() + + assert str(err.value) == "@extends is not repeatable, at: A" diff --git a/tests/test_inaccessible.py b/tests/test_inaccessible.py new file mode 100644 index 0000000..2233e25 --- /dev/null +++ b/tests/test_inaccessible.py @@ -0,0 +1,73 @@ +from pathlib import Path + +import graphene +from graphene import ObjectType + +from graphene_federation import build_schema, inaccessible +from tests.util import file_handlers, sdl_query + +save_file, open_file = file_handlers(Path(__file__)) + + +def test_inaccessible_interface(): + @inaccessible + class ReviewInterface(graphene.Interface): + interfaced_body = graphene.String(required=True) + + @inaccessible + class Review(graphene.ObjectType): + class Meta: + interfaces = (ReviewInterface,) + + id = inaccessible(graphene.Int(required=True)) + body = graphene.String(required=True) + + class Query(ObjectType): + in_stock_count = graphene.Int(required=True) + + build_schema(query=Query, enable_federation_2=True, types=(ReviewInterface, Review)) + + +def test_inaccessible(): + @inaccessible + class Position(graphene.ObjectType): + x = graphene.Int(required=True) + y = inaccessible(graphene.Int(required=True)) + + class Query(ObjectType): + in_stock_count = graphene.Int(required=True) + + schema = build_schema(query=Query, enable_federation_2=True, types=(Position,)) + + assert open_file("1") == str(schema) + assert open_file("2") == sdl_query(schema) + + +def test_inaccessible_union(): + @inaccessible + class Human(graphene.ObjectType): + name = graphene.String() + born_in = graphene.String() + + @inaccessible + class Droid(graphene.ObjectType): + name = inaccessible(graphene.String()) + primary_function = graphene.String() + + @inaccessible + class Starship(graphene.ObjectType): + name = graphene.String() + length = inaccessible(graphene.Int()) + + @inaccessible + class SearchResult(graphene.Union): + class Meta: + types = (Human, Droid, Starship) + + class Query(ObjectType): + in_stock_count = graphene.Int(required=True) + + schema = build_schema(query=Query, enable_federation_2=True, types=(SearchResult,)) + + assert open_file("1") == str(schema) + assert open_file("2") == sdl_query(schema) diff --git a/tests/test_key.py b/tests/test_key.py new file mode 100644 index 0000000..2a2f2ea --- /dev/null +++ b/tests/test_key.py @@ -0,0 +1,148 @@ +from pathlib import Path + +import pytest +from graphene import Field, ID, ObjectType, String + +from graphene_federation import build_schema, key +from tests.util import file_handlers, sdl_query + +save_file, open_file = file_handlers(Path(__file__)) + + +def test_multiple_keys(): + @key("identifier") + @key("email") + class User(ObjectType): + identifier = ID() + email = String() + + class Query(ObjectType): + user = Field(User) + + schema = build_schema(query=Query, enable_federation_2=True) + + assert open_file("1") == str(schema) + assert open_file("2") == sdl_query(schema) + + +def test_key_non_existing_field_failure(): + """ + Test that using the key decorator and providing a field that does not exist fails. + """ + with pytest.raises(ValueError) as err: + + @key("potato") + class A(ObjectType): + id = ID() + + build_schema(types=(A,), enable_federation_2=True) + + assert '@key, field "potato" does not exist on type "A"' == str(err.value) + + +def test_compound_primary_key(): + class Organization(ObjectType): + registration_number = ID() + + @key("id organization { registration_number }") + class User(ObjectType): + id = ID() + organization = Field(Organization) + + class Query(ObjectType): + user = Field(User) + + schema = build_schema(query=Query, enable_federation_2=True) + + assert open_file("1") == str(schema) + assert open_file("2") == sdl_query(schema) + + +def test_compound_primary_key_with_depth(): + class BusinessUnit(ObjectType): + id = ID() + name = String() + + class Organization(ObjectType): + registration_number = ID() + business_unit = Field(BusinessUnit) + + @key("id organization { business_unit {id name}}") + class User(ObjectType): + id = ID() + organization = Field(Organization) + + class Query(ObjectType): + user = Field(User) + + schema = build_schema(query=Query, enable_federation_2=True) + assert open_file("1") == str(schema) + assert open_file("2") == sdl_query(schema) + + +def test_invalid_compound_primary_key_failures(): + class BusinessUnit(ObjectType): + id = ID() + name = String() + + class Organization(ObjectType): + registration_number = ID() + bu = Field(BusinessUnit) + + @key("id name organization { registration_number }") + class User(ObjectType): + id = ID() + organization = Field(Organization) + + class Query(ObjectType): + user = Field(User) + + with pytest.raises(ValueError) as err: + # Field name absent on User ObjectType + build_schema(query=Query, enable_federation_2=True) + + assert '@key, field "name" does not exist on type "User"' == str(err.value) + + @key("id organization { name }") + class User(ObjectType): + id = ID() + organization = Field(Organization) + + class Query(ObjectType): + user = Field(User) + + with pytest.raises(ValueError) as err: + # Presence of invalid field in organization field key + build_schema(query=Query, enable_federation_2=True) + + assert '@key, field "name" does not exist on type "Organization"' == str(err.value) + + @key("id organization { bu }") + class User(ObjectType): + id = ID() + organization = Field(Organization) + + class Query(ObjectType): + user = Field(User) + + with pytest.raises(ValueError) as err: + # Presence of BusinessUnit in the key without subselection + build_schema(query=Query, enable_federation_2=True) + + assert '@key, type Organization, field "bu" needs sub selections.' == str(err.value) + + @key("id organization { bu {name { field }} }") + class User(ObjectType): + id = ID() + organization = Field(Organization) + + class Query(ObjectType): + user = Field(User) + + with pytest.raises(ValueError) as err: + # Presence of subselection for the scalar 'name' field + build_schema(query=Query, enable_federation_2=True) + + assert '@key, type BusinessUnit, field "name" cannot have sub selections.' == str( + err.value + ) diff --git a/tests/test_key_v1.py b/tests/test_key_v1.py new file mode 100644 index 0000000..feecac8 --- /dev/null +++ b/tests/test_key_v1.py @@ -0,0 +1,40 @@ +from pathlib import Path + +import pytest +from graphene import Field, ID, ObjectType, String + +from graphene_federation import build_schema, key +from tests.util import file_handlers, sdl_query + +save_file, open_file = file_handlers(Path(__file__)) + + +def test_multiple_keys(): + @key("identifier") + @key("email") + class User(ObjectType): + identifier = ID() + email = String() + + class Query(ObjectType): + user = Field(User) + + schema = build_schema(query=Query) + + assert open_file("1") == str(schema) + assert open_file("2") == sdl_query(schema) + + +def test_key_non_existing_field_failure(): + """ + Test that using the key decorator and providing a field that does not exist fails. + """ + with pytest.raises(ValueError) as err: + + @key("potato") + class A(ObjectType): + id = ID() + + _ = build_schema(types=(A,)) + + assert '@key, field "potato" does not exist on type "A"' == str(err.value) diff --git a/tests/test_provides.py b/tests/test_provides.py new file mode 100644 index 0000000..c88e5ca --- /dev/null +++ b/tests/test_provides.py @@ -0,0 +1,83 @@ +from pathlib import Path + +from graphene import Field, ObjectType, String +from graphene import Int + +from graphene_federation import build_schema, extends, key +from graphene_federation import external, provides +from tests.util import file_handlers, sdl_query + +save_file, open_file = file_handlers(Path(__file__)) + + +def test_provides(): + """ + https://www.apollographql.com/docs/federation/entities/#resolving-another-services-field-advanced + """ + + @key("sku") + class Product(ObjectType): + sku = external(String(required=True)) + name = external(String()) + weight = external(Int()) + + class InStockCount(ObjectType): + product = provides(Field(Product, required=True), fields="name") + quantity = Int(required=True) + + class Query(ObjectType): + in_stock_count = Field(InStockCount) + + schema = build_schema(query=Query, enable_federation_2=True) + + assert open_file("1") == str(schema) + assert open_file("2") == sdl_query(schema) + + +def test_provides_multiple_fields(): + """ + https://www.apollographql.com/docs/federation/entities/#resolving-another-services-field-advanced + """ + + @key("sku") + class Product(ObjectType): + sku = external(String(required=True)) + name = external(String()) + weight = external(Int()) + + class InStockCount(ObjectType): + product = provides(Field(Product, required=True), fields="name weight") + quantity = Int(required=True) + + class Query(ObjectType): + in_stock_count = Field(InStockCount) + + schema = build_schema(query=Query, enable_federation_2=True) + + assert open_file("1") == str(schema) + assert open_file("2") == sdl_query(schema) + + +def test_provides_multiple_fields_as_list(): + """ + https://www.apollographql.com/docs/federation/entities/#resolving-another-services-field-advanced + """ + + @key("sku") + @extends + class Product(ObjectType): + sku = external(String(required=True)) + name = external(String()) + weight = external(Int()) + + class InStockCount(ObjectType): + product = provides(Field(Product, required=True), fields=["name", "weight"]) + quantity = Int(required=True) + + class Query(ObjectType): + in_stock_count = Field(InStockCount) + + schema = build_schema(query=Query, enable_federation_2=True) + + assert open_file("1") == str(schema) + assert open_file("2") == sdl_query(schema) diff --git a/tests/test_provides_v1.py b/tests/test_provides_v1.py new file mode 100644 index 0000000..2013fc2 --- /dev/null +++ b/tests/test_provides_v1.py @@ -0,0 +1,83 @@ +from pathlib import Path + +from graphene import Field, ObjectType, String +from graphene import Int + +from graphene_federation import build_schema, extends, key +from graphene_federation import external, provides +from tests.util import file_handlers, sdl_query + +save_file, open_file = file_handlers(Path(__file__)) + + +def test_provides(): + """ + https://www.apollographql.com/docs/federation/entities/#resolving-another-services-field-advanced + """ + + @key("sku") + class Product(ObjectType): + sku = external(String(required=True)) + name = external(String()) + weight = external(Int()) + + class InStockCount(ObjectType): + product = provides(Field(Product, required=True), fields="name") + quantity = Int(required=True) + + class Query(ObjectType): + in_stock_count = Field(InStockCount) + + schema = build_schema(query=Query) + + assert open_file("1") == str(schema) + assert open_file("2") == sdl_query(schema) + + +def test_provides_multiple_fields(): + """ + https://www.apollographql.com/docs/federation/entities/#resolving-another-services-field-advanced + """ + + @key("sku") + class Product(ObjectType): + sku = external(String(required=True)) + name = external(String()) + weight = external(Int()) + + class InStockCount(ObjectType): + product = provides(Field(Product, required=True), fields="name weight") + quantity = Int(required=True) + + class Query(ObjectType): + in_stock_count = Field(InStockCount) + + schema = build_schema(query=Query) + + assert open_file("1") == str(schema) + assert open_file("2") == sdl_query(schema) + + +def test_provides_multiple_fields_as_list(): + """ + https://www.apollographql.com/docs/federation/entities/#resolving-another-services-field-advanced + """ + + @key("sku") + @extends + class Product(ObjectType): + sku = external(String(required=True)) + name = external(String()) + weight = external(Int()) + + class InStockCount(ObjectType): + product = provides(Field(Product, required=True), fields=["name", "weight"]) + quantity = Int(required=True) + + class Query(ObjectType): + in_stock_count = Field(InStockCount) + + schema = build_schema(query=Query) + + assert open_file("1") == str(schema) + assert open_file("2") == sdl_query(schema) diff --git a/tests/test_requires.py b/tests/test_requires.py new file mode 100644 index 0000000..0ae0553 --- /dev/null +++ b/tests/test_requires.py @@ -0,0 +1,99 @@ +from pathlib import Path + +import pytest +from graphene import Field, ID, ObjectType, String +from graphene import Int +from graphene_directives import DirectiveValidationError + +from graphene_federation import build_schema, key +from graphene_federation import extends, external, requires +from tests.util import file_handlers, sdl_query + +save_file, open_file = file_handlers(Path(__file__)) + + +def test_chain_requires_failure(): + """ + Check that we can't nest call the requires method on a field. + """ + with pytest.raises(DirectiveValidationError) as err: + + class A(ObjectType): + id = external(ID()) + something = requires(requires(String(), fields="id"), fields="id3") + + assert "@requires is not repeatable" in str(err.value) + + +def test_requires_multiple_fields(): + """ + Check that requires can take more than one field as input. + """ + + @key("sku") + @extends + class Product(ObjectType): + sku = external(ID()) + size = external(Int()) + weight = external(Int()) + shipping_estimate = requires(String(), fields="size weight") + + class Query(ObjectType): + product = Field(Product) + + schema = build_schema(query=Query, enable_federation_2=True) + + save_file(str(schema), "1") + save_file(sdl_query(schema), "2") + + assert open_file("1") == str(schema) + assert open_file("2") == sdl_query(schema) + + +def test_requires_multiple_fields_as_list(): + """ + Check that requires can take more than one field as input. + """ + + @key("sku") + @extends + class Product(ObjectType): + sku = external(ID()) + size = external(Int()) + weight = external(Int()) + shipping_estimate = requires(String(), fields=["size", "weight"]) + + class Query(ObjectType): + product = Field(Product) + + schema = build_schema(query=Query, enable_federation_2=True) + + save_file(str(schema), "1") + save_file(sdl_query(schema), "2") + + assert open_file("1") == str(schema) + assert open_file("2") == sdl_query(schema) + + +def test_requires_with_input(): + """ + Test checking that the issue https://github.com/preply/graphene-federation/pull/47 is resolved. + """ + + @key("id") + @extends + class Acme(ObjectType): + id = external(ID(required=True)) + age = external(Int()) + foo = requires(Field(String, someInput=String()), fields="age") + + class Query(ObjectType): + acme = Field(Acme) + + schema = build_schema(query=Query, enable_federation_2=True) + + save_file(str(schema), "1") + save_file(sdl_query(schema), "2") + + assert open_file("1") == str(schema) + assert open_file("2") == sdl_query(schema) diff --git a/tests/test_requires_v1.py b/tests/test_requires_v1.py new file mode 100644 index 0000000..7570ed4 --- /dev/null +++ b/tests/test_requires_v1.py @@ -0,0 +1,90 @@ +from pathlib import Path + +import pytest +from graphene import Field, ID, ObjectType, String +from graphene import Int +from graphene_directives import DirectiveValidationError + +from graphene_federation import build_schema, key +from graphene_federation import extends, external, requires +from tests.util import file_handlers, sdl_query + +save_file, open_file = file_handlers(Path(__file__)) + + +def test_chain_requires_failure(): + """ + Check that we can't nest call the requires method on a field. + """ + with pytest.raises(DirectiveValidationError) as err: + + class A(ObjectType): + id = external(ID()) + something = requires(requires(String(), fields="id"), fields="id3") + + assert "@requires is not repeatable" in str(err.value) + + +def test_requires_multiple_fields(): + """ + Check that requires can take more than one field as input. + """ + + @key("sku") + @extends + class Product(ObjectType): + sku = external(ID()) + size = external(Int()) + weight = external(Int()) + shipping_estimate = requires(String(), fields="size weight") + + class Query(ObjectType): + product = Field(Product) + + schema = build_schema(query=Query) + + assert open_file("1") == str(schema) + assert open_file("2") == sdl_query(schema) + + +def test_requires_multiple_fields_as_list(): + """ + Check that requires can take more than one field as input. + """ + + @key("sku") + @extends + class Product(ObjectType): + sku = external(ID()) + size = external(Int()) + weight = external(Int()) + shipping_estimate = requires(String(), fields=["size", "weight"]) + + class Query(ObjectType): + product = Field(Product) + + schema = build_schema(query=Query) + + assert open_file("1") == str(schema) + assert open_file("2") == sdl_query(schema) + + +def test_requires_with_input(): + """ + Test checking that the issue https://github.com/preply/graphene-federation/pull/47 is resolved. + """ + + @key("id") + @extends + class Acme(ObjectType): + id = external(ID(required=True)) + age = external(Int()) + foo = requires(Field(String, someInput=String()), fields="age") + + class Query(ObjectType): + acme = Field(Acme) + + schema = build_schema(query=Query) + + assert open_file("1") == str(schema) + assert open_file("2") == sdl_query(schema) diff --git a/tests/test_scalar.py b/tests/test_scalar.py new file mode 100644 index 0000000..d7d9b9d --- /dev/null +++ b/tests/test_scalar.py @@ -0,0 +1,42 @@ +from pathlib import Path +from typing import Any + +import graphene +from graphene import ObjectType, String +from graphene import Scalar + +from graphene_federation import build_schema +from graphene_federation import inaccessible, shareable +from tests.util import file_handlers, sdl_query + +save_file, open_file = file_handlers(Path(__file__)) + + +def test_custom_scalar(): + class AddressScalar(Scalar): + base = String + + @staticmethod + def coerce_address(value: Any): + ... + + serialize = coerce_address + parse_value = coerce_address + + @staticmethod + def parse_literal(ast): + ... + + @shareable + class TestScalar(graphene.ObjectType): + test_shareable_scalar = shareable(String(x=AddressScalar())) + test_inaccessible_scalar = inaccessible(String(x=AddressScalar())) + + class Query(ObjectType): + test = String(x=AddressScalar()) + test2 = graphene.List(AddressScalar, required=True) + + schema = build_schema(query=Query, enable_federation_2=True, types=(TestScalar,)) + + assert open_file("1") == str(schema) + assert open_file("2") == sdl_query(schema) diff --git a/graphene_federation/tests/test_schema_annotation.py b/tests/test_schema_annotation.py similarity index 51% rename from graphene_federation/tests/test_schema_annotation.py rename to tests/test_schema_annotation.py index 7de4fe4..e2db7e1 100644 --- a/graphene_federation/tests/test_schema_annotation.py +++ b/tests/test_schema_annotation.py @@ -1,14 +1,14 @@ -from textwrap import dedent +from pathlib import Path +from graphene import Field, ID, ObjectType, String +from graphene import NonNull from graphql import graphql_sync -from graphene import ObjectType, ID, String, NonNull, Field +from graphene_federation import build_schema, key +from graphene_federation import extends, external +from tests.util import file_handlers, sdl_query -from graphene_federation import external -from graphene_federation.entity import key -from graphene_federation.extend import extend -from graphene_federation.main import build_schema -from graphene_federation.utils import clean_schema +save_file, open_file = file_handlers(Path(__file__)) # ------------------------ # User service @@ -57,7 +57,8 @@ def resolve_user(self, info, user_id, *args, **kwargs): ] -@extend("user_id") +@extends +@key("user_id") class ChatUser(ObjectType): user_id = external(ID(required=True)) @@ -92,34 +93,9 @@ def test_user_schema(): Check that the user schema has been annotated correctly and that a request to retrieve a user works. """ - expected_result = dedent( - """ - schema { - query: UserQuery - } - - type UserQuery { - user(userId: ID!): User - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - } - - type User { - userId: ID! - email: String! - name: String - } - - union _Entity = User - - scalar _Any - - type _Service { - sdl: String - } - """ - ) - assert clean_schema(user_schema) == clean_schema(expected_result) + + assert open_file("1") == str(user_schema) + assert open_file("2") == sdl_query(user_schema) query = """ query { @@ -131,32 +107,6 @@ def test_user_schema(): result = graphql_sync(user_schema.graphql_schema, query) assert not result.errors assert result.data == {"user": {"name": "Jack"}} - # Check the federation service schema definition language - query = """ - query { - _service { - sdl - } - } - """ - result = graphql_sync(user_schema.graphql_schema, query) - assert not result.errors - expected_result = dedent( - """ - extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"]) - - type UserQuery { - user(userId: ID!): User - } - - type User @key(fields: "email") @key(fields: "userId") { - userId: ID! - email: String! - name: String - } - """ - ) - assert clean_schema(result.data["_service"]["sdl"]) == clean_schema(expected_result) def test_chat_schema(): @@ -164,39 +114,9 @@ def test_chat_schema(): Check that the chat schema has been annotated correctly and that a request to retrieve a chat message works. """ - expected_result = dedent( - """ - schema { - query: ChatQuery - } - - type ChatQuery { - message(id: ID!): ChatMessage - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - } - - type ChatMessage { - id: ID! - text: String - userId: ID - user: ChatUser! - } - - type ChatUser { - userId: ID! - } - - union _Entity = ChatUser - - scalar _Any - - type _Service { - sdl: String - } - """ - ) - assert clean_schema(chat_schema) == clean_schema(expected_result) + + assert open_file("1") == str(chat_schema) + assert open_file("2") == sdl_query(chat_schema) # Query the message field query = """ @@ -210,34 +130,3 @@ def test_chat_schema(): result = graphql_sync(chat_schema.graphql_schema, query) assert not result.errors assert result.data == {"message": {"text": "Don't be rude Jack", "userId": "3"}} - - # Check the federation service schema definition language - query = """ - query { - _service { - sdl - } - } - """ - result = graphql_sync(chat_schema.graphql_schema, query) - assert not result.errors - expected_result = dedent( - """ - extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@extends", "@external", "@key"]) - type ChatQuery { - message(id: ID!): ChatMessage - } - - type ChatMessage { - id: ID! - text: String - userId: ID - user: ChatUser! - } - - extend type ChatUser @key(fields: "userId") { - userId: ID! @external - } - """ - ) - assert clean_schema(result.data["_service"]["sdl"]) == clean_schema(expected_result) diff --git a/graphene_federation/tests/test_schema_annotation_v1.py b/tests/test_schema_annotation_v1.py similarity index 52% rename from graphene_federation/tests/test_schema_annotation_v1.py rename to tests/test_schema_annotation_v1.py index 3ef2602..b87fe83 100644 --- a/graphene_federation/tests/test_schema_annotation_v1.py +++ b/tests/test_schema_annotation_v1.py @@ -1,14 +1,14 @@ -from textwrap import dedent +from pathlib import Path +from graphene import Field, ID, ObjectType, String +from graphene import NonNull from graphql import graphql_sync -from graphene import ObjectType, ID, String, NonNull, Field +from graphene_federation import build_schema, key +from graphene_federation import extends, external +from tests.util import file_handlers, sdl_query -from graphene_federation.entity import key -from graphene_federation.extend import extend -from graphene_federation.external import external -from graphene_federation.main import build_schema -from graphene_federation.utils import clean_schema +save_file, open_file = file_handlers(Path(__file__)) # ------------------------ # User service @@ -57,7 +57,8 @@ def resolve_user(self, info, user_id, *args, **kwargs): ] -@extend("user_id") +@key("user_id") +@extends class ChatUser(ObjectType): user_id = external(ID(required=True)) @@ -91,34 +92,9 @@ def test_user_schema(): Check that the user schema has been annotated correctly and that a request to retrieve a user works. """ - expected_result = dedent( - """ - schema { - query: UserQuery - } - - type UserQuery { - user(userId: ID!): User - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - } - - type User { - userId: ID! - email: String! - name: String - } - - union _Entity = User - - scalar _Any - - type _Service { - sdl: String - } - """ - ) - assert clean_schema(user_schema) == clean_schema(expected_result) + assert open_file("1") == str(user_schema) + assert open_file("2") == sdl_query(user_schema) + query = """ query { user(userId: "2") { @@ -126,33 +102,10 @@ def test_user_schema(): } } """ + result = graphql_sync(user_schema.graphql_schema, query) assert not result.errors assert result.data == {"user": {"name": "Jack"}} - # Check the federation service schema definition language - query = """ - query { - _service { - sdl - } - } - """ - result = graphql_sync(user_schema.graphql_schema, query) - assert not result.errors - expected_result = dedent( - """ - type UserQuery { - user(userId: ID!): User - } - - type User @key(fields: "email") @key(fields: "userId") { - userId: ID! - email: String! - name: String - } - """ - ) - assert clean_schema(result.data["_service"]["sdl"]) == clean_schema(expected_result) def test_chat_schema(): @@ -160,39 +113,8 @@ def test_chat_schema(): Check that the chat schema has been annotated correctly and that a request to retrieve a chat message works. """ - expected_result = dedent( - """ - schema { - query: ChatQuery - } - - type ChatQuery { - message(id: ID!): ChatMessage - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - } - - type ChatMessage { - id: ID! - text: String - userId: ID - user: ChatUser! - } - - type ChatUser { - userId: ID! - } - - union _Entity = ChatUser - - scalar _Any - - type _Service { - sdl: String - } - """ - ) - assert clean_schema(chat_schema) == clean_schema(expected_result) + assert open_file("1") == str(chat_schema) + assert open_file("2") == sdl_query(chat_schema) # Query the message field query = """ @@ -206,33 +128,3 @@ def test_chat_schema(): result = graphql_sync(chat_schema.graphql_schema, query) assert not result.errors assert result.data == {"message": {"text": "Don't be rude Jack", "userId": "3"}} - - # Check the federation service schema definition language - query = """ - query { - _service { - sdl - } - } - """ - result = graphql_sync(chat_schema.graphql_schema, query) - assert not result.errors - expected_result = dedent( - """ - type ChatQuery { - message(id: ID!): ChatMessage - } - - type ChatMessage { - id: ID! - text: String - userId: ID - user: ChatUser! - } - - extend type ChatUser @key(fields: "userId") { - userId: ID! @external - } - """ - ) - assert clean_schema(result.data["_service"]["sdl"]) == clean_schema(expected_result) diff --git a/tests/test_shareable.py b/tests/test_shareable.py new file mode 100644 index 0000000..33eb807 --- /dev/null +++ b/tests/test_shareable.py @@ -0,0 +1,83 @@ +from pathlib import Path + +import graphene +import pytest +from graphene import ObjectType +from graphene_directives import DirectiveValidationError + +from graphene_federation import build_schema +from graphene_federation import shareable +from tests.util import file_handlers, sdl_query + +save_file, open_file = file_handlers(Path(__file__)) + + +def test_shareable_interface_failures(): + with pytest.raises(DirectiveValidationError) as err: + + @shareable + class ReviewInterface(graphene.Interface): + interfaced_body = graphene.String(required=True) + + @shareable + class Review(graphene.ObjectType): + class Meta: + interfaces = (ReviewInterface,) + + id = shareable(graphene.Int(required=True)) + body = graphene.String(required=True) + + class Query(ObjectType): + in_stock_count = graphene.Int(required=True) + + build_schema( + query=Query, enable_federation_2=True, types=(ReviewInterface, Review) + ) + + assert "@shareable cannot be used for ReviewInterface" in str(err.value) + + +def test_shareable(): + @shareable + class Position(graphene.ObjectType): + x = graphene.Int(required=True) + y = shareable(graphene.Int(required=True)) + + class Query(ObjectType): + in_stock_count = graphene.Int(required=True) + + schema = build_schema(query=Query, enable_federation_2=True, types=(Position,)) + + assert open_file("1") == str(schema) + assert open_file("2") == sdl_query(schema) + + +def test_shareable_union(): + with pytest.raises(DirectiveValidationError) as err: + + @shareable + class Human(graphene.ObjectType): + name = graphene.String() + born_in = graphene.String() + + @shareable + class Droid(graphene.ObjectType): + name = shareable(graphene.String()) + primary_function = graphene.String() + + @shareable + class Starship(graphene.ObjectType): + name = graphene.String() + length = shareable(graphene.Int()) + + @shareable + class SearchResult(graphene.Union): + class Meta: + types = (Human, Droid, Starship) + + class Query(ObjectType): + in_stock_count = graphene.Int(required=True) + + _ = build_schema(query=Query, enable_federation_2=True, types=(SearchResult,)) + + assert "@shareable cannot be used for SearchResult" in str(err.value) From feb2a592090514923bf198603d3aab552e6ba176 Mon Sep 17 00:00:00 2001 From: M Aswin Kishore <60577077+mak626@users.noreply.github.com> Date: Tue, 16 Jan 2024 00:23:57 +0530 Subject: [PATCH 04/19] refact: is_non_field check --- graphene_federation/directives/utils.py | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/graphene_federation/directives/utils.py b/graphene_federation/directives/utils.py index 5ccf8e4..8ca1acc 100644 --- a/graphene_federation/directives/utils.py +++ b/graphene_federation/directives/utils.py @@ -1,20 +1,6 @@ +import inspect from typing import Any -from graphene import Enum, InputObjectType, Interface, ObjectType, Scalar, Union - def is_non_field(field: Any): - try: - if ( - issubclass(field, ObjectType) - or issubclass(field, Interface) - or issubclass(field, InputObjectType) - or issubclass(field, Enum) - or issubclass(field, Union) - or issubclass(field, Scalar) - ): - return True - else: - return False - except TypeError: - return False + return inspect.isclass(field) From 53f71eb116e9f5ea2ae69733c714d7e29ada7f4d Mon Sep 17 00:00:00 2001 From: M Aswin Kishore <60577077+mak626@users.noreply.github.com> Date: Tue, 16 Jan 2024 09:36:10 +0530 Subject: [PATCH 05/19] refact: relative imports to absolute imports --- graphene_federation/apollo_versions/v1_0.py | 6 +++--- graphene_federation/apollo_versions/v2_0.py | 6 +++--- graphene_federation/apollo_versions/v2_5.py | 2 +- graphene_federation/apollo_versions/v2_6.py | 2 +- graphene_federation/directives/authenticated.py | 4 ++-- graphene_federation/directives/extends.py | 6 +++++- graphene_federation/directives/external.py | 6 +++++- graphene_federation/directives/inaccessible.py | 6 +++++- graphene_federation/directives/interface_object.py | 6 +++++- graphene_federation/directives/key.py | 8 ++++++-- graphene_federation/directives/override.py | 6 +++++- graphene_federation/directives/policy.py | 6 +++++- graphene_federation/directives/provides.py | 8 ++++++-- graphene_federation/directives/requires.py | 8 ++++++-- graphene_federation/directives/requires_scopes.py | 6 +++++- graphene_federation/directives/shareable.py | 6 +++++- graphene_federation/directives/tag.py | 6 +++++- .../schema_directives/compose_directive.py | 6 +++++- graphene_federation/schema_directives/link_directive.py | 2 +- graphene_federation/transform/field_set_case_transform.py | 2 +- 20 files changed, 80 insertions(+), 28 deletions(-) diff --git a/graphene_federation/apollo_versions/v1_0.py b/graphene_federation/apollo_versions/v1_0.py index 9d974f8..53e01ca 100644 --- a/graphene_federation/apollo_versions/v1_0.py +++ b/graphene_federation/apollo_versions/v1_0.py @@ -1,9 +1,9 @@ from graphene_directives import CustomDirective, DirectiveLocation from graphql import GraphQLArgument, GraphQLDirective, GraphQLNonNull -from ..scalars import _FieldSet -from ..transform import field_set_case_transform -from ..validators import validate_key, validate_requires +from graphene_federation.scalars import _FieldSet +from graphene_federation.transform import field_set_case_transform +from graphene_federation.validators import validate_key, validate_requires key_directive = CustomDirective( name="key", diff --git a/graphene_federation/apollo_versions/v2_0.py b/graphene_federation/apollo_versions/v2_0.py index 3609b7c..01a1e3b 100644 --- a/graphene_federation/apollo_versions/v2_0.py +++ b/graphene_federation/apollo_versions/v2_0.py @@ -8,9 +8,9 @@ ) from .v1_0 import extends_directive -from ..scalars import FieldSet -from ..transform import field_set_case_transform -from ..validators import validate_key, validate_requires +from graphene_federation.scalars import FieldSet +from graphene_federation.transform import field_set_case_transform +from graphene_federation.validators import validate_key, validate_requires key_directive = CustomDirective( name="key", diff --git a/graphene_federation/apollo_versions/v2_5.py b/graphene_federation/apollo_versions/v2_5.py index efe9d8e..d569284 100644 --- a/graphene_federation/apollo_versions/v2_5.py +++ b/graphene_federation/apollo_versions/v2_5.py @@ -2,7 +2,7 @@ from graphql import GraphQLArgument, GraphQLDirective, GraphQLList, GraphQLNonNull from .v2_4 import get_directives as get_directives_v2_4 -from ..scalars import Scope +from graphene_federation.scalars import Scope authenticated_directive = CustomDirective( name="authenticated", diff --git a/graphene_federation/apollo_versions/v2_6.py b/graphene_federation/apollo_versions/v2_6.py index 3375292..bf62d36 100644 --- a/graphene_federation/apollo_versions/v2_6.py +++ b/graphene_federation/apollo_versions/v2_6.py @@ -2,7 +2,7 @@ from graphql import GraphQLArgument, GraphQLDirective, GraphQLList, GraphQLNonNull from .v2_5 import get_directives as get_directives_v2_5 -from ..scalars import FederationPolicy +from graphene_federation.scalars import FederationPolicy policy_directive = CustomDirective( name="policy", diff --git a/graphene_federation/directives/authenticated.py b/graphene_federation/directives/authenticated.py index 1d828f4..a2a2748 100644 --- a/graphene_federation/directives/authenticated.py +++ b/graphene_federation/directives/authenticated.py @@ -2,12 +2,12 @@ from graphene_directives import directive_decorator -from .utils import is_non_field -from ..apollo_versions import ( +from graphene_federation.apollo_versions import ( FederationVersion, LATEST_VERSION, get_directive_from_name, ) +from .utils import is_non_field def authenticated( diff --git a/graphene_federation/directives/extends.py b/graphene_federation/directives/extends.py index 4e02645..b103df4 100644 --- a/graphene_federation/directives/extends.py +++ b/graphene_federation/directives/extends.py @@ -3,7 +3,11 @@ from graphene_directives import directive_decorator from .utils import is_non_field -from ..apollo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name +from graphene_federation.apollo_versions import ( + FederationVersion, + LATEST_VERSION, + get_directive_from_name, +) def extends( diff --git a/graphene_federation/directives/external.py b/graphene_federation/directives/external.py index 3e182d0..6811e88 100644 --- a/graphene_federation/directives/external.py +++ b/graphene_federation/directives/external.py @@ -3,7 +3,11 @@ from graphene_directives import directive_decorator from .utils import is_non_field -from ..apollo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name +from graphene_federation.apollo_versions import ( + FederationVersion, + LATEST_VERSION, + get_directive_from_name, +) def external( diff --git a/graphene_federation/directives/inaccessible.py b/graphene_federation/directives/inaccessible.py index eb80126..15e20de 100644 --- a/graphene_federation/directives/inaccessible.py +++ b/graphene_federation/directives/inaccessible.py @@ -3,7 +3,11 @@ from graphene_directives import directive_decorator from .utils import is_non_field -from ..apollo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name +from graphene_federation.apollo_versions import ( + FederationVersion, + LATEST_VERSION, + get_directive_from_name, +) def inaccessible( diff --git a/graphene_federation/directives/interface_object.py b/graphene_federation/directives/interface_object.py index 76eecd0..59082e3 100644 --- a/graphene_federation/directives/interface_object.py +++ b/graphene_federation/directives/interface_object.py @@ -2,8 +2,12 @@ from graphene_directives import directive_decorator +from graphene_federation.apollo_versions import ( + FederationVersion, + LATEST_VERSION, + get_directive_from_name, +) from .utils import is_non_field -from ..apollo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name def interface_object( diff --git a/graphene_federation/directives/key.py b/graphene_federation/directives/key.py index 89297fe..a7623a0 100644 --- a/graphene_federation/directives/key.py +++ b/graphene_federation/directives/key.py @@ -3,8 +3,12 @@ from graphene_directives import directive_decorator from .utils import is_non_field -from ..apollo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name -from ..validators import ast_to_str, build_ast +from graphene_federation.apollo_versions import ( + FederationVersion, + LATEST_VERSION, + get_directive_from_name, +) +from graphene_federation.validators import ast_to_str, build_ast def key( diff --git a/graphene_federation/directives/override.py b/graphene_federation/directives/override.py index aaa13e8..f12b11a 100644 --- a/graphene_federation/directives/override.py +++ b/graphene_federation/directives/override.py @@ -3,7 +3,11 @@ from graphene_directives import directive_decorator from .utils import is_non_field -from ..apollo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name +from graphene_federation.apollo_versions import ( + FederationVersion, + LATEST_VERSION, + get_directive_from_name, +) def override( diff --git a/graphene_federation/directives/policy.py b/graphene_federation/directives/policy.py index b7fef8b..1d78622 100644 --- a/graphene_federation/directives/policy.py +++ b/graphene_federation/directives/policy.py @@ -3,7 +3,11 @@ from graphene_directives import directive_decorator from .utils import is_non_field -from ..apollo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name +from graphene_federation.apollo_versions import ( + FederationVersion, + LATEST_VERSION, + get_directive_from_name, +) def policy( diff --git a/graphene_federation/directives/provides.py b/graphene_federation/directives/provides.py index 85dee57..6ac3af6 100644 --- a/graphene_federation/directives/provides.py +++ b/graphene_federation/directives/provides.py @@ -4,8 +4,12 @@ from graphene_directives import directive_decorator from .utils import is_non_field -from ..apollo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name -from ..validators import ast_to_str, build_ast +from graphene_federation.apollo_versions import ( + FederationVersion, + LATEST_VERSION, + get_directive_from_name, +) +from graphene_federation.validators import ast_to_str, build_ast def provides( diff --git a/graphene_federation/directives/requires.py b/graphene_federation/directives/requires.py index f865a13..971fa68 100644 --- a/graphene_federation/directives/requires.py +++ b/graphene_federation/directives/requires.py @@ -4,8 +4,12 @@ from graphene_directives import directive_decorator from .utils import is_non_field -from ..apollo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name -from ..validators import ast_to_str, build_ast +from graphene_federation.apollo_versions import ( + FederationVersion, + LATEST_VERSION, + get_directive_from_name, +) +from graphene_federation.validators import ast_to_str, build_ast def requires( diff --git a/graphene_federation/directives/requires_scopes.py b/graphene_federation/directives/requires_scopes.py index 572d2e9..2a7176d 100644 --- a/graphene_federation/directives/requires_scopes.py +++ b/graphene_federation/directives/requires_scopes.py @@ -3,7 +3,11 @@ from graphene_directives import directive_decorator from .utils import is_non_field -from ..apollo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name +from graphene_federation.apollo_versions import ( + FederationVersion, + LATEST_VERSION, + get_directive_from_name, +) def requires_scope( diff --git a/graphene_federation/directives/shareable.py b/graphene_federation/directives/shareable.py index 51a6ce2..34a2995 100644 --- a/graphene_federation/directives/shareable.py +++ b/graphene_federation/directives/shareable.py @@ -3,7 +3,11 @@ from graphene_directives import directive_decorator from .utils import is_non_field -from ..apollo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name +from graphene_federation.apollo_versions import ( + FederationVersion, + LATEST_VERSION, + get_directive_from_name, +) def shareable( diff --git a/graphene_federation/directives/tag.py b/graphene_federation/directives/tag.py index 11d33ca..d3182cd 100644 --- a/graphene_federation/directives/tag.py +++ b/graphene_federation/directives/tag.py @@ -3,7 +3,11 @@ from graphene_directives import directive_decorator from .utils import is_non_field -from ..apollo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name +from graphene_federation.apollo_versions import ( + FederationVersion, + LATEST_VERSION, + get_directive_from_name, +) def tag( diff --git a/graphene_federation/schema_directives/compose_directive.py b/graphene_federation/schema_directives/compose_directive.py index 9a87d16..848c2b6 100644 --- a/graphene_federation/schema_directives/compose_directive.py +++ b/graphene_federation/schema_directives/compose_directive.py @@ -1,6 +1,10 @@ from graphene_directives import SchemaDirective -from ..apollo_versions import FederationVersion, LATEST_VERSION, get_directive_from_name +from graphene_federation.apollo_versions import ( + FederationVersion, + LATEST_VERSION, + get_directive_from_name, +) def compose_directive( diff --git a/graphene_federation/schema_directives/link_directive.py b/graphene_federation/schema_directives/link_directive.py index fb0d2e8..006ed3e 100644 --- a/graphene_federation/schema_directives/link_directive.py +++ b/graphene_federation/schema_directives/link_directive.py @@ -8,7 +8,7 @@ GraphQLString, ) -from ..scalars import link_import, link_purpose +from graphene_federation.scalars import link_import, link_purpose _link_directive = CustomDirective( name="link", diff --git a/graphene_federation/transform/field_set_case_transform.py b/graphene_federation/transform/field_set_case_transform.py index 9232f43..9a6793b 100644 --- a/graphene_federation/transform/field_set_case_transform.py +++ b/graphene_federation/transform/field_set_case_transform.py @@ -1,6 +1,6 @@ from graphene_directives import Schema -from ..validators import InternalNamespace, to_case +from graphene_federation.validators import InternalNamespace, to_case def field_set_case_transform(inputs: dict, schema: Schema) -> dict: From 895390911b1c3b7cd5bc1025da9f8ee50dd90026 Mon Sep 17 00:00:00 2001 From: M Aswin Kishore <60577077+mak626@users.noreply.github.com> Date: Tue, 16 Jan 2024 11:12:16 +0530 Subject: [PATCH 06/19] add: validators for @provides --- .../apollo_versions/__init__.py | 8 + graphene_federation/apollo_versions/v1_0.py | 7 +- graphene_federation/apollo_versions/v2_0.py | 23 ++- graphene_federation/apollo_versions/v2_2.py | 2 +- .../directives/authenticated.py | 6 + graphene_federation/directives/extends.py | 7 +- graphene_federation/directives/external.py | 11 +- .../directives/inaccessible.py | 10 +- .../directives/interface_object.py | 9 + graphene_federation/directives/key.py | 13 +- graphene_federation/directives/override.py | 8 +- graphene_federation/directives/policy.py | 8 +- graphene_federation/directives/provides.py | 14 +- graphene_federation/directives/requires.py | 14 +- .../directives/requires_scopes.py | 8 +- graphene_federation/directives/shareable.py | 11 +- graphene_federation/directives/tag.py | 14 +- graphene_federation/directives/utils.py | 5 +- graphene_federation/scalars/_any.py | 1 + .../scalars/federation_policy.py | 1 + graphene_federation/scalars/field_set_v1.py | 1 + graphene_federation/scalars/field_set_v2.py | 1 + graphene_federation/scalars/scope.py | 1 + .../schema_directives/compose_directive.py | 13 ++ .../schema_directives/link_directive.py | 13 ++ .../transform/field_set_case_transform.py | 7 + graphene_federation/validators/__init__.py | 1 + graphene_federation/validators/key.py | 15 +- graphene_federation/validators/provides.py | 51 ++++++ graphene_federation/validators/requires.py | 13 +- graphene_federation/validators/utils.py | 160 ++++++++++++------ 31 files changed, 368 insertions(+), 88 deletions(-) create mode 100644 graphene_federation/validators/provides.py diff --git a/graphene_federation/apollo_versions/__init__.py b/graphene_federation/apollo_versions/__init__.py index 23d2c80..9c08ccd 100644 --- a/graphene_federation/apollo_versions/__init__.py +++ b/graphene_federation/apollo_versions/__init__.py @@ -16,6 +16,11 @@ def get_directives_based_on_version( federation_version: FederationVersion, ) -> dict[str, GraphQLDirective]: + """ + Returns a dictionary of [directive_name, directive] for the specified federation version + + If no match is found for the specified federation version, latest is taken + """ if federation_version == FederationVersion.VERSION_1_0: return get_directives_v1_0() if federation_version == FederationVersion.VERSION_2_0: @@ -39,6 +44,9 @@ def get_directives_based_on_version( def get_directive_from_name( directive_name: str, federation_version: FederationVersion ) -> GraphQLDirective: + """ + Get the GraphQL directive for the specified name with the given federation version + """ directive = get_directives_based_on_version(federation_version).get( directive_name, None ) diff --git a/graphene_federation/apollo_versions/v1_0.py b/graphene_federation/apollo_versions/v1_0.py index 53e01ca..672924e 100644 --- a/graphene_federation/apollo_versions/v1_0.py +++ b/graphene_federation/apollo_versions/v1_0.py @@ -3,7 +3,11 @@ from graphene_federation.scalars import _FieldSet from graphene_federation.transform import field_set_case_transform -from graphene_federation.validators import validate_key, validate_requires +from graphene_federation.validators import ( + validate_key, + validate_provides, + validate_requires, +) key_directive = CustomDirective( name="key", @@ -40,6 +44,7 @@ args={"fields": GraphQLArgument(GraphQLNonNull(_FieldSet))}, description="Federation @provides directive", add_definition_to_schema=False, + field_validator=validate_provides, input_transform=field_set_case_transform, ) diff --git a/graphene_federation/apollo_versions/v2_0.py b/graphene_federation/apollo_versions/v2_0.py index 01a1e3b..827d953 100644 --- a/graphene_federation/apollo_versions/v2_0.py +++ b/graphene_federation/apollo_versions/v2_0.py @@ -7,10 +7,14 @@ GraphQLString, ) -from .v1_0 import extends_directive from graphene_federation.scalars import FieldSet from graphene_federation.transform import field_set_case_transform -from graphene_federation.validators import validate_key, validate_requires +from graphene_federation.validators import ( + validate_key, + validate_provides, + validate_requires, +) +from .v1_0 import extends_directive key_directive = CustomDirective( name="key", @@ -20,7 +24,7 @@ ], args={ "fields": GraphQLArgument(GraphQLNonNull(FieldSet)), - # Changed + # Changed from v1.0 "resolvable": GraphQLArgument(GraphQLBoolean, default_value=True), }, description="Federation @key directive", @@ -35,7 +39,9 @@ locations=[ DirectiveLocation.FIELD_DEFINITION, ], - args={"fields": GraphQLArgument(GraphQLNonNull(FieldSet))}, + args={ + "fields": GraphQLArgument(GraphQLNonNull(FieldSet)) + }, # Changed _FieldSet -> FieldSet description="Federation @requires directive", add_definition_to_schema=False, field_validator=validate_requires, @@ -48,9 +54,12 @@ locations=[ DirectiveLocation.FIELD_DEFINITION, ], - args={"fields": GraphQLArgument(GraphQLNonNull(FieldSet))}, + args={ + "fields": GraphQLArgument(GraphQLNonNull(FieldSet)) + }, # Changed _FieldSet -> FieldSet description="Federation @provides directive", add_definition_to_schema=False, + field_validator=validate_provides, input_transform=field_set_case_transform, ) @@ -58,7 +67,7 @@ external_directive = CustomDirective( name="external", locations=[ - DirectiveLocation.OBJECT, + DirectiveLocation.OBJECT, # Changed from v1.0 DirectiveLocation.FIELD_DEFINITION, ], description="Federation @external directive", @@ -135,7 +144,7 @@ def get_directives() -> dict[str, GraphQLDirective]: provides_directive, external_directive, shareable_directive, - extends_directive, + extends_directive, # From v1.0 override_directive, inaccessible_directive, tag_directive, diff --git a/graphene_federation/apollo_versions/v2_2.py b/graphene_federation/apollo_versions/v2_2.py index e657351..18a7b6e 100644 --- a/graphene_federation/apollo_versions/v2_2.py +++ b/graphene_federation/apollo_versions/v2_2.py @@ -12,7 +12,7 @@ ], description="Federation @shareable directive", add_definition_to_schema=False, - is_repeatable=True, # Changed + is_repeatable=True, # Changed from v2.1 ) diff --git a/graphene_federation/directives/authenticated.py b/graphene_federation/directives/authenticated.py index a2a2748..32ffbb9 100644 --- a/graphene_federation/directives/authenticated.py +++ b/graphene_federation/directives/authenticated.py @@ -15,6 +15,12 @@ def authenticated( *, federation_version: FederationVersion = LATEST_VERSION, ) -> Callable: + """ + Indicates to composition that the target element is accessible only to the authenticated supergraph users. + For more granular access control, see the @requiresScopes directive. + + Reference: https://www.apollographql.com/docs/federation/federated-types/federated-directives/#authenticated + """ directive = get_directive_from_name("authenticated", federation_version) decorator = directive_decorator(directive) diff --git a/graphene_federation/directives/extends.py b/graphene_federation/directives/extends.py index b103df4..f827711 100644 --- a/graphene_federation/directives/extends.py +++ b/graphene_federation/directives/extends.py @@ -2,12 +2,12 @@ from graphene_directives import directive_decorator -from .utils import is_non_field from graphene_federation.apollo_versions import ( FederationVersion, LATEST_VERSION, get_directive_from_name, ) +from .utils import is_non_field def extends( @@ -15,6 +15,11 @@ def extends( *, federation_version: FederationVersion = LATEST_VERSION, ) -> Callable: + """ + Indicates that an object or interface definition is an extension of another definition of that same type. + + Reference: https://www.apollographql.com/docs/federation/federated-types/federated-directives/#extends + """ directive = get_directive_from_name("extends", federation_version) decorator = directive_decorator(directive) diff --git a/graphene_federation/directives/external.py b/graphene_federation/directives/external.py index 6811e88..53c56a0 100644 --- a/graphene_federation/directives/external.py +++ b/graphene_federation/directives/external.py @@ -2,12 +2,12 @@ from graphene_directives import directive_decorator -from .utils import is_non_field from graphene_federation.apollo_versions import ( FederationVersion, LATEST_VERSION, get_directive_from_name, ) +from .utils import is_non_field def external( @@ -15,6 +15,15 @@ def external( *, federation_version: FederationVersion = LATEST_VERSION, ) -> Callable: + """ + Indicates that this subgraph usually can't resolve a particular object field, + but it still needs to define that field for other purposes. + + This directive is always used in combination with another directive that references object fields, + such as @provides or @requires. + + Reference: https://www.apollographql.com/docs/federation/federated-types/federated-directives/#external + """ directive = get_directive_from_name("external", federation_version) decorator = directive_decorator(directive) diff --git a/graphene_federation/directives/inaccessible.py b/graphene_federation/directives/inaccessible.py index 15e20de..2c0bb5d 100644 --- a/graphene_federation/directives/inaccessible.py +++ b/graphene_federation/directives/inaccessible.py @@ -2,12 +2,12 @@ from graphene_directives import directive_decorator -from .utils import is_non_field from graphene_federation.apollo_versions import ( FederationVersion, LATEST_VERSION, get_directive_from_name, ) +from .utils import is_non_field def inaccessible( @@ -15,6 +15,14 @@ def inaccessible( *, federation_version: FederationVersion = LATEST_VERSION, ) -> Callable: + """ + Indicates that a definition in the subgraph schema should be omitted from the router's API schema, + even if that definition is also present in other subgraphs. + + This means that the field is not exposed to clients at all. + + Reference: https://www.apollographql.com/docs/federation/federated-types/federated-directives/#inaccessible + """ directive = get_directive_from_name("inaccessible", federation_version) decorator = directive_decorator(directive) diff --git a/graphene_federation/directives/interface_object.py b/graphene_federation/directives/interface_object.py index 59082e3..92a66b3 100644 --- a/graphene_federation/directives/interface_object.py +++ b/graphene_federation/directives/interface_object.py @@ -15,6 +15,15 @@ def interface_object( *, federation_version: FederationVersion = LATEST_VERSION, ) -> Callable: + """ + Indicates that an object definition serves as an abstraction of another subgraph's entity interface. + + This abstraction enables a subgraph to automatically contribute fields to all entities that implement + a particular entity interface. + + Reference: https://www.apollographql.com/docs/federation/federated-types/federated-directives/#interfaceobject + """ + directive = get_directive_from_name("interfaceObject", federation_version) decorator = directive_decorator(directive) diff --git a/graphene_federation/directives/key.py b/graphene_federation/directives/key.py index a7623a0..45ec193 100644 --- a/graphene_federation/directives/key.py +++ b/graphene_federation/directives/key.py @@ -2,13 +2,13 @@ from graphene_directives import directive_decorator -from .utils import is_non_field from graphene_federation.apollo_versions import ( FederationVersion, LATEST_VERSION, get_directive_from_name, ) from graphene_federation.validators import ast_to_str, build_ast +from .utils import is_non_field def key( @@ -17,11 +17,20 @@ def key( *, federation_version: FederationVersion = LATEST_VERSION, ) -> Any: + """ + Designates an object type as an entity and specifies its key fields + (a set of fields that the subgraph can use to uniquely identify any instance of the entity). + + You can apply multiple @key directives to a single entity (to specify multiple valid sets of key fields) + + Reference: https://www.apollographql.com/docs/federation/federated-types/federated-directives/#key + """ directive = get_directive_from_name("key", federation_version) decorator = directive_decorator(directive) fields = ast_to_str( build_ast( - fields if isinstance(fields, str) else " ".join(fields), + fields=fields if isinstance(fields, str) else " ".join(fields), + directive_name=str(directive), ) ) diff --git a/graphene_federation/directives/override.py b/graphene_federation/directives/override.py index f12b11a..2f5a406 100644 --- a/graphene_federation/directives/override.py +++ b/graphene_federation/directives/override.py @@ -2,12 +2,12 @@ from graphene_directives import directive_decorator -from .utils import is_non_field from graphene_federation.apollo_versions import ( FederationVersion, LATEST_VERSION, get_directive_from_name, ) +from .utils import is_non_field def override( @@ -16,6 +16,12 @@ def override( *, federation_version: FederationVersion = LATEST_VERSION, ) -> Callable: + """ + Indicates that an object field is now resolved by this subgraph instead of another subgraph where it's also defined. + This enables you to migrate a field from one subgraph to another. + + Reference: https://www.apollographql.com/docs/federation/federated-types/federated-directives/#override + """ directive = get_directive_from_name( "override", federation_version=federation_version ) diff --git a/graphene_federation/directives/policy.py b/graphene_federation/directives/policy.py index 1d78622..203306d 100644 --- a/graphene_federation/directives/policy.py +++ b/graphene_federation/directives/policy.py @@ -2,12 +2,12 @@ from graphene_directives import directive_decorator -from .utils import is_non_field from graphene_federation.apollo_versions import ( FederationVersion, LATEST_VERSION, get_directive_from_name, ) +from .utils import is_non_field def policy( @@ -16,6 +16,12 @@ def policy( policies: list[list[str]], federation_version: FederationVersion = LATEST_VERSION, ) -> Callable: + """ + Indicates to composition that the target element is restricted based on authorization policies + that are evaluated in a Rhai script or coprocessor. + + Reference: https://www.apollographql.com/docs/federation/federated-types/federated-directives/#policy + """ directive = get_directive_from_name("policy", federation_version=federation_version) decorator = directive_decorator(directive) diff --git a/graphene_federation/directives/provides.py b/graphene_federation/directives/provides.py index 6ac3af6..640aeb6 100644 --- a/graphene_federation/directives/provides.py +++ b/graphene_federation/directives/provides.py @@ -3,13 +3,13 @@ from graphene_directives import directive_decorator -from .utils import is_non_field from graphene_federation.apollo_versions import ( FederationVersion, LATEST_VERSION, get_directive_from_name, ) from graphene_federation.validators import ast_to_str, build_ast +from .utils import is_non_field def provides( @@ -18,13 +18,23 @@ def provides( *, federation_version: FederationVersion = LATEST_VERSION, ) -> Callable: + """ + Specifies a set of entity fields that a subgraph can resolve, but only at a particular schema path + (at other paths, the subgraph can't resolve those fields). + + If a subgraph can always resolve a particular entity field, do not apply this directive. + + Reference: https://www.apollographql.com/docs/federation/federated-types/federated-directives/#provides + """ + directive = get_directive_from_name( "provides", federation_version=federation_version ) decorator = directive_decorator(directive) fields = ast_to_str( build_ast( - fields if isinstance(fields, str) else " ".join(fields), + fields=fields if isinstance(fields, str) else " ".join(fields), + directive_name=str(directive), ) ) diff --git a/graphene_federation/directives/requires.py b/graphene_federation/directives/requires.py index 971fa68..2275bae 100644 --- a/graphene_federation/directives/requires.py +++ b/graphene_federation/directives/requires.py @@ -3,13 +3,13 @@ from graphene_directives import directive_decorator -from .utils import is_non_field from graphene_federation.apollo_versions import ( FederationVersion, LATEST_VERSION, get_directive_from_name, ) from graphene_federation.validators import ast_to_str, build_ast +from .utils import is_non_field def requires( @@ -18,11 +18,21 @@ def requires( *, federation_version: FederationVersion = LATEST_VERSION, ) -> Callable: + """ + Indicates that the resolver for a particular entity field depends on the values of other entity fields + that are resolved by other subgraphs. + + This tells the router that it needs to fetch the values of those externally defined fields first, + even if the original client query didn't request them. + + Reference: https://www.apollographql.com/docs/federation/federated-types/federated-directives/#requires + """ directive = get_directive_from_name("requires", federation_version) decorator = directive_decorator(directive) fields = ast_to_str( build_ast( - fields if isinstance(fields, str) else " ".join(fields), + fields=fields if isinstance(fields, str) else " ".join(fields), + directive_name=str(directive), ), add_type_name=True, # When resolvers receive the data, it will be type-casted as __typename info is added ) diff --git a/graphene_federation/directives/requires_scopes.py b/graphene_federation/directives/requires_scopes.py index 2a7176d..5424df8 100644 --- a/graphene_federation/directives/requires_scopes.py +++ b/graphene_federation/directives/requires_scopes.py @@ -2,12 +2,12 @@ from graphene_directives import directive_decorator -from .utils import is_non_field from graphene_federation.apollo_versions import ( FederationVersion, LATEST_VERSION, get_directive_from_name, ) +from .utils import is_non_field def requires_scope( @@ -16,6 +16,12 @@ def requires_scope( scopes: list[list[str]], federation_version: FederationVersion = LATEST_VERSION, ) -> Callable: + """ + Indicates to composition that the target element is accessible only to the authenticated supergraph users with + the appropriate JWT scopes. + + Reference: https://www.apollographql.com/docs/federation/federated-types/federated-directives/#requiresscopes + """ directive = get_directive_from_name( "requiresScopes", federation_version=federation_version ) diff --git a/graphene_federation/directives/shareable.py b/graphene_federation/directives/shareable.py index 34a2995..ab58fcb 100644 --- a/graphene_federation/directives/shareable.py +++ b/graphene_federation/directives/shareable.py @@ -2,12 +2,12 @@ from graphene_directives import directive_decorator -from .utils import is_non_field from graphene_federation.apollo_versions import ( FederationVersion, LATEST_VERSION, get_directive_from_name, ) +from .utils import is_non_field def shareable( @@ -15,6 +15,15 @@ def shareable( *, federation_version: FederationVersion = LATEST_VERSION, ) -> Callable: + """ + Indicates that an object type's field is allowed to be resolved by multiple subgraphs + (by default in Federation 2, object fields can be resolved by only one subgraph). + + If applied to an object type definition, all of that type's fields are considered @shareable + + Reference: https://www.apollographql.com/docs/federation/federated-types/federated-directives/#shareable + """ + directive = get_directive_from_name( "shareable", federation_version=federation_version ) diff --git a/graphene_federation/directives/tag.py b/graphene_federation/directives/tag.py index d3182cd..7ecf821 100644 --- a/graphene_federation/directives/tag.py +++ b/graphene_federation/directives/tag.py @@ -2,26 +2,34 @@ from graphene_directives import directive_decorator -from .utils import is_non_field from graphene_federation.apollo_versions import ( FederationVersion, LATEST_VERSION, get_directive_from_name, ) +from .utils import is_non_field def tag( graphene_type=None, *, + name: str, federation_version: FederationVersion = LATEST_VERSION, ) -> Callable: + """ + Applies arbitrary string metadata to a schema location. + Custom tooling can use this metadata during any step of the schema delivery flow, + including composition, static analysis, and documentation + + Reference: https://www.apollographql.com/docs/federation/federated-types/federated-directives/#tag + """ directive = get_directive_from_name("tag", federation_version=federation_version) decorator = directive_decorator(directive) def wrapper(field_or_type): if is_non_field(field_or_type): - return decorator(field=None)(field_or_type) - return decorator(field=field_or_type) + return decorator(field=None, name=name)(field_or_type) + return decorator(field=field_or_type, name=name) if graphene_type: return wrapper(graphene_type) diff --git a/graphene_federation/directives/utils.py b/graphene_federation/directives/utils.py index 8ca1acc..378a5a5 100644 --- a/graphene_federation/directives/utils.py +++ b/graphene_federation/directives/utils.py @@ -2,5 +2,6 @@ from typing import Any -def is_non_field(field: Any): - return inspect.isclass(field) +def is_non_field(graphene_type: Any): + """Check of a given graphene_type is a non-field""" + return inspect.isclass(graphene_type) diff --git a/graphene_federation/scalars/_any.py b/graphene_federation/scalars/_any.py index 63f526a..dc69fc4 100644 --- a/graphene_federation/scalars/_any.py +++ b/graphene_federation/scalars/_any.py @@ -1,6 +1,7 @@ from graphene import Scalar, String +# Reference: https://www.apollographql.com/docs/federation/subgraph-spec/ class _Any(Scalar): name = "_Any" __typename = String(required=True) diff --git a/graphene_federation/scalars/federation_policy.py b/graphene_federation/scalars/federation_policy.py index c456036..463dc8a 100644 --- a/graphene_federation/scalars/federation_policy.py +++ b/graphene_federation/scalars/federation_policy.py @@ -43,6 +43,7 @@ def _parse_string_literal(value_node: ValueNode, _variables: Any = None) -> str: return value_node.value +# Reference: https://www.apollographql.com/docs/federation/subgraph-spec/ FederationPolicy = GraphQLScalarType( name="federation__Policy", serialize=_serialize_string, diff --git a/graphene_federation/scalars/field_set_v1.py b/graphene_federation/scalars/field_set_v1.py index 747ca08..e5fcc02 100644 --- a/graphene_federation/scalars/field_set_v1.py +++ b/graphene_federation/scalars/field_set_v1.py @@ -1,3 +1,4 @@ from graphql import GraphQLScalarType +# Reference: https://www.apollographql.com/docs/federation/subgraph-spec/ _FieldSet = GraphQLScalarType(name="_FieldSet") diff --git a/graphene_federation/scalars/field_set_v2.py b/graphene_federation/scalars/field_set_v2.py index a2a2939..f6b8c3e 100644 --- a/graphene_federation/scalars/field_set_v2.py +++ b/graphene_federation/scalars/field_set_v2.py @@ -1,3 +1,4 @@ from graphql import GraphQLScalarType +# Reference: https://www.apollographql.com/docs/federation/subgraph-spec/ FieldSet = GraphQLScalarType(name="FieldSet") diff --git a/graphene_federation/scalars/scope.py b/graphene_federation/scalars/scope.py index dbaf26f..1b5de0e 100644 --- a/graphene_federation/scalars/scope.py +++ b/graphene_federation/scalars/scope.py @@ -39,6 +39,7 @@ def _parse_string_literal(value_node: ValueNode, _variables: Any = None) -> str: return value_node.value +# Reference: https://www.apollographql.com/docs/federation/subgraph-spec/ Scope = GraphQLScalarType( name="Scope", serialize=_serialize_string, diff --git a/graphene_federation/schema_directives/compose_directive.py b/graphene_federation/schema_directives/compose_directive.py index 848c2b6..535f67d 100644 --- a/graphene_federation/schema_directives/compose_directive.py +++ b/graphene_federation/schema_directives/compose_directive.py @@ -11,6 +11,19 @@ def compose_directive( name: str, federation_version: FederationVersion = LATEST_VERSION, ) -> SchemaDirective: + """ + Indicates to composition that all uses of a particular custom type system directive in the subgraph schema should be + preserved in the supergraph schema + + (by default, composition omits most directives from the supergraph schema). + + Use this in the `schema_directives` argument of `build_schema` + + It is not recommended to use this directive directly, instead use the FederationDirective class to build + a custom directive. It will automatically add the compose and link directive to schema + + Reference: https://www.apollographql.com/docs/federation/federated-types/federated-directives/#composedirective + """ directive = get_directive_from_name("composeDirective", federation_version) return SchemaDirective( target_directive=directive, diff --git a/graphene_federation/schema_directives/link_directive.py b/graphene_federation/schema_directives/link_directive.py index 006ed3e..649d609 100644 --- a/graphene_federation/schema_directives/link_directive.py +++ b/graphene_federation/schema_directives/link_directive.py @@ -33,6 +33,19 @@ def link_directive( for_: Optional[str] = None, import_: Optional[list[str]] = None, ) -> SchemaDirective: + """ + It's used to link types and fields from external subgraphs, creating a unified GraphQL schema + across multiple services + + Use this in the `schema_directives` argument of `build_schema` + + It is not recommended to use this directive directly, instead use the FederationDirective class to build + a custom directive. It will automatically add the compose and link directive to schema + + Also, the apollo directives such as @key, @external, ... are automatically added to the schema via the link directive + + Reference: https://www.apollographql.com/docs/federation/federated-types/federated-directives/ + """ return SchemaDirective( target_directive=_link_directive, arguments={"url": url, "as": as_, "for": for_, "import": import_}, diff --git a/graphene_federation/transform/field_set_case_transform.py b/graphene_federation/transform/field_set_case_transform.py index 9a6793b..907a433 100644 --- a/graphene_federation/transform/field_set_case_transform.py +++ b/graphene_federation/transform/field_set_case_transform.py @@ -4,6 +4,13 @@ def field_set_case_transform(inputs: dict, schema: Schema) -> dict: + """ + Transform the fields from internal representation to schema representation + + Internal representation uses + __union__ for representing ... on + __arg__ for representing (arg1: value1, arg2: value2) + """ fields = inputs.get("fields") if fields: inputs["fields"] = ( diff --git a/graphene_federation/validators/__init__.py b/graphene_federation/validators/__init__.py index ebf7b54..9f13202 100644 --- a/graphene_federation/validators/__init__.py +++ b/graphene_federation/validators/__init__.py @@ -1,4 +1,5 @@ from .key import validate_key +from .provides import validate_provides from .requires import validate_requires from .utils import InternalNamespace from .utils import ast_to_str, build_ast, to_case diff --git a/graphene_federation/validators/key.py b/graphene_federation/validators/key.py index 0d359be..f51b407 100644 --- a/graphene_federation/validators/key.py +++ b/graphene_federation/validators/key.py @@ -7,14 +7,19 @@ def validate_key( - type_: Union[ObjectType, Interface, Field], inputs: dict, schema: Schema + graphene_type: Union[ObjectType, Interface, Field], inputs: dict, schema: Schema ) -> bool: + """ + Used to validate the inputs and graphene_type of @key + """ errors: list[str] = [] - ast_node = build_ast(input_str=to_case(inputs.get("fields"), schema)) + ast_node = build_ast( + fields=to_case(inputs.get("fields"), schema), directive_name="@key" + ) evaluate_ast( - directive_name="key", - nodes=ast_node, - type_=type_, + directive_name="@key", + ast=ast_node, + graphene_type=graphene_type, ignore_fields=[], errors=errors, entity_types=schema.graphql_schema.type_map, diff --git a/graphene_federation/validators/provides.py b/graphene_federation/validators/provides.py new file mode 100644 index 0000000..b1aaa2f --- /dev/null +++ b/graphene_federation/validators/provides.py @@ -0,0 +1,51 @@ +from typing import Union + +from graphene import Field, Interface, ObjectType +from graphene_directives import Schema + +from .utils import ( + InternalNamespace, + build_ast, + evaluate_ast, + to_case, +) + + +def validate_provides( + _parent_type: Union[ObjectType, Interface], + field: Field, + inputs: dict, + schema: Schema, +) -> bool: + """ + Used to validate the inputs and graphene_type of @provides + """ + errors: list[str] = [] + ast_node = build_ast( + fields=to_case(inputs.get("fields"), schema), directive_name="@provides" + ) + + # Get the parent type of the field + field_parent_type = field + while hasattr(field_parent_type, "type") or hasattr(field_parent_type, "of_type"): + if hasattr(field_parent_type, "of_type"): + field_parent_type = field_parent_type.of_type + elif hasattr(field_parent_type, "type"): + field_parent_type = field_parent_type.type + else: + raise ValueError( + f"@provides could not find parent for the field {field} at {_parent_type}" + ) + + evaluate_ast( + directive_name="@provides", + ast=ast_node, + graphene_type=field_parent_type, + ignore_fields=[InternalNamespace.UNION.value], + errors=errors, + entity_types=schema.graphql_schema.type_map, + ) + if errors: + raise ValueError("\n".join(errors)) + + return True diff --git a/graphene_federation/validators/requires.py b/graphene_federation/validators/requires.py index 2984d9e..c222d8c 100644 --- a/graphene_federation/validators/requires.py +++ b/graphene_federation/validators/requires.py @@ -12,12 +12,17 @@ def validate_requires( inputs: dict, schema: Schema, ) -> bool: + """ + Used to validate the inputs and graphene_type of @requires + """ errors: list[str] = [] - ast_node = build_ast(to_case(inputs.get("fields"), schema)) + ast_node = build_ast( + fields=to_case(inputs.get("fields"), schema), directive_name="@requires" + ) evaluate_ast( - directive_name="requires", - nodes=ast_node, - type_=parent_type, + directive_name="@requires", + ast=ast_node, + graphene_type=parent_type, ignore_fields=["__typename", InternalNamespace.UNION.value], errors=errors, entity_types=schema.graphql_schema.type_map, diff --git a/graphene_federation/validators/utils.py b/graphene_federation/validators/utils.py index 24a079a..382e311 100644 --- a/graphene_federation/validators/utils.py +++ b/graphene_federation/validators/utils.py @@ -19,6 +19,18 @@ GraphQLType, ) +""" +@requires, @key, @provides 's field is represented internally in a different way + +A field definition + +"id currency(curreny_value: usd) products{ ... on Bag { id } ... on Cloth { id } }" + +is internally represented as + +"id currency __arg__(curreny_value: usd) products{ __union__ Bag { id } __union__ Cloth { id } }" +""" + class InternalNamespace(Enum): UNION = "__union__" @@ -27,40 +39,54 @@ class InternalNamespace(Enum): def check_fields_exist_on_type( field: str, - type_: Union[ObjectType, Interface, Field, NonNull], + graphene_type: Union[ObjectType, Interface, Field, NonNull], ignore_fields: list[str], entity_types: dict[str, ObjectType], ) -> Union[GraphQLType, GraphQLField, bool]: + """ + Checks if the given field exists on the graphene_type + + :param field: field that needs to be checked for existence + :param graphene_type: Union[ObjectType, Interface, Field, NonNull] + :param ignore_fields: fields that can be ignored for checking example __typename + :param entity_types: A dictionary of [entity_name, graphene_type] + """ if field in ignore_fields or field.startswith( - "__arg__" # todo handle argument validations + "__arg__" # todo handle argument type validations ): return True - if isinstance(type_, GraphQLField): + if isinstance(graphene_type, GraphQLField): return check_fields_exist_on_type( field, - type_.type, # noqa + graphene_type.type, # noqa ignore_fields, entity_types, ) - if isinstance(type_, GraphQLNonNull): + if isinstance(graphene_type, GraphQLNonNull): return check_fields_exist_on_type( - field, type_.of_type, ignore_fields, entity_types + field, graphene_type.of_type, ignore_fields, entity_types ) - if isinstance(type_, GrapheneObjectType): - if field in type_.fields: - return type_.fields[field] - if isinstance(type_, GraphQLList): + if isinstance(graphene_type, GrapheneObjectType): + if field in graphene_type.fields: + return graphene_type.fields[field] + if isinstance(graphene_type, GraphQLList): return check_fields_exist_on_type( - field, type_.of_type, ignore_fields, entity_types + field, graphene_type.of_type, ignore_fields, entity_types ) - if isinstance(type_, GrapheneUnionType): - for union_type in type_.types: + if isinstance(graphene_type, GrapheneUnionType): + for union_type in graphene_type.types: if union_type.name.lower() == field.lower(): return union_type try: - if issubclass(type_, ObjectType) or issubclass(type_, Interface): # noqa - entity_fields = entity_types.get(type_._meta.name) # noqa + if issubclass( + graphene_type, # noqa + ObjectType, + ) or issubclass( + graphene_type, # noqa + Interface, + ): + entity_fields = entity_types.get(graphene_type._meta.name) # noqa if entity_fields is not None: entity_fields = entity_fields.fields # noqa if field in entity_fields: @@ -72,7 +98,7 @@ def check_fields_exist_on_type( def get_type_for_field( - type_, + graphene_field, ) -> tuple[ Union[ GrapheneObjectType, @@ -84,45 +110,53 @@ def get_type_for_field( bool, ]: """ - Returns the type,is_selectable + Finds the base type for a given graphene_field + + Returns the graphene_field_type, is_selectable (indicates whether the type has sub selections) """ - if isinstance(type_, GraphQLField): - return get_type_for_field(type_.type) - if isinstance(type_, GraphQLNonNull): - return get_type_for_field(type_.of_type) - if isinstance(type_, GraphQLList): - return get_type_for_field(type_.of_type) + if isinstance(graphene_field, GraphQLField): + return get_type_for_field(graphene_field.type) + if isinstance(graphene_field, GraphQLNonNull): + return get_type_for_field(graphene_field.of_type) + if isinstance(graphene_field, GraphQLList): + return get_type_for_field(graphene_field.of_type) if ( - isinstance(type_, GrapheneObjectType) - or isinstance(type_, GrapheneInterfaceType) - or isinstance(type_, GrapheneUnionType) + isinstance(graphene_field, GrapheneObjectType) + or isinstance(graphene_field, GrapheneInterfaceType) + or isinstance(graphene_field, GrapheneUnionType) ): - return type_, True - if isinstance(type_, GraphQLScalarType) or isinstance(type_, GrapheneEnumType): - return type_, False + return graphene_field, True + if isinstance(graphene_field, GraphQLScalarType) or isinstance( + graphene_field, GrapheneEnumType + ): + return graphene_field, False - raise NotImplementedError("get_type_for_field", type_) + raise NotImplementedError("get_type_for_field", graphene_field) """" AST FUNCTIONS -For FieldSet Parsing +For @key, @provides, @requires FieldSet Parsing """ -def _tokenize(input_string): - input_string = input_string.strip() +def _tokenize_field_set(fields: str, directive_name: str) -> list[str]: + """ + Splits the fields string to tokens + """ + + fields = fields.strip() tokens = [] current_token = "" open_braces_count = 0 - if input_string.startswith("{"): - raise ValueError("@requires cannot start with {") + if fields.startswith("{"): + raise ValueError(f"{directive_name} cannot start with " + "{") index = 0 - while index < len(input_string): - char = input_string[index] + while index < len(fields): + char = fields[index] if char.isalnum(): current_token += char elif char == "{": @@ -148,8 +182,8 @@ def _tokenize(input_string): current_token = f"{char}" index += 1 mismatched_parenthesis = True - while index < len(input_string): - char = input_string[index] + while index < len(fields): + char = fields[index] if char.isalnum() or char == ",": current_token += char elif char.isspace(): @@ -167,15 +201,15 @@ def _tokenize(input_string): break else: ValueError( - f"@requires({input_string}) has unknown character {char} at argument {current_token}" + f"{directive_name}({fields}) has unknown character {char} at argument {current_token}" ) index += 1 if mismatched_parenthesis: raise ValueError( - f"@requires({input_string}) has mismatched parenthesis" + f"{directive_name}({fields}) has mismatched parenthesis" ) elif char == ")": - raise ValueError(f"@requires({input_string}) has mismatched parenthesis") + raise ValueError(f"{directive_name}({fields}) has mismatched parenthesis") elif char.isspace(): if current_token == "on": tokens.append("__union__") @@ -193,23 +227,28 @@ def _tokenize(input_string): tokens.append(current_token) if open_braces_count != 0: - raise ValueError(f"@requires({input_string}) has mismatched brackets") + raise ValueError(f"{directive_name}({fields}) has mismatched brackets") return tokens def evaluate_ast( directive_name: str, - nodes: dict, - type_: ObjectType, + ast: dict, + graphene_type: ObjectType, ignore_fields: list[str], errors: list[str], entity_types: dict[str, ObjectType], ) -> None: - for field_name, value in nodes.items(): + """ + Checks if the given AST is valid for the graphene_type + + It recursively checks if the fields at a node exist on the graphene_type + """ + for field_name, value in ast.items(): field_type = check_fields_exist_on_type( field_name, - type_, + graphene_type, ignore_fields, entity_types, ) @@ -217,7 +256,7 @@ def evaluate_ast( if not field_type: errors.append( - f'@{directive_name}, field "{field_name}" does not exist on type "{type_}"' + f'{directive_name}, field "{field_name}" does not exist on type "{graphene_type}"' ) continue @@ -232,13 +271,13 @@ def evaluate_ast( if is_selectable and not has_selections: errors.append( - f'@{directive_name}, type {type_}, field "{field_name}" needs sub selections.' + f'{directive_name}, type {graphene_type}, field "{field_name}" needs sub selections.' ) continue if not is_selectable and has_selections: errors.append( - f'@{directive_name}, type {type_}, field "{field_name}" cannot have sub selections.' + f'{directive_name}, type {graphene_type}, field "{field_name}" cannot have sub selections.' ) continue @@ -253,8 +292,14 @@ def evaluate_ast( ) -def build_ast(input_str: str) -> dict: - cleaned_fields = _tokenize(input_str) +def build_ast(fields: str, directive_name: str) -> dict: + """ + Converts the fields string to an AST tree + + :param fields: string fields + :param directive_name: name of the directive + """ + cleaned_fields = _tokenize_field_set(fields, directive_name) parent: dict[str, dict] = {} field_stack: list[str] = [] @@ -271,6 +316,14 @@ def build_ast(input_str: str) -> dict: def ast_to_str(fields: dict, add_type_name: bool = False, level: int = 0) -> str: + """ + Converts the AST of fields to the original string + + :param fields: AST of fields + :param add_type_name: adds __typename to sub ast nodes (for @requires) + :param level: for internal use only + """ + new_fields = [] union_type = False if level != 0 and add_type_name: @@ -304,6 +357,9 @@ def ast_to_str(fields: dict, add_type_name: bool = False, level: int = 0) -> str def to_case(fields: Union[str, None], schema: Schema) -> str: + """ + Converts field str to correct casing according to the schema.auto_camelcase value + """ if not fields: return "" From 30085497c78e0ace7a1da4a0708f16bbb9a9a31f Mon Sep 17 00:00:00 2001 From: M Aswin Kishore <60577077+mak626@users.noreply.github.com> Date: Tue, 16 Jan 2024 11:13:05 +0530 Subject: [PATCH 07/19] chore: make dev-setup --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index bd8d30a..a2216dd 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ integration-tests: ## Run integration tests # ------------------------- dev-setup: ## Install development dependencies - docker-compose up -d && docker-compose exec graphene_federation pip install -e ".[dev]" + docker-compose up --build -d .PHONY: dev-setup tests: ## Run unit tests From 528a4f59a7d142491e530443df9c5784be1694e3 Mon Sep 17 00:00:00 2001 From: M Aswin Kishore <60577077+mak626@users.noreply.github.com> Date: Tue, 16 Jan 2024 12:00:02 +0530 Subject: [PATCH 08/19] docs: add documentation --- README.md | 54 +++++++++++++++++- graphene_federation/directives/key.py | 14 +++-- graphene_federation/directives/provides.py | 11 +++- graphene_federation/directives/requires.py | 12 +++- graphene_federation/entity.py | 7 ++- graphene_federation/federation_directive.py | 19 ++++++- graphene_federation/main.py | 56 ++++++++++++++----- graphene_federation/service.py | 3 + .../transform/field_set_case_transform.py | 4 +- graphene_federation/validators/key.py | 7 ++- graphene_federation/validators/provides.py | 9 ++- graphene_federation/validators/requires.py | 12 +++- graphene_federation/validators/utils.py | 5 +- 13 files changed, 173 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index b2b40bf..e68dde3 100644 --- a/README.md +++ b/README.md @@ -172,9 +172,59 @@ There is also a cool [example](https://github.com/preply/graphene-federation/iss ------------------------ -## Known issues +## Custom field name -1. decorators will not work properly on fields with custom names for example `some_field = String(name='another_name')` +When using decorator on a field with custom name + +1. Case 1 (auto_camelcase=False) + +```python +@key("identifier") +@key("validEmail") +class User(ObjectType): + identifier = ID() + email = String(name="validEmail") + +class Query(ObjectType): + user = Field(User) + +schema = build_schema(query=Query, enable_federation_2=True, auto_camelcase=False) # Disable auto_camelcase +``` + +This works correctly. +By default `fields` of `@key`,`@requires` and `@provides` are not converted to camel case if `auto_camelcase` is set to `False` + +2. Case 2 (auto_camelcase=True) +```python +@key("identifier") +@key("valid_email") +class User(ObjectType): + identifier = ID() + email = String(name="valid_email") + +class Query(ObjectType): + user = Field(User) + +schema = build_schema(query=Query, enable_federation_2=True) # auto_camelcase Enabled +``` + +This will raise an error `@key, field "validEmail" does not exist on type "User"`. +Because The decorator auto camel-cased the `field` value of key, as schema has `auto_camelcase=True` (default) + +To fix this, pass `auto_case=False` in the `@key`, `@requires` or `@provides` argument + +```python +@key("identifier") +@key("valid_email", auto_case=False) +class User(ObjectType): + identifier = ID() + email = String(name="valid_email") + +class Query(ObjectType): + user = Field(User) + +schema = build_schema(query=Query, enable_federation_2=True) # auto_camelcase=True +``` ------------------------ diff --git a/graphene_federation/directives/key.py b/graphene_federation/directives/key.py index 45ec193..1f7b812 100644 --- a/graphene_federation/directives/key.py +++ b/graphene_federation/directives/key.py @@ -7,7 +7,7 @@ LATEST_VERSION, get_directive_from_name, ) -from graphene_federation.validators import ast_to_str, build_ast +from graphene_federation.validators import InternalNamespace, ast_to_str, build_ast from .utils import is_non_field @@ -15,6 +15,7 @@ def key( fields: Union[str, list[str]], resolvable: bool = None, *, + auto_case: bool = True, federation_version: FederationVersion = LATEST_VERSION, ) -> Any: """ @@ -34,11 +35,16 @@ def key( ) ) + if not auto_case: + fields = f"{InternalNamespace.NO_AUTO_CASE.value} {fields}" + def wrapper(field_or_type): if is_non_field(field_or_type): - return decorator(field=None, fields=fields, resolvable=resolvable)( - field_or_type - ) + return decorator( + field=None, + fields=fields, + resolvable=resolvable, + )(field_or_type) raise TypeError( "\n".join( [ diff --git a/graphene_federation/directives/provides.py b/graphene_federation/directives/provides.py index 640aeb6..dc107b3 100644 --- a/graphene_federation/directives/provides.py +++ b/graphene_federation/directives/provides.py @@ -8,7 +8,7 @@ LATEST_VERSION, get_directive_from_name, ) -from graphene_federation.validators import ast_to_str, build_ast +from graphene_federation.validators import InternalNamespace, ast_to_str, build_ast from .utils import is_non_field @@ -16,6 +16,7 @@ def provides( graphene_type, fields: Union[str, list[str]], *, + auto_case: bool = True, federation_version: FederationVersion = LATEST_VERSION, ) -> Callable: """ @@ -38,6 +39,9 @@ def provides( ) ) + if not auto_case: + fields = f"{InternalNamespace.NO_AUTO_CASE.value} {fields}" + def wrapper(field_or_type): if is_non_field(field_or_type): raise TypeError( @@ -51,7 +55,10 @@ def wrapper(field_or_type): ] ) ) - return decorator(field=field_or_type, fields=fields) + return decorator( + field=field_or_type, + fields=fields, + ) if graphene_type: return wrapper(graphene_type) diff --git a/graphene_federation/directives/requires.py b/graphene_federation/directives/requires.py index 2275bae..3910916 100644 --- a/graphene_federation/directives/requires.py +++ b/graphene_federation/directives/requires.py @@ -8,7 +8,7 @@ LATEST_VERSION, get_directive_from_name, ) -from graphene_federation.validators import ast_to_str, build_ast +from graphene_federation.validators import InternalNamespace, ast_to_str, build_ast from .utils import is_non_field @@ -16,6 +16,7 @@ def requires( graphene_type, fields: Union[str, list[str]], *, + auto_case: bool = True, federation_version: FederationVersion = LATEST_VERSION, ) -> Callable: """ @@ -37,6 +38,9 @@ def requires( add_type_name=True, # When resolvers receive the data, it will be type-casted as __typename info is added ) + if not auto_case: + fields = f"{InternalNamespace.NO_AUTO_CASE.value} {fields}" + def wrapper(field_or_type): if is_non_field(field_or_type): raise TypeError( @@ -52,7 +56,11 @@ def wrapper(field_or_type): ] ) ) - return decorator(field=field_or_type, fields=fields) + return decorator( + field=field_or_type, + fields=fields, + auto_case=auto_case, + ) if graphene_type: return wrapper(graphene_type) diff --git a/graphene_federation/entity.py b/graphene_federation/entity.py index cf03cbc..a8b4339 100644 --- a/graphene_federation/entity.py +++ b/graphene_federation/entity.py @@ -81,7 +81,8 @@ def resolve_entities(self, info, representations, sub_field_resolution=False): get_model_attr(k): v for k, v in model_arguments.items() } - # convert subfields of models from dict to a corresponding graphql type + # convert subfields of models from dict to a corresponding graphql type, + # This will be useful when @requires is used for model_field, value in model_arguments.items(): if not hasattr(model, model_field): continue @@ -89,7 +90,7 @@ def resolve_entities(self, info, representations, sub_field_resolution=False): field = getattr(model, model_field) if isinstance(field, Field) and isinstance(value, dict): if value.get("__typename") is None: - value["__typename"] = field.type.of_type._meta.name + value["__typename"] = field.type.of_type._meta.name # noqa model_arguments[model_field] = EntityQuery.resolve_entities( self, info, @@ -116,7 +117,7 @@ def resolve_entities(self, info, representations, sub_field_resolution=False): ): for sub_value in value: if sub_value.get("__typename") is None: - sub_value["__typename"] = field.of_type._meta.name + sub_value["__typename"] = field.of_type._meta.name # noqa model_arguments[model_field] = EntityQuery.resolve_entities( self, info, representations=value, sub_field_resolution=True ) diff --git a/graphene_federation/federation_directive.py b/graphene_federation/federation_directive.py index 73e1b1e..b9f3e34 100644 --- a/graphene_federation/federation_directive.py +++ b/graphene_federation/federation_directive.py @@ -12,16 +12,31 @@ class FederationDirective(GraphQLDirective): def __init__( self, name: str, - spec_url: str, locations: Collection[DirectiveLocation], args: Optional[Dict[str, GraphQLArgument]] = None, is_repeatable: bool = False, description: Optional[str] = None, extensions: Optional[Dict[str, Any]] = None, ast_node: Optional[DirectiveDefinitionNode] = None, + spec_url: str = None, add_to_schema_directives: bool = True, ) -> None: - assert spec_url is not None, "FederationDirective requires spec_url" + """ + Creates a Federation Supported GraphQLDirective + + :param name: (GraphQLDirective param) + :param args: (GraphQLDirective param) + :param is_repeatable: (GraphQLDirective param) + :param description: (GraphQLDirective param) + :param extensions: (GraphQLDirective param) + :param ast_node: (GraphQLDirective param) + + :param spec_url: url of the directive to be set in url of @link + :param add_to_schema_directives: Adds schema_directives @composeDirective and @link to schema automatically + """ + if add_to_schema_directives: + assert spec_url is not None, "FederationDirective requires spec_url" + self.spec_url = spec_url self.add_to_schema_directives = add_to_schema_directives diff --git a/graphene_federation/main.py b/graphene_federation/main.py index 0e9b705..850373d 100644 --- a/graphene_federation/main.py +++ b/graphene_federation/main.py @@ -22,9 +22,12 @@ from .service import get_service_query -def _get_query( +def _get_federation_query( schema: Schema, query_cls: Optional[ObjectType] = None ) -> Type[ObjectType]: + """ + Add Federation required _service and _entities to Query(ObjectType) + """ type_name = "Query" bases = [get_service_query(schema)] entity_cls = get_entity_query(schema) @@ -37,6 +40,26 @@ def _get_query( return federated_query_cls # noqa +def _add_sharable_to_page_info_type( + schema: Schema, + federation_version: FederationVersion, + types: list[ObjectType | Type[ObjectType]], +): + """ + Add @sharable directive to PageInfo type + """ + if PageInfo.__name__ in schema.graphql_schema.type_map: + try: + # PageInfo needs @sharable directive + sharable = get_directive_from_name("shareable", federation_version) + types.append( + directive_decorator(target_directive=sharable)(field=None)(PageInfo) + ) + except ValueError: + # Federation Version does not support @sharable + pass + + def build_schema( query: Union[ObjectType, Type[ObjectType]] = None, mutation: Union[ObjectType, Type[ObjectType]] = None, @@ -76,6 +99,8 @@ def build_schema( higher priority """ + # In case both enable_federation_2 and federation_version are specified, + # federation_version is given higher priority federation_version = ( federation_version if federation_version @@ -89,7 +114,7 @@ def build_schema( _directives = get_directives_based_on_version(federation_version) federation_directives = set(_directives.keys()) - if directives is not None: + if directives is not None: # Add custom directives _directives.update({directive.name: directive for directive in directives}) schema_args = { @@ -103,17 +128,9 @@ def build_schema( schema: Schema = build_directive_schema(query=query, **schema_args) - if "PageInfo" in schema.graphql_schema.type_map: - # PageInfo needs @sharable directive - try: - sharable = get_directive_from_name("shareable", federation_version) - - _types.append( - directive_decorator(target_directive=sharable)(field=None)(PageInfo) - ) - except ValueError: - # Federation Version does not support @sharable - pass + _add_sharable_to_page_info_type( + schema=schema, federation_version=federation_version, types=_types + ) _schema_directives = [] directives_used = schema.get_directives_used() @@ -123,6 +140,7 @@ def build_schema( f"Schema Directives & Directives are not supported on {federation_version=}. Use >=2.0 " ) + # Check if @ComposeDirective needs to be added to schema if ( any( schema_directive.target_directive == ComposeDirective @@ -146,6 +164,7 @@ def build_schema( ) ) + # Add @link directive for Custom Directives provided if directives: url__imports: dict[str, list[str]] = {} for directive in directives: @@ -162,9 +181,11 @@ def build_schema( else: url__imports[directive.spec_url] = [str(directive)] + # Add @link schema directives for spec, imports in url__imports.items(): _schema_directives.append(link_directive(url=spec, import_=sorted(imports))) + # Add @ComposeDirective to schema directives for directive in directives: if not directive.add_to_schema_directives: continue @@ -174,5 +195,12 @@ def build_schema( _schema_directives.extend(list(schema_directives)) schema_args["schema_directives"] = _schema_directives if enable_federation_2 else [] + + # Call it again to rebuild the schema using the schema directives schema = build_directive_schema(query=query, **schema_args) - return build_directive_schema(query=_get_query(schema, schema.query), **schema_args) + + # Add Federation required _service and _entities to Query + return build_directive_schema( + query=_get_federation_query(schema, schema.query), + **schema_args, + ) diff --git a/graphene_federation/service.py b/graphene_federation/service.py index dbd0db6..a772648 100644 --- a/graphene_federation/service.py +++ b/graphene_federation/service.py @@ -12,6 +12,9 @@ def get_sdl(schema) -> str: def get_service_query(schema: Schema): + """ + Gets the Service Query for federation + """ sdl_str = get_sdl(schema) class _Service(ObjectType): diff --git a/graphene_federation/transform/field_set_case_transform.py b/graphene_federation/transform/field_set_case_transform.py index 907a433..7f3acaa 100644 --- a/graphene_federation/transform/field_set_case_transform.py +++ b/graphene_federation/transform/field_set_case_transform.py @@ -12,10 +12,12 @@ def field_set_case_transform(inputs: dict, schema: Schema) -> dict: __arg__ for representing (arg1: value1, arg2: value2) """ fields = inputs.get("fields") + auto_case = InternalNamespace.NO_AUTO_CASE.value not in inputs.get("fields") if fields: inputs["fields"] = ( - to_case(fields, schema) + to_case(fields, schema, auto_case) .replace(InternalNamespace.UNION.value, "... on") .replace(InternalNamespace.ARG.value, "") + .replace(InternalNamespace.NO_AUTO_CASE.value, "") ) return inputs diff --git a/graphene_federation/validators/key.py b/graphene_federation/validators/key.py index f51b407..25e2afe 100644 --- a/graphene_federation/validators/key.py +++ b/graphene_federation/validators/key.py @@ -3,7 +3,7 @@ from graphene import Field, Interface, ObjectType from graphene_directives import Schema -from .utils import build_ast, evaluate_ast, to_case +from .utils import InternalNamespace, build_ast, evaluate_ast, to_case def validate_key( @@ -13,14 +13,15 @@ def validate_key( Used to validate the inputs and graphene_type of @key """ errors: list[str] = [] + auto_case = InternalNamespace.NO_AUTO_CASE.value not in inputs.get("fields") ast_node = build_ast( - fields=to_case(inputs.get("fields"), schema), directive_name="@key" + fields=to_case(inputs.get("fields"), schema, auto_case), directive_name="@key" ) evaluate_ast( directive_name="@key", ast=ast_node, graphene_type=graphene_type, - ignore_fields=[], + ignore_fields=[InternalNamespace.NO_AUTO_CASE.value], errors=errors, entity_types=schema.graphql_schema.type_map, ) diff --git a/graphene_federation/validators/provides.py b/graphene_federation/validators/provides.py index b1aaa2f..dc5c7f7 100644 --- a/graphene_federation/validators/provides.py +++ b/graphene_federation/validators/provides.py @@ -21,8 +21,10 @@ def validate_provides( Used to validate the inputs and graphene_type of @provides """ errors: list[str] = [] + auto_case = InternalNamespace.NO_AUTO_CASE.value not in inputs.get("fields") ast_node = build_ast( - fields=to_case(inputs.get("fields"), schema), directive_name="@provides" + fields=to_case(inputs.get("fields"), schema, auto_case), + directive_name="@provides", ) # Get the parent type of the field @@ -41,7 +43,10 @@ def validate_provides( directive_name="@provides", ast=ast_node, graphene_type=field_parent_type, - ignore_fields=[InternalNamespace.UNION.value], + ignore_fields=[ + InternalNamespace.UNION.value, + InternalNamespace.NO_AUTO_CASE.value, + ], errors=errors, entity_types=schema.graphql_schema.type_map, ) diff --git a/graphene_federation/validators/requires.py b/graphene_federation/validators/requires.py index c222d8c..3ea042f 100644 --- a/graphene_federation/validators/requires.py +++ b/graphene_federation/validators/requires.py @@ -8,7 +8,7 @@ def validate_requires( parent_type: Union[ObjectType, Interface], - _field: Field, + field: Field, inputs: dict, schema: Schema, ) -> bool: @@ -16,14 +16,20 @@ def validate_requires( Used to validate the inputs and graphene_type of @requires """ errors: list[str] = [] + auto_case = InternalNamespace.NO_AUTO_CASE.value not in inputs.get("fields") ast_node = build_ast( - fields=to_case(inputs.get("fields"), schema), directive_name="@requires" + fields=to_case(inputs.get("fields"), schema, auto_case), + directive_name="@requires", ) evaluate_ast( directive_name="@requires", ast=ast_node, graphene_type=parent_type, - ignore_fields=["__typename", InternalNamespace.UNION.value], + ignore_fields=[ + "__typename", + InternalNamespace.NO_AUTO_CASE.value, + InternalNamespace.UNION.value, + ], errors=errors, entity_types=schema.graphql_schema.type_map, ) diff --git a/graphene_federation/validators/utils.py b/graphene_federation/validators/utils.py index 382e311..a812fae 100644 --- a/graphene_federation/validators/utils.py +++ b/graphene_federation/validators/utils.py @@ -35,6 +35,7 @@ class InternalNamespace(Enum): UNION = "__union__" ARG = "__arg__" + NO_AUTO_CASE = "__no_auto_case__" def check_fields_exist_on_type( @@ -356,7 +357,7 @@ def ast_to_str(fields: dict, add_type_name: bool = False, level: int = 0) -> str """ -def to_case(fields: Union[str, None], schema: Schema) -> str: +def to_case(fields: Union[str, None], schema: Schema, auto_case: bool = True) -> str: """ Converts field str to correct casing according to the schema.auto_camelcase value """ @@ -365,7 +366,7 @@ def to_case(fields: Union[str, None], schema: Schema) -> str: skip_next = False - if schema.auto_camelcase: + if schema.auto_camelcase and auto_case: data_fields = [] for field in fields.split(): if field == InternalNamespace.UNION.value: From 18ae1d34d0257f9fc119dbc3f29e4422fb8a4964 Mon Sep 17 00:00:00 2001 From: M Aswin Kishore <60577077+mak626@users.noreply.github.com> Date: Tue, 16 Jan 2024 13:55:03 +0530 Subject: [PATCH 09/19] support: custom field names --- graphene_federation/entity.py | 4 +- graphene_federation/main.py | 5 +- .../transform/field_set_case_transform.py | 2 +- graphene_federation/validators/key.py | 2 +- graphene_federation/validators/provides.py | 2 +- graphene_federation/validators/requires.py | 4 +- integration_tests/tests/tests/test_main.py | 4 +- tests/test_custom_field_names.py | 192 ++++++++++++++++++ 8 files changed, 207 insertions(+), 8 deletions(-) create mode 100644 tests/test_custom_field_names.py diff --git a/graphene_federation/entity.py b/graphene_federation/entity.py index a8b4339..38322cb 100644 --- a/graphene_federation/entity.py +++ b/graphene_federation/entity.py @@ -117,7 +117,9 @@ def resolve_entities(self, info, representations, sub_field_resolution=False): ): for sub_value in value: if sub_value.get("__typename") is None: - sub_value["__typename"] = field.of_type._meta.name # noqa + sub_value[ + "__typename" + ] = field.of_type._meta.name # noqa model_arguments[model_field] = EntityQuery.resolve_entities( self, info, representations=value, sub_field_resolution=True ) diff --git a/graphene_federation/main.py b/graphene_federation/main.py index 850373d..2a4c6da 100644 --- a/graphene_federation/main.py +++ b/graphene_federation/main.py @@ -43,7 +43,7 @@ def _get_federation_query( def _add_sharable_to_page_info_type( schema: Schema, federation_version: FederationVersion, - types: list[ObjectType | Type[ObjectType]], + types: list[Union[ObjectType, Type[ObjectType]]], ): """ Add @sharable directive to PageInfo type @@ -175,6 +175,9 @@ def build_schema( if not directive.add_to_schema_directives: continue + if not directive.spec_url: + continue + _imports = url__imports.get(directive.spec_url) if _imports: _imports.append(str(directive)) diff --git a/graphene_federation/transform/field_set_case_transform.py b/graphene_federation/transform/field_set_case_transform.py index 7f3acaa..c2f3416 100644 --- a/graphene_federation/transform/field_set_case_transform.py +++ b/graphene_federation/transform/field_set_case_transform.py @@ -12,7 +12,7 @@ def field_set_case_transform(inputs: dict, schema: Schema) -> dict: __arg__ for representing (arg1: value1, arg2: value2) """ fields = inputs.get("fields") - auto_case = InternalNamespace.NO_AUTO_CASE.value not in inputs.get("fields") + auto_case = InternalNamespace.NO_AUTO_CASE.value not in inputs.get("fields", ()) if fields: inputs["fields"] = ( to_case(fields, schema, auto_case) diff --git a/graphene_federation/validators/key.py b/graphene_federation/validators/key.py index 25e2afe..cba49d9 100644 --- a/graphene_federation/validators/key.py +++ b/graphene_federation/validators/key.py @@ -13,7 +13,7 @@ def validate_key( Used to validate the inputs and graphene_type of @key """ errors: list[str] = [] - auto_case = InternalNamespace.NO_AUTO_CASE.value not in inputs.get("fields") + auto_case = InternalNamespace.NO_AUTO_CASE.value not in inputs.get("fields", ()) ast_node = build_ast( fields=to_case(inputs.get("fields"), schema, auto_case), directive_name="@key" ) diff --git a/graphene_federation/validators/provides.py b/graphene_federation/validators/provides.py index dc5c7f7..4066981 100644 --- a/graphene_federation/validators/provides.py +++ b/graphene_federation/validators/provides.py @@ -21,7 +21,7 @@ def validate_provides( Used to validate the inputs and graphene_type of @provides """ errors: list[str] = [] - auto_case = InternalNamespace.NO_AUTO_CASE.value not in inputs.get("fields") + auto_case = InternalNamespace.NO_AUTO_CASE.value not in inputs.get("fields", ()) ast_node = build_ast( fields=to_case(inputs.get("fields"), schema, auto_case), directive_name="@provides", diff --git a/graphene_federation/validators/requires.py b/graphene_federation/validators/requires.py index 3ea042f..0aa8203 100644 --- a/graphene_federation/validators/requires.py +++ b/graphene_federation/validators/requires.py @@ -8,7 +8,7 @@ def validate_requires( parent_type: Union[ObjectType, Interface], - field: Field, + _field: Field, inputs: dict, schema: Schema, ) -> bool: @@ -16,7 +16,7 @@ def validate_requires( Used to validate the inputs and graphene_type of @requires """ errors: list[str] = [] - auto_case = InternalNamespace.NO_AUTO_CASE.value not in inputs.get("fields") + auto_case = InternalNamespace.NO_AUTO_CASE.value not in inputs.get("fields", ()) ast_node = build_ast( fields=to_case(inputs.get("fields"), schema, auto_case), directive_name="@requires", diff --git a/integration_tests/tests/tests/test_main.py b/integration_tests/tests/tests/test_main.py index ebc771d..4df72bf 100644 --- a/integration_tests/tests/tests/test_main.py +++ b/integration_tests/tests/tests/test_main.py @@ -71,7 +71,9 @@ def test_external_types(): assert { "id": 1001, "primaryEmail": "frank@frank.com", - } == posts[3]["author"] + } == posts[ + 3 + ]["author"] assert articles == [ { diff --git a/tests/test_custom_field_names.py b/tests/test_custom_field_names.py new file mode 100644 index 0000000..86cf8d4 --- /dev/null +++ b/tests/test_custom_field_names.py @@ -0,0 +1,192 @@ +from pathlib import Path + +import pytest +from graphene import Field, ID, ObjectType, String + +from graphene_federation import build_schema, key, provides, requires +from tests.util import file_handlers + +save_file, open_file = file_handlers(Path(__file__)) + + +def test_key_auto_camelcase_false(): + try: + + @key("identifier") + @key("valid_email") + class User(ObjectType): + identifier = ID() + email = String(name="valid_email") + + class Query(ObjectType): + user = Field(User) + + _schema = build_schema(query=Query, auto_camelcase=False) + except Exception as exc: + pytest.fail(f"Unexpected Error {exc}") + + +def test_key_auto_camelcase_true(): + with pytest.raises(ValueError) as err: + + @key("identifier") + @key("valid_email") + class User(ObjectType): + identifier = ID() + email = String(name="valid_email") + + class Query(ObjectType): + user = Field(User) + + _schema = build_schema(query=Query, auto_camelcase=True) + + assert str(err.value) == '@key, field "validEmail" does not exist on type "User"' + + +def test_key_auto_camelcase_with_auto_case_false(): + try: + + @key("identifier") + @key("valid_email", auto_case=False) + class User(ObjectType): + identifier = ID() + email = String(name="valid_email") + + class Query(ObjectType): + user = Field(User) + + _schema = build_schema(query=Query, auto_camelcase=True) + except Exception as exc: + pytest.fail(f"Unexpected Error {exc}") + + +def test_requires_auto_camelcase_false(): + try: + + class Employee(ObjectType): + identifier = ID() + email = String(name="corp_email") + + class User(ObjectType): + identifier = ID() + employee = Field(Employee) + email = requires(String(), fields="employee { corp_email }") + + class Query(ObjectType): + user = Field(User) + + _schema = build_schema(query=Query, auto_camelcase=False) + except Exception as exc: + pytest.fail(f"Unexpected Error {exc}") + + +def test_requires_auto_camelcase_true(): + with pytest.raises(ValueError) as err: + + class Employee(ObjectType): + identifier = ID() + email = String(name="corp_email") + + class User(ObjectType): + identifier = ID() + employee = Field(Employee) + email = requires(String(), fields="employee { corp_email }") + + class Query(ObjectType): + user = Field(User) + + _schema = build_schema(query=Query, auto_camelcase=True) + + assert ( + str(err.value) + == '@requires, field "corpEmail" does not exist on type "Employee"' + ) + + +def test_requires_auto_camelcase_with_auto_case_false(): + try: + + class Employee(ObjectType): + identifier = ID() + email = String(name="corp_email") + + class User(ObjectType): + identifier = ID() + employee = Field(Employee) + email = requires( + String(), fields="employee { corp_email }", auto_case=False + ) + + class Query(ObjectType): + user = Field(User) + + _schema = build_schema(query=Query, auto_camelcase=True) + + except Exception as exc: + pytest.fail(f"Unexpected Error {exc}") + + +def test_provides_auto_camelcase_false(): + try: + + class Employee(ObjectType): + identifier = ID() + email = String(name="corp_email") + + class User(ObjectType): + identifier = ID() + employee = Field(Employee) + email = String() + + class Query(ObjectType): + user = provides(Field(User), fields="employee { corp_email }") + + _schema = build_schema(query=Query, auto_camelcase=False) + except Exception as exc: + pytest.fail(f"Unexpected Error {exc}") + + +def test_provides_auto_camelcase_true(): + with pytest.raises(ValueError) as err: + + class Employee(ObjectType): + identifier = ID() + email = String(name="corp_email") + + class User(ObjectType): + identifier = ID() + employee = Field(Employee) + email = String() + + class Query(ObjectType): + user = provides(Field(User), fields="employee { corp_email }") + + _schema = build_schema(query=Query, auto_camelcase=True) + + assert ( + str(err.value) + == '@provides, field "corpEmail" does not exist on type "Employee"' + ) + + +def test_provides_auto_camelcase_with_auto_case_false(): + try: + + class Employee(ObjectType): + identifier = ID() + email = String(name="corp_email") + + class User(ObjectType): + identifier = ID() + employee = Field(Employee) + email = String() + + class Query(ObjectType): + user = provides( + Field(User), fields="employee { corp_email }", auto_case=False + ) + + _schema = build_schema(query=Query, auto_camelcase=True) + + except Exception as exc: + pytest.fail(f"Unexpected Error {exc}") From db3b01591dbd3440bd6ecd0a094b9ca604471840 Mon Sep 17 00:00:00 2001 From: M Aswin Kishore <60577077+mak626@users.noreply.github.com> Date: Tue, 16 Jan 2024 13:55:52 +0530 Subject: [PATCH 10/19] docs: update readme --- README.md | 232 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 218 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index e68dde3..ff08cf7 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Federation support for ![Graphene Logo](http://graphene-python.org/favicon.png) This repository is heavily based on the repo it was forked from... Huge thanks to [Preply for setting up the foundations](https://medium.com/preply-engineering/apollo-federation-support-in-graphene-761a0512456d). + WARNING: This version is not compatible with `graphene` version below v3. If you need to use a version compatible with `graphene` v2 I recommend using the version 1.0.0 of `graphene_federation`. @@ -26,14 +27,82 @@ If you need to use a version compatible with `graphene` v2 I recommend using the ## Supported Features -At the moment it supports: - * `sdl` (`_service` on field): enable to add schema in federation (as is) -* `@key` decorator (entity support): enable to perform queries across service boundaries (you can have more than one key per type) -* `@extends`: extend remote types -* `external()`: mark a field as external -* `requires()`: mark that field resolver requires other fields to be pre-fetched -* `provides()`/`@provides`: annotate the expected returned fieldset from a field on a base type that is guaranteed to be selectable by the gateway. + +## Apollo Spec Supported + +- [x] v1.0 +- [x] v2.0 +- [x] v2.1 +- [x] v2.2 +- [x] v2.3 +- [x] v2.4 +- [x] v2.5 +- [x] v2.6 + +All directives could be easily integrated with the help of [graphene-directives](https://github.com/strollby/graphene-directives). +Now every directive's values are validated at run time itself by [graphene-directives](https://github.com/strollby/graphene-directives). + +### Directives (v2.6) + +```graphql +directive @composeDirective(name: String!) repeatable on SCHEMA +directive @extends on OBJECT | INTERFACE +directive @external on OBJECT | FIELD_DEFINITION +directive @key(fields: FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE +directive @inaccessible on + | FIELD_DEFINITION + | OBJECT + | INTERFACE + | UNION + | ENUM + | ENUM_VALUE + | SCALAR + | INPUT_OBJECT + | INPUT_FIELD_DEFINITION + | ARGUMENT_DEFINITION +directive @interfaceObject on OBJECT +directive @override(from: String!) on FIELD_DEFINITION +directive @provides(fields: FieldSet!) on FIELD_DEFINITION +directive @requires(fields: FieldSet!) on FIELD_DEFINITION +directive @shareable repeatable on FIELD_DEFINITION | OBJECT +directive @tag(name: String!) repeatable on + | FIELD_DEFINITION + | INTERFACE + | OBJECT + | UNION + | ARGUMENT_DEFINITION + | SCALAR + | ENUM + | ENUM_VALUE + | INPUT_OBJECT + | INPUT_FIELD_DEFINITION +directive @authenticated on + FIELD_DEFINITION + | OBJECT + | INTERFACE + | SCALAR + | ENUM +directive @requiresScopes(scopes: [[Scope!]!]!) on + FIELD_DEFINITION + | OBJECT + | INTERFACE + | SCALAR + | ENUM +directive @policy(policies: [[federation__Policy!]!]!) on + | FIELD_DEFINITION + | OBJECT + | INTERFACE + | SCALAR + | ENUM +scalar federation__Policy +scalar Scope +scalar FieldSet + +``` + +Read about directives in [official documentation](https://www.apollographql.com/docs/federation/federated-types/federated-directives) + Each type which is decorated with `@key` or `@extends` is added to the `_Entity` union. The [`__resolve_reference` method](https://www.apollographql.com/docs/federation/api/apollo-federation/#__resolvereference) can be defined for each type that is an entity. @@ -58,8 +127,10 @@ First add an account service that expose a `User` type that can then be referenc ```python from graphene import Field, Int, ObjectType, String + from graphene_federation import build_schema, key + @key("id") class User(ObjectType): id = Int(required=True) @@ -71,10 +142,12 @@ class User(ObjectType): """ return User(id=self.id, email=f"user_{self.id}@mail.com") + class Query(ObjectType): me = Field(User) -schema = build_schema(query=Query) + +schema = build_schema(query=Query, enable_federation_2=True) ``` ### Product @@ -82,8 +155,10 @@ The product service exposes a `Product` type that can be used by other services ```python from graphene import Argument, Int, List, ObjectType, String + from graphene_federation import build_schema, key + @key("upc") class Product(ObjectType): upc = String(required=True) @@ -96,10 +171,12 @@ class Product(ObjectType): """ return Product(upc=self.upc, name=f"product {self.upc}") + class Query(ObjectType): topProducts = List(Product, first=Argument(Int, default_value=5)) -schema = build_schema(query=Query) + +schema = build_schema(query=Query, enable_federation_2=True) ``` ### Reviews @@ -110,7 +187,7 @@ On top of that it adds to the `User`/`Product` types (that are both defined in o ```python from graphene import Field, Int, List, ObjectType, String -from graphene_federation import build_schema, extends, external, key, provides +from graphene_federation import build_schema, external, key, provides @key("id") @@ -141,7 +218,7 @@ class Query(ObjectType): review = Field(Review) -schema = build_schema(query=Query) +schema = build_schema(query=Query, enable_federation_2=True) ``` ### Federation @@ -171,12 +248,139 @@ You can find more examples in the unit / integration tests and [examples folder] There is also a cool [example](https://github.com/preply/graphene-federation/issues/1) of integration with Mongoengine. ------------------------ +## Other Notes + +### build_schema new arguments + +- `schema_directives` (`Collection[SchemaDirective]`): Directives that can be defined at `DIRECTIVE_LOCATION.SCHEMA` with their argument values. +- `include_graphql_spec_directives` (`bool`): Includes directives defined by GraphQL spec (`@include`, `@skip`, `@deprecated`, `@specifiedBy`) +- `enable_federation_2` (`bool`): Whether to enable federation 2 directives (default False) +- `federation_version` (`FederationVersion`): Specify the version explicit (default LATEST_VERSION) + +In case both enable_federation_2 and federation_version are specified, federation_version is given higher priority + +### Directives Additional arguments + +- `federation_version`: (`FederationVersion` = `LATEST_VERSION`) : You can use this to take a directive from a particular federation version + +Note: The `federation_version` in `build_schema` is given higher priority. If the directive you have chosen is not compatible, it will raise an error + +### Custom Directives + +You can define custom directives as follows + +```python +from graphene import Field, ObjectType, String +from graphql import GraphQLArgument, GraphQLInt, GraphQLNonNull + +from graphene_federation import DirectiveLocation, FederationDirective +from graphene_federation import build_schema + +CacheDirective = FederationDirective( + name="cache", + locations=[DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.OBJECT], + args={ + "maxAge": GraphQLArgument( + GraphQLNonNull(GraphQLInt), description="Specifies the maximum age for cache in seconds." + ), + }, + description="Caching directive to control cache behavior.", + spec_url="https://specs.example.dev/directives/v1.0", +) + +cache = CacheDirective.decorator() + + +@cache(max_age=20) +class Review(ObjectType): + body = cache(field=String(),max_age=100) + + +class Query(ObjectType): + review = Field(Review) + + +schema = build_schema( + query=Query, + directives=(CacheDirective,), + enable_federation_2=True, +) +``` + +This will automatically add @link and @composeDirective to schema + + +```graphql +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@composeDirective"]) + @link(url: "https://specs.example.dev/directives/v1.0", import: ["@cache"]) + @composeDirective(name: "@cache") + +"""Caching directive to control cache behavior.""" +directive @cache( + """Specifies the maximum age for cache in seconds.""" + maxAge: Int! +) on FIELD_DEFINITION | OBJECT + +type Query { + review: Review + _service: _Service! +} + +type Review @cache(maxAge: 20) { + body: String @cache(maxAge: 100) +} +``` + +If you wish to add the schema_directives `@link` `@composeDirective` manually. +You can pass the `add_to_schema_directives` as `False` + +```python +from graphene import Field, ObjectType, String +from graphql import GraphQLArgument, GraphQLInt, GraphQLNonNull + +from graphene_federation import DirectiveLocation, FederationDirective, build_schema, compose_directive, link_directive + +CacheDirective = FederationDirective( + name="cache", + locations=[DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.OBJECT], + args={ + "maxAge": GraphQLArgument( + GraphQLNonNull(GraphQLInt), description="Specifies the maximum age for cache in seconds." + ), + }, + description="Caching directive to control cache behavior.", + add_to_schema_directives=False +) + +cache = CacheDirective.decorator() + + +@cache(max_age=20) +class Review(ObjectType): + body = cache(field=String(), max_age=100) + + +class Query(ObjectType): + review = Field(Review) + + +schema = build_schema( + query=Query, + directives=(CacheDirective,), + schema_directives=( + link_directive(url="https://specs.example.dev/directives/v1.0", import_=['@cache']), + compose_directive(name='@cache'), + ), + enable_federation_2=True, +) +``` -## Custom field name +### Custom field name When using decorator on a field with custom name -1. Case 1 (auto_camelcase=False) +#### Case 1 (auto_camelcase=False) ```python @key("identifier") @@ -194,7 +398,7 @@ schema = build_schema(query=Query, enable_federation_2=True, auto_camelcase=Fals This works correctly. By default `fields` of `@key`,`@requires` and `@provides` are not converted to camel case if `auto_camelcase` is set to `False` -2. Case 2 (auto_camelcase=True) +#### Case 2 (auto_camelcase=True) ```python @key("identifier") @key("valid_email") From 7c16a4a571579fd0eb018f1b34cd3d51328b6c58 Mon Sep 17 00:00:00 2001 From: M Aswin Kishore <60577077+mak626@users.noreply.github.com> Date: Tue, 16 Jan 2024 17:28:44 +0530 Subject: [PATCH 11/19] docs: add rover support upto 2.5v issue --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index ff08cf7..bb1946b 100644 --- a/README.md +++ b/README.md @@ -432,6 +432,10 @@ schema = build_schema(query=Query, enable_federation_2=True) # auto_camelcase=Tr ------------------------ +## Known Issues + +- Using `@composeDirective` with `@link` in Federation `v2.6` shows error in rover, rover cli only supports upto `v2.5` as of 16/01/2024 + ## Contributing * You can run the unit tests by doing: `make tests`. From cc03581b9bc16b8eacaafc00c4f161cf3e7e4430 Mon Sep 17 00:00:00 2001 From: M Aswin Kishore <60577077+mak626@users.noreply.github.com> Date: Tue, 16 Jan 2024 18:05:41 +0530 Subject: [PATCH 12/19] refact: add description for scalars --- README.md | 4 ++-- federation_spec/federation-v2.5.graphql | 4 ++-- federation_spec/federation-v2.6.graphql | 4 ++-- graphene_federation/apollo_versions/v2_5.py | 6 ++++-- graphene_federation/scalars/__init__.py | 2 +- graphene_federation/scalars/_any.py | 2 +- graphene_federation/scalars/federation_policy.py | 1 + .../scalars/{scope.py => federation_scope.py} | 5 +++-- graphene_federation/scalars/field_set_v1.py | 10 +++++++++- graphene_federation/scalars/field_set_v2.py | 10 +++++++++- graphene_federation/scalars/link_import.py | 8 ++++++-- graphene_federation/scalars/link_purpose.py | 1 + .../test_annotate_object_with_meta_name_1.graphql | 7 ++++++- .../test_annotate_object_with_meta_name_2.graphql | 7 ++++++- .../test_annotated_field_also_used_in_filter_1.graphql | 7 ++++++- .../test_annotated_field_also_used_in_filter_2.graphql | 7 ++++++- .../test_camel_case_field_name_1.graphql | 7 ++++++- .../test_camel_case_field_name_2.graphql | 7 ++++++- ...el_case_field_name_without_auto_camelcase_1.graphql | 7 ++++++- ...el_case_field_name_without_auto_camelcase_2.graphql | 7 ++++++- .../test_similar_field_name_1.graphql | 7 ++++++- .../test_similar_field_name_2.graphql | 7 ++++++- .../test_annotate_object_with_meta_name_1.graphql | 3 +++ .../test_annotate_object_with_meta_name_2.graphql | 3 +++ .../test_annotated_field_also_used_in_filter_1.graphql | 3 +++ .../test_annotated_field_also_used_in_filter_2.graphql | 3 +++ .../test_camel_case_field_name_1.graphql | 3 +++ .../test_camel_case_field_name_2.graphql | 3 +++ ...el_case_field_name_without_auto_camelcase_1.graphql | 3 +++ ...el_case_field_name_without_auto_camelcase_2.graphql | 3 +++ .../test_similar_field_name_1.graphql | 3 +++ .../test_similar_field_name_2.graphql | 3 +++ tests/gql/test_custom_enum/test_custom_enum_1.graphql | 7 ++++++- tests/gql/test_custom_enum/test_custom_enum_2.graphql | 7 ++++++- .../gql/test_inaccessible/test_inaccessible_1.graphql | 7 ++++++- .../gql/test_inaccessible/test_inaccessible_2.graphql | 7 ++++++- .../test_inaccessible_union_1.graphql | 7 ++++++- .../test_inaccessible_union_2.graphql | 7 ++++++- tests/gql/test_key/test_compound_primary_key_1.graphql | 7 ++++++- tests/gql/test_key/test_compound_primary_key_2.graphql | 7 ++++++- .../test_compound_primary_key_with_depth_1.graphql | 7 ++++++- .../test_compound_primary_key_with_depth_2.graphql | 7 ++++++- tests/gql/test_key/test_multiple_keys_1.graphql | 7 ++++++- tests/gql/test_key/test_multiple_keys_2.graphql | 7 ++++++- tests/gql/test_key_v1/test_multiple_keys_1.graphql | 3 +++ tests/gql/test_key_v1/test_multiple_keys_2.graphql | 3 +++ tests/gql/test_provides/test_provides_1.graphql | 7 ++++++- tests/gql/test_provides/test_provides_2.graphql | 7 ++++++- .../test_provides_multiple_fields_1.graphql | 7 ++++++- .../test_provides_multiple_fields_2.graphql | 7 ++++++- .../test_provides_multiple_fields_as_list_1.graphql | 7 ++++++- .../test_provides_multiple_fields_as_list_2.graphql | 7 ++++++- tests/gql/test_provides_v1/test_provides_1.graphql | 3 +++ tests/gql/test_provides_v1/test_provides_2.graphql | 3 +++ .../test_provides_multiple_fields_1.graphql | 3 +++ .../test_provides_multiple_fields_2.graphql | 3 +++ .../test_provides_multiple_fields_as_list_1.graphql | 3 +++ .../test_provides_multiple_fields_as_list_2.graphql | 3 +++ .../test_requires_multiple_fields_1.graphql | 7 ++++++- .../test_requires_multiple_fields_2.graphql | 7 ++++++- .../test_requires_multiple_fields_as_list_1.graphql | 7 ++++++- .../test_requires_multiple_fields_as_list_2.graphql | 7 ++++++- .../test_requires/test_requires_with_input_1.graphql | 7 ++++++- .../test_requires/test_requires_with_input_2.graphql | 7 ++++++- .../test_requires_multiple_fields_1.graphql | 3 +++ .../test_requires_multiple_fields_2.graphql | 3 +++ .../test_requires_multiple_fields_as_list_1.graphql | 3 +++ .../test_requires_multiple_fields_as_list_2.graphql | 3 +++ .../test_requires_with_input_1.graphql | 3 +++ .../test_requires_with_input_2.graphql | 3 +++ tests/gql/test_scalar/test_custom_scalar_1.graphql | 7 ++++++- tests/gql/test_scalar/test_custom_scalar_2.graphql | 7 ++++++- .../test_schema_annotation/test_chat_schema_1.graphql | 7 ++++++- .../test_schema_annotation/test_chat_schema_2.graphql | 7 ++++++- .../test_schema_annotation/test_user_schema_1.graphql | 7 ++++++- .../test_schema_annotation/test_user_schema_2.graphql | 7 ++++++- .../test_chat_schema_1.graphql | 3 +++ .../test_chat_schema_2.graphql | 3 +++ .../test_user_schema_1.graphql | 3 +++ .../test_user_schema_2.graphql | 3 +++ tests/gql/test_shareable/test_shareable_1.graphql | 7 ++++++- tests/gql/test_shareable/test_shareable_2.graphql | 7 ++++++- 82 files changed, 377 insertions(+), 58 deletions(-) rename graphene_federation/scalars/{scope.py => federation_scope.py} (90%) diff --git a/README.md b/README.md index bb1946b..c128ed7 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ directive @authenticated on | INTERFACE | SCALAR | ENUM -directive @requiresScopes(scopes: [[Scope!]!]!) on +directive @requiresScopes(scopes: [[federation__Scope!]!]!) on FIELD_DEFINITION | OBJECT | INTERFACE @@ -96,7 +96,7 @@ directive @policy(policies: [[federation__Policy!]!]!) on | SCALAR | ENUM scalar federation__Policy -scalar Scope +scalar federation__Scope scalar FieldSet ``` diff --git a/federation_spec/federation-v2.5.graphql b/federation_spec/federation-v2.5.graphql index 6089be2..815ff2c 100644 --- a/federation_spec/federation-v2.5.graphql +++ b/federation_spec/federation-v2.5.graphql @@ -35,11 +35,11 @@ directive @authenticated on | INTERFACE | SCALAR | ENUM -directive @requiresScopes(scopes: [[Scope!]!]!) on +directive @requiresScopes(scopes: [[federation__Scope!]!]!) on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM -scalar Scope +scalar federation__Scope scalar FieldSet diff --git a/federation_spec/federation-v2.6.graphql b/federation_spec/federation-v2.6.graphql index 0151c2d..ebff3a8 100644 --- a/federation_spec/federation-v2.6.graphql +++ b/federation_spec/federation-v2.6.graphql @@ -35,7 +35,7 @@ directive @authenticated on | INTERFACE | SCALAR | ENUM -directive @requiresScopes(scopes: [[Scope!]!]!) on +directive @requiresScopes(scopes: [[federation__Scope!]!]!) on FIELD_DEFINITION | OBJECT | INTERFACE @@ -48,5 +48,5 @@ directive @policy(policies: [[federation__Policy!]!]!) on | SCALAR | ENUM scalar federation__Policy -scalar Scope +scalar federation__Scope scalar FieldSet diff --git a/graphene_federation/apollo_versions/v2_5.py b/graphene_federation/apollo_versions/v2_5.py index d569284..190e7de 100644 --- a/graphene_federation/apollo_versions/v2_5.py +++ b/graphene_federation/apollo_versions/v2_5.py @@ -2,7 +2,7 @@ from graphql import GraphQLArgument, GraphQLDirective, GraphQLList, GraphQLNonNull from .v2_4 import get_directives as get_directives_v2_4 -from graphene_federation.scalars import Scope +from graphene_federation.scalars import FederationScope authenticated_directive = CustomDirective( name="authenticated", @@ -29,7 +29,9 @@ args={ "scopes": GraphQLArgument( GraphQLNonNull( - GraphQLList(GraphQLNonNull(GraphQLList(GraphQLNonNull(Scope)))) + GraphQLList( + GraphQLNonNull(GraphQLList(GraphQLNonNull(FederationScope))) + ) ) ), }, diff --git a/graphene_federation/scalars/__init__.py b/graphene_federation/scalars/__init__.py index bccde34..3bb899a 100644 --- a/graphene_federation/scalars/__init__.py +++ b/graphene_federation/scalars/__init__.py @@ -1,7 +1,7 @@ from ._any import _Any from .federation_policy import FederationPolicy +from .federation_scope import FederationScope from .field_set_v1 import _FieldSet from .field_set_v2 import FieldSet from .link_import import link_import from .link_purpose import link_purpose -from .scope import Scope diff --git a/graphene_federation/scalars/_any.py b/graphene_federation/scalars/_any.py index dc69fc4..06e3844 100644 --- a/graphene_federation/scalars/_any.py +++ b/graphene_federation/scalars/_any.py @@ -5,7 +5,7 @@ class _Any(Scalar): name = "_Any" __typename = String(required=True) - description = None + description = "A JSON serialized used for entity representations" specified_by_url = None @staticmethod diff --git a/graphene_federation/scalars/federation_policy.py b/graphene_federation/scalars/federation_policy.py index 463dc8a..d4934ed 100644 --- a/graphene_federation/scalars/federation_policy.py +++ b/graphene_federation/scalars/federation_policy.py @@ -46,6 +46,7 @@ def _parse_string_literal(value_node: ValueNode, _variables: Any = None) -> str: # Reference: https://www.apollographql.com/docs/federation/subgraph-spec/ FederationPolicy = GraphQLScalarType( name="federation__Policy", + description="This string-serialized scalar represents an authorization policy.", serialize=_serialize_string, parse_value=_coerce_string, parse_literal=_parse_string_literal, diff --git a/graphene_federation/scalars/scope.py b/graphene_federation/scalars/federation_scope.py similarity index 90% rename from graphene_federation/scalars/scope.py rename to graphene_federation/scalars/federation_scope.py index 1b5de0e..0ce7c9c 100644 --- a/graphene_federation/scalars/scope.py +++ b/graphene_federation/scalars/federation_scope.py @@ -40,8 +40,9 @@ def _parse_string_literal(value_node: ValueNode, _variables: Any = None) -> str: # Reference: https://www.apollographql.com/docs/federation/subgraph-spec/ -Scope = GraphQLScalarType( - name="Scope", +FederationScope = GraphQLScalarType( + name="federation__Scope", + description="This string-serialized scalar represents a JWT scope", serialize=_serialize_string, parse_value=_coerce_string, parse_literal=_parse_string_literal, diff --git a/graphene_federation/scalars/field_set_v1.py b/graphene_federation/scalars/field_set_v1.py index e5fcc02..1dee96d 100644 --- a/graphene_federation/scalars/field_set_v1.py +++ b/graphene_federation/scalars/field_set_v1.py @@ -1,4 +1,12 @@ from graphql import GraphQLScalarType # Reference: https://www.apollographql.com/docs/federation/subgraph-spec/ -_FieldSet = GraphQLScalarType(name="_FieldSet") +_FieldSet = GraphQLScalarType( + name="_FieldSet", + description=" ".join( + ( + "A string-serialized scalar represents a set of fields that's passed to a federated directive,", + "such as @key, @requires, or @provides", + ) + ), +) diff --git a/graphene_federation/scalars/field_set_v2.py b/graphene_federation/scalars/field_set_v2.py index f6b8c3e..5c6b900 100644 --- a/graphene_federation/scalars/field_set_v2.py +++ b/graphene_federation/scalars/field_set_v2.py @@ -1,4 +1,12 @@ from graphql import GraphQLScalarType # Reference: https://www.apollographql.com/docs/federation/subgraph-spec/ -FieldSet = GraphQLScalarType(name="FieldSet") +FieldSet = GraphQLScalarType( + name="FieldSet", + description=" ".join( + ( + "A string-serialized scalar represents a set of fields that's passed to a federated directive,", + "such as @key, @requires, or @provides", + ) + ), +) diff --git a/graphene_federation/scalars/link_import.py b/graphene_federation/scalars/link_import.py index 86faa8e..ac2a88c 100644 --- a/graphene_federation/scalars/link_import.py +++ b/graphene_federation/scalars/link_import.py @@ -49,10 +49,14 @@ def _parse_string_literal(value_node: ValueNode, _variables: Any = None) -> str: return value_node.value -# Reference: https://www.apollographql.com/docs/federation/subgraph-spec/ - link_import = GraphQLScalarType( name="link__Import", + description=" ".join( + ( + "A string serialized scalar specify which directives from an external federation specification", + "should be imported into the current schema when using @link", + ) + ), serialize=_serialize_string, parse_value=_coerce_string, parse_literal=_parse_string_literal, diff --git a/graphene_federation/scalars/link_purpose.py b/graphene_federation/scalars/link_purpose.py index 2aade95..c9a36df 100644 --- a/graphene_federation/scalars/link_purpose.py +++ b/graphene_federation/scalars/link_purpose.py @@ -4,6 +4,7 @@ link_purpose = GraphQLEnumType( name="link__Purpose", + description="An Enum to clarify the type of directives (security, execution) in the specification", values={ "SECURITY": GraphQLEnumValue( value="SECURITY", diff --git a/tests/gql/test_annotation_corner_cases/test_annotate_object_with_meta_name_1.graphql b/tests/gql/test_annotation_corner_cases/test_annotate_object_with_meta_name_1.graphql index 02c9630..7dc8a97 100644 --- a/tests/gql/test_annotation_corner_cases/test_annotate_object_with_meta_name_1.graphql +++ b/tests/gql/test_annotation_corner_cases/test_annotate_object_with_meta_name_1.graphql @@ -24,8 +24,13 @@ type _Service { sdl: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar FieldSet -scalar Scope +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope +"""This string-serialized scalar represents an authorization policy.""" scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases/test_annotate_object_with_meta_name_2.graphql b/tests/gql/test_annotation_corner_cases/test_annotate_object_with_meta_name_2.graphql index 7d18cc1..c4e3a4d 100644 --- a/tests/gql/test_annotation_corner_cases/test_annotate_object_with_meta_name_2.graphql +++ b/tests/gql/test_annotation_corner_cases/test_annotate_object_with_meta_name_2.graphql @@ -14,8 +14,13 @@ type Potato @key(fields: "id") { id: ID } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar FieldSet -scalar Scope +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope +"""This string-serialized scalar represents an authorization policy.""" scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases/test_annotated_field_also_used_in_filter_1.graphql b/tests/gql/test_annotation_corner_cases/test_annotated_field_also_used_in_filter_1.graphql index c33e145..a8c4bac 100644 --- a/tests/gql/test_annotation_corner_cases/test_annotated_field_also_used_in_filter_1.graphql +++ b/tests/gql/test_annotation_corner_cases/test_annotated_field_also_used_in_filter_1.graphql @@ -24,8 +24,13 @@ type _Service { sdl: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar FieldSet -scalar Scope +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope +"""This string-serialized scalar represents an authorization policy.""" scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases/test_annotated_field_also_used_in_filter_2.graphql b/tests/gql/test_annotation_corner_cases/test_annotated_field_also_used_in_filter_2.graphql index bca01c5..094efb9 100644 --- a/tests/gql/test_annotation_corner_cases/test_annotated_field_also_used_in_filter_2.graphql +++ b/tests/gql/test_annotation_corner_cases/test_annotated_field_also_used_in_filter_2.graphql @@ -14,8 +14,13 @@ type B @key(fields: "id") { id: ID } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar FieldSet -scalar Scope +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope +"""This string-serialized scalar represents an authorization policy.""" scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_1.graphql b/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_1.graphql index a8f41d7..5c17726 100644 --- a/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_1.graphql +++ b/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_1.graphql @@ -22,8 +22,13 @@ type _Service { sdl: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar FieldSet -scalar Scope +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope +"""This string-serialized scalar represents an authorization policy.""" scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_2.graphql b/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_2.graphql index 3e56148..d0cd54f 100644 --- a/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_2.graphql +++ b/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_2.graphql @@ -12,8 +12,13 @@ type Camel @key(fields: "autoCamel") @extends { aCamel: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar FieldSet -scalar Scope +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope +"""This string-serialized scalar represents an authorization policy.""" scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_without_auto_camelcase_1.graphql b/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_without_auto_camelcase_1.graphql index 4475a5a..d4a7c57 100644 --- a/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_without_auto_camelcase_1.graphql +++ b/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_without_auto_camelcase_1.graphql @@ -22,8 +22,13 @@ type _Service { sdl: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar FieldSet -scalar Scope +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope +"""This string-serialized scalar represents an authorization policy.""" scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_without_auto_camelcase_2.graphql b/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_without_auto_camelcase_2.graphql index 2599e85..16b37c1 100644 --- a/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_without_auto_camelcase_2.graphql +++ b/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_without_auto_camelcase_2.graphql @@ -12,8 +12,13 @@ type Camel @extends { aCamel: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar FieldSet -scalar Scope +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope +"""This string-serialized scalar represents an authorization policy.""" scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases/test_similar_field_name_1.graphql b/tests/gql/test_annotation_corner_cases/test_similar_field_name_1.graphql index 74001b6..c4b3f67 100644 --- a/tests/gql/test_annotation_corner_cases/test_similar_field_name_1.graphql +++ b/tests/gql/test_annotation_corner_cases/test_similar_field_name_1.graphql @@ -32,8 +32,13 @@ type _Service { sdl: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar FieldSet -scalar Scope +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope +"""This string-serialized scalar represents an authorization policy.""" scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases/test_similar_field_name_2.graphql b/tests/gql/test_annotation_corner_cases/test_similar_field_name_2.graphql index 0a81854..f4cfea2 100644 --- a/tests/gql/test_annotation_corner_cases/test_similar_field_name_2.graphql +++ b/tests/gql/test_annotation_corner_cases/test_similar_field_name_2.graphql @@ -22,8 +22,13 @@ type ChatUser @key(fields: "id") @extends { ID: ID } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar FieldSet -scalar Scope +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope +"""This string-serialized scalar represents an authorization policy.""" scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases_v1/test_annotate_object_with_meta_name_1.graphql b/tests/gql/test_annotation_corner_cases_v1/test_annotate_object_with_meta_name_1.graphql index 33ea1a2..ab0f8dd 100644 --- a/tests/gql/test_annotation_corner_cases_v1/test_annotate_object_with_meta_name_1.graphql +++ b/tests/gql/test_annotation_corner_cases_v1/test_annotate_object_with_meta_name_1.graphql @@ -21,4 +21,7 @@ type _Service { sdl: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases_v1/test_annotate_object_with_meta_name_2.graphql b/tests/gql/test_annotation_corner_cases_v1/test_annotate_object_with_meta_name_2.graphql index 231213d..d0fb5b5 100644 --- a/tests/gql/test_annotation_corner_cases_v1/test_annotate_object_with_meta_name_2.graphql +++ b/tests/gql/test_annotation_corner_cases_v1/test_annotate_object_with_meta_name_2.graphql @@ -11,4 +11,7 @@ type Potato @key(fields: "id") { id: ID } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases_v1/test_annotated_field_also_used_in_filter_1.graphql b/tests/gql/test_annotation_corner_cases_v1/test_annotated_field_also_used_in_filter_1.graphql index 721113e..27b7f2e 100644 --- a/tests/gql/test_annotation_corner_cases_v1/test_annotated_field_also_used_in_filter_1.graphql +++ b/tests/gql/test_annotation_corner_cases_v1/test_annotated_field_also_used_in_filter_1.graphql @@ -21,4 +21,7 @@ type _Service { sdl: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases_v1/test_annotated_field_also_used_in_filter_2.graphql b/tests/gql/test_annotation_corner_cases_v1/test_annotated_field_also_used_in_filter_2.graphql index 04f332e..2672e53 100644 --- a/tests/gql/test_annotation_corner_cases_v1/test_annotated_field_also_used_in_filter_2.graphql +++ b/tests/gql/test_annotation_corner_cases_v1/test_annotated_field_also_used_in_filter_2.graphql @@ -11,4 +11,7 @@ type B @key(fields: "id") { id: ID } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_1.graphql b/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_1.graphql index 0f68074..7a811f7 100644 --- a/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_1.graphql +++ b/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_1.graphql @@ -19,4 +19,7 @@ type _Service { sdl: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_2.graphql b/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_2.graphql index 7722ee0..7f1c08b 100644 --- a/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_2.graphql +++ b/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_2.graphql @@ -9,4 +9,7 @@ type Camel @key(fields: "autoCamel") @extends { aCamel: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_without_auto_camelcase_1.graphql b/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_without_auto_camelcase_1.graphql index bf84d73..81cbc36 100644 --- a/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_without_auto_camelcase_1.graphql +++ b/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_without_auto_camelcase_1.graphql @@ -19,4 +19,7 @@ type _Service { sdl: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_without_auto_camelcase_2.graphql b/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_without_auto_camelcase_2.graphql index 059614d..8ac3be1 100644 --- a/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_without_auto_camelcase_2.graphql +++ b/tests/gql/test_annotation_corner_cases_v1/test_camel_case_field_name_without_auto_camelcase_2.graphql @@ -9,4 +9,7 @@ type Camel @extends { aCamel: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases_v1/test_similar_field_name_1.graphql b/tests/gql/test_annotation_corner_cases_v1/test_similar_field_name_1.graphql index 2cbfefc..765074e 100644 --- a/tests/gql/test_annotation_corner_cases_v1/test_similar_field_name_1.graphql +++ b/tests/gql/test_annotation_corner_cases_v1/test_similar_field_name_1.graphql @@ -29,4 +29,7 @@ type _Service { sdl: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_annotation_corner_cases_v1/test_similar_field_name_2.graphql b/tests/gql/test_annotation_corner_cases_v1/test_similar_field_name_2.graphql index 2a39f29..ca4b8ef 100644 --- a/tests/gql/test_annotation_corner_cases_v1/test_similar_field_name_2.graphql +++ b/tests/gql/test_annotation_corner_cases_v1/test_similar_field_name_2.graphql @@ -19,4 +19,7 @@ type ChatUser @key(fields: "id") @extends { ID: ID } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_custom_enum/test_custom_enum_1.graphql b/tests/gql/test_custom_enum/test_custom_enum_1.graphql index 69b38f9..1e84768 100644 --- a/tests/gql/test_custom_enum/test_custom_enum_1.graphql +++ b/tests/gql/test_custom_enum/test_custom_enum_1.graphql @@ -22,8 +22,13 @@ type _Service { sdl: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar FieldSet -scalar Scope +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope +"""This string-serialized scalar represents an authorization policy.""" scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_custom_enum/test_custom_enum_2.graphql b/tests/gql/test_custom_enum/test_custom_enum_2.graphql index 2eeaf13..791a0e8 100644 --- a/tests/gql/test_custom_enum/test_custom_enum_2.graphql +++ b/tests/gql/test_custom_enum/test_custom_enum_2.graphql @@ -17,8 +17,13 @@ type Query { test2: [TestCustomEnum]! } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar FieldSet -scalar Scope +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope +"""This string-serialized scalar represents an authorization policy.""" scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_inaccessible/test_inaccessible_1.graphql b/tests/gql/test_inaccessible/test_inaccessible_1.graphql index 7148fb5..4cf7915 100644 --- a/tests/gql/test_inaccessible/test_inaccessible_1.graphql +++ b/tests/gql/test_inaccessible/test_inaccessible_1.graphql @@ -15,8 +15,13 @@ type _Service { sdl: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar FieldSet -scalar Scope +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope +"""This string-serialized scalar represents an authorization policy.""" scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_inaccessible/test_inaccessible_2.graphql b/tests/gql/test_inaccessible/test_inaccessible_2.graphql index e80e6a9..eb973d7 100644 --- a/tests/gql/test_inaccessible/test_inaccessible_2.graphql +++ b/tests/gql/test_inaccessible/test_inaccessible_2.graphql @@ -10,8 +10,13 @@ type Query { inStockCount: Int! } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar FieldSet -scalar Scope +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope +"""This string-serialized scalar represents an authorization policy.""" scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_inaccessible/test_inaccessible_union_1.graphql b/tests/gql/test_inaccessible/test_inaccessible_union_1.graphql index 604651d..462e4f0 100644 --- a/tests/gql/test_inaccessible/test_inaccessible_union_1.graphql +++ b/tests/gql/test_inaccessible/test_inaccessible_union_1.graphql @@ -27,8 +27,13 @@ type _Service { sdl: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar FieldSet -scalar Scope +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope +"""This string-serialized scalar represents an authorization policy.""" scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_inaccessible/test_inaccessible_union_2.graphql b/tests/gql/test_inaccessible/test_inaccessible_union_2.graphql index 2061b12..d3b2645 100644 --- a/tests/gql/test_inaccessible/test_inaccessible_union_2.graphql +++ b/tests/gql/test_inaccessible/test_inaccessible_union_2.graphql @@ -22,8 +22,13 @@ type Query { inStockCount: Int! } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar FieldSet -scalar Scope +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope +"""This string-serialized scalar represents an authorization policy.""" scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_key/test_compound_primary_key_1.graphql b/tests/gql/test_key/test_compound_primary_key_1.graphql index f96641d..a1c644c 100644 --- a/tests/gql/test_key/test_compound_primary_key_1.graphql +++ b/tests/gql/test_key/test_compound_primary_key_1.graphql @@ -24,8 +24,13 @@ type _Service { sdl: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar FieldSet -scalar Scope +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope +"""This string-serialized scalar represents an authorization policy.""" scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_key/test_compound_primary_key_2.graphql b/tests/gql/test_key/test_compound_primary_key_2.graphql index c0fe521..ac94857 100644 --- a/tests/gql/test_key/test_compound_primary_key_2.graphql +++ b/tests/gql/test_key/test_compound_primary_key_2.graphql @@ -14,8 +14,13 @@ type Organization { registrationNumber: ID } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar FieldSet -scalar Scope +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope +"""This string-serialized scalar represents an authorization policy.""" scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_key/test_compound_primary_key_with_depth_1.graphql b/tests/gql/test_key/test_compound_primary_key_with_depth_1.graphql index 712c0dc..fc3ad3e 100644 --- a/tests/gql/test_key/test_compound_primary_key_with_depth_1.graphql +++ b/tests/gql/test_key/test_compound_primary_key_with_depth_1.graphql @@ -30,8 +30,13 @@ type _Service { sdl: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar FieldSet -scalar Scope +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope +"""This string-serialized scalar represents an authorization policy.""" scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_key/test_compound_primary_key_with_depth_2.graphql b/tests/gql/test_key/test_compound_primary_key_with_depth_2.graphql index 7703429..2275a7d 100644 --- a/tests/gql/test_key/test_compound_primary_key_with_depth_2.graphql +++ b/tests/gql/test_key/test_compound_primary_key_with_depth_2.graphql @@ -20,8 +20,13 @@ type BusinessUnit { name: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar FieldSet -scalar Scope +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope +"""This string-serialized scalar represents an authorization policy.""" scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_key/test_multiple_keys_1.graphql b/tests/gql/test_key/test_multiple_keys_1.graphql index f3bc99c..cc813bd 100644 --- a/tests/gql/test_key/test_multiple_keys_1.graphql +++ b/tests/gql/test_key/test_multiple_keys_1.graphql @@ -20,8 +20,13 @@ type _Service { sdl: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar FieldSet -scalar Scope +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope +"""This string-serialized scalar represents an authorization policy.""" scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_key/test_multiple_keys_2.graphql b/tests/gql/test_key/test_multiple_keys_2.graphql index 640a505..e1bf5cd 100644 --- a/tests/gql/test_key/test_multiple_keys_2.graphql +++ b/tests/gql/test_key/test_multiple_keys_2.graphql @@ -10,8 +10,13 @@ type User @key(fields: "email") @key(fields: "identifier") { email: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar FieldSet -scalar Scope +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope +"""This string-serialized scalar represents an authorization policy.""" scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_key_v1/test_multiple_keys_1.graphql b/tests/gql/test_key_v1/test_multiple_keys_1.graphql index d25d8ef..413f025 100644 --- a/tests/gql/test_key_v1/test_multiple_keys_1.graphql +++ b/tests/gql/test_key_v1/test_multiple_keys_1.graphql @@ -17,4 +17,7 @@ type _Service { sdl: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_key_v1/test_multiple_keys_2.graphql b/tests/gql/test_key_v1/test_multiple_keys_2.graphql index 66f2ef3..62870c2 100644 --- a/tests/gql/test_key_v1/test_multiple_keys_2.graphql +++ b/tests/gql/test_key_v1/test_multiple_keys_2.graphql @@ -7,4 +7,7 @@ type User @key(fields: "email") @key(fields: "identifier") { email: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_provides/test_provides_1.graphql b/tests/gql/test_provides/test_provides_1.graphql index 4ca98f8..28c11f0 100644 --- a/tests/gql/test_provides/test_provides_1.graphql +++ b/tests/gql/test_provides/test_provides_1.graphql @@ -26,8 +26,13 @@ type _Service { sdl: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar FieldSet -scalar Scope +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope +"""This string-serialized scalar represents an authorization policy.""" scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_provides/test_provides_2.graphql b/tests/gql/test_provides/test_provides_2.graphql index 8d7805b..a91ca1a 100644 --- a/tests/gql/test_provides/test_provides_2.graphql +++ b/tests/gql/test_provides/test_provides_2.graphql @@ -16,8 +16,13 @@ type Product @key(fields: "sku") { weight: Int @external } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar FieldSet -scalar Scope +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope +"""This string-serialized scalar represents an authorization policy.""" scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_provides/test_provides_multiple_fields_1.graphql b/tests/gql/test_provides/test_provides_multiple_fields_1.graphql index 3279f2d..4904725 100644 --- a/tests/gql/test_provides/test_provides_multiple_fields_1.graphql +++ b/tests/gql/test_provides/test_provides_multiple_fields_1.graphql @@ -26,8 +26,13 @@ type _Service { sdl: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar FieldSet -scalar Scope +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope +"""This string-serialized scalar represents an authorization policy.""" scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_provides/test_provides_multiple_fields_2.graphql b/tests/gql/test_provides/test_provides_multiple_fields_2.graphql index 78241f2..a2721ff 100644 --- a/tests/gql/test_provides/test_provides_multiple_fields_2.graphql +++ b/tests/gql/test_provides/test_provides_multiple_fields_2.graphql @@ -16,8 +16,13 @@ type Product @key(fields: "sku") { weight: Int @external } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar FieldSet -scalar Scope +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope +"""This string-serialized scalar represents an authorization policy.""" scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_provides/test_provides_multiple_fields_as_list_1.graphql b/tests/gql/test_provides/test_provides_multiple_fields_as_list_1.graphql index 3e91464..8ee489d 100644 --- a/tests/gql/test_provides/test_provides_multiple_fields_as_list_1.graphql +++ b/tests/gql/test_provides/test_provides_multiple_fields_as_list_1.graphql @@ -26,8 +26,13 @@ type _Service { sdl: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar FieldSet -scalar Scope +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope +"""This string-serialized scalar represents an authorization policy.""" scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_provides/test_provides_multiple_fields_as_list_2.graphql b/tests/gql/test_provides/test_provides_multiple_fields_as_list_2.graphql index f5edd9a..b64e8cf 100644 --- a/tests/gql/test_provides/test_provides_multiple_fields_as_list_2.graphql +++ b/tests/gql/test_provides/test_provides_multiple_fields_as_list_2.graphql @@ -16,8 +16,13 @@ type Product @key(fields: "sku") @extends { weight: Int @external } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar FieldSet -scalar Scope +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope +"""This string-serialized scalar represents an authorization policy.""" scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_provides_v1/test_provides_1.graphql b/tests/gql/test_provides_v1/test_provides_1.graphql index 97743bb..71d13ff 100644 --- a/tests/gql/test_provides_v1/test_provides_1.graphql +++ b/tests/gql/test_provides_v1/test_provides_1.graphql @@ -23,4 +23,7 @@ type _Service { sdl: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_provides_v1/test_provides_2.graphql b/tests/gql/test_provides_v1/test_provides_2.graphql index 1945c6c..f370561 100644 --- a/tests/gql/test_provides_v1/test_provides_2.graphql +++ b/tests/gql/test_provides_v1/test_provides_2.graphql @@ -13,4 +13,7 @@ type Product @key(fields: "sku") { weight: Int @external } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_provides_v1/test_provides_multiple_fields_1.graphql b/tests/gql/test_provides_v1/test_provides_multiple_fields_1.graphql index 4c05924..6551753 100644 --- a/tests/gql/test_provides_v1/test_provides_multiple_fields_1.graphql +++ b/tests/gql/test_provides_v1/test_provides_multiple_fields_1.graphql @@ -23,4 +23,7 @@ type _Service { sdl: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_provides_v1/test_provides_multiple_fields_2.graphql b/tests/gql/test_provides_v1/test_provides_multiple_fields_2.graphql index c40ea48..8dcca67 100644 --- a/tests/gql/test_provides_v1/test_provides_multiple_fields_2.graphql +++ b/tests/gql/test_provides_v1/test_provides_multiple_fields_2.graphql @@ -13,4 +13,7 @@ type Product @key(fields: "sku") { weight: Int @external } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_provides_v1/test_provides_multiple_fields_as_list_1.graphql b/tests/gql/test_provides_v1/test_provides_multiple_fields_as_list_1.graphql index 17c52ea..e797c61 100644 --- a/tests/gql/test_provides_v1/test_provides_multiple_fields_as_list_1.graphql +++ b/tests/gql/test_provides_v1/test_provides_multiple_fields_as_list_1.graphql @@ -23,4 +23,7 @@ type _Service { sdl: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_provides_v1/test_provides_multiple_fields_as_list_2.graphql b/tests/gql/test_provides_v1/test_provides_multiple_fields_as_list_2.graphql index a83c7c2..6679188 100644 --- a/tests/gql/test_provides_v1/test_provides_multiple_fields_as_list_2.graphql +++ b/tests/gql/test_provides_v1/test_provides_multiple_fields_as_list_2.graphql @@ -13,4 +13,7 @@ type Product @key(fields: "sku") @extends { weight: Int @external } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_requires/test_requires_multiple_fields_1.graphql b/tests/gql/test_requires/test_requires_multiple_fields_1.graphql index 7862db1..ba14d2e 100644 --- a/tests/gql/test_requires/test_requires_multiple_fields_1.graphql +++ b/tests/gql/test_requires/test_requires_multiple_fields_1.graphql @@ -22,8 +22,13 @@ type _Service { sdl: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar FieldSet -scalar Scope +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope +"""This string-serialized scalar represents an authorization policy.""" scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_requires/test_requires_multiple_fields_2.graphql b/tests/gql/test_requires/test_requires_multiple_fields_2.graphql index b95534c..2bd6903 100644 --- a/tests/gql/test_requires/test_requires_multiple_fields_2.graphql +++ b/tests/gql/test_requires/test_requires_multiple_fields_2.graphql @@ -12,8 +12,13 @@ type Product @key(fields: "sku") @extends { shippingEstimate: String @requires(fields: "size weight") } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar FieldSet -scalar Scope +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope +"""This string-serialized scalar represents an authorization policy.""" scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_requires/test_requires_multiple_fields_as_list_1.graphql b/tests/gql/test_requires/test_requires_multiple_fields_as_list_1.graphql index 7862db1..ba14d2e 100644 --- a/tests/gql/test_requires/test_requires_multiple_fields_as_list_1.graphql +++ b/tests/gql/test_requires/test_requires_multiple_fields_as_list_1.graphql @@ -22,8 +22,13 @@ type _Service { sdl: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar FieldSet -scalar Scope +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope +"""This string-serialized scalar represents an authorization policy.""" scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_requires/test_requires_multiple_fields_as_list_2.graphql b/tests/gql/test_requires/test_requires_multiple_fields_as_list_2.graphql index b95534c..2bd6903 100644 --- a/tests/gql/test_requires/test_requires_multiple_fields_as_list_2.graphql +++ b/tests/gql/test_requires/test_requires_multiple_fields_as_list_2.graphql @@ -12,8 +12,13 @@ type Product @key(fields: "sku") @extends { shippingEstimate: String @requires(fields: "size weight") } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar FieldSet -scalar Scope +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope +"""This string-serialized scalar represents an authorization policy.""" scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_requires/test_requires_with_input_1.graphql b/tests/gql/test_requires/test_requires_with_input_1.graphql index 29e0273..267db9d 100644 --- a/tests/gql/test_requires/test_requires_with_input_1.graphql +++ b/tests/gql/test_requires/test_requires_with_input_1.graphql @@ -21,8 +21,13 @@ type _Service { sdl: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar FieldSet -scalar Scope +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope +"""This string-serialized scalar represents an authorization policy.""" scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_requires/test_requires_with_input_2.graphql b/tests/gql/test_requires/test_requires_with_input_2.graphql index 5438141..d3365ab 100644 --- a/tests/gql/test_requires/test_requires_with_input_2.graphql +++ b/tests/gql/test_requires/test_requires_with_input_2.graphql @@ -11,8 +11,13 @@ type Acme @key(fields: "id") @extends { foo(someInput: String ): String @requires(fields: "age") } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar FieldSet -scalar Scope +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope +"""This string-serialized scalar represents an authorization policy.""" scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_requires_v1/test_requires_multiple_fields_1.graphql b/tests/gql/test_requires_v1/test_requires_multiple_fields_1.graphql index 93fdc28..f438018 100644 --- a/tests/gql/test_requires_v1/test_requires_multiple_fields_1.graphql +++ b/tests/gql/test_requires_v1/test_requires_multiple_fields_1.graphql @@ -19,4 +19,7 @@ type _Service { sdl: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_requires_v1/test_requires_multiple_fields_2.graphql b/tests/gql/test_requires_v1/test_requires_multiple_fields_2.graphql index aa1ac97..74d207f 100644 --- a/tests/gql/test_requires_v1/test_requires_multiple_fields_2.graphql +++ b/tests/gql/test_requires_v1/test_requires_multiple_fields_2.graphql @@ -9,4 +9,7 @@ type Product @key(fields: "sku") @extends { shippingEstimate: String @requires(fields: "size weight") } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_requires_v1/test_requires_multiple_fields_as_list_1.graphql b/tests/gql/test_requires_v1/test_requires_multiple_fields_as_list_1.graphql index 93fdc28..f438018 100644 --- a/tests/gql/test_requires_v1/test_requires_multiple_fields_as_list_1.graphql +++ b/tests/gql/test_requires_v1/test_requires_multiple_fields_as_list_1.graphql @@ -19,4 +19,7 @@ type _Service { sdl: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_requires_v1/test_requires_multiple_fields_as_list_2.graphql b/tests/gql/test_requires_v1/test_requires_multiple_fields_as_list_2.graphql index aa1ac97..74d207f 100644 --- a/tests/gql/test_requires_v1/test_requires_multiple_fields_as_list_2.graphql +++ b/tests/gql/test_requires_v1/test_requires_multiple_fields_as_list_2.graphql @@ -9,4 +9,7 @@ type Product @key(fields: "sku") @extends { shippingEstimate: String @requires(fields: "size weight") } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_requires_v1/test_requires_with_input_1.graphql b/tests/gql/test_requires_v1/test_requires_with_input_1.graphql index b99e1a1..55377e1 100644 --- a/tests/gql/test_requires_v1/test_requires_with_input_1.graphql +++ b/tests/gql/test_requires_v1/test_requires_with_input_1.graphql @@ -18,4 +18,7 @@ type _Service { sdl: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_requires_v1/test_requires_with_input_2.graphql b/tests/gql/test_requires_v1/test_requires_with_input_2.graphql index dad747a..d903ea2 100644 --- a/tests/gql/test_requires_v1/test_requires_with_input_2.graphql +++ b/tests/gql/test_requires_v1/test_requires_with_input_2.graphql @@ -8,4 +8,7 @@ type Acme @key(fields: "id") @extends { foo(someInput: String ): String @requires(fields: "age") } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_scalar/test_custom_scalar_1.graphql b/tests/gql/test_scalar/test_custom_scalar_1.graphql index 6207da1..c6fc25a 100644 --- a/tests/gql/test_scalar/test_custom_scalar_1.graphql +++ b/tests/gql/test_scalar/test_custom_scalar_1.graphql @@ -18,8 +18,13 @@ type _Service { sdl: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar FieldSet -scalar Scope +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope +"""This string-serialized scalar represents an authorization policy.""" scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_scalar/test_custom_scalar_2.graphql b/tests/gql/test_scalar/test_custom_scalar_2.graphql index 85df77e..d7e968e 100644 --- a/tests/gql/test_scalar/test_custom_scalar_2.graphql +++ b/tests/gql/test_scalar/test_custom_scalar_2.graphql @@ -13,8 +13,13 @@ type Query { test2: [AddressScalar]! } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar FieldSet -scalar Scope +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope +"""This string-serialized scalar represents an authorization policy.""" scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_schema_annotation/test_chat_schema_1.graphql b/tests/gql/test_schema_annotation/test_chat_schema_1.graphql index ae44804..4776a75 100644 --- a/tests/gql/test_schema_annotation/test_chat_schema_1.graphql +++ b/tests/gql/test_schema_annotation/test_chat_schema_1.graphql @@ -30,8 +30,13 @@ type _Service { sdl: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar FieldSet -scalar Scope +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope +"""This string-serialized scalar represents an authorization policy.""" scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_schema_annotation/test_chat_schema_2.graphql b/tests/gql/test_schema_annotation/test_chat_schema_2.graphql index 9524a41..7a25553 100644 --- a/tests/gql/test_schema_annotation/test_chat_schema_2.graphql +++ b/tests/gql/test_schema_annotation/test_chat_schema_2.graphql @@ -20,8 +20,13 @@ type ChatUser @key(fields: "userId") @extends { userId: ID! @external } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar FieldSet -scalar Scope +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope +"""This string-serialized scalar represents an authorization policy.""" scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_schema_annotation/test_user_schema_1.graphql b/tests/gql/test_schema_annotation/test_user_schema_1.graphql index c94d20d..b72f0ce 100644 --- a/tests/gql/test_schema_annotation/test_user_schema_1.graphql +++ b/tests/gql/test_schema_annotation/test_user_schema_1.graphql @@ -25,8 +25,13 @@ type _Service { sdl: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar FieldSet -scalar Scope +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope +"""This string-serialized scalar represents an authorization policy.""" scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_schema_annotation/test_user_schema_2.graphql b/tests/gql/test_schema_annotation/test_user_schema_2.graphql index 40434d8..6778830 100644 --- a/tests/gql/test_schema_annotation/test_user_schema_2.graphql +++ b/tests/gql/test_schema_annotation/test_user_schema_2.graphql @@ -15,8 +15,13 @@ type User @key(fields: "email") @key(fields: "userId") { name: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar FieldSet -scalar Scope +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope +"""This string-serialized scalar represents an authorization policy.""" scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_schema_annotation_v1/test_chat_schema_1.graphql b/tests/gql/test_schema_annotation_v1/test_chat_schema_1.graphql index 5b58979..cc8a641 100644 --- a/tests/gql/test_schema_annotation_v1/test_chat_schema_1.graphql +++ b/tests/gql/test_schema_annotation_v1/test_chat_schema_1.graphql @@ -27,4 +27,7 @@ type _Service { sdl: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_schema_annotation_v1/test_chat_schema_2.graphql b/tests/gql/test_schema_annotation_v1/test_chat_schema_2.graphql index bd4504a..f0c4d0d 100644 --- a/tests/gql/test_schema_annotation_v1/test_chat_schema_2.graphql +++ b/tests/gql/test_schema_annotation_v1/test_chat_schema_2.graphql @@ -17,4 +17,7 @@ type ChatUser @key(fields: "userId") @extends { userId: ID! @external } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_schema_annotation_v1/test_user_schema_1.graphql b/tests/gql/test_schema_annotation_v1/test_user_schema_1.graphql index eea42b4..7531f14 100644 --- a/tests/gql/test_schema_annotation_v1/test_user_schema_1.graphql +++ b/tests/gql/test_schema_annotation_v1/test_user_schema_1.graphql @@ -22,4 +22,7 @@ type _Service { sdl: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_schema_annotation_v1/test_user_schema_2.graphql b/tests/gql/test_schema_annotation_v1/test_user_schema_2.graphql index 2b091d4..ffbcca5 100644 --- a/tests/gql/test_schema_annotation_v1/test_user_schema_2.graphql +++ b/tests/gql/test_schema_annotation_v1/test_user_schema_2.graphql @@ -12,4 +12,7 @@ type User @key(fields: "email") @key(fields: "userId") { name: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar _FieldSet \ No newline at end of file diff --git a/tests/gql/test_shareable/test_shareable_1.graphql b/tests/gql/test_shareable/test_shareable_1.graphql index c709fdc..d9003c9 100644 --- a/tests/gql/test_shareable/test_shareable_1.graphql +++ b/tests/gql/test_shareable/test_shareable_1.graphql @@ -15,8 +15,13 @@ type _Service { sdl: String } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar FieldSet -scalar Scope +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope +"""This string-serialized scalar represents an authorization policy.""" scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_shareable/test_shareable_2.graphql b/tests/gql/test_shareable/test_shareable_2.graphql index 82759c6..af8ffc2 100644 --- a/tests/gql/test_shareable/test_shareable_2.graphql +++ b/tests/gql/test_shareable/test_shareable_2.graphql @@ -10,8 +10,13 @@ type Query { inStockCount: Int! } +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" scalar FieldSet -scalar Scope +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope +"""This string-serialized scalar represents an authorization policy.""" scalar federation__Policy \ No newline at end of file From 7eb319b0d0fd722c96a4584c8d0827815ad5fc78 Mon Sep 17 00:00:00 2001 From: M Aswin Kishore <60577077+mak626@users.noreply.github.com> Date: Tue, 16 Jan 2024 18:28:12 +0530 Subject: [PATCH 13/19] fix: coveralls testing suite --- .github/workflows/tests.yml | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5e21ede..3d3770d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -3,7 +3,7 @@ name: Unit Tests on: [push, pull_request] jobs: - build: + test: runs-on: ubuntu-latest strategy: max-parallel: 4 @@ -22,9 +22,24 @@ jobs: pip install -e ".[test]" - name: Run Unit Tests run: py.test tests --cov=graphene_federation -vv - - name: Upload Coverage - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Upload coverage run: | pip install coveralls coveralls --service=github + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COVERALLS_FLAG_NAME: ${{ matrix.python-version }} + COVERALLS_PARALLEL: true + + coveralls: + name: Indicate completion to coveralls.io + needs: test + runs-on: ubuntu-latest + container: python:3-slim + steps: + - name: Finished + run: | + pip3 install --upgrade coveralls + coveralls --service=github --finish + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 436eb886f23ae98ad5cb16a0a28a4bdf45973484 Mon Sep 17 00:00:00 2001 From: M Aswin Kishore <60577077+mak626@users.noreply.github.com> Date: Thu, 18 Jan 2024 09:32:21 +0530 Subject: [PATCH 14/19] refact: FederationDirective -> ComposableDirective --- README.md | 8 ++++---- graphene_federation/__init__.py | 6 +++--- .../{federation_directive.py => composable_directive.py} | 4 ++-- graphene_federation/{main.py => schema.py} | 8 ++++---- .../schema_directives/compose_directive.py | 2 +- graphene_federation/schema_directives/link_directive.py | 2 +- 6 files changed, 15 insertions(+), 15 deletions(-) rename graphene_federation/{federation_directive.py => composable_directive.py} (95%) rename graphene_federation/{main.py => schema.py} (97%) diff --git a/README.md b/README.md index c128ed7..775bf56 100644 --- a/README.md +++ b/README.md @@ -273,10 +273,10 @@ You can define custom directives as follows from graphene import Field, ObjectType, String from graphql import GraphQLArgument, GraphQLInt, GraphQLNonNull -from graphene_federation import DirectiveLocation, FederationDirective +from graphene_federation import DirectiveLocation, ComposableDirective from graphene_federation import build_schema -CacheDirective = FederationDirective( +CacheDirective = ComposableDirective( name="cache", locations=[DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.OBJECT], args={ @@ -339,9 +339,9 @@ You can pass the `add_to_schema_directives` as `False` from graphene import Field, ObjectType, String from graphql import GraphQLArgument, GraphQLInt, GraphQLNonNull -from graphene_federation import DirectiveLocation, FederationDirective, build_schema, compose_directive, link_directive +from graphene_federation import DirectiveLocation, ComposableDirective, build_schema, compose_directive, link_directive -CacheDirective = FederationDirective( +CacheDirective = ComposableDirective( name="cache", locations=[DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.OBJECT], args={ diff --git a/graphene_federation/__init__.py b/graphene_federation/__init__.py index e4c45cc..51b477d 100644 --- a/graphene_federation/__init__.py +++ b/graphene_federation/__init__.py @@ -1,6 +1,7 @@ from graphene_directives import DirectiveLocation from .apollo_versions import FederationVersion, LATEST_VERSION +from .composable_directive import ComposableDirective from .directives import ( authenticated, extends, @@ -16,8 +17,7 @@ shareable, tag, ) -from .federation_directive import FederationDirective -from .main import build_schema +from .schema import build_schema from .schema_directives import compose_directive, link_directive from .service import get_sdl @@ -25,7 +25,7 @@ "FederationVersion", "LATEST_VERSION", "build_schema", - "FederationDirective", + "ComposableDirective", "DirectiveLocation", "authenticated", "extends", diff --git a/graphene_federation/federation_directive.py b/graphene_federation/composable_directive.py similarity index 95% rename from graphene_federation/federation_directive.py rename to graphene_federation/composable_directive.py index b9f3e34..1055a1c 100644 --- a/graphene_federation/federation_directive.py +++ b/graphene_federation/composable_directive.py @@ -8,7 +8,7 @@ ) -class FederationDirective(GraphQLDirective): +class ComposableDirective(GraphQLDirective): def __init__( self, name: str, @@ -35,7 +35,7 @@ def __init__( :param add_to_schema_directives: Adds schema_directives @composeDirective and @link to schema automatically """ if add_to_schema_directives: - assert spec_url is not None, "FederationDirective requires spec_url" + assert spec_url is not None, "ComposableDirective requires spec_url" self.spec_url = spec_url self.add_to_schema_directives = add_to_schema_directives diff --git a/graphene_federation/main.py b/graphene_federation/schema.py similarity index 97% rename from graphene_federation/main.py rename to graphene_federation/schema.py index 2a4c6da..1a7d5b3 100644 --- a/graphene_federation/main.py +++ b/graphene_federation/schema.py @@ -9,7 +9,6 @@ ) from graphene_directives.schema import Schema -from . import FederationDirective from .apollo_versions import ( FederationVersion, LATEST_VERSION, @@ -17,6 +16,7 @@ get_directives_based_on_version, ) from .apollo_versions.v2_1 import compose_directive as ComposeDirective +from .composable_directive import ComposableDirective from .entity import get_entity_query from .schema_directives import compose_directive, link_directive from .service import get_service_query @@ -65,7 +65,7 @@ def build_schema( mutation: Union[ObjectType, Type[ObjectType]] = None, subscription: Union[ObjectType, Type[ObjectType]] = None, types: Collection[Union[ObjectType, Type[ObjectType]]] = None, - directives: Union[Collection[FederationDirective], None] = None, + directives: Union[Collection[ComposableDirective], None] = None, include_graphql_spec_directives: bool = True, schema_directives: Collection[SchemaDirective] = None, auto_camelcase: bool = True, @@ -169,8 +169,8 @@ def build_schema( url__imports: dict[str, list[str]] = {} for directive in directives: assert isinstance( - directive, FederationDirective - ), "directives must be of instance FederationDirective" + directive, ComposableDirective + ), "directives must be of instance ComposableDirective" if not directive.add_to_schema_directives: continue diff --git a/graphene_federation/schema_directives/compose_directive.py b/graphene_federation/schema_directives/compose_directive.py index 535f67d..8dbcd77 100644 --- a/graphene_federation/schema_directives/compose_directive.py +++ b/graphene_federation/schema_directives/compose_directive.py @@ -19,7 +19,7 @@ def compose_directive( Use this in the `schema_directives` argument of `build_schema` - It is not recommended to use this directive directly, instead use the FederationDirective class to build + It is not recommended to use this directive directly, instead use the ComposableDirective class to build a custom directive. It will automatically add the compose and link directive to schema Reference: https://www.apollographql.com/docs/federation/federated-types/federated-directives/#composedirective diff --git a/graphene_federation/schema_directives/link_directive.py b/graphene_federation/schema_directives/link_directive.py index 649d609..8f3e35f 100644 --- a/graphene_federation/schema_directives/link_directive.py +++ b/graphene_federation/schema_directives/link_directive.py @@ -39,7 +39,7 @@ def link_directive( Use this in the `schema_directives` argument of `build_schema` - It is not recommended to use this directive directly, instead use the FederationDirective class to build + It is not recommended to use this directive directly, instead use the ComposableDirective class to build a custom directive. It will automatically add the compose and link directive to schema Also, the apollo directives such as @key, @external, ... are automatically added to the schema via the link directive From e4a69edf4e42d2671b6051f570314ad6600cecf5 Mon Sep 17 00:00:00 2001 From: M Aswin Kishore <60577077+mak626@users.noreply.github.com> Date: Fri, 19 Jan 2024 12:30:50 +0530 Subject: [PATCH 15/19] deprecated: enable_federation_2 in favour of federation_version --- README.md | 38 +++++++++---------- examples/inaccessible.py | 4 +- examples/override.py | 4 +- examples/shareable.py | 4 +- examples/tag.py | 6 +-- graphene_federation/__init__.py | 3 +- .../apollo_versions/__init__.py | 1 + graphene_federation/schema.py | 29 +++++--------- integration_tests/service_a/src/schema.py | 9 ++++- integration_tests/service_b/src/schema.py | 8 ++-- integration_tests/service_c/src/schema.py | 12 +++++- integration_tests/service_d/src/schema.py | 4 +- tests/test_annotation_corner_cases.py | 21 +++++++--- tests/test_annotation_corner_cases_v1.py | 25 +++++++++--- tests/test_custom_enum.py | 4 +- tests/test_inaccessible.py | 14 +++++-- tests/test_key.py | 18 ++++----- tests/test_key_v1.py | 6 +-- tests/test_provides.py | 8 ++-- tests/test_provides_v1.py | 8 ++-- tests/test_requires.py | 8 ++-- tests/test_requires_v1.py | 8 ++-- tests/test_scalar.py | 4 +- tests/test_schema_annotation.py | 6 +-- tests/test_schema_annotation_v1.py | 10 +++-- tests/test_shareable.py | 14 +++++-- 26 files changed, 160 insertions(+), 116 deletions(-) diff --git a/README.md b/README.md index 775bf56..009a2c0 100644 --- a/README.md +++ b/README.md @@ -37,8 +37,8 @@ If you need to use a version compatible with `graphene` v2 I recommend using the - [x] v2.2 - [x] v2.3 - [x] v2.4 -- [x] v2.5 -- [x] v2.6 +- [x] v2.5 `STABLE_VERSION` . Rover dev supports only upto v2.5 +- [x] v2.6 `LATEST_VERSION` All directives could be easily integrated with the help of [graphene-directives](https://github.com/strollby/graphene-directives). Now every directive's values are validated at run time itself by [graphene-directives](https://github.com/strollby/graphene-directives). @@ -128,7 +128,7 @@ First add an account service that expose a `User` type that can then be referenc ```python from graphene import Field, Int, ObjectType, String -from graphene_federation import build_schema, key +from graphene_federation import LATEST_VERSION, build_schema, key @key("id") @@ -147,7 +147,7 @@ class Query(ObjectType): me = Field(User) -schema = build_schema(query=Query, enable_federation_2=True) +schema = build_schema(query=Query, federation_version=LATEST_VERSION) ``` ### Product @@ -156,7 +156,7 @@ The product service exposes a `Product` type that can be used by other services ```python from graphene import Argument, Int, List, ObjectType, String -from graphene_federation import build_schema, key +from graphene_federation import LATEST_VERSION, build_schema, key @key("upc") @@ -176,7 +176,7 @@ class Query(ObjectType): topProducts = List(Product, first=Argument(Int, default_value=5)) -schema = build_schema(query=Query, enable_federation_2=True) +schema = build_schema(query=Query, federation_version=LATEST_VERSION) ``` ### Reviews @@ -187,7 +187,7 @@ On top of that it adds to the `User`/`Product` types (that are both defined in o ```python from graphene import Field, Int, List, ObjectType, String -from graphene_federation import build_schema, external, key, provides +from graphene_federation import LATEST_VERSION, build_schema, external, key, provides @key("id") @@ -218,7 +218,7 @@ class Query(ObjectType): review = Field(Review) -schema = build_schema(query=Query, enable_federation_2=True) +schema = build_schema(query=Query, federation_version=LATEST_VERSION) ``` ### Federation @@ -254,10 +254,7 @@ There is also a cool [example](https://github.com/preply/graphene-federation/iss - `schema_directives` (`Collection[SchemaDirective]`): Directives that can be defined at `DIRECTIVE_LOCATION.SCHEMA` with their argument values. - `include_graphql_spec_directives` (`bool`): Includes directives defined by GraphQL spec (`@include`, `@skip`, `@deprecated`, `@specifiedBy`) -- `enable_federation_2` (`bool`): Whether to enable federation 2 directives (default False) -- `federation_version` (`FederationVersion`): Specify the version explicit (default LATEST_VERSION) - -In case both enable_federation_2 and federation_version are specified, federation_version is given higher priority +- `federation_version` (`FederationVersion`): Specify the version explicit (default STABLE_VERSION) ### Directives Additional arguments @@ -273,7 +270,7 @@ You can define custom directives as follows from graphene import Field, ObjectType, String from graphql import GraphQLArgument, GraphQLInt, GraphQLNonNull -from graphene_federation import DirectiveLocation, ComposableDirective +from graphene_federation import ComposableDirective, DirectiveLocation, LATEST_VERSION from graphene_federation import build_schema CacheDirective = ComposableDirective( @@ -293,7 +290,7 @@ cache = CacheDirective.decorator() @cache(max_age=20) class Review(ObjectType): - body = cache(field=String(),max_age=100) + body = cache(field=String(), max_age=100) class Query(ObjectType): @@ -303,7 +300,7 @@ class Query(ObjectType): schema = build_schema( query=Query, directives=(CacheDirective,), - enable_federation_2=True, + federation_version=LATEST_VERSION , ) ``` @@ -339,7 +336,8 @@ You can pass the `add_to_schema_directives` as `False` from graphene import Field, ObjectType, String from graphql import GraphQLArgument, GraphQLInt, GraphQLNonNull -from graphene_federation import DirectiveLocation, ComposableDirective, build_schema, compose_directive, link_directive +from graphene_federation import (ComposableDirective, DirectiveLocation, LATEST_VERSION, build_schema, + compose_directive, link_directive) CacheDirective = ComposableDirective( name="cache", @@ -372,7 +370,7 @@ schema = build_schema( link_directive(url="https://specs.example.dev/directives/v1.0", import_=['@cache']), compose_directive(name='@cache'), ), - enable_federation_2=True, + federation_version=LATEST_VERSION, ) ``` @@ -392,7 +390,7 @@ class User(ObjectType): class Query(ObjectType): user = Field(User) -schema = build_schema(query=Query, enable_federation_2=True, auto_camelcase=False) # Disable auto_camelcase +schema = build_schema(query=Query, federation_version=LATEST_VERSION, auto_camelcase=False) # Disable auto_camelcase ``` This works correctly. @@ -409,7 +407,7 @@ class User(ObjectType): class Query(ObjectType): user = Field(User) -schema = build_schema(query=Query, enable_federation_2=True) # auto_camelcase Enabled +schema = build_schema(query=Query, federation_version=LATEST_VERSION) # auto_camelcase Enabled ``` This will raise an error `@key, field "validEmail" does not exist on type "User"`. @@ -427,7 +425,7 @@ class User(ObjectType): class Query(ObjectType): user = Field(User) -schema = build_schema(query=Query, enable_federation_2=True) # auto_camelcase=True +schema = build_schema(query=Query, federation_version=LATEST_VERSION) # auto_camelcase=True ``` ------------------------ diff --git a/examples/inaccessible.py b/examples/inaccessible.py index 1c9c715..07ebc98 100644 --- a/examples/inaccessible.py +++ b/examples/inaccessible.py @@ -1,7 +1,7 @@ import graphene from graphene_federation import ( - inaccessible, + LATEST_VERSION, inaccessible, external, provides, key, @@ -64,7 +64,7 @@ class Query(graphene.ObjectType): schema = build_schema( - Query, enable_federation_2=True, types=(ReviewInterface, SearchResult, Review) + Query, federation_version=LATEST_VERSION, types=(ReviewInterface, SearchResult, Review) ) query = """ diff --git a/examples/override.py b/examples/override.py index 6e09a07..b076d35 100644 --- a/examples/override.py +++ b/examples/override.py @@ -1,7 +1,7 @@ import graphene from graphene_federation import ( - build_schema, + LATEST_VERSION, build_schema, shareable, external, key, @@ -21,7 +21,7 @@ class Query(graphene.ObjectType): position = graphene.Field(Product) -schema = build_schema(Query, enable_federation_2=True) +schema = build_schema(Query, federation_version=LATEST_VERSION) query = """ query getSDL { diff --git a/examples/shareable.py b/examples/shareable.py index 5c983cf..a9cef2f 100644 --- a/examples/shareable.py +++ b/examples/shareable.py @@ -3,7 +3,7 @@ from graphene_federation.shareable import shareable -from graphene_federation import build_schema +from graphene_federation import LATEST_VERSION, build_schema @shareable @@ -40,7 +40,7 @@ class Query(graphene.ObjectType): position = graphene.Field(Position) -schema = build_schema(Query, enable_federation_2=True, types=(SearchResult,)) +schema = build_schema(Query, federation_version=LATEST_VERSION, types=(SearchResult,)) query = """ query getSDL { diff --git a/examples/tag.py b/examples/tag.py index 6f03da7..c895a59 100644 --- a/examples/tag.py +++ b/examples/tag.py @@ -1,8 +1,8 @@ import graphene - -from graphene_federation import build_schema, key, inaccessible, shareable from graphene_federation.tag import tag +from graphene_federation import LATEST_VERSION, build_schema, inaccessible, shareable + class Product(graphene.ObjectType): id = graphene.ID(required=True) @@ -15,7 +15,7 @@ class Query(graphene.ObjectType): position = graphene.Field(Product) -schema = build_schema(Query, enable_federation_2=True) +schema = build_schema(Query, federation_version=LATEST_VERSION) query = """ query getSDL { diff --git a/graphene_federation/__init__.py b/graphene_federation/__init__.py index 51b477d..5ba433d 100644 --- a/graphene_federation/__init__.py +++ b/graphene_federation/__init__.py @@ -1,6 +1,6 @@ from graphene_directives import DirectiveLocation -from .apollo_versions import FederationVersion, LATEST_VERSION +from .apollo_versions import FederationVersion, LATEST_VERSION, STABLE_VERSION from .composable_directive import ComposableDirective from .directives import ( authenticated, @@ -24,6 +24,7 @@ __all__ = [ "FederationVersion", "LATEST_VERSION", + "STABLE_VERSION", "build_schema", "ComposableDirective", "DirectiveLocation", diff --git a/graphene_federation/apollo_versions/__init__.py b/graphene_federation/apollo_versions/__init__.py index 9c08ccd..becdc7b 100644 --- a/graphene_federation/apollo_versions/__init__.py +++ b/graphene_federation/apollo_versions/__init__.py @@ -11,6 +11,7 @@ from .version import FederationVersion LATEST_VERSION = FederationVersion.VERSION_2_6 +STABLE_VERSION = FederationVersion.VERSION_2_5 def get_directives_based_on_version( diff --git a/graphene_federation/schema.py b/graphene_federation/schema.py index 1a7d5b3..61d6bc9 100644 --- a/graphene_federation/schema.py +++ b/graphene_federation/schema.py @@ -11,7 +11,7 @@ from .apollo_versions import ( FederationVersion, - LATEST_VERSION, + STABLE_VERSION, get_directive_from_name, get_directives_based_on_version, ) @@ -69,7 +69,6 @@ def build_schema( include_graphql_spec_directives: bool = True, schema_directives: Collection[SchemaDirective] = None, auto_camelcase: bool = True, - enable_federation_2: bool = False, federation_version: FederationVersion = None, ) -> Schema: """ @@ -92,23 +91,13 @@ def build_schema( with their argument values. include_graphql_spec_directives (bool): Includes directives defined by GraphQL spec (@include, @skip, @deprecated, @specifiedBy) - enable_federation_2 (bool): Whether to enable federation 2 directives (default False) - federation_version (FederationVersion): Specify the version explicit (default LATEST_VERSION) - - In case both enable_federation_2 and federation_version are specified, federation_version is given - higher priority + federation_version (FederationVersion): Specify the version explicit (default STABLE_VERSION) """ - # In case both enable_federation_2 and federation_version are specified, - # federation_version is given higher priority - federation_version = ( - federation_version - if federation_version - else LATEST_VERSION - if enable_federation_2 - else FederationVersion.VERSION_1_0 + federation_version = federation_version if federation_version else STABLE_VERSION + federation_2_enabled = ( + federation_version.value > FederationVersion.VERSION_1_0.value ) - enable_federation_2 = federation_version.value > FederationVersion.VERSION_1_0.value _types = list(types) if types is not None else [] @@ -135,7 +124,7 @@ def build_schema( _schema_directives = [] directives_used = schema.get_directives_used() if schema_directives or directives: - if not enable_federation_2: + if not federation_2_enabled: raise ValueError( f"Schema Directives & Directives are not supported on {federation_version=}. Use >=2.0 " ) @@ -150,7 +139,7 @@ def build_schema( ): directives_used.append(ComposeDirective) - if directives_used and enable_federation_2: + if directives_used and federation_2_enabled: imports = [ str(directive) for directive in directives_used @@ -197,7 +186,9 @@ def build_schema( if schema_directives: _schema_directives.extend(list(schema_directives)) - schema_args["schema_directives"] = _schema_directives if enable_federation_2 else [] + schema_args["schema_directives"] = ( + _schema_directives if federation_2_enabled else [] + ) # Call it again to rebuild the schema using the schema directives schema = build_directive_schema(query=query, **schema_args) diff --git a/integration_tests/service_a/src/schema.py b/integration_tests/service_a/src/schema.py index 0988bb1..78c8597 100644 --- a/integration_tests/service_a/src/schema.py +++ b/integration_tests/service_a/src/schema.py @@ -1,6 +1,6 @@ from graphene import Field, Int, Interface, List, NonNull, ObjectType, String -from graphene_federation import build_schema, extends, external, key +from graphene_federation import FederationVersion, build_schema, extends, external, key class DecoratedText(Interface): @@ -79,4 +79,9 @@ def resolve_goodbye(root, info): return "See ya!" -schema = build_schema(query=Query, types=[FunnyTextAnother], auto_camelcase=False) +schema = build_schema( + query=Query, + types=[FunnyTextAnother], + federation_version=FederationVersion.VERSION_1_0, + auto_camelcase=False, +) diff --git a/integration_tests/service_b/src/schema.py b/integration_tests/service_b/src/schema.py index 9c31e15..96fca37 100644 --- a/integration_tests/service_b/src/schema.py +++ b/integration_tests/service_b/src/schema.py @@ -1,6 +1,6 @@ -from graphene import ObjectType, String, Int, Interface, Mutation +from graphene import Int, Interface, Mutation, ObjectType, String -from graphene_federation import build_schema, key +from graphene_federation import FederationVersion, build_schema, key class TextInterface(Interface): @@ -70,4 +70,6 @@ class Mutation(ObjectType): types = [FileNode, FunnyText, FileNodeAnother, User] -schema = build_schema(mutation=Mutation, types=types) +schema = build_schema( + mutation=Mutation, types=types, federation_version=FederationVersion.VERSION_1_0 +) diff --git a/integration_tests/service_c/src/schema.py b/integration_tests/service_c/src/schema.py index c7cf06e..cf0b5c6 100644 --- a/integration_tests/service_c/src/schema.py +++ b/integration_tests/service_c/src/schema.py @@ -1,6 +1,14 @@ from graphene import Field, Int, List, NonNull, ObjectType, String -from graphene_federation import build_schema, extends, external, key, provides, requires +from graphene_federation import ( + FederationVersion, + build_schema, + extends, + external, + key, + provides, + requires, +) @key(fields="id") @@ -54,4 +62,4 @@ def resolve_articles_with_author_age_provide(self, info): return [ArticleThatProvideAuthorAge(id=1, text="some text", author=User(id=5))] -schema = build_schema(Query) +schema = build_schema(Query, federation_version=FederationVersion.VERSION_1_0) diff --git a/integration_tests/service_d/src/schema.py b/integration_tests/service_d/src/schema.py index 0ddd882..74d7b4c 100644 --- a/integration_tests/service_d/src/schema.py +++ b/integration_tests/service_d/src/schema.py @@ -1,6 +1,6 @@ from graphene import Field, Int, ObjectType -from graphene_federation import build_schema, extends, external, key +from graphene_federation import FederationVersion, build_schema, extends, external, key """ Alphabet order - matters @@ -28,4 +28,4 @@ class Query(ObjectType): y = Field(Y) -schema = build_schema(query=Query) +schema = build_schema(query=Query, federation_version=FederationVersion.VERSION_1_0) diff --git a/tests/test_annotation_corner_cases.py b/tests/test_annotation_corner_cases.py index b0078d7..87ca6fc 100644 --- a/tests/test_annotation_corner_cases.py +++ b/tests/test_annotation_corner_cases.py @@ -2,7 +2,14 @@ from graphene import Field, ID, ObjectType, String -from graphene_federation import build_schema, extends, external, key, requires +from graphene_federation import ( + LATEST_VERSION, + build_schema, + extends, + external, + key, + requires, +) from tests.util import file_handlers, sdl_query save_file, open_file = file_handlers(Path(__file__)) @@ -29,7 +36,7 @@ class ChatMessage(ObjectType): class ChatQuery(ObjectType): message = Field(ChatMessage, id=ID(required=True)) - schema = build_schema(query=ChatQuery, enable_federation_2=True) + schema = build_schema(query=ChatQuery, federation_version=LATEST_VERSION) assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) @@ -51,7 +58,7 @@ class Camel(ObjectType): class Query(ObjectType): camel = Field(Camel) - schema = build_schema(query=Query, enable_federation_2=True) + schema = build_schema(query=Query, federation_version=LATEST_VERSION) assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) @@ -72,7 +79,9 @@ class Camel(ObjectType): class Query(ObjectType): camel = Field(Camel) - schema = build_schema(query=Query, auto_camelcase=False, enable_federation_2=True) + schema = build_schema( + query=Query, auto_camelcase=False, federation_version=LATEST_VERSION + ) assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) @@ -96,7 +105,7 @@ class A(ObjectType): class Query(ObjectType): a = Field(A) - schema = build_schema(query=Query, enable_federation_2=True) + schema = build_schema(query=Query, federation_version=LATEST_VERSION) assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) @@ -121,7 +130,7 @@ class Meta: class Query(ObjectType): a = Field(A) - schema = build_schema(query=Query, enable_federation_2=True) + schema = build_schema(query=Query, federation_version=LATEST_VERSION) assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) diff --git a/tests/test_annotation_corner_cases_v1.py b/tests/test_annotation_corner_cases_v1.py index 6e50829..4dc25ed 100644 --- a/tests/test_annotation_corner_cases_v1.py +++ b/tests/test_annotation_corner_cases_v1.py @@ -2,7 +2,14 @@ from graphene import Field, ID, ObjectType, String -from graphene_federation import build_schema, extends, external, key, requires +from graphene_federation import ( + FederationVersion, + build_schema, + extends, + external, + key, + requires, +) from tests.util import file_handlers, sdl_query save_file, open_file = file_handlers(Path(__file__)) @@ -29,7 +36,9 @@ class ChatMessage(ObjectType): class ChatQuery(ObjectType): message = Field(ChatMessage, id=ID(required=True)) - schema = build_schema(query=ChatQuery, enable_federation_2=False) + schema = build_schema( + query=ChatQuery, federation_version=FederationVersion.VERSION_1_0 + ) assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) @@ -51,7 +60,7 @@ class Camel(ObjectType): class Query(ObjectType): camel = Field(Camel) - schema = build_schema(query=Query, enable_federation_2=False) + schema = build_schema(query=Query, federation_version=FederationVersion.VERSION_1_0) assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) @@ -72,7 +81,11 @@ class Camel(ObjectType): class Query(ObjectType): camel = Field(Camel) - schema = build_schema(query=Query, auto_camelcase=False, enable_federation_2=False) + schema = build_schema( + query=Query, + auto_camelcase=False, + federation_version=FederationVersion.VERSION_1_0, + ) assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) @@ -96,7 +109,7 @@ class A(ObjectType): class Query(ObjectType): a = Field(A) - schema = build_schema(query=Query, enable_federation_2=False) + schema = build_schema(query=Query, federation_version=FederationVersion.VERSION_1_0) assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) @@ -121,7 +134,7 @@ class Meta: class Query(ObjectType): a = Field(A) - schema = build_schema(query=Query, enable_federation_2=False) + schema = build_schema(query=Query, federation_version=FederationVersion.VERSION_1_0) assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) diff --git a/tests/test_custom_enum.py b/tests/test_custom_enum.py index ebb8bd8..357a04b 100644 --- a/tests/test_custom_enum.py +++ b/tests/test_custom_enum.py @@ -3,7 +3,7 @@ import graphene from graphene import ObjectType -from graphene_federation import build_schema +from graphene_federation import LATEST_VERSION, build_schema from graphene_federation import inaccessible, shareable from tests.util import file_handlers, sdl_query @@ -29,7 +29,7 @@ class Query(ObjectType): test2 = graphene.List(TestCustomEnum, required=True) schema = build_schema( - query=Query, enable_federation_2=True, types=(TestCustomEnum,) + query=Query, federation_version=LATEST_VERSION, types=(TestCustomEnum,) ) assert open_file("1") == str(schema) diff --git a/tests/test_inaccessible.py b/tests/test_inaccessible.py index 2233e25..302693b 100644 --- a/tests/test_inaccessible.py +++ b/tests/test_inaccessible.py @@ -3,7 +3,7 @@ import graphene from graphene import ObjectType -from graphene_federation import build_schema, inaccessible +from graphene_federation import LATEST_VERSION, build_schema, inaccessible from tests.util import file_handlers, sdl_query save_file, open_file = file_handlers(Path(__file__)) @@ -25,7 +25,9 @@ class Meta: class Query(ObjectType): in_stock_count = graphene.Int(required=True) - build_schema(query=Query, enable_federation_2=True, types=(ReviewInterface, Review)) + build_schema( + query=Query, federation_version=LATEST_VERSION, types=(ReviewInterface, Review) + ) def test_inaccessible(): @@ -37,7 +39,9 @@ class Position(graphene.ObjectType): class Query(ObjectType): in_stock_count = graphene.Int(required=True) - schema = build_schema(query=Query, enable_federation_2=True, types=(Position,)) + schema = build_schema( + query=Query, federation_version=LATEST_VERSION, types=(Position,) + ) assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) @@ -67,7 +71,9 @@ class Meta: class Query(ObjectType): in_stock_count = graphene.Int(required=True) - schema = build_schema(query=Query, enable_federation_2=True, types=(SearchResult,)) + schema = build_schema( + query=Query, federation_version=LATEST_VERSION, types=(SearchResult,) + ) assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) diff --git a/tests/test_key.py b/tests/test_key.py index 2a2f2ea..5d1df8c 100644 --- a/tests/test_key.py +++ b/tests/test_key.py @@ -3,7 +3,7 @@ import pytest from graphene import Field, ID, ObjectType, String -from graphene_federation import build_schema, key +from graphene_federation import LATEST_VERSION, build_schema, key from tests.util import file_handlers, sdl_query save_file, open_file = file_handlers(Path(__file__)) @@ -19,7 +19,7 @@ class User(ObjectType): class Query(ObjectType): user = Field(User) - schema = build_schema(query=Query, enable_federation_2=True) + schema = build_schema(query=Query, federation_version=LATEST_VERSION) assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) @@ -35,7 +35,7 @@ def test_key_non_existing_field_failure(): class A(ObjectType): id = ID() - build_schema(types=(A,), enable_federation_2=True) + build_schema(types=(A,), federation_version=LATEST_VERSION) assert '@key, field "potato" does not exist on type "A"' == str(err.value) @@ -52,7 +52,7 @@ class User(ObjectType): class Query(ObjectType): user = Field(User) - schema = build_schema(query=Query, enable_federation_2=True) + schema = build_schema(query=Query, federation_version=LATEST_VERSION) assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) @@ -75,7 +75,7 @@ class User(ObjectType): class Query(ObjectType): user = Field(User) - schema = build_schema(query=Query, enable_federation_2=True) + schema = build_schema(query=Query, federation_version=LATEST_VERSION) assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) @@ -99,7 +99,7 @@ class Query(ObjectType): with pytest.raises(ValueError) as err: # Field name absent on User ObjectType - build_schema(query=Query, enable_federation_2=True) + build_schema(query=Query, federation_version=LATEST_VERSION) assert '@key, field "name" does not exist on type "User"' == str(err.value) @@ -113,7 +113,7 @@ class Query(ObjectType): with pytest.raises(ValueError) as err: # Presence of invalid field in organization field key - build_schema(query=Query, enable_federation_2=True) + build_schema(query=Query, federation_version=LATEST_VERSION) assert '@key, field "name" does not exist on type "Organization"' == str(err.value) @@ -127,7 +127,7 @@ class Query(ObjectType): with pytest.raises(ValueError) as err: # Presence of BusinessUnit in the key without subselection - build_schema(query=Query, enable_federation_2=True) + build_schema(query=Query, federation_version=LATEST_VERSION) assert '@key, type Organization, field "bu" needs sub selections.' == str(err.value) @@ -141,7 +141,7 @@ class Query(ObjectType): with pytest.raises(ValueError) as err: # Presence of subselection for the scalar 'name' field - build_schema(query=Query, enable_federation_2=True) + build_schema(query=Query, federation_version=LATEST_VERSION) assert '@key, type BusinessUnit, field "name" cannot have sub selections.' == str( err.value diff --git a/tests/test_key_v1.py b/tests/test_key_v1.py index feecac8..9708a88 100644 --- a/tests/test_key_v1.py +++ b/tests/test_key_v1.py @@ -3,7 +3,7 @@ import pytest from graphene import Field, ID, ObjectType, String -from graphene_federation import build_schema, key +from graphene_federation import FederationVersion, build_schema, key from tests.util import file_handlers, sdl_query save_file, open_file = file_handlers(Path(__file__)) @@ -19,7 +19,7 @@ class User(ObjectType): class Query(ObjectType): user = Field(User) - schema = build_schema(query=Query) + schema = build_schema(query=Query, federation_version=FederationVersion.VERSION_1_0) assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) @@ -35,6 +35,6 @@ def test_key_non_existing_field_failure(): class A(ObjectType): id = ID() - _ = build_schema(types=(A,)) + _ = build_schema(types=(A,), federation_version=FederationVersion.VERSION_1_0) assert '@key, field "potato" does not exist on type "A"' == str(err.value) diff --git a/tests/test_provides.py b/tests/test_provides.py index c88e5ca..6804de5 100644 --- a/tests/test_provides.py +++ b/tests/test_provides.py @@ -3,7 +3,7 @@ from graphene import Field, ObjectType, String from graphene import Int -from graphene_federation import build_schema, extends, key +from graphene_federation import LATEST_VERSION, build_schema, extends, key from graphene_federation import external, provides from tests.util import file_handlers, sdl_query @@ -28,7 +28,7 @@ class InStockCount(ObjectType): class Query(ObjectType): in_stock_count = Field(InStockCount) - schema = build_schema(query=Query, enable_federation_2=True) + schema = build_schema(query=Query, federation_version=LATEST_VERSION) assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) @@ -52,7 +52,7 @@ class InStockCount(ObjectType): class Query(ObjectType): in_stock_count = Field(InStockCount) - schema = build_schema(query=Query, enable_federation_2=True) + schema = build_schema(query=Query, federation_version=LATEST_VERSION) assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) @@ -77,7 +77,7 @@ class InStockCount(ObjectType): class Query(ObjectType): in_stock_count = Field(InStockCount) - schema = build_schema(query=Query, enable_federation_2=True) + schema = build_schema(query=Query, federation_version=LATEST_VERSION) assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) diff --git a/tests/test_provides_v1.py b/tests/test_provides_v1.py index 2013fc2..ed50119 100644 --- a/tests/test_provides_v1.py +++ b/tests/test_provides_v1.py @@ -3,7 +3,7 @@ from graphene import Field, ObjectType, String from graphene import Int -from graphene_federation import build_schema, extends, key +from graphene_federation import FederationVersion, build_schema, extends, key from graphene_federation import external, provides from tests.util import file_handlers, sdl_query @@ -28,7 +28,7 @@ class InStockCount(ObjectType): class Query(ObjectType): in_stock_count = Field(InStockCount) - schema = build_schema(query=Query) + schema = build_schema(query=Query, federation_version=FederationVersion.VERSION_1_0) assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) @@ -52,7 +52,7 @@ class InStockCount(ObjectType): class Query(ObjectType): in_stock_count = Field(InStockCount) - schema = build_schema(query=Query) + schema = build_schema(query=Query, federation_version=FederationVersion.VERSION_1_0) assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) @@ -77,7 +77,7 @@ class InStockCount(ObjectType): class Query(ObjectType): in_stock_count = Field(InStockCount) - schema = build_schema(query=Query) + schema = build_schema(query=Query, federation_version=FederationVersion.VERSION_1_0) assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) diff --git a/tests/test_requires.py b/tests/test_requires.py index 0ae0553..dfb5077 100644 --- a/tests/test_requires.py +++ b/tests/test_requires.py @@ -5,7 +5,7 @@ from graphene import Int from graphene_directives import DirectiveValidationError -from graphene_federation import build_schema, key +from graphene_federation import LATEST_VERSION, build_schema, key from graphene_federation import extends, external, requires from tests.util import file_handlers, sdl_query @@ -41,7 +41,7 @@ class Product(ObjectType): class Query(ObjectType): product = Field(Product) - schema = build_schema(query=Query, enable_federation_2=True) + schema = build_schema(query=Query, federation_version=LATEST_VERSION) save_file(str(schema), "1") save_file(sdl_query(schema), "2") @@ -66,7 +66,7 @@ class Product(ObjectType): class Query(ObjectType): product = Field(Product) - schema = build_schema(query=Query, enable_federation_2=True) + schema = build_schema(query=Query, federation_version=LATEST_VERSION) save_file(str(schema), "1") save_file(sdl_query(schema), "2") @@ -90,7 +90,7 @@ class Acme(ObjectType): class Query(ObjectType): acme = Field(Acme) - schema = build_schema(query=Query, enable_federation_2=True) + schema = build_schema(query=Query, federation_version=LATEST_VERSION) save_file(str(schema), "1") save_file(sdl_query(schema), "2") diff --git a/tests/test_requires_v1.py b/tests/test_requires_v1.py index 7570ed4..ece474f 100644 --- a/tests/test_requires_v1.py +++ b/tests/test_requires_v1.py @@ -5,7 +5,7 @@ from graphene import Int from graphene_directives import DirectiveValidationError -from graphene_federation import build_schema, key +from graphene_federation import FederationVersion, build_schema, key from graphene_federation import extends, external, requires from tests.util import file_handlers, sdl_query @@ -41,7 +41,7 @@ class Product(ObjectType): class Query(ObjectType): product = Field(Product) - schema = build_schema(query=Query) + schema = build_schema(query=Query, federation_version=FederationVersion.VERSION_1_0) assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) @@ -63,7 +63,7 @@ class Product(ObjectType): class Query(ObjectType): product = Field(Product) - schema = build_schema(query=Query) + schema = build_schema(query=Query, federation_version=FederationVersion.VERSION_1_0) assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) @@ -84,7 +84,7 @@ class Acme(ObjectType): class Query(ObjectType): acme = Field(Acme) - schema = build_schema(query=Query) + schema = build_schema(query=Query, federation_version=FederationVersion.VERSION_1_0) assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) diff --git a/tests/test_scalar.py b/tests/test_scalar.py index d7d9b9d..f296c7c 100644 --- a/tests/test_scalar.py +++ b/tests/test_scalar.py @@ -5,7 +5,7 @@ from graphene import ObjectType, String from graphene import Scalar -from graphene_federation import build_schema +from graphene_federation import LATEST_VERSION, build_schema from graphene_federation import inaccessible, shareable from tests.util import file_handlers, sdl_query @@ -36,7 +36,7 @@ class Query(ObjectType): test = String(x=AddressScalar()) test2 = graphene.List(AddressScalar, required=True) - schema = build_schema(query=Query, enable_federation_2=True, types=(TestScalar,)) + schema = build_schema(query=Query, federation_version=LATEST_VERSION, types=(TestScalar,)) assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) diff --git a/tests/test_schema_annotation.py b/tests/test_schema_annotation.py index e2db7e1..f487e90 100644 --- a/tests/test_schema_annotation.py +++ b/tests/test_schema_annotation.py @@ -4,7 +4,7 @@ from graphene import NonNull from graphql import graphql_sync -from graphene_federation import build_schema, key +from graphene_federation import LATEST_VERSION, build_schema, key from graphene_federation import extends, external from tests.util import file_handlers, sdl_query @@ -42,7 +42,7 @@ def resolve_user(self, info, user_id, *args, **kwargs): return User(**next(filter(lambda x: x["user_id"] == user_id, users))) -user_schema = build_schema(query=UserQuery, enable_federation_2=True) +user_schema = build_schema(query=UserQuery, federation_version=LATEST_VERSION) # ------------------------ # Chat service @@ -80,7 +80,7 @@ def resolve_message(self, info, id, *args, **kwargs): return ChatMessage(**next(filter(lambda x: x["id"] == id, chat_messages))) -chat_schema = build_schema(query=ChatQuery, enable_federation_2=True) +chat_schema = build_schema(query=ChatQuery, federation_version=LATEST_VERSION) # ------------------------ diff --git a/tests/test_schema_annotation_v1.py b/tests/test_schema_annotation_v1.py index b87fe83..69caf55 100644 --- a/tests/test_schema_annotation_v1.py +++ b/tests/test_schema_annotation_v1.py @@ -4,7 +4,7 @@ from graphene import NonNull from graphql import graphql_sync -from graphene_federation import build_schema, key +from graphene_federation import FederationVersion, build_schema, key from graphene_federation import extends, external from tests.util import file_handlers, sdl_query @@ -42,7 +42,9 @@ def resolve_user(self, info, user_id, *args, **kwargs): return User(**next(filter(lambda x: x["user_id"] == user_id, users))) -user_schema = build_schema(query=UserQuery) +user_schema = build_schema( + query=UserQuery, federation_version=FederationVersion.VERSION_1_0 +) # ------------------------ # Chat service @@ -80,7 +82,9 @@ def resolve_message(self, info, id, *args, **kwargs): return ChatMessage(**next(filter(lambda x: x["id"] == id, chat_messages))) -chat_schema = build_schema(query=ChatQuery) +chat_schema = build_schema( + query=ChatQuery, federation_version=FederationVersion.VERSION_1_0 +) # ------------------------ # Tests diff --git a/tests/test_shareable.py b/tests/test_shareable.py index 33eb807..5eebe39 100644 --- a/tests/test_shareable.py +++ b/tests/test_shareable.py @@ -5,7 +5,7 @@ from graphene import ObjectType from graphene_directives import DirectiveValidationError -from graphene_federation import build_schema +from graphene_federation import LATEST_VERSION, build_schema from graphene_federation import shareable from tests.util import file_handlers, sdl_query @@ -31,7 +31,9 @@ class Query(ObjectType): in_stock_count = graphene.Int(required=True) build_schema( - query=Query, enable_federation_2=True, types=(ReviewInterface, Review) + query=Query, + federation_version=LATEST_VERSION, + types=(ReviewInterface, Review), ) assert "@shareable cannot be used for ReviewInterface" in str(err.value) @@ -46,7 +48,9 @@ class Position(graphene.ObjectType): class Query(ObjectType): in_stock_count = graphene.Int(required=True) - schema = build_schema(query=Query, enable_federation_2=True, types=(Position,)) + schema = build_schema( + query=Query, federation_version=LATEST_VERSION, types=(Position,) + ) assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) @@ -78,6 +82,8 @@ class Meta: class Query(ObjectType): in_stock_count = graphene.Int(required=True) - _ = build_schema(query=Query, enable_federation_2=True, types=(SearchResult,)) + _ = build_schema( + query=Query, federation_version=LATEST_VERSION, types=(SearchResult,) + ) assert "@shareable cannot be used for SearchResult" in str(err.value) From 99904a6b30fec798e2418281ef2916176915c22e Mon Sep 17 00:00:00 2001 From: M Aswin Kishore <60577077+mak626@users.noreply.github.com> Date: Sat, 9 Mar 2024 01:05:15 +0530 Subject: [PATCH 16/19] feat: add support for federation v2.7 --- README.md | 10 ++-- federation_spec/federation-v2.7.graphql | 52 +++++++++++++++++++ .../apollo_versions/__init__.py | 13 +++-- graphene_federation/apollo_versions/v2_7.py | 24 +++++++++ .../apollo_versions/version.py | 1 + graphene_federation/directives/override.py | 3 +- ...t_annotate_object_with_meta_name_1.graphql | 2 +- ...t_annotate_object_with_meta_name_2.graphql | 2 +- ...otated_field_also_used_in_filter_1.graphql | 2 +- ...otated_field_also_used_in_filter_2.graphql | 2 +- .../test_camel_case_field_name_1.graphql | 2 +- .../test_camel_case_field_name_2.graphql | 2 +- ...ield_name_without_auto_camelcase_1.graphql | 2 +- ...ield_name_without_auto_camelcase_2.graphql | 2 +- .../test_similar_field_name_1.graphql | 2 +- .../test_similar_field_name_2.graphql | 2 +- .../test_custom_enum_1.graphql | 2 +- .../test_custom_enum_2.graphql | 2 +- .../test_inaccessible_1.graphql | 2 +- .../test_inaccessible_2.graphql | 2 +- .../test_inaccessible_union_1.graphql | 2 +- .../test_inaccessible_union_2.graphql | 2 +- .../test_compound_primary_key_1.graphql | 2 +- .../test_compound_primary_key_2.graphql | 2 +- ..._compound_primary_key_with_depth_1.graphql | 2 +- ..._compound_primary_key_with_depth_2.graphql | 2 +- .../gql/test_key/test_multiple_keys_1.graphql | 2 +- .../gql/test_key/test_multiple_keys_2.graphql | 2 +- .../gql/test_override/test_override_1.graphql | 28 ++++++++++ .../gql/test_override/test_override_2.graphql | 23 ++++++++ .../gql/test_provides/test_provides_1.graphql | 2 +- .../gql/test_provides/test_provides_2.graphql | 2 +- .../test_provides_multiple_fields_1.graphql | 2 +- .../test_provides_multiple_fields_2.graphql | 2 +- ...provides_multiple_fields_as_list_1.graphql | 2 +- ...provides_multiple_fields_as_list_2.graphql | 2 +- .../test_requires_multiple_fields_1.graphql | 2 +- .../test_requires_multiple_fields_2.graphql | 2 +- ...requires_multiple_fields_as_list_1.graphql | 2 +- ...requires_multiple_fields_as_list_2.graphql | 2 +- .../test_requires_with_input_1.graphql | 2 +- .../test_requires_with_input_2.graphql | 2 +- .../test_scalar/test_custom_scalar_1.graphql | 2 +- .../test_scalar/test_custom_scalar_2.graphql | 2 +- .../test_chat_schema_1.graphql | 2 +- .../test_chat_schema_2.graphql | 2 +- .../test_user_schema_1.graphql | 2 +- .../test_user_schema_2.graphql | 2 +- .../test_shareable/test_shareable_1.graphql | 2 +- .../test_shareable/test_shareable_2.graphql | 2 +- tests/test_annotation_corner_cases.py | 15 ++++++ tests/test_annotation_corner_cases_v1.py | 3 ++ tests/test_custom_enum.py | 3 ++ tests/test_inaccessible.py | 6 +++ tests/test_key.py | 10 ++++ tests/test_key_v1.py | 3 ++ tests/test_override.py | 48 +++++++++++++++++ tests/test_provides.py | 9 ++++ tests/test_provides_v1.py | 9 ++++ tests/test_requires.py | 12 ++--- tests/test_requires_v1.py | 9 ++++ tests/test_scalar.py | 7 ++- tests/test_shareable.py | 3 ++ 63 files changed, 316 insertions(+), 59 deletions(-) create mode 100644 federation_spec/federation-v2.7.graphql create mode 100644 graphene_federation/apollo_versions/v2_7.py create mode 100644 tests/gql/test_override/test_override_1.graphql create mode 100644 tests/gql/test_override/test_override_2.graphql create mode 100644 tests/test_override.py diff --git a/README.md b/README.md index 009a2c0..0bf6504 100644 --- a/README.md +++ b/README.md @@ -37,13 +37,14 @@ If you need to use a version compatible with `graphene` v2 I recommend using the - [x] v2.2 - [x] v2.3 - [x] v2.4 -- [x] v2.5 `STABLE_VERSION` . Rover dev supports only upto v2.5 -- [x] v2.6 `LATEST_VERSION` +- [x] v2.5 +- [x] v2.6 `STABLE_VERSION` . Rover dev supports only upto v2.6 +- [x] v2.7 `LATEST_VERSION` All directives could be easily integrated with the help of [graphene-directives](https://github.com/strollby/graphene-directives). Now every directive's values are validated at run time itself by [graphene-directives](https://github.com/strollby/graphene-directives). -### Directives (v2.6) +### Directives (v2.7) ```graphql directive @composeDirective(name: String!) repeatable on SCHEMA @@ -62,7 +63,7 @@ directive @inaccessible on | INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION directive @interfaceObject on OBJECT -directive @override(from: String!) on FIELD_DEFINITION +directive @override(from: String!, label: String) on FIELD_DEFINITION directive @provides(fields: FieldSet!) on FIELD_DEFINITION directive @requires(fields: FieldSet!) on FIELD_DEFINITION directive @shareable repeatable on FIELD_DEFINITION | OBJECT @@ -98,7 +99,6 @@ directive @policy(policies: [[federation__Policy!]!]!) on scalar federation__Policy scalar federation__Scope scalar FieldSet - ``` Read about directives in [official documentation](https://www.apollographql.com/docs/federation/federated-types/federated-directives) diff --git a/federation_spec/federation-v2.7.graphql b/federation_spec/federation-v2.7.graphql new file mode 100644 index 0000000..39e27f6 --- /dev/null +++ b/federation_spec/federation-v2.7.graphql @@ -0,0 +1,52 @@ +directive @composeDirective(name: String!) repeatable on SCHEMA +directive @extends on OBJECT | INTERFACE +directive @external on OBJECT | FIELD_DEFINITION +directive @key(fields: FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE +directive @inaccessible on + | FIELD_DEFINITION + | OBJECT + | INTERFACE + | UNION + | ENUM + | ENUM_VALUE + | SCALAR + | INPUT_OBJECT + | INPUT_FIELD_DEFINITION + | ARGUMENT_DEFINITION +directive @interfaceObject on OBJECT +directive @override(from: String!, label: String) on FIELD_DEFINITION +directive @provides(fields: FieldSet!) on FIELD_DEFINITION +directive @requires(fields: FieldSet!) on FIELD_DEFINITION +directive @shareable repeatable on FIELD_DEFINITION | OBJECT +directive @tag(name: String!) repeatable on + | FIELD_DEFINITION + | INTERFACE + | OBJECT + | UNION + | ARGUMENT_DEFINITION + | SCALAR + | ENUM + | ENUM_VALUE + | INPUT_OBJECT + | INPUT_FIELD_DEFINITION +directive @authenticated on + FIELD_DEFINITION + | OBJECT + | INTERFACE + | SCALAR + | ENUM +directive @requiresScopes(scopes: [[federation__Scope!]!]!) on + FIELD_DEFINITION + | OBJECT + | INTERFACE + | SCALAR + | ENUM +directive @policy(policies: [[federation__Policy!]!]!) on + | FIELD_DEFINITION + | OBJECT + | INTERFACE + | SCALAR + | ENUM +scalar federation__Policy +scalar federation__Scope +scalar FieldSet diff --git a/graphene_federation/apollo_versions/__init__.py b/graphene_federation/apollo_versions/__init__.py index becdc7b..46e398f 100644 --- a/graphene_federation/apollo_versions/__init__.py +++ b/graphene_federation/apollo_versions/__init__.py @@ -8,10 +8,13 @@ from .v2_4 import get_directives as get_directives_v2_4 from .v2_5 import get_directives as get_directives_v2_5 from .v2_6 import get_directives as get_directives_v2_6 +from .v2_7 import get_directives as get_directives_v2_7 from .version import FederationVersion -LATEST_VERSION = FederationVersion.VERSION_2_6 -STABLE_VERSION = FederationVersion.VERSION_2_5 +LATEST_VERSION = FederationVersion.VERSION_2_7 + +# Stable version is determined with the latest version that rover cli supports +STABLE_VERSION = FederationVersion.VERSION_2_6 def get_directives_based_on_version( @@ -20,7 +23,7 @@ def get_directives_based_on_version( """ Returns a dictionary of [directive_name, directive] for the specified federation version - If no match is found for the specified federation version, latest is taken + If no match is found for the specified federation version, the latest is taken """ if federation_version == FederationVersion.VERSION_1_0: return get_directives_v1_0() @@ -38,8 +41,10 @@ def get_directives_based_on_version( return get_directives_v2_5() if federation_version == FederationVersion.VERSION_2_6: return get_directives_v2_6() + if federation_version == FederationVersion.VERSION_2_7: + return get_directives_v2_7() - return get_directives_v2_6() + return get_directives_v2_7() def get_directive_from_name( diff --git a/graphene_federation/apollo_versions/v2_7.py b/graphene_federation/apollo_versions/v2_7.py new file mode 100644 index 0000000..6714bab --- /dev/null +++ b/graphene_federation/apollo_versions/v2_7.py @@ -0,0 +1,24 @@ +from graphene_directives import CustomDirective, DirectiveLocation +from graphql import GraphQLArgument, GraphQLDirective, GraphQLNonNull, GraphQLString + +from .v2_6 import get_directives as get_directives_v2_6 + +override_directive = CustomDirective( + name="override", + locations=[ + DirectiveLocation.FIELD_DEFINITION, + ], + args={ + "from": GraphQLArgument(GraphQLNonNull(GraphQLString)), + "label": GraphQLArgument(GraphQLString), + }, + description="Federation @override directive", + add_definition_to_schema=False, +) + + +# @override Change, Added label argument +def get_directives() -> dict[str, GraphQLDirective]: + directives = get_directives_v2_6() + directives.update({directive.name: directive for directive in [override_directive]}) + return directives diff --git a/graphene_federation/apollo_versions/version.py b/graphene_federation/apollo_versions/version.py index ce2c0e7..fc01edb 100644 --- a/graphene_federation/apollo_versions/version.py +++ b/graphene_federation/apollo_versions/version.py @@ -10,3 +10,4 @@ class FederationVersion(Enum): VERSION_2_4 = "2.4" VERSION_2_5 = "2.5" VERSION_2_6 = "2.6" + VERSION_2_7 = "2.7" diff --git a/graphene_federation/directives/override.py b/graphene_federation/directives/override.py index 2f5a406..ce33447 100644 --- a/graphene_federation/directives/override.py +++ b/graphene_federation/directives/override.py @@ -13,6 +13,7 @@ def override( graphene_type, from_: str, + label: str = None, *, federation_version: FederationVersion = LATEST_VERSION, ) -> Callable: @@ -40,7 +41,7 @@ def wrapper(field_or_type): ] ) ) - return decorator(field=field_or_type, **{"from": from_}) + return decorator(field=field_or_type, **{"from": from_, "label": label}) if graphene_type: return wrapper(graphene_type) diff --git a/tests/gql/test_annotation_corner_cases/test_annotate_object_with_meta_name_1.graphql b/tests/gql/test_annotation_corner_cases/test_annotate_object_with_meta_name_1.graphql index 7dc8a97..b89ff4a 100644 --- a/tests/gql/test_annotation_corner_cases/test_annotate_object_with_meta_name_1.graphql +++ b/tests/gql/test_annotation_corner_cases/test_annotate_object_with_meta_name_1.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@extends", "@external", "@key"]) + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@extends", "@external", "@key"]) type Query { a: Banana diff --git a/tests/gql/test_annotation_corner_cases/test_annotate_object_with_meta_name_2.graphql b/tests/gql/test_annotation_corner_cases/test_annotate_object_with_meta_name_2.graphql index c4e3a4d..562cbf8 100644 --- a/tests/gql/test_annotation_corner_cases/test_annotate_object_with_meta_name_2.graphql +++ b/tests/gql/test_annotation_corner_cases/test_annotate_object_with_meta_name_2.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@extends", "@external", "@key"]) + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@extends", "@external", "@key"]) type Query { a: Banana diff --git a/tests/gql/test_annotation_corner_cases/test_annotated_field_also_used_in_filter_1.graphql b/tests/gql/test_annotation_corner_cases/test_annotated_field_also_used_in_filter_1.graphql index a8c4bac..b1c768d 100644 --- a/tests/gql/test_annotation_corner_cases/test_annotated_field_also_used_in_filter_1.graphql +++ b/tests/gql/test_annotation_corner_cases/test_annotated_field_also_used_in_filter_1.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@extends", "@external", "@key"]) + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@extends", "@external", "@key"]) type Query { a: A diff --git a/tests/gql/test_annotation_corner_cases/test_annotated_field_also_used_in_filter_2.graphql b/tests/gql/test_annotation_corner_cases/test_annotated_field_also_used_in_filter_2.graphql index 094efb9..a19de00 100644 --- a/tests/gql/test_annotation_corner_cases/test_annotated_field_also_used_in_filter_2.graphql +++ b/tests/gql/test_annotation_corner_cases/test_annotated_field_also_used_in_filter_2.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@extends", "@external", "@key"]) + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@extends", "@external", "@key"]) type Query { a: A diff --git a/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_1.graphql b/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_1.graphql index 5c17726..53b2a74 100644 --- a/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_1.graphql +++ b/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_1.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@extends", "@external", "@key", "@requires"]) + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@extends", "@external", "@key", "@requires"]) type Query { camel: Camel diff --git a/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_2.graphql b/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_2.graphql index d0cd54f..f57b03e 100644 --- a/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_2.graphql +++ b/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_2.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@extends", "@external", "@key", "@requires"]) + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@extends", "@external", "@key", "@requires"]) type Query { camel: Camel diff --git a/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_without_auto_camelcase_1.graphql b/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_without_auto_camelcase_1.graphql index d4a7c57..45dddc2 100644 --- a/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_without_auto_camelcase_1.graphql +++ b/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_without_auto_camelcase_1.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@extends", "@external", "@requires"]) + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@extends", "@external", "@requires"]) type Query { camel: Camel diff --git a/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_without_auto_camelcase_2.graphql b/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_without_auto_camelcase_2.graphql index 16b37c1..8edc83c 100644 --- a/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_without_auto_camelcase_2.graphql +++ b/tests/gql/test_annotation_corner_cases/test_camel_case_field_name_without_auto_camelcase_2.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@extends", "@external", "@requires"]) + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@extends", "@external", "@requires"]) type Query { camel: Camel diff --git a/tests/gql/test_annotation_corner_cases/test_similar_field_name_1.graphql b/tests/gql/test_annotation_corner_cases/test_similar_field_name_1.graphql index c4b3f67..c02d8d3 100644 --- a/tests/gql/test_annotation_corner_cases/test_similar_field_name_1.graphql +++ b/tests/gql/test_annotation_corner_cases/test_similar_field_name_1.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@extends", "@external", "@key"]) + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@extends", "@external", "@key"]) schema { query: ChatQuery diff --git a/tests/gql/test_annotation_corner_cases/test_similar_field_name_2.graphql b/tests/gql/test_annotation_corner_cases/test_similar_field_name_2.graphql index f4cfea2..0bc8d56 100644 --- a/tests/gql/test_annotation_corner_cases/test_similar_field_name_2.graphql +++ b/tests/gql/test_annotation_corner_cases/test_similar_field_name_2.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@extends", "@external", "@key"]) + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@extends", "@external", "@key"]) schema { query: ChatQuery diff --git a/tests/gql/test_custom_enum/test_custom_enum_1.graphql b/tests/gql/test_custom_enum/test_custom_enum_1.graphql index 1e84768..dc39872 100644 --- a/tests/gql/test_custom_enum/test_custom_enum_1.graphql +++ b/tests/gql/test_custom_enum/test_custom_enum_1.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@inaccessible", "@shareable"]) + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@inaccessible", "@shareable"]) type TestCustomEnum @shareable { testShareableScalar: Episode @shareable diff --git a/tests/gql/test_custom_enum/test_custom_enum_2.graphql b/tests/gql/test_custom_enum/test_custom_enum_2.graphql index 791a0e8..e9f3d2e 100644 --- a/tests/gql/test_custom_enum/test_custom_enum_2.graphql +++ b/tests/gql/test_custom_enum/test_custom_enum_2.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@inaccessible", "@shareable"]) + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@inaccessible", "@shareable"]) type TestCustomEnum @shareable { testShareableScalar: Episode @shareable diff --git a/tests/gql/test_inaccessible/test_inaccessible_1.graphql b/tests/gql/test_inaccessible/test_inaccessible_1.graphql index 4cf7915..adbf745 100644 --- a/tests/gql/test_inaccessible/test_inaccessible_1.graphql +++ b/tests/gql/test_inaccessible/test_inaccessible_1.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@inaccessible"]) + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@inaccessible"]) type Position @inaccessible { x: Int! diff --git a/tests/gql/test_inaccessible/test_inaccessible_2.graphql b/tests/gql/test_inaccessible/test_inaccessible_2.graphql index eb973d7..9bd20e8 100644 --- a/tests/gql/test_inaccessible/test_inaccessible_2.graphql +++ b/tests/gql/test_inaccessible/test_inaccessible_2.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@inaccessible"]) + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@inaccessible"]) type Position @inaccessible { x: Int! diff --git a/tests/gql/test_inaccessible/test_inaccessible_union_1.graphql b/tests/gql/test_inaccessible/test_inaccessible_union_1.graphql index 462e4f0..eb0927d 100644 --- a/tests/gql/test_inaccessible/test_inaccessible_union_1.graphql +++ b/tests/gql/test_inaccessible/test_inaccessible_union_1.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@inaccessible"]) + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@inaccessible"]) union SearchResult @inaccessible = Human | Droid | Starship diff --git a/tests/gql/test_inaccessible/test_inaccessible_union_2.graphql b/tests/gql/test_inaccessible/test_inaccessible_union_2.graphql index d3b2645..a9403a5 100644 --- a/tests/gql/test_inaccessible/test_inaccessible_union_2.graphql +++ b/tests/gql/test_inaccessible/test_inaccessible_union_2.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@inaccessible"]) + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@inaccessible"]) union SearchResult @inaccessible = Human | Droid | Starship diff --git a/tests/gql/test_key/test_compound_primary_key_1.graphql b/tests/gql/test_key/test_compound_primary_key_1.graphql index a1c644c..3bcc601 100644 --- a/tests/gql/test_key/test_compound_primary_key_1.graphql +++ b/tests/gql/test_key/test_compound_primary_key_1.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@key"]) + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@key"]) type Query { user: User diff --git a/tests/gql/test_key/test_compound_primary_key_2.graphql b/tests/gql/test_key/test_compound_primary_key_2.graphql index ac94857..3c48134 100644 --- a/tests/gql/test_key/test_compound_primary_key_2.graphql +++ b/tests/gql/test_key/test_compound_primary_key_2.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@key"]) + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@key"]) type Query { user: User diff --git a/tests/gql/test_key/test_compound_primary_key_with_depth_1.graphql b/tests/gql/test_key/test_compound_primary_key_with_depth_1.graphql index fc3ad3e..c8dfebd 100644 --- a/tests/gql/test_key/test_compound_primary_key_with_depth_1.graphql +++ b/tests/gql/test_key/test_compound_primary_key_with_depth_1.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@key"]) + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@key"]) type Query { user: User diff --git a/tests/gql/test_key/test_compound_primary_key_with_depth_2.graphql b/tests/gql/test_key/test_compound_primary_key_with_depth_2.graphql index 2275a7d..54430d3 100644 --- a/tests/gql/test_key/test_compound_primary_key_with_depth_2.graphql +++ b/tests/gql/test_key/test_compound_primary_key_with_depth_2.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@key"]) + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@key"]) type Query { user: User diff --git a/tests/gql/test_key/test_multiple_keys_1.graphql b/tests/gql/test_key/test_multiple_keys_1.graphql index cc813bd..9e1d066 100644 --- a/tests/gql/test_key/test_multiple_keys_1.graphql +++ b/tests/gql/test_key/test_multiple_keys_1.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@key"]) + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@key"]) type Query { user: User diff --git a/tests/gql/test_key/test_multiple_keys_2.graphql b/tests/gql/test_key/test_multiple_keys_2.graphql index e1bf5cd..815b80c 100644 --- a/tests/gql/test_key/test_multiple_keys_2.graphql +++ b/tests/gql/test_key/test_multiple_keys_2.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@key"]) + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@key"]) type Query { user: User diff --git a/tests/gql/test_override/test_override_1.graphql b/tests/gql/test_override/test_override_1.graphql new file mode 100644 index 0000000..86554c8 --- /dev/null +++ b/tests/gql/test_override/test_override_1.graphql @@ -0,0 +1,28 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@override"]) + +type Query { + product: Product + _service: _Service! +} + +type Product { + sku: ID @override(from: "subgraph-1") + size: Int @override(from: "subgraph-2") + weight: Int @override(from: "subgraph-3", label: "Test label") +} + +type _Service { + sdl: String +} + +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" +scalar FieldSet + +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope + +"""This string-serialized scalar represents an authorization policy.""" +scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_override/test_override_2.graphql b/tests/gql/test_override/test_override_2.graphql new file mode 100644 index 0000000..3d717b6 --- /dev/null +++ b/tests/gql/test_override/test_override_2.graphql @@ -0,0 +1,23 @@ +extend schema + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@override"]) + +type Query { + product: Product +} + +type Product { + sku: ID @override(from: "subgraph-1") + size: Int @override(from: "subgraph-2") + weight: Int @override(from: "subgraph-3", label: "Test label") +} + +""" +A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides +""" +scalar FieldSet + +"""This string-serialized scalar represents a JWT scope""" +scalar federation__Scope + +"""This string-serialized scalar represents an authorization policy.""" +scalar federation__Policy \ No newline at end of file diff --git a/tests/gql/test_provides/test_provides_1.graphql b/tests/gql/test_provides/test_provides_1.graphql index 28c11f0..aa03470 100644 --- a/tests/gql/test_provides/test_provides_1.graphql +++ b/tests/gql/test_provides/test_provides_1.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@external", "@key", "@provides"]) + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@external", "@key", "@provides"]) type Query { inStockCount: InStockCount diff --git a/tests/gql/test_provides/test_provides_2.graphql b/tests/gql/test_provides/test_provides_2.graphql index a91ca1a..a18d895 100644 --- a/tests/gql/test_provides/test_provides_2.graphql +++ b/tests/gql/test_provides/test_provides_2.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@external", "@key", "@provides"]) + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@external", "@key", "@provides"]) type Query { inStockCount: InStockCount diff --git a/tests/gql/test_provides/test_provides_multiple_fields_1.graphql b/tests/gql/test_provides/test_provides_multiple_fields_1.graphql index 4904725..85dad3c 100644 --- a/tests/gql/test_provides/test_provides_multiple_fields_1.graphql +++ b/tests/gql/test_provides/test_provides_multiple_fields_1.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@external", "@key", "@provides"]) + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@external", "@key", "@provides"]) type Query { inStockCount: InStockCount diff --git a/tests/gql/test_provides/test_provides_multiple_fields_2.graphql b/tests/gql/test_provides/test_provides_multiple_fields_2.graphql index a2721ff..a5f1511 100644 --- a/tests/gql/test_provides/test_provides_multiple_fields_2.graphql +++ b/tests/gql/test_provides/test_provides_multiple_fields_2.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@external", "@key", "@provides"]) + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@external", "@key", "@provides"]) type Query { inStockCount: InStockCount diff --git a/tests/gql/test_provides/test_provides_multiple_fields_as_list_1.graphql b/tests/gql/test_provides/test_provides_multiple_fields_as_list_1.graphql index 8ee489d..83672e2 100644 --- a/tests/gql/test_provides/test_provides_multiple_fields_as_list_1.graphql +++ b/tests/gql/test_provides/test_provides_multiple_fields_as_list_1.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@extends", "@external", "@key", "@provides"]) + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@extends", "@external", "@key", "@provides"]) type Query { inStockCount: InStockCount diff --git a/tests/gql/test_provides/test_provides_multiple_fields_as_list_2.graphql b/tests/gql/test_provides/test_provides_multiple_fields_as_list_2.graphql index b64e8cf..ff687af 100644 --- a/tests/gql/test_provides/test_provides_multiple_fields_as_list_2.graphql +++ b/tests/gql/test_provides/test_provides_multiple_fields_as_list_2.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@extends", "@external", "@key", "@provides"]) + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@extends", "@external", "@key", "@provides"]) type Query { inStockCount: InStockCount diff --git a/tests/gql/test_requires/test_requires_multiple_fields_1.graphql b/tests/gql/test_requires/test_requires_multiple_fields_1.graphql index ba14d2e..d408da1 100644 --- a/tests/gql/test_requires/test_requires_multiple_fields_1.graphql +++ b/tests/gql/test_requires/test_requires_multiple_fields_1.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@extends", "@external", "@key", "@requires"]) + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@extends", "@external", "@key", "@requires"]) type Query { product: Product diff --git a/tests/gql/test_requires/test_requires_multiple_fields_2.graphql b/tests/gql/test_requires/test_requires_multiple_fields_2.graphql index 2bd6903..7ff53e0 100644 --- a/tests/gql/test_requires/test_requires_multiple_fields_2.graphql +++ b/tests/gql/test_requires/test_requires_multiple_fields_2.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@extends", "@external", "@key", "@requires"]) + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@extends", "@external", "@key", "@requires"]) type Query { product: Product diff --git a/tests/gql/test_requires/test_requires_multiple_fields_as_list_1.graphql b/tests/gql/test_requires/test_requires_multiple_fields_as_list_1.graphql index ba14d2e..d408da1 100644 --- a/tests/gql/test_requires/test_requires_multiple_fields_as_list_1.graphql +++ b/tests/gql/test_requires/test_requires_multiple_fields_as_list_1.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@extends", "@external", "@key", "@requires"]) + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@extends", "@external", "@key", "@requires"]) type Query { product: Product diff --git a/tests/gql/test_requires/test_requires_multiple_fields_as_list_2.graphql b/tests/gql/test_requires/test_requires_multiple_fields_as_list_2.graphql index 2bd6903..7ff53e0 100644 --- a/tests/gql/test_requires/test_requires_multiple_fields_as_list_2.graphql +++ b/tests/gql/test_requires/test_requires_multiple_fields_as_list_2.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@extends", "@external", "@key", "@requires"]) + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@extends", "@external", "@key", "@requires"]) type Query { product: Product diff --git a/tests/gql/test_requires/test_requires_with_input_1.graphql b/tests/gql/test_requires/test_requires_with_input_1.graphql index 267db9d..a898ef2 100644 --- a/tests/gql/test_requires/test_requires_with_input_1.graphql +++ b/tests/gql/test_requires/test_requires_with_input_1.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@extends", "@external", "@key", "@requires"]) + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@extends", "@external", "@key", "@requires"]) type Query { acme: Acme diff --git a/tests/gql/test_requires/test_requires_with_input_2.graphql b/tests/gql/test_requires/test_requires_with_input_2.graphql index d3365ab..2f4d703 100644 --- a/tests/gql/test_requires/test_requires_with_input_2.graphql +++ b/tests/gql/test_requires/test_requires_with_input_2.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@extends", "@external", "@key", "@requires"]) + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@extends", "@external", "@key", "@requires"]) type Query { acme: Acme diff --git a/tests/gql/test_scalar/test_custom_scalar_1.graphql b/tests/gql/test_scalar/test_custom_scalar_1.graphql index c6fc25a..e1607ae 100644 --- a/tests/gql/test_scalar/test_custom_scalar_1.graphql +++ b/tests/gql/test_scalar/test_custom_scalar_1.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@inaccessible", "@shareable"]) + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@inaccessible", "@shareable"]) type TestScalar @shareable { testShareableScalar(x: AddressScalar): String @shareable diff --git a/tests/gql/test_scalar/test_custom_scalar_2.graphql b/tests/gql/test_scalar/test_custom_scalar_2.graphql index d7e968e..da23193 100644 --- a/tests/gql/test_scalar/test_custom_scalar_2.graphql +++ b/tests/gql/test_scalar/test_custom_scalar_2.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@inaccessible", "@shareable"]) + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@inaccessible", "@shareable"]) type TestScalar @shareable { testShareableScalar(x: AddressScalar): String @shareable diff --git a/tests/gql/test_schema_annotation/test_chat_schema_1.graphql b/tests/gql/test_schema_annotation/test_chat_schema_1.graphql index 4776a75..c1ba222 100644 --- a/tests/gql/test_schema_annotation/test_chat_schema_1.graphql +++ b/tests/gql/test_schema_annotation/test_chat_schema_1.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@extends", "@external", "@key"]) + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@extends", "@external", "@key"]) schema { query: ChatQuery diff --git a/tests/gql/test_schema_annotation/test_chat_schema_2.graphql b/tests/gql/test_schema_annotation/test_chat_schema_2.graphql index 7a25553..2ea0c63 100644 --- a/tests/gql/test_schema_annotation/test_chat_schema_2.graphql +++ b/tests/gql/test_schema_annotation/test_chat_schema_2.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@extends", "@external", "@key"]) + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@extends", "@external", "@key"]) schema { query: ChatQuery diff --git a/tests/gql/test_schema_annotation/test_user_schema_1.graphql b/tests/gql/test_schema_annotation/test_user_schema_1.graphql index b72f0ce..3f80e75 100644 --- a/tests/gql/test_schema_annotation/test_user_schema_1.graphql +++ b/tests/gql/test_schema_annotation/test_user_schema_1.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@key"]) + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@key"]) schema { query: UserQuery diff --git a/tests/gql/test_schema_annotation/test_user_schema_2.graphql b/tests/gql/test_schema_annotation/test_user_schema_2.graphql index 6778830..e20f2cc 100644 --- a/tests/gql/test_schema_annotation/test_user_schema_2.graphql +++ b/tests/gql/test_schema_annotation/test_user_schema_2.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@key"]) + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@key"]) schema { query: UserQuery diff --git a/tests/gql/test_shareable/test_shareable_1.graphql b/tests/gql/test_shareable/test_shareable_1.graphql index d9003c9..417c965 100644 --- a/tests/gql/test_shareable/test_shareable_1.graphql +++ b/tests/gql/test_shareable/test_shareable_1.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@shareable"]) + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@shareable"]) type Position @shareable { x: Int! diff --git a/tests/gql/test_shareable/test_shareable_2.graphql b/tests/gql/test_shareable/test_shareable_2.graphql index af8ffc2..5e585f5 100644 --- a/tests/gql/test_shareable/test_shareable_2.graphql +++ b/tests/gql/test_shareable/test_shareable_2.graphql @@ -1,5 +1,5 @@ extend schema - @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@shareable"]) + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@shareable"]) type Position @shareable { x: Int! diff --git a/tests/test_annotation_corner_cases.py b/tests/test_annotation_corner_cases.py index 87ca6fc..276cae5 100644 --- a/tests/test_annotation_corner_cases.py +++ b/tests/test_annotation_corner_cases.py @@ -38,6 +38,9 @@ class ChatQuery(ObjectType): schema = build_schema(query=ChatQuery, federation_version=LATEST_VERSION) + # save_file(str(schema), "1") + # save_file(sdl_query(schema), "2") + assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) @@ -60,6 +63,9 @@ class Query(ObjectType): schema = build_schema(query=Query, federation_version=LATEST_VERSION) + # save_file(str(schema), "1") + # save_file(sdl_query(schema), "2") + assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) @@ -83,6 +89,9 @@ class Query(ObjectType): query=Query, auto_camelcase=False, federation_version=LATEST_VERSION ) + # save_file(str(schema), "1") + # save_file(sdl_query(schema), "2") + assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) @@ -107,6 +116,9 @@ class Query(ObjectType): schema = build_schema(query=Query, federation_version=LATEST_VERSION) + # save_file(str(schema), "1") + # save_file(sdl_query(schema), "2") + assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) @@ -132,5 +144,8 @@ class Query(ObjectType): schema = build_schema(query=Query, federation_version=LATEST_VERSION) + # save_file(str(schema), "1") + # save_file(sdl_query(schema), "2") + assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) diff --git a/tests/test_annotation_corner_cases_v1.py b/tests/test_annotation_corner_cases_v1.py index 4dc25ed..70c0e0f 100644 --- a/tests/test_annotation_corner_cases_v1.py +++ b/tests/test_annotation_corner_cases_v1.py @@ -40,6 +40,9 @@ class ChatQuery(ObjectType): query=ChatQuery, federation_version=FederationVersion.VERSION_1_0 ) + # save_file(str(schema), "1") + # save_file(sdl_query(schema), "2") + assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) diff --git a/tests/test_custom_enum.py b/tests/test_custom_enum.py index 357a04b..867b4cd 100644 --- a/tests/test_custom_enum.py +++ b/tests/test_custom_enum.py @@ -32,5 +32,8 @@ class Query(ObjectType): query=Query, federation_version=LATEST_VERSION, types=(TestCustomEnum,) ) + # save_file(str(schema), "1") + # save_file(sdl_query(schema), "2") + assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) diff --git a/tests/test_inaccessible.py b/tests/test_inaccessible.py index 302693b..f3d485f 100644 --- a/tests/test_inaccessible.py +++ b/tests/test_inaccessible.py @@ -43,6 +43,9 @@ class Query(ObjectType): query=Query, federation_version=LATEST_VERSION, types=(Position,) ) + # save_file(str(schema), "1") + # save_file(sdl_query(schema), "2") + assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) @@ -75,5 +78,8 @@ class Query(ObjectType): query=Query, federation_version=LATEST_VERSION, types=(SearchResult,) ) + # save_file(str(schema), "1") + # save_file(sdl_query(schema), "2") + assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) diff --git a/tests/test_key.py b/tests/test_key.py index 5d1df8c..affab19 100644 --- a/tests/test_key.py +++ b/tests/test_key.py @@ -21,6 +21,9 @@ class Query(ObjectType): schema = build_schema(query=Query, federation_version=LATEST_VERSION) + # save_file(str(schema), "1") + # save_file(sdl_query(schema), "2") + assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) @@ -54,6 +57,9 @@ class Query(ObjectType): schema = build_schema(query=Query, federation_version=LATEST_VERSION) + # save_file(str(schema), "1") + # save_file(sdl_query(schema), "2") + assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) @@ -76,6 +82,10 @@ class Query(ObjectType): user = Field(User) schema = build_schema(query=Query, federation_version=LATEST_VERSION) + + # save_file(str(schema), "1") + # save_file(sdl_query(schema), "2") + assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) diff --git a/tests/test_key_v1.py b/tests/test_key_v1.py index 9708a88..ca8363b 100644 --- a/tests/test_key_v1.py +++ b/tests/test_key_v1.py @@ -21,6 +21,9 @@ class Query(ObjectType): schema = build_schema(query=Query, federation_version=FederationVersion.VERSION_1_0) + # save_file(str(schema), "1") + # save_file(sdl_query(schema), "2") + assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) diff --git a/tests/test_override.py b/tests/test_override.py new file mode 100644 index 0000000..e16baf8 --- /dev/null +++ b/tests/test_override.py @@ -0,0 +1,48 @@ +from pathlib import Path + +import pytest +from graphene import Field, ID, ObjectType, String +from graphene import Int +from graphene_directives import DirectiveValidationError + +from graphene_federation import LATEST_VERSION, build_schema +from graphene_federation import override +from tests.util import file_handlers, sdl_query + +save_file, open_file = file_handlers(Path(__file__)) + + +def test_chain_requires_failure(): + """ + Check that we can't nest call the override method on a field. + """ + with pytest.raises(DirectiveValidationError) as err: + + class A(ObjectType): + something = override( + override(String(), from_="subgraph-1"), from_="subgraph-2" + ) + + assert "@override is not repeatable" in str(err.value) + + +def test_override(): + """ + Check that requires can take more than one field as input. + """ + + class Product(ObjectType): + sku = override(ID(), from_="subgraph-1") + size = override(Int(), from_="subgraph-2") + weight = override(Int(), from_="subgraph-3", label="Test label") + + class Query(ObjectType): + product = Field(Product) + + schema = build_schema(query=Query, federation_version=LATEST_VERSION) + + # save_file(str(schema), "1") + # save_file(sdl_query(schema), "2") + + assert open_file("1") == str(schema) + assert open_file("2") == sdl_query(schema) diff --git a/tests/test_provides.py b/tests/test_provides.py index 6804de5..7a23687 100644 --- a/tests/test_provides.py +++ b/tests/test_provides.py @@ -30,6 +30,9 @@ class Query(ObjectType): schema = build_schema(query=Query, federation_version=LATEST_VERSION) + # save_file(str(schema), "1") + # save_file(sdl_query(schema), "2") + assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) @@ -54,6 +57,9 @@ class Query(ObjectType): schema = build_schema(query=Query, federation_version=LATEST_VERSION) + # save_file(str(schema), "1") + # save_file(sdl_query(schema), "2") + assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) @@ -79,5 +85,8 @@ class Query(ObjectType): schema = build_schema(query=Query, federation_version=LATEST_VERSION) + # save_file(str(schema), "1") + # save_file(sdl_query(schema), "2") + assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) diff --git a/tests/test_provides_v1.py b/tests/test_provides_v1.py index ed50119..c58f8f5 100644 --- a/tests/test_provides_v1.py +++ b/tests/test_provides_v1.py @@ -30,6 +30,9 @@ class Query(ObjectType): schema = build_schema(query=Query, federation_version=FederationVersion.VERSION_1_0) + # save_file(str(schema), "1") + # save_file(sdl_query(schema), "2") + assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) @@ -54,6 +57,9 @@ class Query(ObjectType): schema = build_schema(query=Query, federation_version=FederationVersion.VERSION_1_0) + # save_file(str(schema), "1") + # save_file(sdl_query(schema), "2") + assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) @@ -79,5 +85,8 @@ class Query(ObjectType): schema = build_schema(query=Query, federation_version=FederationVersion.VERSION_1_0) + # save_file(str(schema), "1") + # save_file(sdl_query(schema), "2") + assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) diff --git a/tests/test_requires.py b/tests/test_requires.py index dfb5077..daa8190 100644 --- a/tests/test_requires.py +++ b/tests/test_requires.py @@ -43,8 +43,8 @@ class Query(ObjectType): schema = build_schema(query=Query, federation_version=LATEST_VERSION) - save_file(str(schema), "1") - save_file(sdl_query(schema), "2") + # save_file(str(schema), "1") + # save_file(sdl_query(schema), "2") assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) @@ -68,8 +68,8 @@ class Query(ObjectType): schema = build_schema(query=Query, federation_version=LATEST_VERSION) - save_file(str(schema), "1") - save_file(sdl_query(schema), "2") + # save_file(str(schema), "1") + # save_file(sdl_query(schema), "2") assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) @@ -92,8 +92,8 @@ class Query(ObjectType): schema = build_schema(query=Query, federation_version=LATEST_VERSION) - save_file(str(schema), "1") - save_file(sdl_query(schema), "2") + # save_file(str(schema), "1") + # save_file(sdl_query(schema), "2") assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) diff --git a/tests/test_requires_v1.py b/tests/test_requires_v1.py index ece474f..2c1722e 100644 --- a/tests/test_requires_v1.py +++ b/tests/test_requires_v1.py @@ -43,6 +43,9 @@ class Query(ObjectType): schema = build_schema(query=Query, federation_version=FederationVersion.VERSION_1_0) + # save_file(str(schema), "1") + # save_file(sdl_query(schema), "2") + assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) @@ -65,6 +68,9 @@ class Query(ObjectType): schema = build_schema(query=Query, federation_version=FederationVersion.VERSION_1_0) + # save_file(str(schema), "1") + # save_file(sdl_query(schema), "2") + assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) @@ -86,5 +92,8 @@ class Query(ObjectType): schema = build_schema(query=Query, federation_version=FederationVersion.VERSION_1_0) + # save_file(str(schema), "1") + # save_file(sdl_query(schema), "2") + assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) diff --git a/tests/test_scalar.py b/tests/test_scalar.py index f296c7c..88df382 100644 --- a/tests/test_scalar.py +++ b/tests/test_scalar.py @@ -36,7 +36,12 @@ class Query(ObjectType): test = String(x=AddressScalar()) test2 = graphene.List(AddressScalar, required=True) - schema = build_schema(query=Query, federation_version=LATEST_VERSION, types=(TestScalar,)) + schema = build_schema( + query=Query, federation_version=LATEST_VERSION, types=(TestScalar,) + ) + + # save_file(str(schema), "1") + # save_file(sdl_query(schema), "2") assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) diff --git a/tests/test_shareable.py b/tests/test_shareable.py index 5eebe39..4eaefa0 100644 --- a/tests/test_shareable.py +++ b/tests/test_shareable.py @@ -52,6 +52,9 @@ class Query(ObjectType): query=Query, federation_version=LATEST_VERSION, types=(Position,) ) + # save_file(str(schema), "1") + # save_file(sdl_query(schema), "2") + assert open_file("1") == str(schema) assert open_file("2") == sdl_query(schema) From 06c5e1e4f8c6cf83dfdd56dcc7f3b6f439e88c52 Mon Sep 17 00:00:00 2001 From: M Aswin Kishore <60577077+mak626@users.noreply.github.com> Date: Fri, 22 Mar 2024 15:10:29 +0530 Subject: [PATCH 17/19] feat: Added type conversion when using @requires --- examples/inaccessible.py | 8 +++++--- examples/override.py | 7 +++---- graphene_federation/entity.py | 8 +++++++- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/examples/inaccessible.py b/examples/inaccessible.py index 07ebc98..aac4f16 100644 --- a/examples/inaccessible.py +++ b/examples/inaccessible.py @@ -1,12 +1,12 @@ import graphene from graphene_federation import ( - LATEST_VERSION, inaccessible, + LATEST_VERSION, + inaccessible, external, provides, key, override, - shareable, ) from graphene_federation import build_schema @@ -64,7 +64,9 @@ class Query(graphene.ObjectType): schema = build_schema( - Query, federation_version=LATEST_VERSION, types=(ReviewInterface, SearchResult, Review) + Query, + federation_version=LATEST_VERSION, + types=(ReviewInterface, SearchResult, Review), ) query = """ diff --git a/examples/override.py b/examples/override.py index b076d35..0c9bf67 100644 --- a/examples/override.py +++ b/examples/override.py @@ -1,12 +1,11 @@ import graphene from graphene_federation import ( - LATEST_VERSION, build_schema, - shareable, - external, + LATEST_VERSION, + build_schema, + inaccessible, key, override, - inaccessible, ) diff --git a/graphene_federation/entity.py b/graphene_federation/entity.py index 38322cb..74330d7 100644 --- a/graphene_federation/entity.py +++ b/graphene_federation/entity.py @@ -3,7 +3,7 @@ from typing import Any from typing import Dict, Type -from graphene import Field, List, NonNull, ObjectType, Union +from graphene import Enum, Field, List, NonNull, ObjectType, Scalar, Union from graphene.types.schema import TypeMap from graphene_directives import Schema from graphene_directives.utils import has_non_field_attribute @@ -123,6 +123,12 @@ def resolve_entities(self, info, representations, sub_field_resolution=False): model_arguments[model_field] = EntityQuery.resolve_entities( self, info, representations=value, sub_field_resolution=True ) + elif isinstance(field, Scalar) and getattr( + field, "parse_value", None + ): + model_arguments[model_field] = field.parse_value(value) + elif isinstance(field, Enum): + model_arguments[model_field] = field._meta.enum[value] # noqa model_instance = model(**model_arguments) From bee1ded30d736d9e9a3cdd5b307254ff4731f0d6 Mon Sep 17 00:00:00 2001 From: M Aswin Kishore <60577077+mak626@users.noreply.github.com> Date: Mon, 22 Apr 2024 15:41:57 +0530 Subject: [PATCH 18/19] fix: incorrect @sharable definition in federation v2.2 --- graphene_federation/apollo_versions/v2_2.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/graphene_federation/apollo_versions/v2_2.py b/graphene_federation/apollo_versions/v2_2.py index 18a7b6e..7842917 100644 --- a/graphene_federation/apollo_versions/v2_2.py +++ b/graphene_federation/apollo_versions/v2_2.py @@ -1,7 +1,6 @@ from graphene_directives import CustomDirective, DirectiveLocation from graphql import GraphQLDirective -from .v2_0 import shareable_directive as sharable_directive_v2_0 from .v2_1 import get_directives as get_directives_v2_1 shareable_directive = CustomDirective( @@ -19,6 +18,6 @@ def get_directives() -> dict[str, GraphQLDirective]: directives = get_directives_v2_1() directives.update( - {directive.name: directive for directive in [sharable_directive_v2_0]} + {directive.name: directive for directive in [shareable_directive]} ) return directives From 9fc339112be4248ea2a59d3902f88b06d6699929 Mon Sep 17 00:00:00 2001 From: M Aswin Kishore <60577077+mak626@users.noreply.github.com> Date: Mon, 22 Apr 2024 15:53:37 +0530 Subject: [PATCH 19/19] fix: @sharable is applied multiple times on PageInfo in multithreaded environments --- graphene_federation/schema.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/graphene_federation/schema.py b/graphene_federation/schema.py index 61d6bc9..62c96fb 100644 --- a/graphene_federation/schema.py +++ b/graphene_federation/schema.py @@ -48,12 +48,15 @@ def _add_sharable_to_page_info_type( """ Add @sharable directive to PageInfo type """ - if PageInfo.__name__ in schema.graphql_schema.type_map: + + if page_info := schema.graphql_schema.type_map.get(PageInfo.__name__): try: # PageInfo needs @sharable directive sharable = get_directive_from_name("shareable", federation_version) types.append( - directive_decorator(target_directive=sharable)(field=None)(PageInfo) + directive_decorator(target_directive=sharable)(field=None)( + page_info.graphene_type + ) ) except ValueError: # Federation Version does not support @sharable