From f538e503c3cdb152bd29f77804217100cac0f648 Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Wed, 24 Apr 2024 14:23:01 -0400 Subject: [PATCH] Handle @inaccessible on types & more Federation tests (#6086) * Handle @inaccessible on types & more Federation tests * Add Fed v1 test --- .changeset/honest-trains-rule.md | 5 + packages/federation/src/utils.ts | 6 +- .../fed1-external-extends/supergraph.graphql | 58 ++++++++++ .../fed1-external-extends/tests.json | 18 +++ .../requires-interface/supergraph.graphql | 86 ++++++++++++++ .../requires-interface/tests.json | 48 ++++++++ .../supergraph.graphql | 107 ++++++++++++++++++ .../requires-with-fragments/tests.json | 79 +++++++++++++ .../union-intersection/supergraph.graphql | 11 +- .../union-intersection/tests.json | 19 ++++ 10 files changed, 435 insertions(+), 2 deletions(-) create mode 100644 .changeset/honest-trains-rule.md create mode 100644 packages/federation/test/fixtures/federation-compatibility/fed1-external-extends/supergraph.graphql create mode 100644 packages/federation/test/fixtures/federation-compatibility/fed1-external-extends/tests.json create mode 100644 packages/federation/test/fixtures/federation-compatibility/requires-interface/supergraph.graphql create mode 100644 packages/federation/test/fixtures/federation-compatibility/requires-interface/tests.json create mode 100644 packages/federation/test/fixtures/federation-compatibility/requires-with-fragments/supergraph.graphql create mode 100644 packages/federation/test/fixtures/federation-compatibility/requires-with-fragments/tests.json diff --git a/.changeset/honest-trains-rule.md b/.changeset/honest-trains-rule.md new file mode 100644 index 00000000000..ecb1c473d40 --- /dev/null +++ b/.changeset/honest-trains-rule.md @@ -0,0 +1,5 @@ +--- +"@graphql-tools/federation": patch +--- + +Handle @inaccessible types correctly diff --git a/packages/federation/src/utils.ts b/packages/federation/src/utils.ts index fdef64e68d9..cc500e4a2da 100644 --- a/packages/federation/src/utils.ts +++ b/packages/federation/src/utils.ts @@ -51,7 +51,11 @@ const internalTypeNames = ['_Entity', '_Any', '_FieldSet', '_Service']; export function filterInternalFieldsAndTypes(finalSchema: GraphQLSchema) { return mapSchema(finalSchema, { [MapperKind.TYPE]: type => { - if (internalTypeNames.includes(type.name) || type.name.startsWith('link__')) { + if ( + internalTypeNames.includes(type.name) || + type.name.startsWith('link__') || + type.astNode?.directives?.some(d => d.name.value === 'inaccessible') + ) { return null; } return type; diff --git a/packages/federation/test/fixtures/federation-compatibility/fed1-external-extends/supergraph.graphql b/packages/federation/test/fixtures/federation-compatibility/fed1-external-extends/supergraph.graphql new file mode 100644 index 00000000000..ba5fe3f58bd --- /dev/null +++ b/packages/federation/test/fixtures/federation-compatibility/fed1-external-extends/supergraph.graphql @@ -0,0 +1,58 @@ +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) +{ + query: Query +} + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__FieldSet + +enum join__Graph { + A @join__graph(name: "a", url: "https://federation-compatibility.theguild.workers.dev/fed1-external-extends/a") + B @join__graph(name: "b", url: "https://federation-compatibility.theguild.workers.dev/fed1-external-extends/b") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: A) + @join__type(graph: B) +{ + randomUser: User @join__field(graph: A) + userById(id: ID): User @join__field(graph: B) +} + +type User + @join__type(graph: A, key: "id") + @join__type(graph: B, key: "id") +{ + id: ID! + name: String! @join__field(graph: B) + nickname: String @join__field(graph: B) +} \ No newline at end of file diff --git a/packages/federation/test/fixtures/federation-compatibility/fed1-external-extends/tests.json b/packages/federation/test/fixtures/federation-compatibility/fed1-external-extends/tests.json new file mode 100644 index 00000000000..08fa7a268d9 --- /dev/null +++ b/packages/federation/test/fixtures/federation-compatibility/fed1-external-extends/tests.json @@ -0,0 +1,18 @@ +[ + { + "query": "\n query {\n randomUser {\n id\n name\n }\n userById(id: \"u2\") {\n id\n name\n nickname\n }\n }\n ", + "expected": { + "data": { + "randomUser": { + "id": "u1", + "name": "u1-name" + }, + "userById": { + "id": "u2", + "name": "u2-name", + "nickname": "u2-nickname" + } + } + } + } +] \ No newline at end of file diff --git a/packages/federation/test/fixtures/federation-compatibility/requires-interface/supergraph.graphql b/packages/federation/test/fixtures/federation-compatibility/requires-interface/supergraph.graphql new file mode 100644 index 00000000000..2f87d44a4fa --- /dev/null +++ b/packages/federation/test/fixtures/federation-compatibility/requires-interface/supergraph.graphql @@ -0,0 +1,86 @@ +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) +{ + query: Query +} + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +interface Address + @join__type(graph: A) + @join__type(graph: B) +{ + id: ID! +} + +type HomeAddress implements Address + @join__implements(graph: A, interface: "Address") + @join__implements(graph: B, interface: "Address") + @join__type(graph: A, key: "id") + @join__type(graph: B, key: "id") +{ + id: ID! + city: String +} + +scalar join__FieldSet + +enum join__Graph { + A @join__graph(name: "a", url: "https://federation-compatibility.theguild.workers.dev/requires-interface/a") + B @join__graph(name: "b", url: "https://federation-compatibility.theguild.workers.dev/requires-interface/b") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: A) + @join__type(graph: B) +{ + a: User @join__field(graph: A) + b: User @join__field(graph: B) +} + +type User + @join__type(graph: A, key: "id") + @join__type(graph: B, key: "id") +{ + id: ID! + name: String! + address: Address @join__field(graph: A, external: true) @join__field(graph: B) + city: String @join__field(graph: A, requires: "address { id }") +} + +type WorkAddress implements Address + @join__implements(graph: A, interface: "Address") + @join__implements(graph: B, interface: "Address") + @join__type(graph: A, key: "id") + @join__type(graph: B, key: "id") +{ + id: ID! + city: String +} \ No newline at end of file diff --git a/packages/federation/test/fixtures/federation-compatibility/requires-interface/tests.json b/packages/federation/test/fixtures/federation-compatibility/requires-interface/tests.json new file mode 100644 index 00000000000..478de9fd777 --- /dev/null +++ b/packages/federation/test/fixtures/federation-compatibility/requires-interface/tests.json @@ -0,0 +1,48 @@ +[ + { + "query": "\n query {\n a {\n city\n }\n }\n ", + "expected": { + "data": { + "a": { + "city": "a1-city" + } + } + } + }, + { + "query": "\n query {\n b {\n city\n }\n }\n ", + "expected": { + "data": { + "b": { + "city": "a2-city" + } + } + } + }, + { + "query": "\n query {\n a {\n address {\n __typename\n id\n }\n }\n }\n ", + "expected": { + "data": { + "a": { + "address": { + "__typename": "HomeAddress", + "id": "a1" + } + } + } + } + }, + { + "query": "\n query {\n b {\n address {\n __typename\n id\n }\n }\n }\n ", + "expected": { + "data": { + "b": { + "address": { + "__typename": "WorkAddress", + "id": "a2" + } + } + } + } + } +] \ No newline at end of file diff --git a/packages/federation/test/fixtures/federation-compatibility/requires-with-fragments/supergraph.graphql b/packages/federation/test/fixtures/federation-compatibility/requires-with-fragments/supergraph.graphql new file mode 100644 index 00000000000..0394cb07c12 --- /dev/null +++ b/packages/federation/test/fixtures/federation-compatibility/requires-with-fragments/supergraph.graphql @@ -0,0 +1,107 @@ +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/inaccessible/v0.2", for: SECURITY) +{ + query: Query +} + +directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +interface Bar implements Foo + @join__implements(graph: A, interface: "Foo") + @join__implements(graph: B, interface: "Foo") + @join__type(graph: A) + @join__type(graph: B) +{ + foo: String! + bar: String! +} + +type Baz implements Foo & Bar + @join__implements(graph: A, interface: "Foo") + @join__implements(graph: A, interface: "Bar") + @join__implements(graph: B, interface: "Foo") + @join__implements(graph: B, interface: "Bar") + @join__type(graph: A) + @join__type(graph: B) + @inaccessible +{ + foo: String! + bar: String! + baz: String! +} + +type Entity + @join__type(graph: A, key: "id") + @join__type(graph: B, key: "id") +{ + id: ID! + data: Foo @join__field(graph: A) @join__field(graph: B, external: true) + requirer: String! @join__field(graph: B, requires: "data {\n foo\n ... on Bar {\n bar\n ... on Baz {\n baz\n }\n ... on Qux {\n qux\n }\n }\n}") + requirer2: String! @join__field(graph: B, requires: "data {\n ... on Foo {\n foo\n }\n}") +} + +interface Foo + @join__type(graph: A) + @join__type(graph: B) +{ + foo: String! +} + +scalar join__FieldSet + +enum join__Graph { + A @join__graph(name: "a", url: "https://federation-compatibility.theguild.workers.dev/requires-with-fragments/a") + B @join__graph(name: "b", url: "https://federation-compatibility.theguild.workers.dev/requires-with-fragments/b") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: A) + @join__type(graph: B) +{ + a: Entity @join__field(graph: A) + b: Entity @join__field(graph: B) + bb: Entity @join__field(graph: B) +} + +type Qux implements Foo & Bar + @join__implements(graph: A, interface: "Foo") + @join__implements(graph: A, interface: "Bar") + @join__implements(graph: B, interface: "Foo") + @join__implements(graph: B, interface: "Bar") + @join__type(graph: A) + @join__type(graph: B) +{ + foo: String! + bar: String! + qux: String! +} \ No newline at end of file diff --git a/packages/federation/test/fixtures/federation-compatibility/requires-with-fragments/tests.json b/packages/federation/test/fixtures/federation-compatibility/requires-with-fragments/tests.json new file mode 100644 index 00000000000..73e0b010421 --- /dev/null +++ b/packages/federation/test/fixtures/federation-compatibility/requires-with-fragments/tests.json @@ -0,0 +1,79 @@ +[ + { + "query": "\n query {\n b {\n id\n data {\n __typename\n }\n }\n bb {\n id\n data {\n __typename\n }\n }\n }\n ", + "expected": { + "data": { + "b": { + "id": "e1", + "data": null + }, + "bb": { + "id": "e2", + "data": { + "__typename": "Qux" + } + } + } + } + }, + { + "query": "\n query {\n a {\n requirer\n }\n }\n ", + "expected": { + "data": { + "a": { + "requirer": "q1-foo_requirer" + } + } + } + }, + { + "query": "\n query {\n a {\n data {\n __typename\n }\n requirer\n }\n }\n ", + "expected": { + "data": { + "a": { + "data": { + "__typename": "Qux" + }, + "requirer": "q1-foo_requirer" + } + } + } + }, + { + "query": "\n query {\n bb {\n data {\n __typename\n }\n requirer\n }\n }\n ", + "expected": { + "data": { + "bb": { + "data": { + "__typename": "Qux" + }, + "requirer": "q1-foo_requirer" + } + } + } + }, + { + "query": "\n query {\n b {\n data {\n __typename\n }\n requirer\n }\n }\n ", + "expected": { + "data": { + "b": { + "data": null, + "requirer": "b1-foo_requirer" + } + } + } + }, + { + "query": "\n query {\n bb {\n data {\n __typename\n }\n requirer2\n }\n }\n ", + "expected": { + "data": { + "bb": { + "data": { + "__typename": "Qux" + }, + "requirer2": "q1-foo_requirer2" + } + } + } + } +] \ No newline at end of file diff --git a/packages/federation/test/fixtures/federation-compatibility/union-intersection/supergraph.graphql b/packages/federation/test/fixtures/federation-compatibility/union-intersection/supergraph.graphql index dfaa7e23e49..44d1afa2da3 100644 --- a/packages/federation/test/fixtures/federation-compatibility/union-intersection/supergraph.graphql +++ b/packages/federation/test/fixtures/federation-compatibility/union-intersection/supergraph.graphql @@ -52,8 +52,9 @@ union Media @join__type(graph: B) @join__unionMember(graph: A, member: "Book") @join__unionMember(graph: B, member: "Book") + @join__unionMember(graph: A, member: "Song") @join__unionMember(graph: B, member: "Movie") - = Book | Movie + = Book | Song | Movie type Movie @join__type(graph: B) @@ -66,4 +67,12 @@ type Query @join__type(graph: B) { media: Media + book: Media @join__field(graph: A, type: "Book") @join__field(graph: B, type: "Media") + song: Media @join__field(graph: A) +} + +type Song + @join__type(graph: A) +{ + title: String! } \ No newline at end of file diff --git a/packages/federation/test/fixtures/federation-compatibility/union-intersection/tests.json b/packages/federation/test/fixtures/federation-compatibility/union-intersection/tests.json index 76731c201a9..8bce33a9e63 100644 --- a/packages/federation/test/fixtures/federation-compatibility/union-intersection/tests.json +++ b/packages/federation/test/fixtures/federation-compatibility/union-intersection/tests.json @@ -26,5 +26,24 @@ } } } + }, + { + "query": "\n query {\n media {\n __typename\n ... on Song {\n title\n }\n ... on Movie {\n title\n }\n ... on Book {\n title\n }\n }\n book {\n __typename\n ... on Song {\n title\n }\n ... on Movie {\n title\n }\n ... on Book {\n title\n }\n }\n song {\n __typename\n ... on Song {\n title\n }\n ... on Movie {\n title\n }\n ... on Book {\n title\n }\n }\n }\n ", + "expected": { + "data": { + "media": { + "__typename": "Book", + "title": "The Lord of the Rings" + }, + "book": { + "__typename": "Book", + "title": "The Lord of the Rings" + }, + "song": { + "__typename": "Song", + "title": "Song Title" + } + } + } } ] \ No newline at end of file