diff --git a/CHANGELOG.md b/CHANGELOG.md index 75796d704..1b3e5c543 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Added `InputType` for setting Python representations of GraphQL Input types - Added support for passing `Enum` types directly to `make_executable_schema` - Added `convert_names_case` option to `make_federated_schema`. +- Added support for the `@interfaceObject` directive in Apollo Federation. ## 0.18.1 (2023-02-22) diff --git a/ariadne/contrib/federation/definitions/fed1_0.graphql b/ariadne/contrib/federation/definitions/fed1_0.graphql new file mode 100644 index 000000000..892576582 --- /dev/null +++ b/ariadne/contrib/federation/definitions/fed1_0.graphql @@ -0,0 +1,6 @@ +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 +directive @extends on OBJECT | INTERFACE +scalar _FieldSet diff --git a/ariadne/contrib/federation/definitions/fed2_0.graphql b/ariadne/contrib/federation/definitions/fed2_0.graphql new file mode 100644 index 000000000..4ece1778a --- /dev/null +++ b/ariadne/contrib/federation/definitions/fed2_0.graphql @@ -0,0 +1,46 @@ +# +# https://specs.apollo.dev/federation/v2.0/federation-v2.0.graphql +# + +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 + +# +# https://specs.apollo.dev/link/v1.0/link-v1.0.graphql +# + +directive @link( + url: String!, + as: String, + import: [Import]) +repeatable on SCHEMA + +scalar Import diff --git a/ariadne/contrib/federation/definitions/fed2_1.graphql b/ariadne/contrib/federation/definitions/fed2_1.graphql new file mode 100644 index 000000000..5d39d31ee --- /dev/null +++ b/ariadne/contrib/federation/definitions/fed2_1.graphql @@ -0,0 +1,52 @@ +# +# https://specs.apollo.dev/federation/v2.0/federation-v2.0.graphql +# + +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 + +# +# federation-v2.1 +# + +directive @composeDirective(name: String!) repeatable on SCHEMA + +# +# https://specs.apollo.dev/link/v1.0/link-v1.0.graphql +# + +directive @link( + url: String!, + as: String, + import: [Import]) +repeatable on SCHEMA + +scalar Import diff --git a/ariadne/contrib/federation/definitions/fed2_2.graphql b/ariadne/contrib/federation/definitions/fed2_2.graphql new file mode 100644 index 000000000..de346da04 --- /dev/null +++ b/ariadne/contrib/federation/definitions/fed2_2.graphql @@ -0,0 +1,57 @@ +# +# https://specs.apollo.dev/federation/v2.0/federation-v2.0.graphql +# + +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 @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 + +# +# federation-v2.1 +# + +directive @composeDirective(name: String!) repeatable on SCHEMA + +# +# https://specs.apollo.dev/link/v1.0/link-v1.0.graphql +# + +directive @link( + url: String!, + as: String, + import: [Import]) +repeatable on SCHEMA + +scalar Import + +# +# federation-v2.2 +# + +directive @shareable repeatable on FIELD_DEFINITION | OBJECT diff --git a/ariadne/contrib/federation/definitions/fed2_3.graphql b/ariadne/contrib/federation/definitions/fed2_3.graphql new file mode 100644 index 000000000..4466f9604 --- /dev/null +++ b/ariadne/contrib/federation/definitions/fed2_3.graphql @@ -0,0 +1,63 @@ +# +# https://specs.apollo.dev/federation/v2.0/federation-v2.0.graphql +# + +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 @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 + +# +# federation-v2.1 +# + +directive @composeDirective(name: String!) repeatable on SCHEMA + +# +# https://specs.apollo.dev/link/v1.0/link-v1.0.graphql +# + +directive @link( + url: String!, + as: String, + import: [Import]) +repeatable on SCHEMA + +scalar Import + +# +# federation-v2.2 +# + +directive @shareable repeatable on FIELD_DEFINITION | OBJECT + +# +# federation-v2.3 +# + +directive @interfaceObject on OBJECT diff --git a/ariadne/contrib/federation/schema.py b/ariadne/contrib/federation/schema.py index 6ec9e9635..e9b975390 100644 --- a/ariadne/contrib/federation/schema.py +++ b/ariadne/contrib/federation/schema.py @@ -1,4 +1,5 @@ import re +import os from typing import Dict, List, Optional, Type, Union, cast from graphql import extend_schema, parse @@ -17,6 +18,7 @@ ) from ...schema_names import SchemaNameConverter from ...schema_visitor import SchemaDirectiveVisitor +from ...load_schema import load_schema_from_path from .utils import get_entity_types, purge_schema_directives, resolve_entities base_federation_service_type_defs = """ @@ -29,27 +31,6 @@ {type_token} Query {{ _service: _Service! }} - - directive @external on FIELD_DEFINITION - directive @requires(fields: String!) on FIELD_DEFINITION - directive @provides(fields: String!) on FIELD_DEFINITION - directive @extends on OBJECT | INTERFACE -""" - -# fed 1 typedefs; only @key differs from the rest -federation_one_service_type_defs = """ - directive @key(fields: String!) repeatable on OBJECT | INTERFACE - directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION -""" - -# fed 2 typedefs; adds the new directives, although they are gateway-driven, not subgraph-driven -federation_two_service_type_defs = """ - directive @key(fields: String!, resolvable: Boolean) repeatable on OBJECT | INTERFACE - directive @link(import: [String!], url: String!) repeatable on SCHEMA - directive @shareable on OBJECT | FIELD_DEFINITION - directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION - directive @override(from: String!) on FIELD_DEFINITION - directive @inaccessible on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION """ federation_entity_type_defs = """ @@ -94,10 +75,29 @@ def make_federated_schema( link = re.search( r"(?<=@link).*?url:.*?\"(.*?)\".?[^)]+?", sdl, re.MULTILINE | re.DOTALL ) # use regex to parse if it's fed 1 or fed 2; adds dedicated typedefs per spec + dirname = os.path.dirname(__file__) if link and link.group(1) == "https://specs.apollo.dev/federation/v2.0": - tdl.append(federation_two_service_type_defs) + definitions = load_schema_from_path( + os.path.join(dirname, "./definitions/fed2_0.graphql") + ) + elif link and link.group(1) == "https://specs.apollo.dev/federation/v2.1": + definitions = load_schema_from_path( + os.path.join(dirname, "./definitions/fed2_1.graphql") + ) + elif link and link.group(1) == "https://specs.apollo.dev/federation/v2.2": + definitions = load_schema_from_path( + os.path.join(dirname, "./definitions/fed2_2.graphql") + ) + elif link and link.group(1) == "https://specs.apollo.dev/federation/v2.3": + definitions = load_schema_from_path( + os.path.join(dirname, "./definitions/fed2_3.graphql") + ) else: - tdl.append(federation_one_service_type_defs) + definitions = load_schema_from_path( + os.path.join(dirname, "./definitions/fed1_0.graphql") + ) + + tdl.append(definitions) type_defs = join_type_defs(tdl) schema = make_executable_schema( diff --git a/ariadne/contrib/federation/utils.py b/ariadne/contrib/federation/utils.py index 98585db27..91880535a 100644 --- a/ariadne/contrib/federation/utils.py +++ b/ariadne/contrib/federation/utils.py @@ -57,6 +57,8 @@ "tag", # Federation 2 directive. "override", # Federation 2 directive. "inaccessible", # Federation 2 directive. + "composeDirective", # Federation 2.1 directive. + "interfaceObject", # Federation 2.3 directive. ] diff --git a/tests/federation/test_schema.py b/tests/federation/test_schema.py index f8f21aaad..3a1f1dad5 100644 --- a/tests/federation/test_schema.py +++ b/tests/federation/test_schema.py @@ -21,7 +21,19 @@ def test_federation_one_schema_mark_type_tags(): type_defs = """ type Query - + + directive @tag(name: String!) repeatable on + | FIELD_DEFINITION + | INTERFACE + | OBJECT + | UNION + | ARGUMENT_DEFINITION + | SCALAR + | ENUM + | ENUM_VALUE + | INPUT_OBJECT + | INPUT_FIELD_DEFINITION + type Product @tag(name: "test") { upc: String! name: String @@ -45,7 +57,19 @@ def test_federation_one_schema_mark_type_tags(): def test_federation_one_schema_mark_type_repeated_tags(): type_defs = """ type Query - + + directive @tag(name: String!) repeatable on + | FIELD_DEFINITION + | INTERFACE + | OBJECT + | UNION + | ARGUMENT_DEFINITION + | SCALAR + | ENUM + | ENUM_VALUE + | INPUT_OBJECT + | INPUT_FIELD_DEFINITION + type Product @tag(name: "test") @tag(name: "test3") { upc: String! name: String @@ -884,37 +908,3 @@ def test_federated_schema_without_query_is_valid(): assert result.errors is None assert sic(result.data["_service"]["sdl"]) == sic(type_defs) - - -def test_federation_2_0_version_is_detected_in_schema(): - type_defs = """ - extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@shareable", "@provides", "@external", "@tag", "@extends", "@override"]) - - type Product @key(fields: "upc") { - upc: String! - name: String - price: Int - weight: Int - } - - type User @key(fields: "email") @extends { - email: ID! @external - name: String @override(from:"users") - totalProductsCreated: Int @external - } - """ - - schema = make_federated_schema(type_defs) - result = graphql_sync( - schema, - """ - query GetServiceDetails { - _service { - sdl - } - } - """, - ) - - assert result.errors is None - assert sic(result.data["_service"]["sdl"]) == sic(type_defs) diff --git a/tests/federation/test_schema_v2.py b/tests/federation/test_schema_v2.py new file mode 100644 index 000000000..979b5bd64 --- /dev/null +++ b/tests/federation/test_schema_v2.py @@ -0,0 +1,164 @@ +from graphql import graphql_sync +from graphql.utilities import strip_ignored_characters as sic + +from ariadne.contrib.federation import ( + make_federated_schema, +) + + +def test_federation_2_0_version_is_detected_in_schema(): + type_defs = """ + extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@shareable", "@provides", "@external", "@tag", "@extends", "@override"]) + + type Product @key(fields: "upc") { + upc: String! + name: String + price: Int + weight: Int + } + + type User @key(fields: "email") @extends { + email: ID! @external + name: String @override(from:"users") + totalProductsCreated: Int @external + } + """ + + schema = make_federated_schema(type_defs) + result = graphql_sync( + schema, + """ + query GetServiceDetails { + _service { + sdl + } + } + """, + ) + + assert result.errors is None + assert sic(result.data["_service"]["sdl"]) == sic(type_defs) + + +def test_federation_2_1_version_is_detected_in_schema(): + type_defs = """ + extend schema @link(url: "https://specs.apollo.dev/federation/v2.1", import: ["@key", "@shareable", "@provides", "@external", "@tag", "@extends", "@override"]) + + type Product @key(fields: "upc") { + upc: String! + name: String + price: Int + weight: Int + } + """ + + schema = make_federated_schema(type_defs) + result = graphql_sync( + schema, + """ + query GetServiceDetails { + _service { + sdl + } + } + """, + ) + + assert result.errors is None + assert sic(result.data["_service"]["sdl"]) == sic(type_defs) + + +def test_federation_2_2_version_is_detected_in_schema(): + type_defs = """ + extend schema @link(url: "https://specs.apollo.dev/federation/v2.2", import: ["@key", "@shareable", "@provides", "@external", "@tag", "@extends", "@override"]) + + type Product @key(fields: "upc") { + upc: String! + name: String + price: Int + weight: Int + } + """ + + schema = make_federated_schema(type_defs) + result = graphql_sync( + schema, + """ + query GetServiceDetails { + _service { + sdl + } + } + """, + ) + + assert result.errors is None + assert sic(result.data["_service"]["sdl"]) == sic(type_defs) + + +def test_federated_schema_query_service_interface_object_federation_directive(): + type_defs = """ + extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.3", + import: [ + "@key", + "@shareable", + "@provides", + "@external", + "@tag", + "@extends", + "@override", + "@interfaceObject" + ] + ) + + type Query { + rootField: Review + } + + type Review @interfaceObject @key(fields: "id") { + id: ID! + } + """ + + schema = make_federated_schema(type_defs) + + result = graphql_sync( + schema, + """ + query GetServiceDetails { + _service { + sdl + } + } + """, + ) + + assert result.errors is None + assert sic(result.data["_service"]["sdl"]) == sic( + """ + extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.3", + import: [ + "@key", + "@shareable", + "@provides", + "@external", + "@tag", + "@extends", + "@override", + "@interfaceObject" + ] + ) + + type Query { + rootField: Review + } + + type Review @interfaceObject @key(fields: "id") { + id: ID! + } + """ + )