From a2e796025e8d6206e1ed383a09772c6dd757ff36 Mon Sep 17 00:00:00 2001 From: shylock Date: Wed, 24 Jul 2024 19:30:51 +0800 Subject: [PATCH] feat: add transformers to flatten single field types automatically (#2356) Co-authored-by: Tushar Mathur --- .../transformer/flatten_single_field.rs | 114 ++++++++++++++++++ src/core/config/transformer/mod.rs | 2 + src/core/config/transformer/preset.rs | 4 + ...__test__type_name_generator_transform.snap | 56 +++++++++ .../configs/flatten_single_field.graphql | 56 +++++++++ 5 files changed, 232 insertions(+) create mode 100644 src/core/config/transformer/flatten_single_field.rs create mode 100644 src/core/config/transformer/snapshots/tailcall__core__config__transformer__flatten_single_field__test__type_name_generator_transform.snap create mode 100644 tailcall-fixtures/fixtures/configs/flatten_single_field.graphql diff --git a/src/core/config/transformer/flatten_single_field.rs b/src/core/config/transformer/flatten_single_field.rs new file mode 100644 index 0000000000..d43323ab33 --- /dev/null +++ b/src/core/config/transformer/flatten_single_field.rs @@ -0,0 +1,114 @@ +use std::collections::HashSet; + +use crate::core::config::{AddField, Config, Omit}; +use crate::core::transform::Transform; +use crate::core::valid::{Valid, Validator}; + +/// Flat single field type and inline to Query directly by addField +#[derive(Default)] +pub struct FlattenSingleField; + +fn get_single_field_path( + config: &Config, + field_name: &str, + type_name: &str, + visited_types: &mut HashSet, +) -> Option> { + if visited_types.contains(type_name) { + // recursive type + return None; + } + visited_types.insert(type_name.to_owned()); + let mut path = Vec::new(); + path.push(field_name.to_owned()); + if config.is_scalar(type_name) || config.enums.contains_key(type_name) { + return Some(path); + } + let ty = config.types.get(type_name); + if let Some(ty) = ty { + if ty.fields.len() == 1 { + if let Some((sub_field_name, sub_field)) = ty.fields.first_key_value() { + let sub_path = get_single_field_path( + config, + sub_field_name, + &sub_field.type_of, + visited_types, + ); + if let Some(sub_path) = sub_path { + path.extend(sub_path); + Some(path) + } else { + None + } + } else { + None + } + } else { + None + } + } else { + None + } +} + +impl Transform for FlattenSingleField { + type Value = Config; + type Error = String; + fn transform(&self, mut config: Self::Value) -> Valid { + let origin_config = config.clone(); + if let Some(root) = &config.schema.query { + let root_query = config.types.get_mut(root); + if let Some(root_query) = root_query { + let field_trans = + Valid::from_iter(root_query.fields.iter_mut(), |(name, field)| { + let mut visited_types = HashSet::::new(); + if let Some(path) = get_single_field_path( + &origin_config, + name, + &field.type_of, + &mut visited_types, + ) { + if path.len() > 1 { + field.omit = Some(Omit {}); + root_query + .added_fields + .push(AddField { name: name.to_owned(), path }); + } + } + Valid::succeed(()) + }); + field_trans.map(|_| config) + } else { + Valid::fail("Query type is not existed.".to_owned()) + } + } else { + Valid::succeed(config) + } + } +} + +#[cfg(test)] +mod test { + use std::fs; + + use tailcall_fixtures::configs; + + use super::FlattenSingleField; + use crate::core::config::Config; + use crate::core::transform::Transform; + use crate::core::valid::Validator; + + fn read_fixture(path: &str) -> String { + fs::read_to_string(path).unwrap() + } + + #[test] + fn test_type_name_generator_transform() { + let config = Config::from_sdl(read_fixture(configs::FLATTEN_SINGLE_FIELD).as_str()) + .to_result() + .unwrap(); + + let transformed_config = FlattenSingleField.transform(config).to_result().unwrap(); + insta::assert_snapshot!(transformed_config.to_sdl()); + } +} diff --git a/src/core/config/transformer/mod.rs b/src/core/config/transformer/mod.rs index 4ac358a34f..86f3ed5581 100644 --- a/src/core/config/transformer/mod.rs +++ b/src/core/config/transformer/mod.rs @@ -1,5 +1,6 @@ mod ambiguous_type; mod consolidate_url; +mod flatten_single_field; mod improve_type_names; mod merge_types; mod nested_unions; @@ -10,6 +11,7 @@ mod union_input_type; pub use ambiguous_type::{AmbiguousType, Resolution}; pub use consolidate_url::ConsolidateURL; +pub use flatten_single_field::FlattenSingleField; pub use improve_type_names::ImproveTypeNames; pub use merge_types::TypeMerger; pub use nested_unions::NestedUnions; diff --git a/src/core/config/transformer/preset.rs b/src/core/config/transformer/preset.rs index 53112771f3..e0778714e5 100644 --- a/src/core/config/transformer/preset.rs +++ b/src/core/config/transformer/preset.rs @@ -11,6 +11,7 @@ pub struct Preset { pub consolidate_url: f32, pub tree_shake: bool, pub use_better_names: bool, + unwrap_single_field_types: bool, } impl Preset { @@ -20,6 +21,7 @@ impl Preset { consolidate_url: 0.0, tree_shake: false, use_better_names: false, + unwrap_single_field_types: true, } } } @@ -39,6 +41,7 @@ impl Transform for Preset { super::TypeMerger::new(self.merge_type) .when(super::TypeMerger::is_enabled(self.merge_type)), ) + .pipe(super::FlattenSingleField.when(self.unwrap_single_field_types)) .pipe(super::ImproveTypeNames.when(self.use_better_names)) .pipe( super::ConsolidateURL::new(self.consolidate_url) @@ -55,6 +58,7 @@ impl Default for Preset { consolidate_url: 0.5, use_better_names: true, tree_shake: true, + unwrap_single_field_types: true, } } } diff --git a/src/core/config/transformer/snapshots/tailcall__core__config__transformer__flatten_single_field__test__type_name_generator_transform.snap b/src/core/config/transformer/snapshots/tailcall__core__config__transformer__flatten_single_field__test__type_name_generator_transform.snap new file mode 100644 index 0000000000..9ae5feaaf2 --- /dev/null +++ b/src/core/config/transformer/snapshots/tailcall__core__config__transformer__flatten_single_field__test__type_name_generator_transform.snap @@ -0,0 +1,56 @@ +--- +source: src/core/config/transformer/flatten_single_field.rs +expression: transformed_config.to_sdl() +--- +schema @server @upstream { + query: Query +} + +type Bar { + a: Int +} + +type Connection { + user: User +} + +type Foo { + bar: Bar +} + +type NotSingle { + f1: Int + f2: Int +} + +type NotSingleMiddle { + t1: Type1 +} + +type Query @addField(name: "foo", path: ["foo", "bar", "a"]) { + foo: Foo @omit + not_single: NotSingle + not_single_middle: NotSingleMiddle + user: User +} + +type Type1 { + t2: Type2 +} + +type Type2 { + t3: Type3 + t4: Type4 +} + +type Type3 { + t5: Int +} + +type Type4 { + t6: Bool +} + +type User { + connections: [Connection] +} diff --git a/tailcall-fixtures/fixtures/configs/flatten_single_field.graphql b/tailcall-fixtures/fixtures/configs/flatten_single_field.graphql new file mode 100644 index 0000000000..8eba1e7287 --- /dev/null +++ b/tailcall-fixtures/fixtures/configs/flatten_single_field.graphql @@ -0,0 +1,56 @@ +schema { + query: Query +} + +type Query { + foo: Foo + not_single: NotSingle + not_single_middle: NotSingleMiddle + user: User +} + +# Type with only one field +type Foo { + bar: Bar +} + +# Type with only one field +type Bar { + a: Int +} + +type NotSingle { + f1: Int + f2: Int +} + +# Middle nested type is not single +type NotSingleMiddle { + t1: Type1 +} + +type Type1 { + t2: Type2 +} + +type Type2 { + t3: Type3 + t4: Type4 +} + +type Type3 { + t5: Int +} + +type Type4 { + t6: Bool +} + +# Recursive type +type User { + connections: [Connection] +} + +type Connection { + user: User +}