Skip to content

Commit ada971e

Browse files
captbaritonefacebook-github-bot
authored andcommitted
Dissallow some readtime fatures in mutation responses
Reviewed By: alunyov Differential Revision: D52644165 fbshipit-source-id: ab8d8326e16d85b6f62754bd6c30fb7e340b9e8b
1 parent 2f9e88a commit ada971e

29 files changed

+517
-48
lines changed

compiler/crates/common/src/feature_flags.rs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,27 @@ pub struct FeatureFlags {
8484
/// Perform strict validations when custom scalar types are used
8585
#[serde(default)]
8686
pub enable_strict_custom_scalars: bool,
87+
88+
/// Relay Resolvers are a read-time feature that are not actually handled in
89+
/// our mutation APIs. We are in the process of removing any existing
90+
/// examples, but this flag is part of a process of removing any existing
91+
/// examples.
92+
#[serde(default)]
93+
pub allow_resolvers_in_mutation_response: FeatureFlag,
94+
95+
/// @required with an action of THROW is read-time feature that is not
96+
/// compatible with our mutation APIs. We are in the process of removing
97+
/// any existing examples, but this flag is part of a process of removing
98+
/// any existing examples.
99+
#[serde(default)]
100+
pub allow_required_in_mutation_response: FeatureFlag,
87101
}
88102

89-
#[derive(Debug, Deserialize, Clone, Serialize)]
103+
#[derive(Debug, Deserialize, Clone, Serialize, Default)]
90104
#[serde(tag = "kind", rename_all = "lowercase")]
91105
pub enum FeatureFlag {
92106
/// Fully disabled: developers may not use this feature
107+
#[default]
93108
Disabled,
94109

95110
/// Fully enabled: developers may use this feature
@@ -102,12 +117,6 @@ pub enum FeatureFlag {
102117
Rollout { rollout: Rollout },
103118
}
104119

105-
impl Default for FeatureFlag {
106-
fn default() -> Self {
107-
FeatureFlag::Disabled
108-
}
109-
}
110-
111120
impl FeatureFlag {
112121
pub fn is_enabled_for(&self, name: StringKey) -> bool {
113122
match self {

compiler/crates/graphql-ir/src/errors.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,16 @@ pub enum ValidationMessage {
516516
literal_kind: String,
517517
scalar_type_name: ScalarName,
518518
},
519+
520+
#[error(
521+
"Unexpected `@required(action: THROW)` directive in mutation response. The use of `@required(action: THROW)` is not supported in mutations."
522+
)]
523+
RequiredInMutation,
524+
525+
#[error(
526+
"Unexpected `@RelayResolver` field referenced in mutation response. Relay Resolver fields may not be read as part of a mutation response."
527+
)]
528+
ResolverInMutation,
519529
}
520530

521531
#[derive(Clone, Debug, Error, Eq, PartialEq, Ord, PartialOrd, Hash)]

compiler/crates/relay-compiler/src/build_project/validate.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use errors::try_all;
1515
use graphql_ir::Program;
1616
use relay_config::ProjectConfig;
1717
use relay_transforms::disallow_circular_no_inline_fragments;
18+
use relay_transforms::disallow_readtime_features_in_mutations;
1819
use relay_transforms::disallow_reserved_aliases;
1920
use relay_transforms::disallow_typename_on_root;
2021
use relay_transforms::validate_assignable_directive;
@@ -67,6 +68,16 @@ pub fn validate(
6768
} else {
6869
Ok(())
6970
},
71+
disallow_readtime_features_in_mutations(
72+
program,
73+
&project_config
74+
.feature_flags
75+
.allow_resolvers_in_mutation_response,
76+
&project_config
77+
.feature_flags
78+
.allow_required_in_mutation_response,
79+
project_config.feature_flags.enable_relay_resolver_mutations,
80+
),
7081
]);
7182

7283
match output {

compiler/crates/relay-compiler/tests/compile_relay_artifacts/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@ pub async fn transform_fixture(fixture: &Fixture<'_>) -> Result<String, String>
122122
relay_resolvers_allow_legacy_verbose_syntax: FeatureFlag::Disabled,
123123
enable_relay_resolver_mutations: false,
124124
enable_strict_custom_scalars: false,
125+
allow_required_in_mutation_response: FeatureFlag::Disabled,
126+
allow_resolvers_in_mutation_response: FeatureFlag::Disabled,
125127
};
126128

127129
let default_project_config = ProjectConfig {

compiler/crates/relay-compiler/tests/compile_relay_artifacts_with_custom_id/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ pub async fn transform_fixture(fixture: &Fixture<'_>) -> Result<String, String>
101101
relay_resolvers_allow_legacy_verbose_syntax: FeatureFlag::Disabled,
102102
enable_relay_resolver_mutations: false,
103103
enable_strict_custom_scalars: false,
104+
allow_required_in_mutation_response: FeatureFlag::Disabled,
105+
allow_resolvers_in_mutation_response: FeatureFlag::Disabled,
104106
};
105107

106108
let default_schema_config = SchemaConfig::default();

compiler/crates/relay-transforms/Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# @generated by autocargo from //relay/oss/crates/relay-transforms:[apply_fragment_arguments_test,assignable_directive_test,assignable_fragment_spread_test,client_edges_test,client_extensions_test,declarative_connection_test,disallow_typename_on_root_test,fragment_alias_directive_test,generate_data_driven_dependency_metadata_test,generate_live_query_metadata_test,generate_relay_resolvers_operations_for_nested_objects_test,graphql-client_extensions_abstract_types-test,graphql-defer_stream-test,graphql-disallow_non_node_id_fields-test,graphql-disallow_reserved_aliases-test,graphql-flatten-test,graphql-generate_id_field-test,graphql-generate_typename-test,graphql-inline_fragments-test,graphql-mask-test,graphql-match-local-test,graphql-match-test,graphql-node_identifier-test,graphql-refetchable_fragment_test,graphql-skip_client_extensions-test,graphql-skip_redundant_nodes-test,graphql-skip_unreachable_nodes-test,graphql-sort_selections-test,graphql-subscription_transform-test,graphql-validate_deprecated_fields_test,graphql-validate_module_names-test,graphql-validate_relay_directives-test,graphql-validate_required_arguments_test,graphql-validate_server_only_directives-test,graphql-validate_unused_variables-test,inline_data_fragment_test,provided-variable-fragment-transform-test,relay-actor-change-test,relay-transforms,relay_resolvers_abstract_types_test,relay_resolvers_test,relay_test_operation_test,required_directive_test,skip_unused_variables_test,transform_connections_test,updatable_directive_test,updatable_fragment_spread_test,validate_connections_schema_test,validate_connections_test,validate_global_variable_names_test,validate_global_variables-test,validate_no_double_underscore_alias_test,validate_no_unselectable_selections_test,validate_static_args]
1+
# @generated by autocargo from //relay/oss/crates/relay-transforms:[apply_fragment_arguments_test,assignable_directive_test,assignable_fragment_spread_test,client_edges_test,client_extensions_test,declarative_connection_test,disallow_typename_on_root_test,fragment_alias_directive_test,generate_data_driven_dependency_metadata_test,generate_live_query_metadata_test,generate_relay_resolvers_operations_for_nested_objects_test,graphql-client_extensions_abstract_types-test,graphql-defer_stream-test,graphql-disallow_non_node_id_fields-test,graphql-disallow_reserved_aliases-test,graphql-disallowreadtime_features_in_mutations-test,graphql-flatten-test,graphql-generate_id_field-test,graphql-generate_typename-test,graphql-inline_fragments-test,graphql-mask-test,graphql-match-local-test,graphql-match-test,graphql-node_identifier-test,graphql-refetchable_fragment_test,graphql-skip_client_extensions-test,graphql-skip_redundant_nodes-test,graphql-skip_unreachable_nodes-test,graphql-sort_selections-test,graphql-subscription_transform-test,graphql-validate_deprecated_fields_test,graphql-validate_module_names-test,graphql-validate_relay_directives-test,graphql-validate_required_arguments_test,graphql-validate_server_only_directives-test,graphql-validate_unused_variables-test,inline_data_fragment_test,provided-variable-fragment-transform-test,relay-actor-change-test,relay-transforms,relay_resolvers_abstract_types_test,relay_resolvers_test,relay_test_operation_test,required_directive_test,skip_unused_variables_test,transform_connections_test,updatable_directive_test,updatable_fragment_spread_test,validate_connections_schema_test,validate_connections_test,validate_global_variable_names_test,validate_global_variables-test,validate_no_double_underscore_alias_test,validate_no_unselectable_selections_test,validate_static_args]
22

33
[package]
44
name = "relay-transforms"
@@ -24,6 +24,10 @@ path = "tests/disallow_non_node_id_fields_test.rs"
2424
name = "graphql_disallow_reserved_aliases_test"
2525
path = "tests/disallow_reserved_aliases_test.rs"
2626

27+
[[test]]
28+
name = "graphql_disallowreadtime_features_in_mutations_test"
29+
path = "tests/disallow_readtime_features_in_mutations_test.rs"
30+
2731
[[test]]
2832
name = "graphql_flatten_test"
2933
path = "tests/flatten_test.rs"

compiler/crates/relay-transforms/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ pub use required_directive::RequiredMetadataDirective;
179179
pub use required_directive::ACTION_ARGUMENT;
180180
pub use required_directive::CHILDREN_CAN_BUBBLE_METADATA_KEY;
181181
pub use required_directive::REQUIRED_DIRECTIVE_NAME;
182+
pub use required_directive::THROW_ACTION;
182183
pub use skip_client_directives::skip_client_directives;
183184
pub use skip_client_extensions::skip_client_extensions;
184185
pub use skip_null_arguments_transform::skip_null_arguments_transform;

compiler/crates/relay-transforms/src/required_directive/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ lazy_static! {
5050
pub static ref ACTION_ARGUMENT: ArgumentName = ArgumentName("action".intern());
5151
pub static ref CHILDREN_CAN_BUBBLE_METADATA_KEY: DirectiveName =
5252
DirectiveName("__childrenCanBubbleNull".intern());
53-
static ref THROW_ACTION: StringKey = "THROW".intern();
53+
pub static ref THROW_ACTION: StringKey = "THROW".intern();
5454
static ref LOG_ACTION: StringKey = "LOG".intern();
5555
static ref NONE_ACTION: StringKey = "NONE".intern();
5656
static ref INLINE_DIRECTIVE_NAME: DirectiveName = DirectiveName("inline".intern());
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
use common::Diagnostic;
9+
use common::DiagnosticsResult;
10+
use common::FeatureFlag;
11+
use common::NamedItem;
12+
use docblock_shared::RELAY_RESOLVER_DIRECTIVE_NAME;
13+
use graphql_ir::ConstantValue;
14+
use graphql_ir::Field;
15+
use graphql_ir::FragmentDefinition;
16+
use graphql_ir::FragmentSpread;
17+
use graphql_ir::LinkedField;
18+
use graphql_ir::OperationDefinition;
19+
use graphql_ir::Program;
20+
use graphql_ir::ScalarField;
21+
use graphql_ir::ValidationMessage;
22+
use graphql_ir::Validator;
23+
use schema::Schema;
24+
25+
use crate::ACTION_ARGUMENT;
26+
use crate::REQUIRED_DIRECTIVE_NAME;
27+
use crate::THROW_ACTION;
28+
29+
/// Some Relay features will cause a field to throw or suspend at read time.
30+
/// These behaviors are incompatible with our mutation APIs.
31+
/// This validator checks that no such features are used in mutations.
32+
pub fn disallow_readtime_features_in_mutations(
33+
program: &Program,
34+
allow_resolvers_mutation_response: &FeatureFlag,
35+
allow_required_in_mutation_response: &FeatureFlag,
36+
enable_relay_resolver_mutations: bool,
37+
) -> DiagnosticsResult<()> {
38+
let mut validator = DisallowReadtimeFeaturesInMutations::new(
39+
program,
40+
allow_resolvers_mutation_response.clone(),
41+
allow_required_in_mutation_response.clone(),
42+
enable_relay_resolver_mutations,
43+
);
44+
validator.validate_program(program)
45+
}
46+
47+
struct DisallowReadtimeFeaturesInMutations<'program> {
48+
program: &'program Program,
49+
allow_resolvers_mutation_response: FeatureFlag,
50+
allow_required_in_mutation_response: FeatureFlag,
51+
enable_relay_resolver_mutations: bool,
52+
allow_resolvers_for_this_mutation: bool,
53+
allow_required_for_this_mutation: bool,
54+
}
55+
56+
impl<'program> DisallowReadtimeFeaturesInMutations<'program> {
57+
fn new(
58+
program: &'program Program,
59+
allow_resolvers_mutation_response: FeatureFlag,
60+
allow_required_in_mutation_response: FeatureFlag,
61+
enable_relay_resolver_mutations: bool,
62+
) -> Self {
63+
Self {
64+
program,
65+
allow_resolvers_mutation_response,
66+
allow_required_in_mutation_response,
67+
enable_relay_resolver_mutations,
68+
allow_resolvers_for_this_mutation: false,
69+
allow_required_for_this_mutation: false,
70+
}
71+
}
72+
73+
fn validate_field(&self, field: &impl Field) -> DiagnosticsResult<()> {
74+
if !self.allow_required_for_this_mutation {
75+
if let Some(directive) = field.directives().named(*REQUIRED_DIRECTIVE_NAME) {
76+
let action = directive
77+
.arguments
78+
.named(*ACTION_ARGUMENT)
79+
.and_then(|arg| arg.value.item.get_constant());
80+
if let Some(ConstantValue::Enum(action)) = action {
81+
if *action == *THROW_ACTION {
82+
return Err(vec![Diagnostic::error(
83+
ValidationMessage::RequiredInMutation,
84+
directive.name.location,
85+
)]);
86+
}
87+
}
88+
}
89+
}
90+
if !self.allow_resolvers_for_this_mutation
91+
&& self
92+
.program
93+
.schema
94+
.field(field.definition().item)
95+
.directives
96+
.named(*RELAY_RESOLVER_DIRECTIVE_NAME)
97+
.is_some()
98+
{
99+
return Err(vec![Diagnostic::error(
100+
ValidationMessage::ResolverInMutation,
101+
field.alias_or_name_location(),
102+
)]);
103+
}
104+
105+
Ok(())
106+
}
107+
}
108+
109+
impl Validator for DisallowReadtimeFeaturesInMutations<'_> {
110+
const NAME: &'static str = "disallow_readtime_features_in_mutations";
111+
const VALIDATE_ARGUMENTS: bool = false;
112+
const VALIDATE_DIRECTIVES: bool = false;
113+
114+
fn validate_operation(&mut self, operation: &OperationDefinition) -> DiagnosticsResult<()> {
115+
if !operation.is_mutation() {
116+
// No need to traverse into non-mutation operations
117+
return Ok(());
118+
}
119+
self.allow_resolvers_for_this_mutation = self.enable_relay_resolver_mutations
120+
|| self
121+
.allow_resolvers_mutation_response
122+
.is_enabled_for(operation.name.item.0);
123+
self.allow_required_for_this_mutation = self
124+
.allow_required_in_mutation_response
125+
.is_enabled_for(operation.name.item.0);
126+
let result = self.default_validate_operation(operation);
127+
128+
// Reset state
129+
self.allow_resolvers_for_this_mutation = false;
130+
self.allow_required_for_this_mutation = false;
131+
132+
result
133+
}
134+
135+
fn validate_fragment(&mut self, _fragment: &FragmentDefinition) -> DiagnosticsResult<()> {
136+
// We only care about mutations
137+
Ok(())
138+
}
139+
140+
fn validate_fragment_spread(&mut self, _spread: &FragmentSpread) -> DiagnosticsResult<()> {
141+
// Values nested within fragment spreads are fine since they are not read as part of the
142+
// mutation response.
143+
Ok(())
144+
}
145+
146+
fn validate_scalar_field(&mut self, field: &ScalarField) -> DiagnosticsResult<()> {
147+
self.validate_field(field)
148+
}
149+
fn validate_linked_field(&mut self, field: &LinkedField) -> DiagnosticsResult<()> {
150+
self.validate_field(field)?;
151+
self.default_validate_linked_field(field)
152+
}
153+
}

compiler/crates/relay-transforms/src/validations/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
mod deprecated_fields;
99
mod disallow_circular_no_inline_fragments;
1010
mod disallow_non_node_id_fields;
11+
mod disallow_readtime_features_in_mutations;
1112
mod disallow_reserved_aliases;
1213
mod disallow_typename_on_root;
1314
mod validate_connections;
@@ -29,6 +30,7 @@ pub use deprecated_fields::deprecated_fields;
2930
pub use deprecated_fields::deprecated_fields_for_executable_definition;
3031
pub use disallow_circular_no_inline_fragments::disallow_circular_no_inline_fragments;
3132
pub use disallow_non_node_id_fields::disallow_non_node_id_fields;
33+
pub use disallow_readtime_features_in_mutations::disallow_readtime_features_in_mutations;
3234
pub use disallow_reserved_aliases::disallow_reserved_aliases;
3335
pub use disallow_typename_on_root::disallow_typename_on_root;
3436
pub use validate_connections::validate_connections;

0 commit comments

Comments
 (0)