From a121b33d3eeaa49ec4bcd9a703fb2f2a94e9f5e9 Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Thu, 21 Jul 2022 11:54:25 -0700 Subject: [PATCH] Support fragment spreads on abstract types defined in client schema extensions Reviewed By: alunyov Differential Revision: D37944141 fbshipit-source-id: 6149ecc78a523768139000fa040b31b279b5900d --- .../crates/relay-codegen/src/build_ast.rs | 43 +++- .../crates/relay-codegen/src/constants.rs | 2 + ...agment_spread_on_client_interface.expected | 71 ++++++ ...ragment_spread_on_client_interface.graphql | 23 ++ ..._on_client_interface_transitively.expected | 91 +++++++ ...d_on_client_interface_transitively.graphql | 27 ++ .../fragment_spread_on_client_union.expected | 74 ++++++ .../fragment_spread_on_client_union.graphql | 25 ++ ...line_fragment_on_client_interface.expected | 60 +++++ ...nline_fragment_on_client_interface.graphql | 21 ++ .../client_extensions_abstract_types/mod.rs | 64 +++++ .../client_extensions_abstract_types_test.rs | 41 ++++ .../client-inline-fragments-in-query.expected | 5 +- .../relay-transforms/src/apply_transforms.rs | 5 + .../src/client_extensions_abstract_types.rs | 230 ++++++++++++++++++ compiler/crates/relay-transforms/src/lib.rs | 3 + ...agment_spread_on_client_interface.expected | 45 ++++ ...ragment_spread_on_client_interface.graphql | 23 ++ ..._on_client_interface_transitively.expected | 53 ++++ ...d_on_client_interface_transitively.graphql | 27 ++ .../fragment_spread_on_client_union.expected | 48 ++++ .../fragment_spread_on_client_union.graphql | 25 ++ ...line_fragment_on_client_interface.expected | 41 ++++ ...nline_fragment_on_client_interface.graphql | 21 ++ .../client_extensions_abstract_types/mod.rs | 49 ++++ .../client_extensions_abstract_types_test.rs | 41 ++++ .../store/RelayResponseNormalizer.js | 22 ++ ...ayModernEnvironment-TypeRefinement-test.js | 47 ++++ .../__tests__/RelayResponseNormalizer-test.js | 59 +++++ ...finementTestClientAbstractQuery.graphql.js | 125 ++++++++++ ...peRefinementTestClientInterface.graphql.js | 62 +++++ ...izerTestClientInterfaceFragment.graphql.js | 62 +++++ ...malizerTestClientInterfaceQuery.graphql.js | 149 ++++++++++++ .../relay-runtime/util/NormalizationNode.js | 3 + .../schema-extensions/ClientInterface.graphql | 15 ++ .../schema-extensions/ClientUnion.graphql | 13 + 36 files changed, 1712 insertions(+), 3 deletions(-) create mode 100644 compiler/crates/relay-codegen/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface.expected create mode 100644 compiler/crates/relay-codegen/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface.graphql create mode 100644 compiler/crates/relay-codegen/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface_transitively.expected create mode 100644 compiler/crates/relay-codegen/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface_transitively.graphql create mode 100644 compiler/crates/relay-codegen/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_union.expected create mode 100644 compiler/crates/relay-codegen/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_union.graphql create mode 100644 compiler/crates/relay-codegen/tests/client_extensions_abstract_types/fixtures/inline_fragment_on_client_interface.expected create mode 100644 compiler/crates/relay-codegen/tests/client_extensions_abstract_types/fixtures/inline_fragment_on_client_interface.graphql create mode 100644 compiler/crates/relay-codegen/tests/client_extensions_abstract_types/mod.rs create mode 100644 compiler/crates/relay-codegen/tests/client_extensions_abstract_types_test.rs create mode 100644 compiler/crates/relay-transforms/src/client_extensions_abstract_types.rs create mode 100644 compiler/crates/relay-transforms/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface.expected create mode 100644 compiler/crates/relay-transforms/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface.graphql create mode 100644 compiler/crates/relay-transforms/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface_transitively.expected create mode 100644 compiler/crates/relay-transforms/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface_transitively.graphql create mode 100644 compiler/crates/relay-transforms/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_union.expected create mode 100644 compiler/crates/relay-transforms/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_union.graphql create mode 100644 compiler/crates/relay-transforms/tests/client_extensions_abstract_types/fixtures/inline_fragment_on_client_interface.expected create mode 100644 compiler/crates/relay-transforms/tests/client_extensions_abstract_types/fixtures/inline_fragment_on_client_interface.graphql create mode 100644 compiler/crates/relay-transforms/tests/client_extensions_abstract_types/mod.rs create mode 100644 compiler/crates/relay-transforms/tests/client_extensions_abstract_types_test.rs create mode 100644 packages/relay-runtime/store/__tests__/__generated__/RelayModernEnvironmentTypeRefinementTestClientAbstractQuery.graphql.js create mode 100644 packages/relay-runtime/store/__tests__/__generated__/RelayModernEnvironmentTypeRefinementTestClientInterface.graphql.js create mode 100644 packages/relay-runtime/store/__tests__/__generated__/RelayResponseNormalizerTestClientInterfaceFragment.graphql.js create mode 100644 packages/relay-runtime/store/__tests__/__generated__/RelayResponseNormalizerTestClientInterfaceQuery.graphql.js create mode 100644 packages/relay-test-utils-internal/schema-extensions/ClientInterface.graphql create mode 100644 packages/relay-test-utils-internal/schema-extensions/ClientUnion.graphql diff --git a/compiler/crates/relay-codegen/src/build_ast.rs b/compiler/crates/relay-codegen/src/build_ast.rs index 96173bc76744f..43a728746a172 100644 --- a/compiler/crates/relay-codegen/src/build_ast.rs +++ b/compiler/crates/relay-codegen/src/build_ast.rs @@ -45,6 +45,7 @@ use relay_transforms::generate_abstract_type_refinement_key; use relay_transforms::remove_directive; use relay_transforms::ClientEdgeMetadata; use relay_transforms::ClientEdgeMetadataDirective; +use relay_transforms::ClientExtensionAbstractTypeMetadataDirective; use relay_transforms::ConnectionConstants; use relay_transforms::ConnectionMetadata; use relay_transforms::DeferDirective; @@ -254,16 +255,54 @@ impl<'schema, 'builder, 'config> CodegenBuilder<'schema, 'builder, 'config> { let argument_definitions = self.build_operation_variable_definitions(&operation.variable_definitions); let selections = self.build_selections(&mut context, operation.selections.iter()); - self.object(object! { + let mut fields = object! { argument_definitions: Primitive::Key(argument_definitions), kind: Primitive::String(CODEGEN_CONSTANTS.operation_value), name: Primitive::String(operation.name.item), selections: selections, - }) + }; + if let Some(client_abstract_types) = + self.maybe_build_client_abstract_types(operation) + { + fields.push(client_abstract_types); + } + self.object(fields) } } } + fn maybe_build_client_abstract_types( + &mut self, + operation: &OperationDefinition, + ) -> Option { + // If the query contains frament spreads on abstract types which are + // defined in the client schema, we attach extra metadata so that we + // know which concrete types match these type conditions at runtime. + ClientExtensionAbstractTypeMetadataDirective::find(&operation.directives).map(|directive| { + let entries = directive + .abstract_types + .iter() + .map(|abstract_type| { + let concrete_types = self.array( + abstract_type + .concrete + .iter() + .map(|concrete| Primitive::String(*concrete)) + .collect(), + ); + ObjectEntry { + key: abstract_type.name, + value: Primitive::Key(concrete_types), + } + }) + .collect(); + ObjectEntry { + key: CODEGEN_CONSTANTS.client_abstract_types, + value: Primitive::Key(self.object(entries)), + } + }) + } + pub(crate) fn build_fragment( &mut self, fragment: &FragmentDefinition, diff --git a/compiler/crates/relay-codegen/src/constants.rs b/compiler/crates/relay-codegen/src/constants.rs index ec1e0372c07d2..780a564bacd4e 100644 --- a/compiler/crates/relay-codegen/src/constants.rs +++ b/compiler/crates/relay-codegen/src/constants.rs @@ -20,6 +20,7 @@ pub struct CodegenConstants { pub argument_definitions: StringKey, pub backward: StringKey, pub cache_id: StringKey, + pub client_abstract_types: StringKey, pub client_component: StringKey, pub client_edge_backing_field_key: StringKey, pub client_edge_to_client_object: StringKey, @@ -123,6 +124,7 @@ lazy_static! { argument_definitions: "argumentDefinitions".intern(), backward: "backward".intern(), cache_id: "cacheID".intern(), + client_abstract_types: "clientAbstractTypes".intern(), client_component: "ClientComponent".intern(), client_edge_to_server_object: "ClientEdgeToServerObject".intern(), client_edge_to_client_object: "ClientEdgeToClientObject".intern(), diff --git a/compiler/crates/relay-codegen/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface.expected b/compiler/crates/relay-codegen/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface.expected new file mode 100644 index 0000000000000..f2bbe342ee9d6 --- /dev/null +++ b/compiler/crates/relay-codegen/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface.expected @@ -0,0 +1,71 @@ +==================================== INPUT ==================================== +query Foo { + client_type { + ...MyFragment + } +} + +fragment MyFragment on ClientNamed { + name +} + +# %extensions% + +extend type Query { + client_type: ClientType +} + +type ClientType implements ClientNamed { + name: String +} + +interface ClientNamed { + name: String +} +==================================== OUTPUT =================================== +{ + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "MyFragment", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + } + ], + "type": "ClientNamed", + "abstractKey": "__isClientNamed" +} + +{ + "argumentDefinitions": [], + "kind": "Operation", + "name": "Foo", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "ClientType", + "kind": "LinkedField", + "name": "client_type", + "plural": false, + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "MyFragment" + } + ], + "storageKey": null + } + ], + "clientAbstractTypes": { + "__isClientNamed": [ + "ClientType" + ] + } +} diff --git a/compiler/crates/relay-codegen/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface.graphql b/compiler/crates/relay-codegen/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface.graphql new file mode 100644 index 0000000000000..79ddf7254c64a --- /dev/null +++ b/compiler/crates/relay-codegen/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface.graphql @@ -0,0 +1,23 @@ +query Foo { + client_type { + ...MyFragment + } +} + +fragment MyFragment on ClientNamed { + name +} + +# %extensions% + +extend type Query { + client_type: ClientType +} + +type ClientType implements ClientNamed { + name: String +} + +interface ClientNamed { + name: String +} diff --git a/compiler/crates/relay-codegen/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface_transitively.expected b/compiler/crates/relay-codegen/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface_transitively.expected new file mode 100644 index 0000000000000..0a51748ea00be --- /dev/null +++ b/compiler/crates/relay-codegen/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface_transitively.expected @@ -0,0 +1,91 @@ +==================================== INPUT ==================================== +query Foo { + ...QueryFragment +} + +fragment QueryFragment on Query { + client_type { + ...MyFragment + } +} + +fragment MyFragment on ClientNamed { + name +} + +# %extensions% + +extend type Query { + client_type: ClientType +} + +type ClientType implements ClientNamed { + name: String +} + +interface ClientNamed { + name: String +} +==================================== OUTPUT =================================== +{ + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "MyFragment", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + } + ], + "type": "ClientNamed", + "abstractKey": "__isClientNamed" +} + +{ + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "QueryFragment", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "ClientType", + "kind": "LinkedField", + "name": "client_type", + "plural": false, + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "MyFragment" + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null +} + +{ + "argumentDefinitions": [], + "kind": "Operation", + "name": "Foo", + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "QueryFragment" + } + ], + "clientAbstractTypes": { + "__isClientNamed": [ + "ClientType" + ] + } +} diff --git a/compiler/crates/relay-codegen/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface_transitively.graphql b/compiler/crates/relay-codegen/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface_transitively.graphql new file mode 100644 index 0000000000000..f7b0ed48079e0 --- /dev/null +++ b/compiler/crates/relay-codegen/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface_transitively.graphql @@ -0,0 +1,27 @@ +query Foo { + ...QueryFragment +} + +fragment QueryFragment on Query { + client_type { + ...MyFragment + } +} + +fragment MyFragment on ClientNamed { + name +} + +# %extensions% + +extend type Query { + client_type: ClientType +} + +type ClientType implements ClientNamed { + name: String +} + +interface ClientNamed { + name: String +} diff --git a/compiler/crates/relay-codegen/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_union.expected b/compiler/crates/relay-codegen/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_union.expected new file mode 100644 index 0000000000000..be9cebe9fa5e5 --- /dev/null +++ b/compiler/crates/relay-codegen/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_union.expected @@ -0,0 +1,74 @@ +==================================== INPUT ==================================== +query Foo { + client_type { + ...MyFragment + } +} + +fragment MyFragment on ClientUnion { + __typename +} + +# %extensions% + +extend type Query { + client_type: ClientType +} + +type ClientType { + name: String +} + +type OtherClientType { + name: String +} + +union ClientUnion = ClientType | OtherClientType +==================================== OUTPUT =================================== +{ + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "MyFragment", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null + } + ], + "type": "ClientUnion", + "abstractKey": "__isClientUnion" +} + +{ + "argumentDefinitions": [], + "kind": "Operation", + "name": "Foo", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "ClientType", + "kind": "LinkedField", + "name": "client_type", + "plural": false, + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "MyFragment" + } + ], + "storageKey": null + } + ], + "clientAbstractTypes": { + "__isClientUnion": [ + "ClientType", + "OtherClientType" + ] + } +} diff --git a/compiler/crates/relay-codegen/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_union.graphql b/compiler/crates/relay-codegen/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_union.graphql new file mode 100644 index 0000000000000..8fdebffefcbb6 --- /dev/null +++ b/compiler/crates/relay-codegen/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_union.graphql @@ -0,0 +1,25 @@ +query Foo { + client_type { + ...MyFragment + } +} + +fragment MyFragment on ClientUnion { + __typename +} + +# %extensions% + +extend type Query { + client_type: ClientType +} + +type ClientType { + name: String +} + +type OtherClientType { + name: String +} + +union ClientUnion = ClientType | OtherClientType diff --git a/compiler/crates/relay-codegen/tests/client_extensions_abstract_types/fixtures/inline_fragment_on_client_interface.expected b/compiler/crates/relay-codegen/tests/client_extensions_abstract_types/fixtures/inline_fragment_on_client_interface.expected new file mode 100644 index 0000000000000..125918b21a10e --- /dev/null +++ b/compiler/crates/relay-codegen/tests/client_extensions_abstract_types/fixtures/inline_fragment_on_client_interface.expected @@ -0,0 +1,60 @@ +==================================== INPUT ==================================== +query Foo { + client_type { + ... on ClientNamed { + name + } + } +} + +# %extensions% + +extend type Query { + client_type: ClientType +} + +type ClientType implements ClientNamed { + name: String +} + +interface ClientNamed { + name: String +} +==================================== OUTPUT =================================== +{ + "argumentDefinitions": [], + "kind": "Operation", + "name": "Foo", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "ClientType", + "kind": "LinkedField", + "name": "client_type", + "plural": false, + "selections": [ + { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + } + ], + "type": "ClientNamed", + "abstractKey": "__isClientNamed" + } + ], + "storageKey": null + } + ], + "clientAbstractTypes": { + "__isClientNamed": [ + "ClientType" + ] + } +} diff --git a/compiler/crates/relay-codegen/tests/client_extensions_abstract_types/fixtures/inline_fragment_on_client_interface.graphql b/compiler/crates/relay-codegen/tests/client_extensions_abstract_types/fixtures/inline_fragment_on_client_interface.graphql new file mode 100644 index 0000000000000..524f654356f54 --- /dev/null +++ b/compiler/crates/relay-codegen/tests/client_extensions_abstract_types/fixtures/inline_fragment_on_client_interface.graphql @@ -0,0 +1,21 @@ +query Foo { + client_type { + ... on ClientNamed { + name + } + } +} + +# %extensions% + +extend type Query { + client_type: ClientType +} + +type ClientType implements ClientNamed { + name: String +} + +interface ClientNamed { + name: String +} diff --git a/compiler/crates/relay-codegen/tests/client_extensions_abstract_types/mod.rs b/compiler/crates/relay-codegen/tests/client_extensions_abstract_types/mod.rs new file mode 100644 index 0000000000000..693469d03dcfb --- /dev/null +++ b/compiler/crates/relay-codegen/tests/client_extensions_abstract_types/mod.rs @@ -0,0 +1,64 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +use common::SourceLocationKey; +use fixture_tests::Fixture; +use graphql_ir::build; +use graphql_ir::Program; +use graphql_syntax::parse_executable; +use relay_codegen::print_fragment; +use relay_codegen::print_operation; +use relay_codegen::JsModuleFormat; +use relay_config::ProjectConfig; +use relay_test_schema::get_test_schema_with_extensions; +use relay_transforms::client_extensions_abstract_types; +use relay_transforms::sort_selections; +use std::sync::Arc; + +pub fn transform_fixture(fixture: &Fixture<'_>) -> Result { + let parts: Vec<_> = fixture.content.split("%extensions%").collect(); + if let [base, extensions] = parts.as_slice() { + let ast = parse_executable(base, SourceLocationKey::standalone(fixture.file_name)).unwrap(); + let schema = get_test_schema_with_extensions(extensions); + let ir = build(&schema, &ast.definitions).unwrap(); + let program = Program::from_definitions(Arc::clone(&schema), ir); + let next_program = sort_selections(&client_extensions_abstract_types(&program)); + let mut result = next_program + .fragments() + .map(|def| { + let mut import_statements = Default::default(); + let fragment = print_fragment( + &schema, + def, + &ProjectConfig { + js_module_format: JsModuleFormat::Haste, + ..Default::default() + }, + &mut import_statements, + ); + format!("{}{}", import_statements, fragment) + }) + .chain(next_program.operations().map(|def| { + let mut import_statements = Default::default(); + let operation = print_operation( + &schema, + def, + &ProjectConfig { + js_module_format: JsModuleFormat::Haste, + ..Default::default() + }, + &mut import_statements, + ); + format!("{}{}", import_statements, operation) + })) + .collect::>(); + result.sort_unstable(); + Ok(result.join("\n\n")) + } else { + panic!("Expected exactly one %extensions% section marker.") + } +} diff --git a/compiler/crates/relay-codegen/tests/client_extensions_abstract_types_test.rs b/compiler/crates/relay-codegen/tests/client_extensions_abstract_types_test.rs new file mode 100644 index 0000000000000..7bf36359d5df8 --- /dev/null +++ b/compiler/crates/relay-codegen/tests/client_extensions_abstract_types_test.rs @@ -0,0 +1,41 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @generated SignedSource<> + */ + +mod client_extensions_abstract_types; + +use client_extensions_abstract_types::transform_fixture; +use fixture_tests::test_fixture; + +#[test] +fn fragment_spread_on_client_interface() { + let input = include_str!("client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface.graphql"); + let expected = include_str!("client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface.expected"); + test_fixture(transform_fixture, "fragment_spread_on_client_interface.graphql", "client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface.expected", input, expected); +} + +#[test] +fn fragment_spread_on_client_interface_transitively() { + let input = include_str!("client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface_transitively.graphql"); + let expected = include_str!("client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface_transitively.expected"); + test_fixture(transform_fixture, "fragment_spread_on_client_interface_transitively.graphql", "client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface_transitively.expected", input, expected); +} + +#[test] +fn fragment_spread_on_client_union() { + let input = include_str!("client_extensions_abstract_types/fixtures/fragment_spread_on_client_union.graphql"); + let expected = include_str!("client_extensions_abstract_types/fixtures/fragment_spread_on_client_union.expected"); + test_fixture(transform_fixture, "fragment_spread_on_client_union.graphql", "client_extensions_abstract_types/fixtures/fragment_spread_on_client_union.expected", input, expected); +} + +#[test] +fn inline_fragment_on_client_interface() { + let input = include_str!("client_extensions_abstract_types/fixtures/inline_fragment_on_client_interface.graphql"); + let expected = include_str!("client_extensions_abstract_types/fixtures/inline_fragment_on_client_interface.expected"); + test_fixture(transform_fixture, "inline_fragment_on_client_interface.graphql", "client_extensions_abstract_types/fixtures/inline_fragment_on_client_interface.expected", input, expected); +} diff --git a/compiler/crates/relay-compiler/tests/compile_relay_artifacts/fixtures/client-inline-fragments-in-query.expected b/compiler/crates/relay-compiler/tests/compile_relay_artifacts/fixtures/client-inline-fragments-in-query.expected index c9d3e5187355d..c5df8e896a99f 100644 --- a/compiler/crates/relay-compiler/tests/compile_relay_artifacts/fixtures/client-inline-fragments-in-query.expected +++ b/compiler/crates/relay-compiler/tests/compile_relay_artifacts/fixtures/client-inline-fragments-in-query.expected @@ -143,7 +143,10 @@ interface ClientNamed { ], "storageKey": null } - ] + ], + "clientAbstractTypes": { + "__isClientNamed": [] + } }, "params": { "cacheID": "0fdcace16a82aaf469e14bd8f7d6e5bd", diff --git a/compiler/crates/relay-transforms/src/apply_transforms.rs b/compiler/crates/relay-transforms/src/apply_transforms.rs index 01d13f2e944ee..b175bb895ad89 100644 --- a/compiler/crates/relay-transforms/src/apply_transforms.rs +++ b/compiler/crates/relay-transforms/src/apply_transforms.rs @@ -11,6 +11,7 @@ use crate::apply_custom_transforms::apply_before_custom_transforms; use crate::apply_custom_transforms::CustomTransformsConfig; use crate::assignable_fragment_spread::annotate_updatable_fragment_spreads; use crate::assignable_fragment_spread::replace_updatable_fragment_spreads; +use crate::client_extensions_abstract_types::client_extensions_abstract_types; use crate::match_::hash_supported_argument; use common::sync::try_join; use common::DiagnosticsResult; @@ -406,6 +407,10 @@ fn apply_normalization_transforms( print_stats("skip_unreachable_node", &program); } + program = log_event.time("client_extnsions_abstract_types", || { + client_extensions_abstract_types(&program) + }); + program = log_event.time("inline_fragments", || inline_fragments(&program)); if let Some(print_stats) = maybe_print_stats { print_stats("inline_fragments", &program); diff --git a/compiler/crates/relay-transforms/src/client_extensions_abstract_types.rs b/compiler/crates/relay-transforms/src/client_extensions_abstract_types.rs new file mode 100644 index 0000000000000..8aa91648102a3 --- /dev/null +++ b/compiler/crates/relay-transforms/src/client_extensions_abstract_types.rs @@ -0,0 +1,230 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +use graphql_ir::associated_data_impl; +use graphql_ir::FragmentDefinition; +use graphql_ir::FragmentSpread; +use graphql_ir::InlineFragment; +use graphql_ir::OperationDefinition; +use graphql_ir::Program; +use graphql_ir::Selection; +use graphql_ir::Transformed; +use graphql_ir::Transformer; +use intern::string_key::StringKey; +use intern::string_key::StringKeySet; +use schema::ObjectID; +use schema::Schema; +use schema::Type; +use std::collections::hash_map::Entry; +use std::collections::HashMap; +use std::mem; +use std::sync::Arc; + +use crate::generate_abstract_type_refinement_key; + +/// A transform collects data about which concrete types implement client schema +/// extension abstract types (interfaces and unions). +pub fn client_extensions_abstract_types(program: &Program) -> Program { + let mut transform = ClientExtensionsAbstactTypesTransform::new(program); + transform + .transform_program(program) + .replace_or_else(|| program.clone()) +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct AbstractTypeImplements { + pub name: StringKey, + pub concrete: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct ClientExtensionAbstractTypeMetadataDirective { + pub abstract_types: Vec, +} +associated_data_impl!(ClientExtensionAbstractTypeMetadataDirective); + +/// Maps an abstract type (union or interface) to a list of +/// concrete types which implement that interface. +type TypeMap = HashMap; + +struct ClientExtensionsAbstactTypesTransform<'program> { + program: &'program Program, + abstract_type_map: TypeMap, + fragment_type_maps: HashMap, +} + +impl<'program> ClientExtensionsAbstactTypesTransform<'program> { + fn new(program: &'program Program) -> Self { + Self { + program, + abstract_type_map: Default::default(), + fragment_type_maps: Default::default(), + } + } + + /// Records all nessesary type relationships for a fragment type condition. + fn handle_type_condition(&mut self, type_condition: Type) { + match type_condition { + Type::Interface(interface_id) => { + let interface = self.program.schema.interface(interface_id); + if interface.is_extension { + self.add_abstract_type(type_condition, &interface.implementing_objects); + } else { + // TODO: We should also record type information about + // concrete types defined in the client schema extension + // which implement a server interface + } + } + Type::Union(union_id) => { + let union = self.program.schema.union(union_id); + if union.is_extension { + self.add_abstract_type(type_condition, &union.members) + } else { + // TODO: We should also record type information about + // concrete types defined in the client schema extension + // which implement a server interface + } + } + Type::Object(_) => { + // For concerete type conditions, we don't need to record any additional data. + } + _ => panic!("Expected type condition to be on an Interface, Object or Union"), + }; + } + + /// Record that a list of concrete types match an abstract type + fn add_abstract_type(&mut self, abstract_type: Type, object_ids: &[ObjectID]) { + let abstract_type_name = + generate_abstract_type_refinement_key(&self.program.schema, abstract_type); + let names_iter = object_ids + .iter() + .map(|object_id| self.program.schema.object(*object_id).name.item); + match self.abstract_type_map.entry(abstract_type_name) { + Entry::Occupied(mut occupied) => { + occupied.get_mut().extend(names_iter); + } + Entry::Vacant(vacant) => { + vacant.insert(names_iter.collect::()); + } + } + } + + /// Add all type relationships that are referenced within a fragment definition. + /// Caches on a per-definition basis so each fragment is explored at most once. + fn traverse_into_fragment_spread(&mut self, fragment_definition: &Arc) { + let fragment_type_map = match self.fragment_type_maps.get(&fragment_definition.name.item) { + Some(type_map) => type_map.clone(), + None => { + // Set aside the previously discovered types + let parent_type_map = mem::take(&mut self.abstract_type_map); + + // Collect type information from the fragment definition + self.default_transform_fragment(fragment_definition); + + // Reset back to the previously seen type information + let fragment_type_map = mem::replace(&mut self.abstract_type_map, parent_type_map); + + // Cache fragment results + self.fragment_type_maps + .insert(fragment_definition.name.item, fragment_type_map.clone()); + + // Return the fragment's type map + fragment_type_map + } + }; + + // Augment this queries type map with the type information from the fragment. + for (key, value) in fragment_type_map.into_iter() { + match self.abstract_type_map.entry(key) { + Entry::Occupied(mut occupied) => { + occupied.get_mut().extend(value); + } + Entry::Vacant(vacant) => { + vacant.insert(value); + } + } + } + } +} + +impl Transformer for ClientExtensionsAbstactTypesTransform<'_> { + const NAME: &'static str = "ClientExtensionsAbstactTypesTransform"; + const VISIT_ARGUMENTS: bool = false; + const VISIT_DIRECTIVES: bool = false; + + fn transform_operation( + &mut self, + operation: &OperationDefinition, + ) -> Transformed { + self.default_transform_operation(operation); + if self.abstract_type_map.is_empty() { + Transformed::Keep + } else { + // Get this operation's type map, and also reset the state for the next operation. + let abstract_type_map = mem::take(&mut self.abstract_type_map); + + let mut directives = operation.directives.clone(); + + // Convert hashmap/hashset into Vecs in order to be compatible with + // AssociatedData which must implmenent Hash. + let mut abstract_types = abstract_type_map + .into_iter() + .map(|(name, concrete)| { + let mut concrete_vec = concrete.into_iter().collect::>(); + // Sort to ensure stable output + concrete_vec.sort(); + AbstractTypeImplements { + name, + concrete: concrete_vec, + } + }) + .collect::>(); + + // Sort to ensure stable output + abstract_types.sort_by_key(|a| a.name); + + directives.push(ClientExtensionAbstractTypeMetadataDirective { abstract_types }.into()); + + Transformed::Replace(OperationDefinition { + directives, + ..operation.clone() + }) + } + } + + fn transform_fragment( + &mut self, + _fragment: &FragmentDefinition, + ) -> Transformed { + // We don't care about fragment definitions themselves, only ones that + // we encounter as fragment spreads. We traverse fragments only when we + // encounter it being spread into an operation. + Transformed::Keep + } + + /// Record type information about inline fragments with type conditions, and + /// traverse into the inline fragment's selections. + fn transform_inline_fragment(&mut self, fragment: &InlineFragment) -> Transformed { + if let Some(type_condition) = fragment.type_condition { + self.handle_type_condition(type_condition) + } + self.default_transform_inline_fragment(fragment) + } + + /// Record type information about the type condition of a spread fragment, + /// as well as any type information needed for the contents of the named fragment. + fn transform_fragment_spread(&mut self, spread: &FragmentSpread) -> Transformed { + let maybe_fragment_definition = self.program.fragment(spread.fragment.item); + + if let Some(fragment_definition) = maybe_fragment_definition { + self.handle_type_condition(fragment_definition.type_condition); + + self.traverse_into_fragment_spread(fragment_definition); + } + self.default_transform_fragment_spread(spread) + } +} diff --git a/compiler/crates/relay-transforms/src/lib.rs b/compiler/crates/relay-transforms/src/lib.rs index 0f868a0e36dbe..0517c58e3740c 100644 --- a/compiler/crates/relay-transforms/src/lib.rs +++ b/compiler/crates/relay-transforms/src/lib.rs @@ -18,6 +18,7 @@ mod apply_transforms; mod assignable_fragment_spread; mod client_edges; mod client_extensions; +mod client_extensions_abstract_types; mod connections; mod declarative_connection; mod defer_stream; @@ -110,6 +111,8 @@ pub use client_edges::CLIENT_EDGE_SOURCE_NAME; pub use client_edges::CLIENT_EDGE_WATERFALL_DIRECTIVE_NAME; pub use client_extensions::client_extensions; pub use client_extensions::CLIENT_EXTENSION_DIRECTIVE_NAME; +pub use client_extensions_abstract_types::client_extensions_abstract_types; +pub use client_extensions_abstract_types::ClientExtensionAbstractTypeMetadataDirective; pub use connections::extract_connection_metadata_from_directive; pub use connections::ConnectionConstants; pub use connections::ConnectionInterface; diff --git a/compiler/crates/relay-transforms/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface.expected b/compiler/crates/relay-transforms/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface.expected new file mode 100644 index 0000000000000..7e1c453fa828e --- /dev/null +++ b/compiler/crates/relay-transforms/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface.expected @@ -0,0 +1,45 @@ +==================================== INPUT ==================================== +query Foo { + client_type { + ...MyFragment + } +} + +fragment MyFragment on ClientNamed { + name +} + +# %extensions% + +extend type Query { + client_type: ClientType +} + +type ClientType implements ClientNamed { + name: String +} + +interface ClientNamed { + name: String +} +==================================== OUTPUT =================================== +fragment MyFragment on ClientNamed { + name +} + +query Foo @__ClientExtensionAbstractTypeMetadataDirective +# ClientExtensionAbstractTypeMetadataDirective { +# abstract_types: [ +# AbstractTypeImplements { +# name: "__isClientNamed", +# concrete: [ +# "ClientType", +# ], +# }, +# ], +# } + { + client_type { + ...MyFragment + } +} diff --git a/compiler/crates/relay-transforms/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface.graphql b/compiler/crates/relay-transforms/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface.graphql new file mode 100644 index 0000000000000..79ddf7254c64a --- /dev/null +++ b/compiler/crates/relay-transforms/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface.graphql @@ -0,0 +1,23 @@ +query Foo { + client_type { + ...MyFragment + } +} + +fragment MyFragment on ClientNamed { + name +} + +# %extensions% + +extend type Query { + client_type: ClientType +} + +type ClientType implements ClientNamed { + name: String +} + +interface ClientNamed { + name: String +} diff --git a/compiler/crates/relay-transforms/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface_transitively.expected b/compiler/crates/relay-transforms/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface_transitively.expected new file mode 100644 index 0000000000000..5e0ea3f8b42ac --- /dev/null +++ b/compiler/crates/relay-transforms/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface_transitively.expected @@ -0,0 +1,53 @@ +==================================== INPUT ==================================== +query Foo { + ...QueryFragment +} + +fragment QueryFragment on Query { + client_type { + ...MyFragment + } +} + +fragment MyFragment on ClientNamed { + name +} + +# %extensions% + +extend type Query { + client_type: ClientType +} + +type ClientType implements ClientNamed { + name: String +} + +interface ClientNamed { + name: String +} +==================================== OUTPUT =================================== +fragment MyFragment on ClientNamed { + name +} + +fragment QueryFragment on Query { + client_type { + ...MyFragment + } +} + +query Foo @__ClientExtensionAbstractTypeMetadataDirective +# ClientExtensionAbstractTypeMetadataDirective { +# abstract_types: [ +# AbstractTypeImplements { +# name: "__isClientNamed", +# concrete: [ +# "ClientType", +# ], +# }, +# ], +# } + { + ...QueryFragment +} diff --git a/compiler/crates/relay-transforms/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface_transitively.graphql b/compiler/crates/relay-transforms/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface_transitively.graphql new file mode 100644 index 0000000000000..f7b0ed48079e0 --- /dev/null +++ b/compiler/crates/relay-transforms/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface_transitively.graphql @@ -0,0 +1,27 @@ +query Foo { + ...QueryFragment +} + +fragment QueryFragment on Query { + client_type { + ...MyFragment + } +} + +fragment MyFragment on ClientNamed { + name +} + +# %extensions% + +extend type Query { + client_type: ClientType +} + +type ClientType implements ClientNamed { + name: String +} + +interface ClientNamed { + name: String +} diff --git a/compiler/crates/relay-transforms/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_union.expected b/compiler/crates/relay-transforms/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_union.expected new file mode 100644 index 0000000000000..68cc0b853dee5 --- /dev/null +++ b/compiler/crates/relay-transforms/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_union.expected @@ -0,0 +1,48 @@ +==================================== INPUT ==================================== +query Foo { + client_type { + ...MyFragment + } +} + +fragment MyFragment on ClientUnion { + __typename +} + +# %extensions% + +extend type Query { + client_type: ClientType +} + +type ClientType { + name: String +} + +type OtherClientType { + name: String +} + +union ClientUnion = ClientType | OtherClientType +==================================== OUTPUT =================================== +fragment MyFragment on ClientUnion { + __typename +} + +query Foo @__ClientExtensionAbstractTypeMetadataDirective +# ClientExtensionAbstractTypeMetadataDirective { +# abstract_types: [ +# AbstractTypeImplements { +# name: "__isClientUnion", +# concrete: [ +# "ClientType", +# "OtherClientType", +# ], +# }, +# ], +# } + { + client_type { + ...MyFragment + } +} diff --git a/compiler/crates/relay-transforms/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_union.graphql b/compiler/crates/relay-transforms/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_union.graphql new file mode 100644 index 0000000000000..8fdebffefcbb6 --- /dev/null +++ b/compiler/crates/relay-transforms/tests/client_extensions_abstract_types/fixtures/fragment_spread_on_client_union.graphql @@ -0,0 +1,25 @@ +query Foo { + client_type { + ...MyFragment + } +} + +fragment MyFragment on ClientUnion { + __typename +} + +# %extensions% + +extend type Query { + client_type: ClientType +} + +type ClientType { + name: String +} + +type OtherClientType { + name: String +} + +union ClientUnion = ClientType | OtherClientType diff --git a/compiler/crates/relay-transforms/tests/client_extensions_abstract_types/fixtures/inline_fragment_on_client_interface.expected b/compiler/crates/relay-transforms/tests/client_extensions_abstract_types/fixtures/inline_fragment_on_client_interface.expected new file mode 100644 index 0000000000000..85f76f5c8983c --- /dev/null +++ b/compiler/crates/relay-transforms/tests/client_extensions_abstract_types/fixtures/inline_fragment_on_client_interface.expected @@ -0,0 +1,41 @@ +==================================== INPUT ==================================== +query Foo { + client_type { + ... on ClientNamed { + name + } + } +} + +# %extensions% + +extend type Query { + client_type: ClientType +} + +type ClientType implements ClientNamed { + name: String +} + +interface ClientNamed { + name: String +} +==================================== OUTPUT =================================== +query Foo @__ClientExtensionAbstractTypeMetadataDirective +# ClientExtensionAbstractTypeMetadataDirective { +# abstract_types: [ +# AbstractTypeImplements { +# name: "__isClientNamed", +# concrete: [ +# "ClientType", +# ], +# }, +# ], +# } + { + client_type { + ... on ClientNamed { + name + } + } +} diff --git a/compiler/crates/relay-transforms/tests/client_extensions_abstract_types/fixtures/inline_fragment_on_client_interface.graphql b/compiler/crates/relay-transforms/tests/client_extensions_abstract_types/fixtures/inline_fragment_on_client_interface.graphql new file mode 100644 index 0000000000000..524f654356f54 --- /dev/null +++ b/compiler/crates/relay-transforms/tests/client_extensions_abstract_types/fixtures/inline_fragment_on_client_interface.graphql @@ -0,0 +1,21 @@ +query Foo { + client_type { + ... on ClientNamed { + name + } + } +} + +# %extensions% + +extend type Query { + client_type: ClientType +} + +type ClientType implements ClientNamed { + name: String +} + +interface ClientNamed { + name: String +} diff --git a/compiler/crates/relay-transforms/tests/client_extensions_abstract_types/mod.rs b/compiler/crates/relay-transforms/tests/client_extensions_abstract_types/mod.rs new file mode 100644 index 0000000000000..2783802e09e7d --- /dev/null +++ b/compiler/crates/relay-transforms/tests/client_extensions_abstract_types/mod.rs @@ -0,0 +1,49 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +use common::SourceLocationKey; +use fixture_tests::Fixture; +use graphql_ir::build; +use graphql_ir::Program; +use graphql_syntax::parse_executable; +use graphql_text_printer::print_fragment; +use graphql_text_printer::print_operation; +use graphql_text_printer::PrinterOptions; +use relay_test_schema::get_test_schema_with_extensions; +use relay_transforms::client_extensions_abstract_types; +use std::sync::Arc; + +pub fn transform_fixture(fixture: &Fixture<'_>) -> Result { + let parts: Vec<_> = fixture.content.split("%extensions%").collect(); + if let [base, extensions] = parts.as_slice() { + let source_location = SourceLocationKey::standalone(fixture.file_name); + let ast = parse_executable(base, source_location).unwrap(); + let schema = get_test_schema_with_extensions(extensions); + let ir = build(&schema, &ast.definitions).unwrap(); + let program = Program::from_definitions(Arc::clone(&schema), ir); + let next_program = client_extensions_abstract_types(&program); + + let printer_options = PrinterOptions { + debug_directive_data: true, + ..Default::default() + }; + + let mut printed = next_program + .operations() + .map(|def| print_operation(&schema, def, printer_options.clone())) + .chain( + next_program + .fragments() + .map(|def| print_fragment(&schema, def, printer_options.clone())), + ) + .collect::>(); + printed.sort(); + Ok(printed.join("\n\n")) + } else { + panic!("Expected exactly one %extensions% section marker.") + } +} diff --git a/compiler/crates/relay-transforms/tests/client_extensions_abstract_types_test.rs b/compiler/crates/relay-transforms/tests/client_extensions_abstract_types_test.rs new file mode 100644 index 0000000000000..7bf36359d5df8 --- /dev/null +++ b/compiler/crates/relay-transforms/tests/client_extensions_abstract_types_test.rs @@ -0,0 +1,41 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @generated SignedSource<> + */ + +mod client_extensions_abstract_types; + +use client_extensions_abstract_types::transform_fixture; +use fixture_tests::test_fixture; + +#[test] +fn fragment_spread_on_client_interface() { + let input = include_str!("client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface.graphql"); + let expected = include_str!("client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface.expected"); + test_fixture(transform_fixture, "fragment_spread_on_client_interface.graphql", "client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface.expected", input, expected); +} + +#[test] +fn fragment_spread_on_client_interface_transitively() { + let input = include_str!("client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface_transitively.graphql"); + let expected = include_str!("client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface_transitively.expected"); + test_fixture(transform_fixture, "fragment_spread_on_client_interface_transitively.graphql", "client_extensions_abstract_types/fixtures/fragment_spread_on_client_interface_transitively.expected", input, expected); +} + +#[test] +fn fragment_spread_on_client_union() { + let input = include_str!("client_extensions_abstract_types/fixtures/fragment_spread_on_client_union.graphql"); + let expected = include_str!("client_extensions_abstract_types/fixtures/fragment_spread_on_client_union.expected"); + test_fixture(transform_fixture, "fragment_spread_on_client_union.graphql", "client_extensions_abstract_types/fixtures/fragment_spread_on_client_union.expected", input, expected); +} + +#[test] +fn inline_fragment_on_client_interface() { + let input = include_str!("client_extensions_abstract_types/fixtures/inline_fragment_on_client_interface.graphql"); + let expected = include_str!("client_extensions_abstract_types/fixtures/inline_fragment_on_client_interface.expected"); + test_fixture(transform_fixture, "inline_fragment_on_client_interface.graphql", "client_extensions_abstract_types/fixtures/inline_fragment_on_client_interface.expected", input, expected); +} diff --git a/packages/relay-runtime/store/RelayResponseNormalizer.js b/packages/relay-runtime/store/RelayResponseNormalizer.js index 66e954cd2b1ae..b1bad23690d0a 100644 --- a/packages/relay-runtime/store/RelayResponseNormalizer.js +++ b/packages/relay-runtime/store/RelayResponseNormalizer.js @@ -171,6 +171,7 @@ class RelayResponseNormalizer { 'RelayResponseNormalizer(): Expected root record `%s` to exist.', dataID, ); + this._assignClientAbstractTypes(node); this._traverseSelections(node, record, data); return { errors: null, @@ -182,6 +183,27 @@ class RelayResponseNormalizer { }; } + // For abstract types defined in the client schema extension, we won't be + // getting `__is` hints from the server. To handle this, the + // compiler attaches additional metadata on the normalization artifact, + // which we need to record into the store. + _assignClientAbstractTypes(node: NormalizationNode) { + const {clientAbstractTypes} = node; + if (clientAbstractTypes != null) { + for (const abstractType of Object.keys(clientAbstractTypes)) { + for (const concreteType of clientAbstractTypes[abstractType]) { + const typeID = generateTypeID(concreteType); + let typeRecord = this._recordSource.get(typeID); + if (typeRecord == null) { + typeRecord = RelayModernRecord.create(typeID, TYPE_SCHEMA_TYPE); + this._recordSource.set(typeID, typeRecord); + } + RelayModernRecord.setValue(typeRecord, abstractType, true); + } + } + } + } + _getVariableValue(name: string): mixed { invariant( this._variables.hasOwnProperty(name), diff --git a/packages/relay-runtime/store/__tests__/RelayModernEnvironment-TypeRefinement-test.js b/packages/relay-runtime/store/__tests__/RelayModernEnvironment-TypeRefinement-test.js index c5736e5667b06..da3bcf586a616 100644 --- a/packages/relay-runtime/store/__tests__/RelayModernEnvironment-TypeRefinement-test.js +++ b/packages/relay-runtime/store/__tests__/RelayModernEnvironment-TypeRefinement-test.js @@ -50,6 +50,7 @@ const { const {getSingularSelector} = require('../RelayModernSelector'); const RelayModernStore = require('../RelayModernStore'); const RelayRecordSource = require('../RelayRecordSource'); +const {ROOT_ID} = require('../RelayStoreUtils'); const {generateTypeID} = require('../TypeID'); const nullthrows = require('nullthrows'); const { @@ -90,11 +91,13 @@ describe('missing data detection', () => { RelayModernEnvironmentTypeRefinementTestParentQuery$data, >; let AbstractQuery; + let AbstractClientQuery; let ConcreteQuery; let ConcreteUserFragment; let ConcreteInlineRefinementFragment; let AbstractActorFragment; let AbstractInlineRefinementFragment; + let AbstractClientInterfaceFragment; let environment; let operation; let concreteOperation; @@ -132,6 +135,15 @@ describe('missing data detection', () => { } `; + // version of the query with only abstract refinements + AbstractClientQuery = graphql` + query RelayModernEnvironmentTypeRefinementTestClientAbstractQuery { + client_interface { + ...RelayModernEnvironmentTypeRefinementTestClientInterface + } + } + `; + // identical fragments except for User (concrete) / Actor (interface) ConcreteUserFragment = graphql` fragment RelayModernEnvironmentTypeRefinementTestConcreteUserFragment on User { @@ -149,6 +161,12 @@ describe('missing data detection', () => { } `; + AbstractClientInterfaceFragment = graphql` + fragment RelayModernEnvironmentTypeRefinementTestClientInterface on ClientInterface { + description + } + `; + // identical except for inline fragments on User / Actor // note fragment type is Node in both cases to avoid any // flattening @@ -1839,4 +1857,33 @@ describe('missing data detection', () => { expect(environment.check(operation).status).toBe('available'); }); }); + + describe('Abstract types defined in client schema extension', () => { + it('knows when concrete types match abstract types by metadata attached to normalizaiton AST', () => { + operation = createOperationDescriptor(AbstractClientQuery, {}); + environment.commitUpdate(store => { + const rootRecord = nullthrows(store.get(ROOT_ID)); + const clientObj = store.create( + '4', + 'OtherClientTypeImplementingClientInterface', + ); + clientObj.setValue('4', 'id'); + clientObj.setValue('My Description', 'description'); + rootRecord.setLinkedRecord(clientObj, 'client_interface'); + }); + environment.commitPayload(operation, {}); + const parentSnapshot: $FlowFixMe = environment.lookup(operation.fragment); + const fragmentSelector = nullthrows( + getSingularSelector( + AbstractClientInterfaceFragment, + parentSnapshot.data.client_interface, + ), + ); + const fragmentSnapshot = environment.lookup(fragmentSelector); + expect(fragmentSnapshot.data).toEqual({ + description: 'My Description', + }); + expect(fragmentSnapshot.isMissingData).toBe(false); + }); + }); }); diff --git a/packages/relay-runtime/store/__tests__/RelayResponseNormalizer-test.js b/packages/relay-runtime/store/__tests__/RelayResponseNormalizer-test.js index 93ad4b71bf582..fde9302be6a25 100644 --- a/packages/relay-runtime/store/__tests__/RelayResponseNormalizer-test.js +++ b/packages/relay-runtime/store/__tests__/RelayResponseNormalizer-test.js @@ -4639,5 +4639,64 @@ describe('RelayResponseNormalizer', () => { }, }); }); + it('records which concrete types implement which client schema extension interfaces', () => { + graphql` + fragment RelayResponseNormalizerTestClientInterfaceFragment on ClientInterface { + description + } + `; + // This query contains abstract client types referenced in both named and + // inline fragments as well as including both interfaces and unions. + const query = graphql` + query RelayResponseNormalizerTestClientInterfaceQuery { + client_interface { + ...RelayResponseNormalizerTestClientInterfaceFragment + } + client_union { + ... on ClientUnion { + __typename + } + } + } + `; + + const payload = {}; + const recordSource = new RelayRecordSource(); + recordSource.set(ROOT_ID, RelayModernRecord.create(ROOT_ID, ROOT_TYPE)); + + const result = normalize( + recordSource, + createNormalizationSelector(query.operation, ROOT_ID, {}), + payload, + {...defaultOptions}, + ); + + expect(result.source.toJSON()).toEqual({ + 'client:__type:ClientTypeImplementingClientInterface': { + __id: 'client:__type:ClientTypeImplementingClientInterface', + __isClientInterface: true, + __typename: '__TypeSchema', + }, + 'client:__type:OtherClientTypeImplementingClientInterface': { + __id: 'client:__type:OtherClientTypeImplementingClientInterface', + __isClientInterface: true, + __typename: '__TypeSchema', + }, + 'client:__type:ClientTypeInUnion': { + __id: 'client:__type:ClientTypeInUnion', + __isClientUnion: true, + __typename: '__TypeSchema', + }, + 'client:__type:OtherClientTypeInUnion': { + __id: 'client:__type:OtherClientTypeInUnion', + __isClientUnion: true, + __typename: '__TypeSchema', + }, + 'client:root': { + __id: 'client:root', + __typename: '__Root', + }, + }); + }); }); }); diff --git a/packages/relay-runtime/store/__tests__/__generated__/RelayModernEnvironmentTypeRefinementTestClientAbstractQuery.graphql.js b/packages/relay-runtime/store/__tests__/__generated__/RelayModernEnvironmentTypeRefinementTestClientAbstractQuery.graphql.js new file mode 100644 index 0000000000000..4c5d97e6841ec --- /dev/null +++ b/packages/relay-runtime/store/__tests__/__generated__/RelayModernEnvironmentTypeRefinementTestClientAbstractQuery.graphql.js @@ -0,0 +1,125 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @generated SignedSource<<19a4d8e4249e58f7ad3fe3ebe59f904a>> + * @flow + * @lightSyntaxTransform + * @nogrep + */ + +/* eslint-disable */ + +'use strict'; + +/*:: +import type { ClientRequest, ClientQuery } from 'relay-runtime'; +type RelayModernEnvironmentTypeRefinementTestClientInterface$fragmentType = any; +export type RelayModernEnvironmentTypeRefinementTestClientAbstractQuery$variables = {||}; +export type RelayModernEnvironmentTypeRefinementTestClientAbstractQuery$data = {| + +client_interface: ?{| + +$fragmentSpreads: RelayModernEnvironmentTypeRefinementTestClientInterface$fragmentType, + |}, +|}; +export type RelayModernEnvironmentTypeRefinementTestClientAbstractQuery = {| + response: RelayModernEnvironmentTypeRefinementTestClientAbstractQuery$data, + variables: RelayModernEnvironmentTypeRefinementTestClientAbstractQuery$variables, +|}; +*/ + +var node/*: ClientRequest*/ = { + "fragment": { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "RelayModernEnvironmentTypeRefinementTestClientAbstractQuery", + "selections": [ + { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": null, + "kind": "LinkedField", + "name": "client_interface", + "plural": false, + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "RelayModernEnvironmentTypeRefinementTestClientInterface" + } + ], + "storageKey": null + } + ] + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [], + "kind": "Operation", + "name": "RelayModernEnvironmentTypeRefinementTestClientAbstractQuery", + "selections": [ + { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": null, + "kind": "LinkedField", + "name": "client_interface", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "description", + "storageKey": null + } + ], + "storageKey": null + } + ] + } + ], + "clientAbstractTypes": { + "__isClientInterface": [ + "ClientTypeImplementingClientInterface", + "OtherClientTypeImplementingClientInterface" + ] + } + }, + "params": { + "cacheID": "ad9adf6c7d765caca998d65e7cd8ec41", + "id": null, + "metadata": {}, + "name": "RelayModernEnvironmentTypeRefinementTestClientAbstractQuery", + "operationKind": "query", + "text": null + } +}; + +if (__DEV__) { + (node/*: any*/).hash = "f6eba7c5be21b5bc892e69ffa6f017d6"; +} + +module.exports = ((node/*: any*/)/*: ClientQuery< + RelayModernEnvironmentTypeRefinementTestClientAbstractQuery$variables, + RelayModernEnvironmentTypeRefinementTestClientAbstractQuery$data, +>*/); diff --git a/packages/relay-runtime/store/__tests__/__generated__/RelayModernEnvironmentTypeRefinementTestClientInterface.graphql.js b/packages/relay-runtime/store/__tests__/__generated__/RelayModernEnvironmentTypeRefinementTestClientInterface.graphql.js new file mode 100644 index 0000000000000..0bf3dec5416b1 --- /dev/null +++ b/packages/relay-runtime/store/__tests__/__generated__/RelayModernEnvironmentTypeRefinementTestClientInterface.graphql.js @@ -0,0 +1,62 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @generated SignedSource<<3f6eac175a3d34599c61428d097702ac>> + * @flow + * @lightSyntaxTransform + * @nogrep + */ + +/* eslint-disable */ + +'use strict'; + +/*:: +import type { Fragment, ReaderFragment } from 'relay-runtime'; +import type { FragmentType } from "relay-runtime"; +declare export opaque type RelayModernEnvironmentTypeRefinementTestClientInterface$fragmentType: FragmentType; +export type RelayModernEnvironmentTypeRefinementTestClientInterface$data = {| + +description: ?string, + +$fragmentType: RelayModernEnvironmentTypeRefinementTestClientInterface$fragmentType, +|}; +export type RelayModernEnvironmentTypeRefinementTestClientInterface$key = { + +$data?: RelayModernEnvironmentTypeRefinementTestClientInterface$data, + +$fragmentSpreads: RelayModernEnvironmentTypeRefinementTestClientInterface$fragmentType, + ... +}; +*/ + +var node/*: ReaderFragment*/ = { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "RelayModernEnvironmentTypeRefinementTestClientInterface", + "selections": [ + { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "description", + "storageKey": null + } + ] + } + ], + "type": "ClientInterface", + "abstractKey": "__isClientInterface" +}; + +if (__DEV__) { + (node/*: any*/).hash = "287f546a52c69fb148055d6382052c98"; +} + +module.exports = ((node/*: any*/)/*: Fragment< + RelayModernEnvironmentTypeRefinementTestClientInterface$fragmentType, + RelayModernEnvironmentTypeRefinementTestClientInterface$data, +>*/); diff --git a/packages/relay-runtime/store/__tests__/__generated__/RelayResponseNormalizerTestClientInterfaceFragment.graphql.js b/packages/relay-runtime/store/__tests__/__generated__/RelayResponseNormalizerTestClientInterfaceFragment.graphql.js new file mode 100644 index 0000000000000..702c50fa8822f --- /dev/null +++ b/packages/relay-runtime/store/__tests__/__generated__/RelayResponseNormalizerTestClientInterfaceFragment.graphql.js @@ -0,0 +1,62 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @generated SignedSource<> + * @flow + * @lightSyntaxTransform + * @nogrep + */ + +/* eslint-disable */ + +'use strict'; + +/*:: +import type { Fragment, ReaderFragment } from 'relay-runtime'; +import type { FragmentType } from "relay-runtime"; +declare export opaque type RelayResponseNormalizerTestClientInterfaceFragment$fragmentType: FragmentType; +export type RelayResponseNormalizerTestClientInterfaceFragment$data = {| + +description: ?string, + +$fragmentType: RelayResponseNormalizerTestClientInterfaceFragment$fragmentType, +|}; +export type RelayResponseNormalizerTestClientInterfaceFragment$key = { + +$data?: RelayResponseNormalizerTestClientInterfaceFragment$data, + +$fragmentSpreads: RelayResponseNormalizerTestClientInterfaceFragment$fragmentType, + ... +}; +*/ + +var node/*: ReaderFragment*/ = { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "RelayResponseNormalizerTestClientInterfaceFragment", + "selections": [ + { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "description", + "storageKey": null + } + ] + } + ], + "type": "ClientInterface", + "abstractKey": "__isClientInterface" +}; + +if (__DEV__) { + (node/*: any*/).hash = "8750bf96f36b4fbad1dd3506ec7d4c1d"; +} + +module.exports = ((node/*: any*/)/*: Fragment< + RelayResponseNormalizerTestClientInterfaceFragment$fragmentType, + RelayResponseNormalizerTestClientInterfaceFragment$data, +>*/); diff --git a/packages/relay-runtime/store/__tests__/__generated__/RelayResponseNormalizerTestClientInterfaceQuery.graphql.js b/packages/relay-runtime/store/__tests__/__generated__/RelayResponseNormalizerTestClientInterfaceQuery.graphql.js new file mode 100644 index 0000000000000..8b0fbff9ede31 --- /dev/null +++ b/packages/relay-runtime/store/__tests__/__generated__/RelayResponseNormalizerTestClientInterfaceQuery.graphql.js @@ -0,0 +1,149 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @generated SignedSource<<84326274f0d8b10e6b74d19ff9d0c864>> + * @flow + * @lightSyntaxTransform + * @nogrep + */ + +/* eslint-disable */ + +'use strict'; + +/*:: +import type { ClientRequest, ClientQuery } from 'relay-runtime'; +type RelayResponseNormalizerTestClientInterfaceFragment$fragmentType = any; +export type RelayResponseNormalizerTestClientInterfaceQuery$variables = {||}; +export type RelayResponseNormalizerTestClientInterfaceQuery$data = {| + +client_interface: ?{| + +$fragmentSpreads: RelayResponseNormalizerTestClientInterfaceFragment$fragmentType, + |}, + +client_union: ?{| + +__typename: string, + |}, +|}; +export type RelayResponseNormalizerTestClientInterfaceQuery = {| + response: RelayResponseNormalizerTestClientInterfaceQuery$data, + variables: RelayResponseNormalizerTestClientInterfaceQuery$variables, +|}; +*/ + +var node/*: ClientRequest*/ = (function(){ +var v0 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null +}, +v1 = { + "alias": null, + "args": null, + "concreteType": null, + "kind": "LinkedField", + "name": "client_union", + "plural": false, + "selections": [ + (v0/*: any*/) + ], + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "RelayResponseNormalizerTestClientInterfaceQuery", + "selections": [ + { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": null, + "kind": "LinkedField", + "name": "client_interface", + "plural": false, + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "RelayResponseNormalizerTestClientInterfaceFragment" + } + ], + "storageKey": null + }, + (v1/*: any*/) + ] + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [], + "kind": "Operation", + "name": "RelayResponseNormalizerTestClientInterfaceQuery", + "selections": [ + { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": null, + "kind": "LinkedField", + "name": "client_interface", + "plural": false, + "selections": [ + (v0/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "description", + "storageKey": null + } + ], + "storageKey": null + }, + (v1/*: any*/) + ] + } + ], + "clientAbstractTypes": { + "__isClientInterface": [ + "ClientTypeImplementingClientInterface", + "OtherClientTypeImplementingClientInterface" + ], + "__isClientUnion": [ + "ClientTypeInUnion", + "OtherClientTypeInUnion" + ] + } + }, + "params": { + "cacheID": "f032f4115a3c158be2a330847665b868", + "id": null, + "metadata": {}, + "name": "RelayResponseNormalizerTestClientInterfaceQuery", + "operationKind": "query", + "text": null + } +}; +})(); + +if (__DEV__) { + (node/*: any*/).hash = "fda6737d4bb601d57ab80c634726d8a3"; +} + +module.exports = ((node/*: any*/)/*: ClientQuery< + RelayResponseNormalizerTestClientInterfaceQuery$variables, + RelayResponseNormalizerTestClientInterfaceQuery$data, +>*/); diff --git a/packages/relay-runtime/util/NormalizationNode.js b/packages/relay-runtime/util/NormalizationNode.js index 4872ca2423151..d5997c44b7c93 100644 --- a/packages/relay-runtime/util/NormalizationNode.js +++ b/packages/relay-runtime/util/NormalizationNode.js @@ -21,6 +21,9 @@ export type NormalizationOperation = { +name: string, +argumentDefinitions: $ReadOnlyArray, +selections: $ReadOnlyArray, + +clientAbstractTypes?: { + +[string]: $ReadOnlyArray, + }, }; export type NormalizationHandle = diff --git a/packages/relay-test-utils-internal/schema-extensions/ClientInterface.graphql b/packages/relay-test-utils-internal/schema-extensions/ClientInterface.graphql new file mode 100644 index 0000000000000..22b94b35d95a4 --- /dev/null +++ b/packages/relay-test-utils-internal/schema-extensions/ClientInterface.graphql @@ -0,0 +1,15 @@ +interface ClientInterface { + description: String +} + +type ClientTypeImplementingClientInterface implements ClientInterface { + description: String +} + +type OtherClientTypeImplementingClientInterface implements ClientInterface { + description: String +} + +extend type Query { + client_interface: ClientInterface +} diff --git a/packages/relay-test-utils-internal/schema-extensions/ClientUnion.graphql b/packages/relay-test-utils-internal/schema-extensions/ClientUnion.graphql new file mode 100644 index 0000000000000..ab6642151e28f --- /dev/null +++ b/packages/relay-test-utils-internal/schema-extensions/ClientUnion.graphql @@ -0,0 +1,13 @@ +type ClientTypeInUnion { + description: String +} + +type OtherClientTypeInUnion { + description: String +} + +union ClientUnion = ClientTypeInUnion | OtherClientTypeInUnion + +extend type Query { + client_union: ClientUnion +}