Skip to content

Commit 14f4731

Browse files
committed
Allow updatable fragment spreads inside @alias'd inline fragments
An @alias'd inline fragment acts as a type boundary (like a linked field), so we can relax the restrictions that require a discriminated union or disallow top-level inline fragments. This adds an AliasedInlineFragment path item that performs the supertype check without requiring discriminated union validation, and updates error messages to suggest @alias as a fix.
1 parent 3d7bc92 commit 14f4731

11 files changed

Lines changed: 159 additions & 7 deletions

compiler/crates/relay-transforms/src/assignable_fragment_spread/errors.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,22 +121,30 @@ pub enum ValidationMessage {
121121
UpdatableFragmentSpreadNoDirectives,
122122

123123
#[error(
124-
"This updatable fragment has type `{updatable_fragment_type}`, and is spread at the top level of a definition with type `{containing_type}`. However, if a record has the type `{containing_type}`, it does not necessarily have the type `{updatable_fragment_type}`."
124+
"This updatable fragment has type `{updatable_fragment_type}`, and is spread at the top level of a definition with type `{containing_type}`. However, if a record has the type `{containing_type}`, it does not necessarily have the type `{updatable_fragment_type}`. You can use an inline fragment with `@alias` to narrow the type, e.g. `... on {updatable_fragment_type} @alias`."
125125
)]
126126
UpdatableFragmentSpreadSubtypeOrEqualContainingType {
127127
updatable_fragment_type: StringKey,
128128
containing_type: StringKey,
129129
},
130130

131-
#[error("Updatable fragments cannot be spread inside an inline fragment at the top level without an enclosing linked field.")]
131+
#[error("Updatable fragments cannot be spread inside an inline fragment at the top level without an enclosing linked field. Try adding `@alias` to the inline fragment.")]
132132
UpdatableFragmentTopLevelInlineFragment,
133133

134134
#[error("Updatable fragments cannot be contained in @skip or @if.")]
135135
UpdatableFragmentSpreadNoCondition,
136136

137-
#[error("Updatable fragments can only be nested within at most a single inline fragment.")]
137+
#[error("Updatable fragments can only be nested within at most a single inline fragment. Try adding `@alias` to the inline fragments.")]
138138
UpdatableFragmentSpreadContainingInlineFragmentSingleNesting,
139139

140+
#[error(
141+
"This updatable fragment has type `{updatable_fragment_type}`, and is found within an `@alias`'d inline fragment with type condition `{aliased_inline_fragment_type}`. However, if a record has the type `{aliased_inline_fragment_type}`, it does not necessarily have the type `{updatable_fragment_type}`."
142+
)]
143+
UpdatableFragmentSpreadSubtypeOrEqualAliasedInlineFragment {
144+
updatable_fragment_type: StringKey,
145+
aliased_inline_fragment_type: StringKey,
146+
},
147+
140148
#[error(
141149
"This updatable fragment has type `{updatable_fragment_type}`, and is found within a linked field with type `{linked_field_type}`. However, if a record has the type `{linked_field_inner_type}`, it does not necessarily have the type `{updatable_fragment_type}`."
142150
)]

compiler/crates/relay-transforms/src/assignable_fragment_spread/validate_updatable_fragment_spread.rs

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use schema::TypeReference;
2323
use super::ValidationMessage;
2424
use super::ensure_discriminated_union_is_created;
2525
use crate::UPDATABLE_DIRECTIVE;
26+
use crate::fragment_alias_directive::FRAGMENT_ALIAS_DIRECTIVE_NAME;
2627
use crate::fragment_alias_directive::FRAGMENT_DANGEROUSLY_UNALIAS_DIRECTIVE_NAME;
2728

2829
pub fn validate_updatable_fragment_spread(program: &Program) -> DiagnosticsResult<()> {
@@ -45,6 +46,7 @@ pub fn validate_updatable_fragment_spread(program: &Program) -> DiagnosticsResul
4546
/// additional validation that ensures that a discriminated union is created.
4647
enum PathItem {
4748
InlineFragment,
49+
AliasedInlineFragment { type_condition: Type },
4850
LinkedField(LinkedFieldPathItem),
4951
Condition,
5052
}
@@ -116,6 +118,34 @@ impl UpdatableFragmentSpread<'_> {
116118
}
117119
encountered_inline_fragment = true;
118120
}
121+
PathItem::AliasedInlineFragment { type_condition } => {
122+
// An aliased inline fragment acts as a type boundary (like a linked field).
123+
// We check that the fragment's type is compatible, but we do NOT set
124+
// `encountered_inline_fragment` or `should_ensure_discriminated_union_is_created`
125+
// because @alias provides its own type narrowing semantics.
126+
if !self.program.schema.is_type_subtype_of(
127+
&TypeReference::Named(*type_condition),
128+
&TypeReference::Named(fragment_definition.type_condition),
129+
) {
130+
errors.push(Diagnostic::error(
131+
ValidationMessage::UpdatableFragmentSpreadSubtypeOrEqualAliasedInlineFragment {
132+
updatable_fragment_type: self
133+
.program
134+
.schema
135+
.get_type_name(fragment_definition.type_condition),
136+
aliased_inline_fragment_type: self
137+
.program
138+
.schema
139+
.get_type_name(*type_condition),
140+
},
141+
fragment_spread.fragment.location,
142+
));
143+
}
144+
// Treat as a type boundary — prevents the top-level containing type
145+
// check from firing.
146+
encountered_linked_field = true;
147+
break;
148+
}
119149
PathItem::LinkedField(linked_field_path_item) => {
120150
encountered_linked_field = true;
121151

@@ -283,7 +313,21 @@ impl Validator for UpdatableFragmentSpread<'_> {
283313
&mut self,
284314
inline_fragment: &InlineFragment,
285315
) -> DiagnosticsResult<()> {
286-
self.path.push(PathItem::InlineFragment);
316+
let has_alias = inline_fragment
317+
.directives
318+
.named(*FRAGMENT_ALIAS_DIRECTIVE_NAME)
319+
.is_some();
320+
321+
if has_alias {
322+
if let Some(type_condition) = inline_fragment.type_condition {
323+
self.path
324+
.push(PathItem::AliasedInlineFragment { type_condition });
325+
} else {
326+
self.path.push(PathItem::InlineFragment);
327+
}
328+
} else {
329+
self.path.push(PathItem::InlineFragment);
330+
}
287331
let result = self.default_validate_inline_fragment(inline_fragment);
288332
self.path.pop().expect("path should not be empty");
289333
result
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
==================================== INPUT ====================================
2+
fragment Foo on Query {
3+
node(id: "4") {
4+
... on User @alias {
5+
...Updatable_user
6+
}
7+
}
8+
}
9+
10+
fragment Updatable_user on User @updatable {
11+
__typename
12+
}
13+
==================================== OUTPUT ===================================
14+
OK
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
fragment Foo on Query {
2+
node(id: "4") {
3+
... on User @alias {
4+
...Updatable_user
5+
}
6+
}
7+
}
8+
9+
fragment Updatable_user on User @updatable {
10+
__typename
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
==================================== INPUT ====================================
2+
fragment Foo on Node {
3+
... on User @alias {
4+
...Updatable_user
5+
}
6+
}
7+
8+
fragment Updatable_user on User @updatable {
9+
__typename
10+
}
11+
==================================== OUTPUT ===================================
12+
OK
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
fragment Foo on Node {
2+
... on User @alias {
3+
...Updatable_user
4+
}
5+
}
6+
7+
fragment Updatable_user on User @updatable {
8+
__typename
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
==================================== INPUT ====================================
2+
# expected-to-throw
3+
fragment Foo on Query {
4+
node(id: "4") {
5+
... on Node @alias {
6+
...Updatable_user
7+
}
8+
}
9+
}
10+
11+
fragment Updatable_user on User @updatable {
12+
__typename
13+
}
14+
==================================== ERROR ====================================
15+
✖︎ This updatable fragment has type `User`, and is found within an `@alias`'d inline fragment with type condition `Node`. However, if a record has the type `Node`, it does not necessarily have the type `User`.
16+
17+
updatable_fragment_spread_aliased_inline_fragment_wrong_type.invalid.graphql:5:10
18+
4 │ ... on Node @alias {
19+
5 │ ...Updatable_user
20+
│ ^^^^^^^^^^^^^^
21+
6 │ }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# expected-to-throw
2+
fragment Foo on Query {
3+
node(id: "4") {
4+
... on Node @alias {
5+
...Updatable_user
6+
}
7+
}
8+
}
9+
10+
fragment Updatable_user on User @updatable {
11+
__typename
12+
}

compiler/crates/relay-transforms/tests/updatable_fragment_spread/fixtures/updatable_fragment_spread_top_level_in_inline_fragment.invalid.expected

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ fragment Updatable_user on User @updatable {
1010
__typename
1111
}
1212
==================================== ERROR ====================================
13-
✖︎ Updatable fragments cannot be spread inside an inline fragment at the top level without an enclosing linked field.
13+
✖︎ Updatable fragments cannot be spread inside an inline fragment at the top level without an enclosing linked field. Try adding `@alias` to the inline fragment.
1414

1515
updatable_fragment_spread_top_level_in_inline_fragment.invalid.graphql:4:8
1616
3 │ ... on User {

compiler/crates/relay-transforms/tests/updatable_fragment_spread/fixtures/updatable_fragment_spread_top_level_wrong_type.invalid.expected

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ fragment Updatable_user on User @updatable {
88
__typename
99
}
1010
==================================== ERROR ====================================
11-
✖︎ This updatable fragment has type `User`, and is spread at the top level of a definition with type `Node`. However, if a record has the type `Node`, it does not necessarily have the type `User`.
11+
✖︎ This updatable fragment has type `User`, and is spread at the top level of a definition with type `Node`. However, if a record has the type `Node`, it does not necessarily have the type `User`. You can use an inline fragment with `@alias` to narrow the type, e.g. `... on User @alias`.
1212

1313
updatable_fragment_spread_top_level_wrong_type.invalid.graphql:3:6
1414
2 │ fragment Foo on Node {

0 commit comments

Comments
 (0)