From d00df5837570b0491296e29a0b880f8b1999b08c Mon Sep 17 00:00:00 2001 From: Robert Balicki Date: Tue, 6 Dec 2022 12:00:56 -0800 Subject: [PATCH] Add support for refetchable interfaces Summary: In https://fb.workplace.com/groups/relay.support/permalink/10086229424758915/, it was revealed that fetchable interfaces are not currently supported by relay, even though they could be. * Add support for (re) fetchable interfaces Reviewed By: davidmccabe Differential Revision: D41747533 fbshipit-source-id: 1cf1ae88645c9b9dcbff7e97a2060232a9a6550d --- .../fetchable_query_generator.rs | 40 +++++--- .../validation_message.rs | 8 +- ...ations-not-implement-node.invalid.expected | 2 +- ...-all-implementing-types-impl-node.expected | 47 ++++++++++ ...e-all-implementing-types-impl-node.graphql | 18 ++++ ...terface-but-no-implementing-types.expected | 46 ++++++++++ ...nterface-but-no-implementing-types.graphql | 17 ++++ ...le-interface-some-types-impl-node.expected | 52 +++++++++++ ...ble-interface-some-types-impl-node.graphql | 23 +++++ .../fixtures/refetchable-interface.expected | 92 +++++++++++++++++++ .../fixtures/refetchable-interface.graphql | 32 +++++++ .../tests/refetchable_fragment_test.rs | 30 +++++- 12 files changed, 388 insertions(+), 19 deletions(-) create mode 100644 compiler/crates/relay-transforms/tests/refetchable_fragment/fixtures/refetchable-interface-all-implementing-types-impl-node.expected create mode 100644 compiler/crates/relay-transforms/tests/refetchable_fragment/fixtures/refetchable-interface-all-implementing-types-impl-node.graphql create mode 100644 compiler/crates/relay-transforms/tests/refetchable_fragment/fixtures/refetchable-interface-but-no-implementing-types.expected create mode 100644 compiler/crates/relay-transforms/tests/refetchable_fragment/fixtures/refetchable-interface-but-no-implementing-types.graphql create mode 100644 compiler/crates/relay-transforms/tests/refetchable_fragment/fixtures/refetchable-interface-some-types-impl-node.expected create mode 100644 compiler/crates/relay-transforms/tests/refetchable_fragment/fixtures/refetchable-interface-some-types-impl-node.graphql create mode 100644 compiler/crates/relay-transforms/tests/refetchable_fragment/fixtures/refetchable-interface.expected create mode 100644 compiler/crates/relay-transforms/tests/refetchable_fragment/fixtures/refetchable-interface.graphql diff --git a/compiler/crates/relay-transforms/src/refetchable_fragment/fetchable_query_generator.rs b/compiler/crates/relay-transforms/src/refetchable_fragment/fetchable_query_generator.rs index 5e5516b936453..f76fff183453e 100644 --- a/compiler/crates/relay-transforms/src/refetchable_fragment/fetchable_query_generator.rs +++ b/compiler/crates/relay-transforms/src/refetchable_fragment/fetchable_query_generator.rs @@ -125,23 +125,33 @@ fn get_fetchable_field_name( fragment: &FragmentDefinition, schema: &SDLSchema, ) -> DiagnosticsResult> { - if let Type::Object(id) = fragment.type_condition { - let object = schema.object(id); - if let Some(fetchable) = object.directives.named(CONSTANTS.fetchable) { - let field_name_arg = fetchable.arguments.named(CONSTANTS.field_name); - if let Some(field_name_arg) = field_name_arg { - if let Some(value) = field_name_arg.value.get_string_literal() { - return Ok(Some(value)); - } + let fetchable_directive = match fragment.type_condition { + Type::Interface(interface_id) => { + let interface = schema.interface(interface_id); + interface.directives.named(CONSTANTS.fetchable) + } + Type::Object(object_id) => { + let object = schema.object(object_id); + object.directives.named(CONSTANTS.fetchable) + } + _ => None, + }; + + if let Some(fetchable) = fetchable_directive { + let field_name_arg = fetchable.arguments.named(CONSTANTS.field_name); + if let Some(field_name_arg) = field_name_arg { + if let Some(value) = field_name_arg.value.get_string_literal() { + return Ok(Some(value)); } - return Err(vec![Diagnostic::error( - ValidationMessage::InvalidRefetchDirectiveDefinition { - fragment_name: fragment.name.item.0, - }, - fragment.name.location, - )]); } + return Err(vec![Diagnostic::error( + ValidationMessage::InvalidRefetchDirectiveDefinition { + fragment_name: fragment.name.item, + }, + fragment.name.location, + )]); } + Ok(None) } @@ -231,6 +241,6 @@ fn enforce_selections_with_id_field( pub const FETCHABLE_QUERY_GENERATOR: QueryGenerator = QueryGenerator { // T138625502 we should support interfaces and maybe unions - description: "server objects with the @fetchable directive", + description: "server objects and interfaces with the @fetchable directive", build_refetch_operation, }; diff --git a/compiler/crates/relay-transforms/src/refetchable_fragment/validation_message.rs b/compiler/crates/relay-transforms/src/refetchable_fragment/validation_message.rs index d9cf2f827cba4..15fabf517218e 100644 --- a/compiler/crates/relay-transforms/src/refetchable_fragment/validation_message.rs +++ b/compiler/crates/relay-transforms/src/refetchable_fragment/validation_message.rs @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +use graphql_ir::FragmentDefinitionName; use graphql_ir::VariableName; use intern::string_key::StringKey; use thiserror::Error; @@ -76,10 +77,13 @@ pub(super) enum ValidationMessage { )] InvalidViewerSchemaForRefetchableFragmentOnViewer { fragment_name: StringKey }, + // T139416294 this error message doesn't appear to be accurate #[error( - "Invalid use of @refetchable with @connection in fragment '{fragment_name}', check that your schema defines a `directive @fetchable(field_name: String!) on OBJECT`." + "Invalid use of @refetchable with @connection in fragment '{fragment_name}', check that your schema defines a `directive @fetchable(field_name: String!) on OBJECT` or on `INTERFACE`." )] - InvalidRefetchDirectiveDefinition { fragment_name: StringKey }, + InvalidRefetchDirectiveDefinition { + fragment_name: FragmentDefinitionName, + }, #[error( "Invalid use of @refetchable on fragment '{fragment_name}', the type '{type_name}' is @fetchable but the identifying field '{identifier_field_name}' does not have type 'ID'." diff --git a/compiler/crates/relay-transforms/tests/refetchable_fragment/fixtures/fragment-on-interface-which-implementations-not-implement-node.invalid.expected b/compiler/crates/relay-transforms/tests/refetchable_fragment/fixtures/fragment-on-interface-which-implementations-not-implement-node.invalid.expected index 705d97dc7a11b..81a5bbaad3164 100644 --- a/compiler/crates/relay-transforms/tests/refetchable_fragment/fixtures/fragment-on-interface-which-implementations-not-implement-node.invalid.expected +++ b/compiler/crates/relay-transforms/tests/refetchable_fragment/fixtures/fragment-on-interface-which-implementations-not-implement-node.invalid.expected @@ -9,7 +9,7 @@ fragment UserName on UserNameRenderable - the Viewer type - the Query type - the Node interface, object types that implement the Node interface, interfaces whose implementing objects all implement Node, and unions whose members all implement Node - - server objects with the @fetchable directive + - server objects and interfaces with the @fetchable directive fragment-on-interface-which-implementations-not-implement-node.invalid.graphql:2:10 1 │ # expected-to-throw diff --git a/compiler/crates/relay-transforms/tests/refetchable_fragment/fixtures/refetchable-interface-all-implementing-types-impl-node.expected b/compiler/crates/relay-transforms/tests/refetchable_fragment/fixtures/refetchable-interface-all-implementing-types-impl-node.expected new file mode 100644 index 0000000000000..89d2ad10bbbec --- /dev/null +++ b/compiler/crates/relay-transforms/tests/refetchable_fragment/fixtures/refetchable-interface-all-implementing-types-impl-node.expected @@ -0,0 +1,47 @@ +==================================== INPUT ==================================== +fragment RefetchableFragment on RefetchableInterface + @refetchable(queryName: "RefetchableFragmentQuery") { + id +} + +# %extensions% + +interface RefetchableInterface @fetchable(field_name: "id") { + id: ID! +} + +extend type Query { + fetch__RefetchableInterface(id: ID!): RefetchableInterface +} + +type ConcreteTypeImplementingRefetchableInterface implements RefetchableInterface & Node { + id: ID! +} +==================================== OUTPUT =================================== +query RefetchableFragmentQuery( + $id: ID! +) @__RefetchableDerivedFromMetadata +# RefetchableDerivedFromMetadata( +# FragmentDefinitionName( +# "RefetchableFragment", +# ), +# ) + { + node(id: $id) { + ...RefetchableFragment + } +} + +fragment RefetchableFragment on RefetchableInterface @refetchable(queryName: "RefetchableFragmentQuery") @__RefetchableMetadata +# RefetchableMetadata { +# operation_name: "RefetchableFragmentQuery", +# path: [ +# "node", +# ], +# identifier_field: Some( +# "id", +# ), +# } + { + id +} diff --git a/compiler/crates/relay-transforms/tests/refetchable_fragment/fixtures/refetchable-interface-all-implementing-types-impl-node.graphql b/compiler/crates/relay-transforms/tests/refetchable_fragment/fixtures/refetchable-interface-all-implementing-types-impl-node.graphql new file mode 100644 index 0000000000000..7ca8d70cc46f8 --- /dev/null +++ b/compiler/crates/relay-transforms/tests/refetchable_fragment/fixtures/refetchable-interface-all-implementing-types-impl-node.graphql @@ -0,0 +1,18 @@ +fragment RefetchableFragment on RefetchableInterface + @refetchable(queryName: "RefetchableFragmentQuery") { + id +} + +# %extensions% + +interface RefetchableInterface @fetchable(field_name: "id") { + id: ID! +} + +extend type Query { + fetch__RefetchableInterface(id: ID!): RefetchableInterface +} + +type ConcreteTypeImplementingRefetchableInterface implements RefetchableInterface & Node { + id: ID! +} diff --git a/compiler/crates/relay-transforms/tests/refetchable_fragment/fixtures/refetchable-interface-but-no-implementing-types.expected b/compiler/crates/relay-transforms/tests/refetchable_fragment/fixtures/refetchable-interface-but-no-implementing-types.expected new file mode 100644 index 0000000000000..98d8e4e1ca2ee --- /dev/null +++ b/compiler/crates/relay-transforms/tests/refetchable_fragment/fixtures/refetchable-interface-but-no-implementing-types.expected @@ -0,0 +1,46 @@ +==================================== INPUT ==================================== +# Because there are no implementing types, "all implementing types impl Node" +# is true, so we generated a Node refetch query. +fragment RefetchableFragmentFoo on RefetchableInterfaceFoo + @refetchable(queryName: "RefetchableFragmentFooQuery") { + id +} + + +# %extensions% + +interface RefetchableInterfaceFoo @fetchable(field_name: "id") { + id: ID! +} + +extend type Query { + fetch__RefetchableInterfaceFoo(id: ID!): RefetchableInterfaceFoo +} +==================================== OUTPUT =================================== +query RefetchableFragmentFooQuery( + $id: ID! +) @__RefetchableDerivedFromMetadata +# RefetchableDerivedFromMetadata( +# FragmentDefinitionName( +# "RefetchableFragmentFoo", +# ), +# ) + { + node(id: $id) { + ...RefetchableFragmentFoo + } +} + +fragment RefetchableFragmentFoo on RefetchableInterfaceFoo @refetchable(queryName: "RefetchableFragmentFooQuery") @__RefetchableMetadata +# RefetchableMetadata { +# operation_name: "RefetchableFragmentFooQuery", +# path: [ +# "node", +# ], +# identifier_field: Some( +# "id", +# ), +# } + { + id +} diff --git a/compiler/crates/relay-transforms/tests/refetchable_fragment/fixtures/refetchable-interface-but-no-implementing-types.graphql b/compiler/crates/relay-transforms/tests/refetchable_fragment/fixtures/refetchable-interface-but-no-implementing-types.graphql new file mode 100644 index 0000000000000..326c1eb7adc7d --- /dev/null +++ b/compiler/crates/relay-transforms/tests/refetchable_fragment/fixtures/refetchable-interface-but-no-implementing-types.graphql @@ -0,0 +1,17 @@ +# Because there are no implementing types, "all implementing types impl Node" +# is true, so we generated a Node refetch query. +fragment RefetchableFragmentFoo on RefetchableInterfaceFoo + @refetchable(queryName: "RefetchableFragmentFooQuery") { + id +} + + +# %extensions% + +interface RefetchableInterfaceFoo @fetchable(field_name: "id") { + id: ID! +} + +extend type Query { + fetch__RefetchableInterfaceFoo(id: ID!): RefetchableInterfaceFoo +} diff --git a/compiler/crates/relay-transforms/tests/refetchable_fragment/fixtures/refetchable-interface-some-types-impl-node.expected b/compiler/crates/relay-transforms/tests/refetchable_fragment/fixtures/refetchable-interface-some-types-impl-node.expected new file mode 100644 index 0000000000000..4f6c2be1943d2 --- /dev/null +++ b/compiler/crates/relay-transforms/tests/refetchable_fragment/fixtures/refetchable-interface-some-types-impl-node.expected @@ -0,0 +1,52 @@ +==================================== INPUT ==================================== +fragment RefetchableFragment on RefetchableInterface + @refetchable(queryName: "RefetchableFragmentQuery") { + id +} + +# %extensions% + +interface RefetchableInterface @fetchable(field_name: "id") { + id: ID! +} + +extend type Query { + fetch__RefetchableInterface(id: ID!): RefetchableInterface +} + +type ConcreteTypeImplementingRefetchableInterface implements RefetchableInterface & Node { + id: ID! +} + +type ConcreteType2ImplementingRefetchableInterface implements RefetchableInterface { + id: ID! +} +==================================== OUTPUT =================================== +query RefetchableFragmentQuery( + $id: ID! +) @__RefetchableDerivedFromMetadata +# RefetchableDerivedFromMetadata( +# FragmentDefinitionName( +# "RefetchableFragment", +# ), +# ) + { + fetch__RefetchableInterface(id: $id) { + ...RefetchableFragment + } +} + +fragment RefetchableFragment on RefetchableInterface @refetchable(queryName: "RefetchableFragmentQuery") @__RefetchableMetadata +# RefetchableMetadata { +# operation_name: "RefetchableFragmentQuery", +# path: [ +# "fetch__RefetchableInterface", +# ], +# identifier_field: Some( +# "id", +# ), +# } + { + id + __token +} diff --git a/compiler/crates/relay-transforms/tests/refetchable_fragment/fixtures/refetchable-interface-some-types-impl-node.graphql b/compiler/crates/relay-transforms/tests/refetchable_fragment/fixtures/refetchable-interface-some-types-impl-node.graphql new file mode 100644 index 0000000000000..356b15b57c85d --- /dev/null +++ b/compiler/crates/relay-transforms/tests/refetchable_fragment/fixtures/refetchable-interface-some-types-impl-node.graphql @@ -0,0 +1,23 @@ +fragment RefetchableFragment on RefetchableInterface + @refetchable(queryName: "RefetchableFragmentQuery") { + id +} + +# %extensions% + +interface RefetchableInterface @fetchable(field_name: "id") { + id: ID! +} + +extend type Query { + fetch__RefetchableInterface(id: ID!): RefetchableInterface +} + +type ConcreteTypeImplementingRefetchableInterface implements RefetchableInterface & Node { + id: ID! +} + +type ConcreteType2ImplementingRefetchableInterface implements RefetchableInterface { + id: ID! +} + diff --git a/compiler/crates/relay-transforms/tests/refetchable_fragment/fixtures/refetchable-interface.expected b/compiler/crates/relay-transforms/tests/refetchable_fragment/fixtures/refetchable-interface.expected new file mode 100644 index 0000000000000..991d35f20e49b --- /dev/null +++ b/compiler/crates/relay-transforms/tests/refetchable_fragment/fixtures/refetchable-interface.expected @@ -0,0 +1,92 @@ +==================================== INPUT ==================================== +fragment RefetchableFragment on RefetchableInterface + @refetchable(queryName: "RefetchableFragmentQuery") { + id +} + +fragment RefetchableFragment2 on RefetchableInterface2 + @refetchable(queryName: "RefetchableFragmentQuery2") { + __typename +} + +# %extensions% + +interface RefetchableInterface @fetchable(field_name: "id") { + id: ID! +} + +interface RefetchableInterface2 @fetchable(field_name: "not_id") { + not_id: ID! +} + +extend type Query { + fetch__RefetchableInterface(id: ID!): RefetchableInterface + fetch__RefetchableInterface2(id: ID!): RefetchableInterface2 +} + +type ConcreteTypeImplementingRefetchableInterface implements RefetchableInterface { + id: ID! +} + +type ConcreteTypeImplementingRefetchableInterface2 implements RefetchableInterface2 { + not_id: ID! +} +==================================== OUTPUT =================================== +query RefetchableFragmentQuery( + $id: ID! +) @__RefetchableDerivedFromMetadata +# RefetchableDerivedFromMetadata( +# FragmentDefinitionName( +# "RefetchableFragment", +# ), +# ) + { + fetch__RefetchableInterface(id: $id) { + ...RefetchableFragment + } +} + +query RefetchableFragmentQuery2( + $id: ID! +) @__RefetchableDerivedFromMetadata +# RefetchableDerivedFromMetadata( +# FragmentDefinitionName( +# "RefetchableFragment2", +# ), +# ) + { + fetch__RefetchableInterface2(id: $id) { + ...RefetchableFragment2 + } +} + +fragment RefetchableFragment on RefetchableInterface @refetchable(queryName: "RefetchableFragmentQuery") @__RefetchableMetadata +# RefetchableMetadata { +# operation_name: "RefetchableFragmentQuery", +# path: [ +# "fetch__RefetchableInterface", +# ], +# identifier_field: Some( +# "id", +# ), +# } + { + id + __token +} + +fragment RefetchableFragment2 on RefetchableInterface2 @refetchable(queryName: "RefetchableFragmentQuery2") @__RefetchableMetadata +# RefetchableMetadata { +# operation_name: "RefetchableFragmentQuery2", +# path: [ +# "fetch__RefetchableInterface2", +# ], +# identifier_field: Some( +# "not_id", +# ), +# } + { + __typename + not_id + __token +} diff --git a/compiler/crates/relay-transforms/tests/refetchable_fragment/fixtures/refetchable-interface.graphql b/compiler/crates/relay-transforms/tests/refetchable_fragment/fixtures/refetchable-interface.graphql new file mode 100644 index 0000000000000..e243d920f09e1 --- /dev/null +++ b/compiler/crates/relay-transforms/tests/refetchable_fragment/fixtures/refetchable-interface.graphql @@ -0,0 +1,32 @@ +fragment RefetchableFragment on RefetchableInterface + @refetchable(queryName: "RefetchableFragmentQuery") { + id +} + +fragment RefetchableFragment2 on RefetchableInterface2 + @refetchable(queryName: "RefetchableFragmentQuery2") { + __typename +} + +# %extensions% + +interface RefetchableInterface @fetchable(field_name: "id") { + id: ID! +} + +interface RefetchableInterface2 @fetchable(field_name: "not_id") { + not_id: ID! +} + +extend type Query { + fetch__RefetchableInterface(id: ID!): RefetchableInterface + fetch__RefetchableInterface2(id: ID!): RefetchableInterface2 +} + +type ConcreteTypeImplementingRefetchableInterface implements RefetchableInterface { + id: ID! +} + +type ConcreteTypeImplementingRefetchableInterface2 implements RefetchableInterface2 { + not_id: ID! +} diff --git a/compiler/crates/relay-transforms/tests/refetchable_fragment_test.rs b/compiler/crates/relay-transforms/tests/refetchable_fragment_test.rs index fe3426a328ce7..1a4cd55cfc1b5 100644 --- a/compiler/crates/relay-transforms/tests/refetchable_fragment_test.rs +++ b/compiler/crates/relay-transforms/tests/refetchable_fragment_test.rs @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<5d6010d9b0356775854a925e90881577>> */ mod refetchable_fragment; @@ -172,3 +172,31 @@ fn refetchable_fragment_with_connection_with_stream() { let expected = include_str!("refetchable_fragment/fixtures/refetchable-fragment-with-connection-with-stream.expected"); test_fixture(transform_fixture, "refetchable-fragment-with-connection-with-stream.graphql", "refetchable_fragment/fixtures/refetchable-fragment-with-connection-with-stream.expected", input, expected); } + +#[test] +fn refetchable_interface() { + let input = include_str!("refetchable_fragment/fixtures/refetchable-interface.graphql"); + let expected = include_str!("refetchable_fragment/fixtures/refetchable-interface.expected"); + test_fixture(transform_fixture, "refetchable-interface.graphql", "refetchable_fragment/fixtures/refetchable-interface.expected", input, expected); +} + +#[test] +fn refetchable_interface_all_implementing_types_impl_node() { + let input = include_str!("refetchable_fragment/fixtures/refetchable-interface-all-implementing-types-impl-node.graphql"); + let expected = include_str!("refetchable_fragment/fixtures/refetchable-interface-all-implementing-types-impl-node.expected"); + test_fixture(transform_fixture, "refetchable-interface-all-implementing-types-impl-node.graphql", "refetchable_fragment/fixtures/refetchable-interface-all-implementing-types-impl-node.expected", input, expected); +} + +#[test] +fn refetchable_interface_but_no_implementing_types() { + let input = include_str!("refetchable_fragment/fixtures/refetchable-interface-but-no-implementing-types.graphql"); + let expected = include_str!("refetchable_fragment/fixtures/refetchable-interface-but-no-implementing-types.expected"); + test_fixture(transform_fixture, "refetchable-interface-but-no-implementing-types.graphql", "refetchable_fragment/fixtures/refetchable-interface-but-no-implementing-types.expected", input, expected); +} + +#[test] +fn refetchable_interface_some_types_impl_node() { + let input = include_str!("refetchable_fragment/fixtures/refetchable-interface-some-types-impl-node.graphql"); + let expected = include_str!("refetchable_fragment/fixtures/refetchable-interface-some-types-impl-node.expected"); + test_fixture(transform_fixture, "refetchable-interface-some-types-impl-node.graphql", "refetchable_fragment/fixtures/refetchable-interface-some-types-impl-node.expected", input, expected); +}