diff --git a/graphene_federation/appolo_versions/__init__.py b/graphene_federation/appolo_versions/__init__.py index 6ef0c50..5a94dde 100644 --- a/graphene_federation/appolo_versions/__init__.py +++ b/graphene_federation/appolo_versions/__init__.py @@ -30,6 +30,6 @@ def get_directive_from_name( ) if directive is None: raise ValueError( - f"@{directive_name} not found on federation version {federation_version}" + 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/appolo_versions/v1_0.py index 5e2f907..65079ab 100644 --- a/graphene_federation/appolo_versions/v1_0.py +++ b/graphene_federation/appolo_versions/v1_0.py @@ -2,6 +2,7 @@ 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( @@ -15,6 +16,7 @@ is_repeatable=True, add_definition_to_schema=False, non_field_validator=validate_key, + input_transform=field_set_case_transform, ) requires_directive = CustomDirective( @@ -27,6 +29,7 @@ is_repeatable=True, add_definition_to_schema=False, field_validator=validate_requires, + input_transform=field_set_case_transform, ) diff --git a/graphene_federation/appolo_versions/v2_0.py b/graphene_federation/appolo_versions/v2_0.py index 0dccdce..a703562 100644 --- a/graphene_federation/appolo_versions/v2_0.py +++ b/graphene_federation/appolo_versions/v2_0.py @@ -9,6 +9,7 @@ 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( @@ -26,6 +27,7 @@ is_repeatable=True, add_definition_to_schema=False, non_field_validator=validate_key, + input_transform=field_set_case_transform, ) requires_directive = CustomDirective( @@ -38,6 +40,7 @@ is_repeatable=True, add_definition_to_schema=False, field_validator=validate_requires, + input_transform=field_set_case_transform, ) 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/setup.py b/setup.py index 41555f6..26df441 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,7 @@ def read(*rnames): install_requires=[ "graphene>=3.1", "graphql-core>=3.1", - "graphene-directives>=0.4.0", + "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..389d60f --- /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 + testInaccessibleScalar: Episode +} + +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..82523a9 --- /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 + testInaccessibleScalar: Episode +} + +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"]