From 17f0c1051e55d0e2c768fb46d442bb10f689a1aa Mon Sep 17 00:00:00 2001 From: Josh Watzman Date: Sat, 11 Oct 2025 20:38:54 +0100 Subject: [PATCH] Filter raw_response output types for concrete fields When generating a raw_response type for a concrete fields, we may (e.g.,) still be spreading a fragment which is for an abstract type. We should cut down our type selections based on the actual concrete type of the field, to generate more specific raw_response types. We should also mark the selections has having the same concrete type as our field, preventing redundant arms of the final TypeScript/Flow union types from being output. This makes the output raw response types a lot more pleasant to work with. Fixes #5090 --- compiler/crates/relay-typegen/src/visit.rs | 18 ++++ .../relay-resolver-raw-response.expected | 6 +- ...ace-fragment-on-concrete-raw-type.expected | 84 +++++++++++++++++++ ...face-fragment-on-concrete-raw-type.graphql | 23 +++++ .../relay-typegen/tests/generate_flow_test.rs | 7 ++ ...esolver-on-query-with-output-type.expected | 2 - ...ace-fragment-on-concrete-raw-type.expected | 82 ++++++++++++++++++ ...face-fragment-on-concrete-raw-type.graphql | 23 +++++ .../tests/generate_typescript_test.rs | 7 ++ 9 files changed, 246 insertions(+), 6 deletions(-) create mode 100644 compiler/crates/relay-typegen/tests/generate_flow/fixtures/spread-interface-fragment-on-concrete-raw-type.expected create mode 100644 compiler/crates/relay-typegen/tests/generate_flow/fixtures/spread-interface-fragment-on-concrete-raw-type.graphql create mode 100644 compiler/crates/relay-typegen/tests/generate_typescript/fixtures/spread-interface-fragment-on-concrete-raw-type.expected create mode 100644 compiler/crates/relay-typegen/tests/generate_typescript/fixtures/spread-interface-fragment-on-concrete-raw-type.graphql diff --git a/compiler/crates/relay-typegen/src/visit.rs b/compiler/crates/relay-typegen/src/visit.rs index d6666ca63b45f..1f59d1ea1df01 100644 --- a/compiler/crates/relay-typegen/src/visit.rs +++ b/compiler/crates/relay-typegen/src/visit.rs @@ -2389,6 +2389,24 @@ pub(crate) fn raw_response_visit_selections( ), } } + + if let Some(concrete_type) = enclosing_linked_field_concrete_type { + // If we are generating for a concrete field type, we should 1. remove + // any selections that are for a different concrete type, since they are + // not applicable to our field, and 2. mark any selections without a + // concrete type as being for our field's concrete type, so that + // `raw_response_selections_to_babel` doesn't generate redundant types. + type_selections.retain_mut(|type_selection| { + match type_selection.get_enclosing_concrete_type() { + Some(selection_concrete_type) => selection_concrete_type == concrete_type, + None => { + type_selection.set_concrete_type(concrete_type); + true + } + } + }); + } + type_selections } diff --git a/compiler/crates/relay-typegen/tests/generate_flow/fixtures/relay-resolver-raw-response.expected b/compiler/crates/relay-typegen/tests/generate_flow/fixtures/relay-resolver-raw-response.expected index 00cd81ef72abb..3c61a9bb11e59 100644 --- a/compiler/crates/relay-typegen/tests/generate_flow/fixtures/relay-resolver-raw-response.expected +++ b/compiler/crates/relay-typegen/tests/generate_flow/fixtures/relay-resolver-raw-response.expected @@ -31,12 +31,10 @@ export type relayResolver_Query$data = {| |}, |}; export type relayResolver_Query$rawResponse = {| - +me: ?({| - +id: string, - |} | {| + +me: ?{| +id: string, +name: ?string, - |}), + |}, |}; export type relayResolver_Query = {| rawResponse: relayResolver_Query$rawResponse, diff --git a/compiler/crates/relay-typegen/tests/generate_flow/fixtures/spread-interface-fragment-on-concrete-raw-type.expected b/compiler/crates/relay-typegen/tests/generate_flow/fixtures/spread-interface-fragment-on-concrete-raw-type.expected new file mode 100644 index 0000000000000..e1dac0e97193a --- /dev/null +++ b/compiler/crates/relay-typegen/tests/generate_flow/fixtures/spread-interface-fragment-on-concrete-raw-type.expected @@ -0,0 +1,84 @@ +==================================== INPUT ==================================== +fragment MyFragment on Actor { + name + ... on User { + canViewerLike + } + ... on Page { + subscribeStatus + } +} + +query MyQuery @raw_response_type { + me { + canViewerComment + ...MyFragment + } +} + +mutation MyMutation @raw_response_type { + setName(name: "test") { + canViewerComment + ...MyFragment + } +} +==================================== OUTPUT =================================== +import type { MyFragment$fragmentType } from "MyFragment.graphql"; +export type MyMutation$variables = {||}; +export type MyMutation$data = {| + +setName: ?{| + +canViewerComment: ?CustomBoolean, + +$fragmentSpreads: MyFragment$fragmentType, + |}, +|}; +export type MyMutation$rawResponse = {| + +setName: ?{| + +__isActor: "User", + +canViewerComment: ?CustomBoolean, + +canViewerLike: ?CustomBoolean, + +id: string, + +name: ?string, + |}, +|}; +export type MyMutation = {| + rawResponse: MyMutation$rawResponse, + response: MyMutation$data, + variables: MyMutation$variables, +|}; +------------------------------------------------------------------------------- +import type { MyFragment$fragmentType } from "MyFragment.graphql"; +export type MyQuery$variables = {||}; +export type MyQuery$data = {| + +me: ?{| + +canViewerComment: ?CustomBoolean, + +$fragmentSpreads: MyFragment$fragmentType, + |}, +|}; +export type MyQuery$rawResponse = {| + +me: ?{| + +__isActor: "User", + +canViewerComment: ?CustomBoolean, + +canViewerLike: ?CustomBoolean, + +id: string, + +name: ?string, + |}, +|}; +export type MyQuery = {| + rawResponse: MyQuery$rawResponse, + response: MyQuery$data, + variables: MyQuery$variables, +|}; +------------------------------------------------------------------------------- +import type { FragmentType } from "relay-runtime"; +declare export opaque type MyFragment$fragmentType: FragmentType; +export type MyFragment$data = {| + +canViewerLike?: ?CustomBoolean, + +name: ?string, + +subscribeStatus?: ?string, + +$fragmentType: MyFragment$fragmentType, +|}; +export type MyFragment$key = { + +$data?: MyFragment$data, + +$fragmentSpreads: MyFragment$fragmentType, + ... +}; diff --git a/compiler/crates/relay-typegen/tests/generate_flow/fixtures/spread-interface-fragment-on-concrete-raw-type.graphql b/compiler/crates/relay-typegen/tests/generate_flow/fixtures/spread-interface-fragment-on-concrete-raw-type.graphql new file mode 100644 index 0000000000000..2a00b64f14017 --- /dev/null +++ b/compiler/crates/relay-typegen/tests/generate_flow/fixtures/spread-interface-fragment-on-concrete-raw-type.graphql @@ -0,0 +1,23 @@ +fragment MyFragment on Actor { + name + ... on User { + canViewerLike + } + ... on Page { + subscribeStatus + } +} + +query MyQuery @raw_response_type { + me { + canViewerComment + ...MyFragment + } +} + +mutation MyMutation @raw_response_type { + setName(name: "test") { + canViewerComment + ...MyFragment + } +} diff --git a/compiler/crates/relay-typegen/tests/generate_flow_test.rs b/compiler/crates/relay-typegen/tests/generate_flow_test.rs index b4413c90c643c..b2870d356a7ba 100644 --- a/compiler/crates/relay-typegen/tests/generate_flow_test.rs +++ b/compiler/crates/relay-typegen/tests/generate_flow_test.rs @@ -992,6 +992,13 @@ async fn simple() { test_fixture(transform_fixture, file!(), "simple.graphql", "generate_flow/fixtures/simple.expected", input, expected).await; } +#[tokio::test] +async fn spread_interface_fragment_on_concrete_raw_type() { + let input = include_str!("generate_flow/fixtures/spread-interface-fragment-on-concrete-raw-type.graphql"); + let expected = include_str!("generate_flow/fixtures/spread-interface-fragment-on-concrete-raw-type.expected"); + test_fixture(transform_fixture, file!(), "spread-interface-fragment-on-concrete-raw-type.graphql", "generate_flow/fixtures/spread-interface-fragment-on-concrete-raw-type.expected", input, expected).await; +} + #[tokio::test] async fn typename_in_union_with_other_fields() { let input = include_str!("generate_flow/fixtures/typename-in-union-with-other-fields.graphql"); diff --git a/compiler/crates/relay-typegen/tests/generate_typescript/fixtures/relay-resolver-on-query-with-output-type.expected b/compiler/crates/relay-typegen/tests/generate_typescript/fixtures/relay-resolver-on-query-with-output-type.expected index ef682bea0927d..15dadf4af6ca7 100644 --- a/compiler/crates/relay-typegen/tests/generate_typescript/fixtures/relay-resolver-on-query-with-output-type.expected +++ b/compiler/crates/relay-typegen/tests/generate_typescript/fixtures/relay-resolver-on-query-with-output-type.expected @@ -46,8 +46,6 @@ export type Foo_user$rawResponse = { readonly id: string; readonly lastName: string | null | undefined; }>; - } | { - readonly id: string; } | null | undefined; }; export type Foo_user = { diff --git a/compiler/crates/relay-typegen/tests/generate_typescript/fixtures/spread-interface-fragment-on-concrete-raw-type.expected b/compiler/crates/relay-typegen/tests/generate_typescript/fixtures/spread-interface-fragment-on-concrete-raw-type.expected new file mode 100644 index 0000000000000..4849c24da80d6 --- /dev/null +++ b/compiler/crates/relay-typegen/tests/generate_typescript/fixtures/spread-interface-fragment-on-concrete-raw-type.expected @@ -0,0 +1,82 @@ +==================================== INPUT ==================================== +fragment MyFragment on Actor { + name + ... on User { + canViewerLike + } + ... on Page { + subscribeStatus + } +} + +query MyQuery @raw_response_type { + me { + canViewerComment + ...MyFragment + } +} + +mutation MyMutation @raw_response_type { + setName(name: "test") { + canViewerComment + ...MyFragment + } +} +==================================== OUTPUT =================================== +import { FragmentRefs } from "relay-runtime"; +export type MyMutation$variables = Record; +export type MyMutation$data = { + readonly setName: { + readonly canViewerComment: boolean | null | undefined; + readonly " $fragmentSpreads": FragmentRefs<"MyFragment">; + } | null | undefined; +}; +export type MyMutation$rawResponse = { + readonly setName: { + readonly __isActor: "User"; + readonly canViewerComment: boolean | null | undefined; + readonly canViewerLike: boolean | null | undefined; + readonly id: string; + readonly name: string | null | undefined; + } | null | undefined; +}; +export type MyMutation = { + rawResponse: MyMutation$rawResponse; + response: MyMutation$data; + variables: MyMutation$variables; +}; +------------------------------------------------------------------------------- +import { FragmentRefs } from "relay-runtime"; +export type MyQuery$variables = Record; +export type MyQuery$data = { + readonly me: { + readonly canViewerComment: boolean | null | undefined; + readonly " $fragmentSpreads": FragmentRefs<"MyFragment">; + } | null | undefined; +}; +export type MyQuery$rawResponse = { + readonly me: { + readonly __isActor: "User"; + readonly canViewerComment: boolean | null | undefined; + readonly canViewerLike: boolean | null | undefined; + readonly id: string; + readonly name: string | null | undefined; + } | null | undefined; +}; +export type MyQuery = { + rawResponse: MyQuery$rawResponse; + response: MyQuery$data; + variables: MyQuery$variables; +}; +------------------------------------------------------------------------------- +import { FragmentRefs } from "relay-runtime"; +export type MyFragment$data = { + readonly canViewerLike?: boolean | null | undefined; + readonly name: string | null | undefined; + readonly subscribeStatus?: string | null | undefined; + readonly " $fragmentType": "MyFragment"; +}; +export type MyFragment$key = { + readonly " $data"?: MyFragment$data; + readonly " $fragmentSpreads": FragmentRefs<"MyFragment">; +}; diff --git a/compiler/crates/relay-typegen/tests/generate_typescript/fixtures/spread-interface-fragment-on-concrete-raw-type.graphql b/compiler/crates/relay-typegen/tests/generate_typescript/fixtures/spread-interface-fragment-on-concrete-raw-type.graphql new file mode 100644 index 0000000000000..2a00b64f14017 --- /dev/null +++ b/compiler/crates/relay-typegen/tests/generate_typescript/fixtures/spread-interface-fragment-on-concrete-raw-type.graphql @@ -0,0 +1,23 @@ +fragment MyFragment on Actor { + name + ... on User { + canViewerLike + } + ... on Page { + subscribeStatus + } +} + +query MyQuery @raw_response_type { + me { + canViewerComment + ...MyFragment + } +} + +mutation MyMutation @raw_response_type { + setName(name: "test") { + canViewerComment + ...MyFragment + } +} diff --git a/compiler/crates/relay-typegen/tests/generate_typescript_test.rs b/compiler/crates/relay-typegen/tests/generate_typescript_test.rs index 081aa80df50c4..4ea6f0dd96acc 100644 --- a/compiler/crates/relay-typegen/tests/generate_typescript_test.rs +++ b/compiler/crates/relay-typegen/tests/generate_typescript_test.rs @@ -593,6 +593,13 @@ async fn simple_use_import_type_syntax() { test_fixture(transform_fixture, file!(), "simple-use-import-type-syntax.graphql", "generate_typescript/fixtures/simple-use-import-type-syntax.expected", input, expected).await; } +#[tokio::test] +async fn spread_interface_fragment_on_concrete_raw_type() { + let input = include_str!("generate_typescript/fixtures/spread-interface-fragment-on-concrete-raw-type.graphql"); + let expected = include_str!("generate_typescript/fixtures/spread-interface-fragment-on-concrete-raw-type.expected"); + test_fixture(transform_fixture, file!(), "spread-interface-fragment-on-concrete-raw-type.graphql", "generate_typescript/fixtures/spread-interface-fragment-on-concrete-raw-type.expected", input, expected).await; +} + #[tokio::test] async fn typename_in_union_with_other_fields() { let input = include_str!("generate_typescript/fixtures/typename-in-union-with-other-fields.graphql");