From 0453c2dbaf96a69d722fb78d5820aa891288f0e2 Mon Sep 17 00:00:00 2001 From: Sandipsinh Rathod <62684960+ssddOnTop@users.noreply.github.com> Date: Sat, 4 May 2024 16:30:49 +0530 Subject: [PATCH 1/2] fix(grpc): resolve ambiguous types (#1718) Co-authored-by: Tushar Mathur Co-authored-by: hazyone <59229672+hazyone@users.noreply.github.com> Co-authored-by: meskill <8974488+meskill@users.noreply.github.com> Co-authored-by: Everett Pompeii Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Shashi Kant Co-authored-by: shylock Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Eddy Oyieko <67474838+mobley-trent@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Tim Burrows Co-authored-by: Tim Burrows Co-authored-by: Amit Singh Co-authored-by: codingsh Co-authored-by: Kunam Balaram Reddy Co-authored-by: Ezhil Shanmugham Co-authored-by: Biswarghya Biswas Co-authored-by: Ranjit Mahadik <43403528+ranjitmahadik@users.noreply.github.com> --- generated/.tailcallrc.schema.json | 5 - src/blueprint/definitions.rs | 6 +- src/blueprint/mustache.rs | 2 +- src/blueprint/schema.rs | 2 +- src/config/config.rs | 264 ++++++++++++++++++----------- src/config/config_module.rs | 269 ++++++++++++++++++++++++------ src/config/from_document.rs | 2 +- src/generator/from_proto.rs | 3 +- src/generator/generator.rs | 11 +- src/scalar/mod.rs | 4 +- tailcall-query-plan/src/plan.rs | 4 +- 11 files changed, 408 insertions(+), 164 deletions(-) diff --git a/generated/.tailcallrc.schema.json b/generated/.tailcallrc.schema.json index b2f55892fe..8b538b80ba 100644 --- a/generated/.tailcallrc.schema.json +++ b/generated/.tailcallrc.schema.json @@ -1205,7 +1205,6 @@ }, "cache": { "description": "Setting to indicate if the type can be cached.", - "default": null, "anyOf": [ { "$ref": "#/definitions/Cache" @@ -1264,10 +1263,6 @@ } ] }, - "scalar": { - "description": "Flag to indicate if the type is a scalar.", - "type": "boolean" - }, "tag": { "description": "Contains source information for the type.", "anyOf": [ diff --git a/src/blueprint/definitions.rs b/src/blueprint/definitions.rs index a129ae7718..ef6127e323 100644 --- a/src/blueprint/definitions.rs +++ b/src/blueprint/definitions.rs @@ -126,7 +126,7 @@ fn process_field_within_type(context: ProcessFieldWithinTypeContext) -> Valid( fn validate_field_type_exist(config: &Config, field: &Field) -> Valid<(), String> { let field_type = &field.type_of; - if !scalar::is_scalar(field_type) && !config.contains(field_type) { + if !scalar::is_predefined_scalar(field_type) && !config.contains(field_type) { Valid::fail(format!("Undeclared type '{field_type}' was found")) } else { Valid::succeed(()) @@ -531,7 +531,7 @@ pub fn to_definitions<'a>() -> TryFold<'a, ConfigModule, Vec, String } else { Valid::fail("No variants found for enum".to_string()) } - } else if type_.scalar { + } else if type_.scalar() { to_scalar_type_definition(name).trace(name) } else if dbl_usage { Valid::fail("type is used in input and output".to_string()).trace(name) diff --git a/src/blueprint/mustache.rs b/src/blueprint/mustache.rs index f755cc83d5..9bcafaef59 100644 --- a/src/blueprint/mustache.rs +++ b/src/blueprint/mustache.rs @@ -29,7 +29,7 @@ impl<'a> MustachePartsValidator<'a> { if !is_query && val_type.is_nullable() { return Err(format!("value '{}' is a nullable type", item.as_str())); - } else if len == 1 && !scalar::is_scalar(val_type.name()) { + } else if len == 1 && !scalar::is_predefined_scalar(val_type.name()) { return Err(format!("value '{}' is not of a scalar type", item.as_str())); } else if len == 1 { break; diff --git a/src/blueprint/schema.rs b/src/blueprint/schema.rs index bfbce996f9..9ba08bad55 100644 --- a/src/blueprint/schema.rs +++ b/src/blueprint/schema.rs @@ -51,7 +51,7 @@ pub fn validate_field_has_resolver( let type_name = &field.type_of; if let Some(ty) = types.get(type_name) { // It's an enum - if ty.variants.is_some() || ty.scalar { + if ty.variants.is_some() || ty.scalar() { return true; } let res = validate_type_has_resolvers(type_name, ty, types); diff --git a/src/config/config.rs b/src/config/config.rs index d294bac5b5..fe4e31d2f1 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -14,11 +14,11 @@ use crate::config::from_document::from_document; use crate::config::source::Source; use crate::directive::DirectiveCodec; use crate::http::Method; -use crate::is_default; use crate::json::JsonSchema; use crate::macros::MergeRight; use crate::merge_right::MergeRight; use crate::valid::{Valid, Validator}; +use crate::{is_default, scalar}; #[derive( Serialize, @@ -72,98 +72,6 @@ pub struct Config { pub telemetry: Telemetry, } -impl Config { - pub fn port(&self) -> u16 { - self.server.port.unwrap_or(8000) - } - - pub fn find_type(&self, name: &str) -> Option<&Type> { - self.types.get(name) - } - - pub fn find_union(&self, name: &str) -> Option<&Union> { - self.unions.get(name) - } - - pub fn to_yaml(&self) -> Result { - Ok(serde_yaml::to_string(self)?) - } - - pub fn to_json(&self, pretty: bool) -> Result { - if pretty { - Ok(serde_json::to_string_pretty(self)?) - } else { - Ok(serde_json::to_string(self)?) - } - } - - pub fn to_document(&self) -> ServiceDocument { - self.clone().into() - } - - pub fn to_sdl(&self) -> String { - let doc = self.to_document(); - crate::document::print(doc) - } - - pub fn query(mut self, query: &str) -> Self { - self.schema.query = Some(query.to_string()); - self - } - - pub fn types(mut self, types: Vec<(&str, Type)>) -> Self { - let mut graphql_types = BTreeMap::new(); - for (name, type_) in types { - graphql_types.insert(name.to_string(), type_); - } - self.types = graphql_types; - self - } - - pub fn contains(&self, name: &str) -> bool { - self.types.contains_key(name) || self.unions.contains_key(name) - } - - /// Gets all the type names used in the schema. - pub fn get_all_used_type_names(&self) -> HashSet { - let mut set = HashSet::new(); - let mut stack = Vec::new(); - if let Some(query) = &self.schema.query { - stack.push(query.clone()); - } - if let Some(mutation) = &self.schema.mutation { - stack.push(mutation.clone()); - } - while let Some(type_name) = stack.pop() { - if let Some(typ) = self.types.get(&type_name) { - set.insert(type_name); - for field in typ.fields.values() { - stack.extend(field.args.values().map(|arg| arg.type_of.clone())); - stack.push(field.type_of.clone()); - } - } - } - - set - } - - pub fn get_all_unused_types(&self) -> HashSet { - let used_types = self.get_all_used_type_names(); - let all_types: HashSet = self.types.keys().cloned().collect(); - all_types.difference(&used_types).cloned().collect() - } - - /// Removes all types that are not used in the schema. - pub fn remove_unused_types(mut self) -> Self { - let unused_types = self.get_all_unused_types(); - for unused_type in unused_types { - self.types.remove(&unused_type); - } - - self - } -} - /// /// Represents a GraphQL type. /// A type can be an object, interface, enum or scalar. @@ -196,10 +104,6 @@ pub struct Type { pub variants: Option>, #[serde(default, skip_serializing_if = "is_default")] /// - /// Flag to indicate if the type is a scalar. - pub scalar: bool, - #[serde(default)] - /// /// Setting to indicate if the type can be cached. pub cache: Option, /// @@ -221,6 +125,10 @@ impl Type { self.fields = graphql_fields; self } + + pub fn scalar(&self) -> bool { + self.fields.is_empty() && self.variants.is_none() + } } #[derive( @@ -467,7 +375,7 @@ pub struct Inline { pub path: Vec, } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, schemars::JsonSchema)] +#[derive(Default, Serialize, Deserialize, Clone, Debug, PartialEq, Eq, schemars::JsonSchema)] pub struct Arg { #[serde(rename = "type")] pub type_of: String, @@ -693,6 +601,57 @@ pub struct AddField { } impl Config { + pub fn port(&self) -> u16 { + self.server.port.unwrap_or(8000) + } + + pub fn find_type(&self, name: &str) -> Option<&Type> { + self.types.get(name) + } + + pub fn find_union(&self, name: &str) -> Option<&Union> { + self.unions.get(name) + } + + pub fn to_yaml(&self) -> Result { + Ok(serde_yaml::to_string(self)?) + } + + pub fn to_json(&self, pretty: bool) -> Result { + if pretty { + Ok(serde_json::to_string_pretty(self)?) + } else { + Ok(serde_json::to_string(self)?) + } + } + + pub fn to_document(&self) -> ServiceDocument { + self.clone().into() + } + + pub fn to_sdl(&self) -> String { + let doc = self.to_document(); + crate::document::print(doc) + } + + pub fn query(mut self, query: &str) -> Self { + self.schema.query = Some(query.to_string()); + self + } + + pub fn types(mut self, types: Vec<(&str, Type)>) -> Self { + let mut graphql_types = BTreeMap::new(); + for (name, type_) in types { + graphql_types.insert(name.to_string(), type_); + } + self.types = graphql_types; + self + } + + pub fn contains(&self, name: &str) -> bool { + self.types.contains_key(name) || self.unions.contains_key(name) + } + pub fn from_json(json: &str) -> Result { Ok(serde_json::from_str(json)?) } @@ -720,6 +679,115 @@ impl Config { pub fn n_plus_one(&self) -> Vec> { super::n_plus_one::n_plus_one(self) } + + /// + /// Given a starting type, this function searches for all the unique types + /// that this type can be connected to via it's fields + fn find_connections(&self, type_of: &str, mut types: HashSet) -> HashSet { + if let Some(type_) = self.find_type(type_of) { + types.insert(type_of.into()); + for (_, field) in type_.fields.iter() { + if !types.contains(&field.type_of) { + types.insert(field.type_of.clone()); + types = self.find_connections(&field.type_of, types); + } + } + } + types + } + + /// + /// Checks if a type is a scalar or not. + pub fn is_scalar(&self, type_name: &str) -> bool { + self.types + .get(type_name) + .map_or(scalar::is_predefined_scalar(type_name), |ty| ty.scalar()) + } + + /// + /// Goes through the complete config and finds all the types that are used + /// as inputs directly ot indirectly. + pub fn input_types(&self) -> HashSet { + self.arguments() + .iter() + .filter(|(_, arg)| !self.is_scalar(&arg.type_of)) + .map(|(_, arg)| arg.type_of.as_str()) + .fold(HashSet::new(), |types, type_of| { + self.find_connections(type_of, types) + }) + } + + /// Returns a list of all the types that are not used as inputs + pub fn output_types(&self) -> HashSet { + let mut types = HashSet::new(); + let input_types = self.input_types(); + + if let Some(ref query) = &self.schema.query { + types.insert(query.clone()); + } + + if let Some(ref mutation) = &self.schema.mutation { + types.insert(mutation.clone()); + } + + for (type_name, type_of) in self.types.iter() { + if (type_of.interface || !type_of.fields.is_empty()) && !input_types.contains(type_name) + { + for (_, field) in type_of.fields.iter() { + types.insert(field.type_of.clone()); + } + } + } + + types + } + + /// Returns a list of all the arguments in the configuration + fn arguments(&self) -> Vec<(&String, &Arg)> { + self.types + .iter() + .filter(|(_, value)| !value.interface) + .flat_map(|(_, type_of)| type_of.fields.iter()) + .flat_map(|(_, field)| field.args.iter()) + .collect::>() + } + /// Removes all types that are passed in the set + pub fn remove_types(mut self, types: HashSet) -> Self { + for unused_type in types { + self.types.remove(&unused_type); + } + + self + } + + pub fn unused_types(&self) -> HashSet { + let used_types = self.get_all_used_type_names(); + let all_types: HashSet = self.types.keys().cloned().collect(); + all_types.difference(&used_types).cloned().collect() + } + + /// Gets all the type names used in the schema. + pub fn get_all_used_type_names(&self) -> HashSet { + let mut set = HashSet::new(); + let mut stack = Vec::new(); + if let Some(query) = &self.schema.query { + stack.push(query.clone()); + } + if let Some(mutation) = &self.schema.mutation { + stack.push(mutation.clone()); + } + while let Some(type_name) = stack.pop() { + if let Some(typ) = self.types.get(&type_name) { + set.insert(type_name); + for field in typ.fields.values() { + stack.extend(field.args.values().map(|arg| arg.type_of.clone())); + stack.push(field.type_of.clone()); + } + } + } + + set + } } #[derive( diff --git a/src/config/config_module.rs b/src/config/config_module.rs index 57ff789409..8e75dadf0c 100644 --- a/src/config/config_module.rs +++ b/src/config/config_module.rs @@ -12,7 +12,6 @@ use crate::macros::MergeRight; use crate::merge_right::MergeRight; use crate::proto_reader::ProtoMetadata; use crate::rest::{EndpointSet, Unchecked}; -use crate::scalar; /// A wrapper on top of Config that contains all the resolved extensions and /// computed values. @@ -94,73 +93,247 @@ impl Deref for ConfigModule { } } -fn recurse_type(config: &Config, type_of: &str, types: &mut HashSet) { - if let Some(type_) = config.find_type(type_of) { - for (_, field) in type_.fields.iter() { - if !types.contains(&field.type_of) { - types.insert(field.type_of.clone()); - recurse_type(config, &field.type_of, types); - } - } +pub struct Resolution { + pub input: String, + pub output: String, +} + +fn insert_resolution( + mut map: HashMap, + current_name: &str, + resolution: Resolution, +) -> HashMap { + if resolution.input.eq(&resolution.output) { + tracing::warn!( + "Unable to resolve input and output type: {}", + resolution.input + ); } + + if !map.contains_key(current_name) { + map.entry(current_name.to_string()).or_insert(resolution); + } + + map } -fn get_input_types(config: &Config) -> HashSet { - let mut types = HashSet::new(); - - for (_, type_of) in config.types.iter() { - if !type_of.interface { - for (_, field) in type_of.fields.iter() { - for (_, arg) in field - .args - .iter() - .filter(|(_, arg)| !scalar::is_scalar(&arg.type_of)) - { - if let Some(t) = config.find_type(&arg.type_of) { - if t.scalar { - continue; - } +impl ConfigModule { + /// This function resolves the ambiguous types by renaming the input and + /// output types. The resolver function should return a Resolution + /// object containing the new input and output types. + /// The function will return a new ConfigModule with the resolved types. + pub fn resolve_ambiguous_types(mut self, resolver: impl Fn(&str) -> Resolution) -> Self { + let mut resolution_map = HashMap::new(); - t.fields.iter().for_each(|(_, f)| { - types.insert(f.type_of.clone()); - recurse_type(config, &f.type_of, &mut types) - }) + // iterate over intersection of input and output types + for current_name in self.input_types.intersection(&self.output_types) { + let resolution = resolver(current_name); + + resolution_map = insert_resolution(resolution_map, current_name, resolution); + + if let Some(ty) = self.config.types.get(current_name) { + for field in ty.fields.values() { + for args in field.args.values() { + // if arg is of output type then it should be changed to that of newly + // created input type. + if self.output_types.contains(&args.type_of) + && !resolution_map.contains_key(&args.type_of) + { + let resolution = resolver(args.type_of.as_str()); + resolution_map = insert_resolution( + resolution_map, + args.type_of.as_str(), + resolution, + ); + } } - types.insert(arg.type_of.clone()); } } } - } - types -} -fn get_output_types(config: &Config, input_types: &HashSet) -> HashSet { - let mut types = HashSet::new(); + // insert newly created types to the config. + for (current_name, resolution) in &resolution_map { + let input_name = &resolution.input; + let output_name = &resolution.output; - if let Some(ref query) = &config.schema.query { - types.insert(query.clone()); - } + let og_ty = self.config.types.get(current_name).cloned(); - if let Some(ref mutation) = &config.schema.mutation { - types.insert(mutation.clone()); - } + // remove old types + self.config.types.remove(current_name); + self.input_types.remove(current_name); + self.output_types.remove(current_name); - for (type_name, type_of) in config.types.iter() { - if (type_of.interface || !type_of.fields.is_empty()) && !input_types.contains(type_name) { - for (_, field) in type_of.fields.iter() { - types.insert(field.type_of.clone()); + // add new types + if let Some(og_ty) = og_ty { + self.config.types.insert(input_name.clone(), og_ty.clone()); + self.input_types.insert(input_name.clone()); + + self.config.types.insert(output_name.clone(), og_ty); + self.output_types.insert(output_name.clone()); } } - } - types + let keys = self.config.types.keys().cloned().collect::>(); + + for k in keys { + if let Some(ty) = self.config.types.get_mut(&k) { + for field in ty.fields.values_mut() { + if let Some(resolution) = resolution_map.get(&field.type_of) { + if self.output_types.contains(&k) { + field.type_of = resolution.output.clone(); + } else if self.input_types.contains(&k) { + field.type_of = resolution.input.clone(); + } + } + for arg in field.args.values_mut() { + if let Some(resolution) = resolution_map.get(&arg.type_of) { + arg.type_of = resolution.input.clone(); + } + } + } + } + } + + self + } } impl From for ConfigModule { fn from(config: Config) -> Self { - let input_types = get_input_types(&config); - let output_types = get_output_types(&config, &input_types); + let input_types = config.input_types(); + let output_types = config.output_types(); ConfigModule { config, input_types, output_types, ..Default::default() } } } + +#[cfg(test)] +mod tests { + use std::collections::HashSet; + + use maplit::hashset; + use pretty_assertions::assert_eq; + + use crate::config::{Config, ConfigModule, Resolution, Type}; + use crate::generator::Source; + + fn build_qry(mut config: Config) -> Config { + let mut type1 = Type::default(); + let mut field1 = + crate::config::Field { type_of: "Type1".to_string(), ..Default::default() }; + + let arg1 = crate::config::Arg { type_of: "Type1".to_string(), ..Default::default() }; + + field1.args.insert("arg1".to_string(), arg1); + + let arg2 = crate::config::Arg { type_of: "Type2".to_string(), ..Default::default() }; + + let _field3 = crate::config::Field { type_of: "Type3".to_string(), ..Default::default() }; + let arg3 = crate::config::Arg { type_of: "Type3".to_string(), ..Default::default() }; + + field1.args.insert("arg2".to_string(), arg2); + field1.args.insert("arg3".to_string(), arg3); + + let mut field2 = field1.clone(); + field2.type_of = "Type2".to_string(); + + type1.fields.insert("field1".to_string(), field1); + type1.fields.insert("field2".to_string(), field2); + + config.types.insert("Query".to_string(), type1); + config = config.query("Query"); + + config + } + + #[test] + fn test_resolve_ambiguous_types() { + // Create a ConfigModule instance with ambiguous types + let mut config = Config::default(); + + let mut type1 = Type::default(); + let mut type2 = Type::default(); + let mut type3 = Type::default(); + + type1.fields.insert( + "name".to_string(), + crate::config::Field::default().type_of("String".to_string()), + ); + + type2.fields.insert( + "ty1".to_string(), + crate::config::Field::default().type_of("Type1".to_string()), + ); + type2.fields.insert( + "ty3".to_string(), + crate::config::Field::default().type_of("Type3".to_string()), + ); + + type3.fields.insert( + "ty1".to_string(), + crate::config::Field::default().type_of("Type1".to_string()), + ); + type3.fields.insert( + "ty2".to_string(), + crate::config::Field::default().type_of("Type2".to_string()), + ); + + config.types.insert("Type1".to_string(), type1); + config.types.insert("Type2".to_string(), type2); + config.types.insert("Type3".to_string(), type3); + + config = build_qry(config); + + let mut config_module = ConfigModule::from(config); + + let resolver = |type_name: &str| -> Resolution { + Resolution { + input: format!("{}Input", type_name), + output: format!("{}Output", type_name), + } + }; + + config_module = config_module.resolve_ambiguous_types(resolver); + + let actual = config_module + .config + .types + .keys() + .map(|s| s.as_str()) + .collect::>(); + + let expected = hashset![ + "Query", + "Type1Input", + "Type1Output", + "Type2Output", + "Type2Input", + "Type3", + ]; + + assert_eq!(actual, expected); + } + #[tokio::test] + async fn test_resolve_ambiguous_news_types() -> anyhow::Result<()> { + let gen = crate::generator::Generator::init(crate::runtime::test::init(None)); + let news = tailcall_fixtures::protobuf::NEWS; + let config_module = gen.read_all(Source::PROTO, &[news], "Query").await?; + let actual = config_module + .config + .types + .keys() + .map(|s| s.as_str()) + .collect::>(); + + let expected = hashset![ + "Query", + "OUT_NEWS_NEWS", + "IN_NEWS_NEWS", + "NEWS_MULTIPLE_NEWS_ID", + "NEWS_NEWS_ID", + "NEWS_NEWS_LIST", + ]; + assert_eq!(actual, expected); + Ok(()) + } +} diff --git a/src/config/from_document.rs b/src/config/from_document.rs index 176b22189c..ab87b20cbd 100644 --- a/src/config/from_document.rs +++ b/src/config/from_document.rs @@ -172,7 +172,7 @@ fn to_types( }) } fn to_scalar_type() -> config::Type { - config::Type { scalar: true, ..Default::default() } + config::Type { ..Default::default() } } fn to_union_types( type_definitions: &[&Positioned], diff --git a/src/generator/from_proto.rs b/src/generator/from_proto.rs index 603d149fdc..d86bcfa958 100644 --- a/src/generator/from_proto.rs +++ b/src/generator/from_proto.rs @@ -256,7 +256,8 @@ pub fn from_proto(descriptor_sets: &[FileDescriptorSet], query: &str) -> Config } } - ctx.config = ctx.config.remove_unused_types(); + let unused_types = ctx.config.unused_types(); + ctx.config = ctx.config.remove_types(unused_types); ctx.config } diff --git a/src/generator/generator.rs b/src/generator/generator.rs index ab0ee46314..71d037fb4c 100644 --- a/src/generator/generator.rs +++ b/src/generator/generator.rs @@ -1,6 +1,6 @@ use anyhow::Result; -use crate::config::{Config, Link, LinkType}; +use crate::config::{Config, ConfigModule, Link, LinkType, Resolution}; use crate::generator::from_proto::from_proto; use crate::generator::source::Source; use crate::merge_right::MergeRight; @@ -23,7 +23,7 @@ impl Generator { input_source: Source, files: &[T], query: &str, - ) -> Result { + ) -> Result { let mut links = vec![]; let proto_metadata = self.proto_reader.read_all(files).await?; @@ -38,7 +38,12 @@ impl Generator { } config.links = links; - Ok(config) + Ok( + ConfigModule::from(config).resolve_ambiguous_types(|v| Resolution { + input: format!("IN_{}", v), + output: format!("OUT_{}", v), + }), + ) } } diff --git a/src/scalar/mod.rs b/src/scalar/mod.rs index fadaeb2b89..3c7f23f137 100644 --- a/src/scalar/mod.rs +++ b/src/scalar/mod.rs @@ -46,7 +46,9 @@ lazy_static! { }; } -pub fn is_scalar(type_name: &str) -> bool { +/// +/// Check if the type is a predefined scalar +pub fn is_predefined_scalar(type_name: &str) -> bool { SCALAR_TYPES.contains(type_name) } diff --git a/tailcall-query-plan/src/plan.rs b/tailcall-query-plan/src/plan.rs index ca26de7033..c48537a9d2 100644 --- a/tailcall-query-plan/src/plan.rs +++ b/tailcall-query-plan/src/plan.rs @@ -6,7 +6,7 @@ use async_graphql::{Name, Value}; use indenter::indented; use indexmap::IndexMap; use tailcall::blueprint::{Definition, Type}; -use tailcall::scalar::is_scalar; +use tailcall::scalar::is_predefined_scalar; use super::execution::executor::{ExecutionResult, ResolvedEntry}; use super::resolver::{FieldPlan, FieldPlanSelection, Id}; @@ -94,7 +94,7 @@ impl FieldTree { None }; - let plan = if is_scalar(type_name) { + let plan = if is_predefined_scalar(type_name) { Self { field_plan_id: id, entry: FieldTreeEntry::Scalar } } else { Self::from_operation( From 51373fea364fdde58ac7972ac3c8bff21e9ad451 Mon Sep 17 00:00:00 2001 From: Ranjit Mahadik <43403528+ranjitmahadik@users.noreply.github.com> Date: Sat, 4 May 2024 16:39:55 +0530 Subject: [PATCH 2/2] fix: updated the contribution guidelines link. (#1855) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e11bd0c634..6e2f24a8ba 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ Head out to [docs] to learn about other powerful tailcall features. Your contributions are invaluable! Kindly go through our [contribution guidelines] if you are a first time contributor. -[contribution guidelines]: https://tailcall.run/docs/contributors/guidelines/ +[contribution guidelines]: https://tailcall.run/docs/contributors/ ### Support Us