diff --git a/ariadne/contrib/federation/definitions/fed1_0.graphql b/ariadne/contrib/federation/definitions/fed_v1.0.graphql similarity index 100% rename from ariadne/contrib/federation/definitions/fed1_0.graphql rename to ariadne/contrib/federation/definitions/fed_v1.0.graphql diff --git a/ariadne/contrib/federation/definitions/fed2_0.graphql b/ariadne/contrib/federation/definitions/fed_v2.0.graphql similarity index 100% rename from ariadne/contrib/federation/definitions/fed2_0.graphql rename to ariadne/contrib/federation/definitions/fed_v2.0.graphql diff --git a/ariadne/contrib/federation/definitions/fed2_1.graphql b/ariadne/contrib/federation/definitions/fed_v2.1.graphql similarity index 100% rename from ariadne/contrib/federation/definitions/fed2_1.graphql rename to ariadne/contrib/federation/definitions/fed_v2.1.graphql diff --git a/ariadne/contrib/federation/definitions/fed2_2.graphql b/ariadne/contrib/federation/definitions/fed_v2.2.graphql similarity index 100% rename from ariadne/contrib/federation/definitions/fed2_2.graphql rename to ariadne/contrib/federation/definitions/fed_v2.2.graphql diff --git a/ariadne/contrib/federation/definitions/fed2_3.graphql b/ariadne/contrib/federation/definitions/fed_v2.3.graphql similarity index 100% rename from ariadne/contrib/federation/definitions/fed2_3.graphql rename to ariadne/contrib/federation/definitions/fed_v2.3.graphql diff --git a/ariadne/contrib/federation/definitions/fed_v2.4.graphql b/ariadne/contrib/federation/definitions/fed_v2.4.graphql new file mode 100644 index 000000000..4466f9604 --- /dev/null +++ b/ariadne/contrib/federation/definitions/fed_v2.4.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/definitions/fed_v2.5.graphql b/ariadne/contrib/federation/definitions/fed_v2.5.graphql new file mode 100644 index 000000000..7e9ea9bf6 --- /dev/null +++ b/ariadne/contrib/federation/definitions/fed_v2.5.graphql @@ -0,0 +1,84 @@ +# +# 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 + +# +# federation-v2.5 +# + +directive @authenticated on + FIELD_DEFINITION + | OBJECT + | INTERFACE + | SCALAR + | ENUM + +scalar federation__Scope + +directive @requiresScopes(scopes: [[federation__Scope!]!]!) on + FIELD_DEFINITION + | OBJECT + | INTERFACE + | SCALAR + | ENUM + diff --git a/ariadne/contrib/federation/definitions/fed_v2.6.graphql b/ariadne/contrib/federation/definitions/fed_v2.6.graphql new file mode 100644 index 000000000..dd89b8497 --- /dev/null +++ b/ariadne/contrib/federation/definitions/fed_v2.6.graphql @@ -0,0 +1,95 @@ +# +# 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 + +# +# federation-v2.5 +# + +directive @authenticated on + FIELD_DEFINITION + | OBJECT + | INTERFACE + | SCALAR + | ENUM + +scalar federation__Scope + +directive @requiresScopes(scopes: [[federation__Scope!]!]!) on + FIELD_DEFINITION + | OBJECT + | INTERFACE + | SCALAR + | ENUM + +# +# federation-v2.5 +# + +scalar federation__Policy +directive @policy(policies: [[federation__Policy!]!]!) on + | FIELD_DEFINITION + | OBJECT + | INTERFACE + | SCALAR + | ENUM \ No newline at end of file diff --git a/ariadne/contrib/federation/schema.py b/ariadne/contrib/federation/schema.py index e9b975390..20d724b27 100644 --- a/ariadne/contrib/federation/schema.py +++ b/ariadne/contrib/federation/schema.py @@ -76,25 +76,26 @@ def make_federated_schema( 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": - 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") + definitions_folder = os.path.join(dirname, "definitions") + definition_files = os.listdir(definitions_folder) + definition_files.sort() + last_version_file = definition_files[-1] + + if link and link.group(1).startswith("https://specs.apollo.dev/federation/"): + fed_version = link.group(1).split("/")[-1] + versioned_schema_file = os.path.join( + definitions_folder, f"fed_{fed_version}.graphql" ) + + if os.path.isfile(versioned_schema_file): + definitions = load_schema_from_path(versioned_schema_file) + else: + definitions = load_schema_from_path( + os.path.join(definitions_folder, last_version_file) + ) else: definitions = load_schema_from_path( - os.path.join(dirname, "./definitions/fed1_0.graphql") + os.path.join(dirname, "./definitions/fed_v1.0.graphql") ) tdl.append(definitions) diff --git a/ariadne/contrib/federation/utils.py b/ariadne/contrib/federation/utils.py index 6a0f483e2..bb6bc1d83 100644 --- a/ariadne/contrib/federation/utils.py +++ b/ariadne/contrib/federation/utils.py @@ -60,6 +60,9 @@ "inaccessible", # Federation 2 directive. "composeDirective", # Federation 2.1 directive. "interfaceObject", # Federation 2.3 directive. + "authenticated", # Federation 2.5 directive. + "requiresScopes", # Federation 2.5 directive. + "policy", # Federation 2.6 directive. ] diff --git a/tests/federation/test_schema_v2.py b/tests/federation/test_schema_v2.py index 979b5bd64..8f8921bd3 100644 --- a/tests/federation/test_schema_v2.py +++ b/tests/federation/test_schema_v2.py @@ -162,3 +162,321 @@ def test_federated_schema_query_service_interface_object_federation_directive(): } """ ) + + +def test_federation_2_4_version_is_detected_in_schema(): + type_defs = """ + extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.4", + 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.4", + import: [ + "@key", + "@shareable", + "@provides", + "@external", + "@tag", + "@extends", + "@override", + "@interfaceObject" + ] + ) + + type Query { + rootField: Review + } + + type Review @interfaceObject @key(fields: "id") { + id: ID! + } + """ + ) + + +def test_federation_2_5_version_is_detected_in_schema(): + type_defs = """ + extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.5", + import: [ + "@key", + "@shareable", + "@provides", + "@external", + "@tag", + "@extends", + "@override", + "@interfaceObject", + "@authenticated", + "@requiresScopes", + + ] + ) + + type Query { + rootField: Review + } + + type Review + @interfaceObject + @key(fields: "id") + @authenticated + @requiresScopes(scopes: [["read:review"]]) { + 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.5", + import: [ + "@key", + "@shareable", + "@provides", + "@external", + "@tag", + "@extends", + "@override", + "@interfaceObject" + "@authenticated", + "@requiresScopes", + ] + ) + + type Query { + rootField: Review + } + + type Review + @interfaceObject @key(fields: "id") + @authenticated + @requiresScopes(scopes: [["read:review"]]) { + id: ID! + } + """ + ) + + +def test_federation_2_6_version_is_detected_in_schema(): + type_defs = """ + extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.6", + import: [ + "@key", + "@shareable", + "@provides", + "@external", + "@tag", + "@extends", + "@override", + "@interfaceObject", + "@authenticated", + "@requiresScopes", + + ] + ) + + type Query { + rootField: Review + } + + type Review + @interfaceObject + @key(fields: "id") + @authenticated + @requiresScopes(scopes: [["read:review"]]) + @policy(policies: [["role:admin"]]) + { + 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.6", + import: [ + "@key", + "@shareable", + "@provides", + "@external", + "@tag", + "@extends", + "@override", + "@interfaceObject" + "@authenticated", + "@requiresScopes", + ] + ) + + type Query { + rootField: Review + } + + type Review + @interfaceObject + @key(fields: "id") + @authenticated + @requiresScopes(scopes: [["read:review"]]) + @policy(policies: [["role:admin"]]) + { + id: ID! + } + """ + ) + + +def test_federation_version_not_supported_is_detected_in_schema(): + type_defs = """ + extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.25", + import: [ + "@key", + "@shareable", + "@provides", + "@external", + "@tag", + "@extends", + "@override", + "@interfaceObject", + "@authenticated", + "@requiresScopes", + + ] + ) + + type Query { + rootField: Review + } + + type Review + @interfaceObject + @key(fields: "id") + @authenticated + @requiresScopes(scopes: [["read:review"]]) + @policy(policies: [["role:admin"]]) + { + 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.25", + import: [ + "@key", + "@shareable", + "@provides", + "@external", + "@tag", + "@extends", + "@override", + "@interfaceObject" + "@authenticated", + "@requiresScopes", + ] + ) + + type Query { + rootField: Review + } + + type Review + @interfaceObject + @key(fields: "id") + @authenticated + @requiresScopes(scopes: [["read:review"]]) + @policy(policies: [["role:admin"]]) + { + id: ID! + } + """ + )