diff --git a/generated/.tailcallrc.schema.json b/generated/.tailcallrc.schema.json index aa51b9a7c6..833152e491 100644 --- a/generated/.tailcallrc.schema.json +++ b/generated/.tailcallrc.schema.json @@ -411,6 +411,9 @@ } ] }, + "default_value": { + "description": "Stores the default value for the field" + }, "doc": { "description": "Publicly visible documentation for the field.", "type": [ diff --git a/src/core/blueprint/blueprint.rs b/src/core/blueprint/blueprint.rs index 1af7219041..a0776515d7 100644 --- a/src/core/blueprint/blueprint.rs +++ b/src/core/blueprint/blueprint.rs @@ -143,6 +143,7 @@ pub struct FieldDefinition { pub resolver: Option, pub directives: Vec, pub description: Option, + pub default_value: Option, } impl FieldDefinition { diff --git a/src/core/blueprint/definitions.rs b/src/core/blueprint/definitions.rs index 50a0897f04..68ed88356f 100644 --- a/src/core/blueprint/definitions.rs +++ b/src/core/blueprint/definitions.rs @@ -41,7 +41,7 @@ pub fn to_input_object_type_definition( .map(|field| InputFieldDefinition { name: field.name.clone(), description: field.description.clone(), - default_value: None, + default_value: field.default_value.clone(), of_type: field.of_type.clone(), }) .collect(), @@ -279,6 +279,7 @@ fn update_args<'a>( of_type: to_type(*field, None), directives: Vec::new(), resolver: None, + default_value: field.default_value.clone(), }) }, ) diff --git a/src/core/blueprint/into_schema.rs b/src/core/blueprint/into_schema.rs index c53b848de0..45eab366d6 100644 --- a/src/core/blueprint/into_schema.rs +++ b/src/core/blueprint/into_schema.rs @@ -34,6 +34,29 @@ fn to_type_ref(type_of: &Type) -> dynamic::TypeRef { } } +/// We set the default value for an `InputValue` by reading it from the +/// blueprint and assigning it to the provided `InputValue` during the +/// generation of the `async_graphql::Schema`. The `InputValue` represents the +/// structure of arguments and their types that can be passed to a field. In +/// other GraphQL implementations, this is commonly referred to as +/// `InputValueDefinition`. +fn set_default_value( + input_value: dynamic::InputValue, + value: Option, +) -> dynamic::InputValue { + if let Some(value) = value { + match ConstValue::from_json(value) { + Ok(const_value) => input_value.default_value(const_value), + Err(err) => { + tracing::warn!("conversion from serde_json::Value to ConstValue failed for default_value with error {err:?}"); + input_value + } + } + } else { + input_value + } +} + fn to_type(def: &Definition) -> dynamic::Type { match def { Definition::Object(def) => { @@ -46,6 +69,11 @@ fn to_type(def: &Definition) -> dynamic::Type { field_name, type_ref.clone(), move |ctx| { + // region: HOT CODE + // -------------------------------------------------- + // HOT CODE STARTS HERE + // -------------------------------------------------- + let req_ctx = ctx.ctx.data::>().unwrap(); let field_name = &field.name; @@ -82,15 +110,20 @@ fn to_type(def: &Definition) -> dynamic::Type { ) } } + + // -------------------------------------------------- + // HOT CODE ENDS HERE + // -------------------------------------------------- + // endregion: hot_code }, ); if let Some(description) = &field.description { dyn_schema_field = dyn_schema_field.description(description); } for arg in field.args.iter() { - dyn_schema_field = dyn_schema_field.argument(dynamic::InputValue::new( - arg.name.clone(), - to_type_ref(&arg.of_type), + dyn_schema_field = dyn_schema_field.argument(set_default_value( + dynamic::InputValue::new(arg.name.clone(), to_type_ref(&arg.of_type)), + arg.default_value.clone(), )); } object = object.field(dyn_schema_field); @@ -123,6 +156,7 @@ fn to_type(def: &Definition) -> dynamic::Type { if let Some(description) = &field.description { input_field = input_field.description(description); } + let input_field = set_default_value(input_field, field.default_value.clone()); input_object = input_object.field(input_field); } if let Some(description) = &def.description { diff --git a/src/core/config/config.rs b/src/core/config/config.rs index c169ddc723..12377abb94 100644 --- a/src/core/config/config.rs +++ b/src/core/config/config.rs @@ -262,10 +262,16 @@ pub struct Field { /// /// Sets the cache configuration for a field pub cache: Option, + /// /// Marks field as protected by auth provider #[serde(default)] pub protected: Option, + + /// + /// Stores the default value for the field + #[serde(default, skip_serializing_if = "is_default")] + pub default_value: Option, } // It's a terminal implementation of MergeRight diff --git a/src/core/config/from_document.rs b/src/core/config/from_document.rs index 01896659d2..9c2bfe18aa 100644 --- a/src/core/config/from_document.rs +++ b/src/core/config/from_document.rs @@ -7,6 +7,7 @@ use async_graphql::parser::types::{ }; use async_graphql::parser::Positioned; use async_graphql::Name; +use async_graphql_value::ConstValue; use super::telemetry::Telemetry; use super::{Tag, JS}; @@ -15,7 +16,7 @@ use crate::core::config::{ Server, Union, Upstream, }; use crate::core::directive::DirectiveCodec; -use crate::core::valid::{Valid, Validator}; +use crate::core::valid::{Valid, ValidationError, Validator}; const DEFAULT_SCHEMA_DEFINITION: &SchemaDefinition = &SchemaDefinition { extend: false, @@ -291,23 +292,36 @@ fn to_input_object_fields( to_fields_inner(input_object_fields, to_input_object_field) } fn to_field(field_definition: &FieldDefinition) -> Valid { - to_common_field(field_definition, to_args(field_definition)) + to_common_field(field_definition, to_args(field_definition), None) } fn to_input_object_field(field_definition: &InputValueDefinition) -> Valid { - to_common_field(field_definition, BTreeMap::new()) + to_common_field( + field_definition, + BTreeMap::new(), + field_definition + .default_value + .as_ref() + .map(|f| f.node.clone()), + ) } fn to_common_field( field: &F, args: BTreeMap, + default_value: Option, ) -> Valid where - F: Fieldlike, + F: FieldLike, { let type_of = field.type_of(); let base = &type_of.base; let nullable = &type_of.nullable; let description = field.description(); let directives = field.directives(); + let default_value = default_value + .map(ConstValue::into_json) + .transpose() + .map_err(|err| ValidationError::new(err.to_string())) + .into(); let type_of = to_type_of(type_of); let list = matches!(&base, BaseType::List(_)); @@ -322,8 +336,9 @@ where .fuse(JS::from_directives(directives.iter())) .fuse(Call::from_directives(directives.iter())) .fuse(Protected::from_directives(directives.iter())) + .fuse(default_value) .map( - |(http, graphql, cache, grpc, omit, modify, script, call, protected)| { + |(http, graphql, cache, grpc, omit, modify, script, call, protected, default_value)| { let const_field = to_const_field(directives); config::Field { type_of, @@ -342,6 +357,7 @@ where cache, call, protected, + default_value, } }, ) @@ -445,12 +461,12 @@ impl HasName for InputValueDefinition { } } -trait Fieldlike { +trait FieldLike { fn type_of(&self) -> &Type; fn description(&self) -> &Option>; fn directives(&self) -> &[Positioned]; } -impl Fieldlike for FieldDefinition { +impl FieldLike for FieldDefinition { fn type_of(&self) -> &Type { &self.ty.node } @@ -461,7 +477,7 @@ impl Fieldlike for FieldDefinition { &self.directives } } -impl Fieldlike for InputValueDefinition { +impl FieldLike for InputValueDefinition { fn type_of(&self) -> &Type { &self.ty.node } diff --git a/src/core/config/into_document.rs b/src/core/config/into_document.rs index c9f3223007..34f8a5f46d 100644 --- a/src/core/config/into_document.rs +++ b/src/core/config/into_document.rs @@ -9,6 +9,11 @@ use crate::core::directive::DirectiveCodec; fn pos(a: A) -> Positioned { Positioned::new(a, Pos::default()) } + +fn transform_default_value(value: Option) -> Option { + value.map(ConstValue::from_json).and_then(Result::ok) +} + fn config_document(config: &ConfigModule) -> ServiceDocument { let mut definitions = Vec::new(); let mut directives = vec![ @@ -112,7 +117,8 @@ fn config_document(config: &ConfigModule) -> ServiceDocument { name: pos(Name::new(name.clone())), ty: pos(Type { nullable: !field.required, base: base_type }), - default_value: None, + default_value: transform_default_value(field.default_value.clone()) + .map(pos), directives, }) }) @@ -167,10 +173,10 @@ fn config_document(config: &ConfigModule) -> ServiceDocument { name: pos(Name::new(name.clone())), ty: pos(Type { nullable: !arg.required, base: base_type }), - default_value: arg - .default_value - .clone() - .map(|v| pos(ConstValue::String(v.to_string()))), + default_value: transform_default_value( + arg.default_value.clone(), + ) + .map(pos), directives: Vec::new(), }) }) diff --git a/src/core/document.rs b/src/core/document.rs index 3f9cfb85fd..c822697e27 100644 --- a/src/core/document.rs +++ b/src/core/document.rs @@ -197,7 +197,13 @@ fn print_field(field: &async_graphql::parser::types::FieldDefinition) -> String .iter() .map(|arg| { let nullable = if arg.node.ty.node.nullable { "" } else { "!" }; - format!("{}: {}{}", arg.node.name, arg.node.ty.node.base, nullable) + format!( + "{}: {}{}{}", + arg.node.name, + arg.node.ty.node.base, + nullable, + print_default_value(arg.node.default_value.as_ref()) + ) }) .collect::>() .join(", "); @@ -215,14 +221,25 @@ fn print_field(field: &async_graphql::parser::types::FieldDefinition) -> String doc + node.trim_end() } +fn print_default_value(value: Option<&Positioned>) -> String { + value + .as_ref() + .map(|val| format!(" = {val}")) + .unwrap_or_default() +} + fn print_input_value(field: &async_graphql::parser::types::InputValueDefinition) -> String { let directives_str = print_directives(&field.directives); let doc = field.description.as_ref().map_or(String::new(), |d| { format!(r#" """{} {}{} """{}"#, "\n", d.node, "\n", "\n") }); format!( - "{} {}: {}{}", - doc, field.name.node, field.ty.node, directives_str + "{} {}: {}{}{}", + doc, + field.name.node, + field.ty.node, + directives_str, + print_default_value(field.default_value.as_ref()) ) } fn print_directive(directive: &DirectiveDefinition) -> String { diff --git a/tests/core/snapshots/default-value-arg.md_0.snap b/tests/core/snapshots/default-value-arg.md_0.snap new file mode 100644 index 0000000000..c3d9995606 --- /dev/null +++ b/tests/core/snapshots/default-value-arg.md_0.snap @@ -0,0 +1,15 @@ +--- +source: tests/core/spec.rs +expression: response +--- +{ + "status": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "data": { + "bar": 1 + } + } +} diff --git a/tests/core/snapshots/default-value-arg.md_1.snap b/tests/core/snapshots/default-value-arg.md_1.snap new file mode 100644 index 0000000000..7fb28bfef9 --- /dev/null +++ b/tests/core/snapshots/default-value-arg.md_1.snap @@ -0,0 +1,15 @@ +--- +source: tests/core/spec.rs +expression: response +--- +{ + "status": 200, + "headers": { + "content-type": "application/json" + }, + "body": { + "data": { + "bar": 2 + } + } +} diff --git a/tests/core/snapshots/default-value-arg.md_client.snap b/tests/core/snapshots/default-value-arg.md_client.snap new file mode 100644 index 0000000000..913b7b4b0f --- /dev/null +++ b/tests/core/snapshots/default-value-arg.md_client.snap @@ -0,0 +1,49 @@ +--- +source: tests/core/spec.rs +expression: formatted +--- +scalar Bytes + +scalar Date + +scalar Email + +scalar Empty + +input Input { + id: Int! +} + +scalar Int128 + +scalar Int16 + +scalar Int32 + +scalar Int64 + +scalar Int8 + +scalar JSON + +scalar PhoneNumber + +type Query { + bar(input: Input = {id: 1}): Int +} + +scalar UInt128 + +scalar UInt16 + +scalar UInt32 + +scalar UInt64 + +scalar UInt8 + +scalar Url + +schema { + query: Query +} diff --git a/tests/core/snapshots/default-value-arg.md_merged.snap b/tests/core/snapshots/default-value-arg.md_merged.snap new file mode 100644 index 0000000000..027b09d6f7 --- /dev/null +++ b/tests/core/snapshots/default-value-arg.md_merged.snap @@ -0,0 +1,15 @@ +--- +source: tests/core/spec.rs +expression: formatter +--- +schema @server @upstream(baseURL: "http://abc.com") { + query: Query +} + +input Input { + id: Int! +} + +type Query { + bar(input: Input = {id: 1}): Int @http(path: "/bar/{{.args.input.id}}") +} diff --git a/tests/core/snapshots/default-value-config.md_client.snap b/tests/core/snapshots/default-value-config.md_client.snap new file mode 100644 index 0000000000..2db365a1f7 --- /dev/null +++ b/tests/core/snapshots/default-value-config.md_client.snap @@ -0,0 +1,50 @@ +--- +source: tests/core/spec.rs +expression: formatted +--- +scalar Bytes + +scalar Date + +scalar Email + +scalar Empty + +input Input { + id: Int = 1 +} + +scalar Int128 + +scalar Int16 + +scalar Int32 + +scalar Int64 + +scalar Int8 + +scalar JSON + +scalar PhoneNumber + +type Query { + bar(input: Input = {id: 3}): Int + foo(input: Input!): Int +} + +scalar UInt128 + +scalar UInt16 + +scalar UInt32 + +scalar UInt64 + +scalar UInt8 + +scalar Url + +schema { + query: Query +} diff --git a/tests/core/snapshots/default-value-config.md_merged.snap b/tests/core/snapshots/default-value-config.md_merged.snap new file mode 100644 index 0000000000..0016893a7c --- /dev/null +++ b/tests/core/snapshots/default-value-config.md_merged.snap @@ -0,0 +1,16 @@ +--- +source: tests/core/spec.rs +expression: formatter +--- +schema @server @upstream(baseURL: "http://abc.com") { + query: Query +} + +input Input { + id: Int = 1 +} + +type Query { + bar(input: Input = {id: 3}): Int @http(path: "/foo/{{.args.input.id}}") + foo(input: Input!): Int @http(path: "/foo/{{.args.input.id}}") +} diff --git a/tests/execution/default-value-arg.md b/tests/execution/default-value-arg.md new file mode 100644 index 0000000000..a1735e2929 --- /dev/null +++ b/tests/execution/default-value-arg.md @@ -0,0 +1,48 @@ +# default value for input Type + +```graphql @config +schema @upstream(baseURL: "http://abc.com") { + query: Query +} + +type Query { + bar(input: Input = {id: 1}): Int @http(path: "/bar/{{.args.input.id}}") +} + +input Input { + id: Int! +} +``` + +```yml @mock +- request: + method: GET + url: http://abc.com/bar/1 + response: + status: 200 + body: 1 + +- request: + method: GET + url: http://abc.com/bar/2 + response: + status: 200 + body: 2 +``` + +```yml @test +- method: POST + url: http://localhost:8080/graphql + body: + query: > + query { + bar + } +- method: POST + url: http://localhost:8080/graphql + body: + query: > + query { + bar(input: {id:2}) + } +``` diff --git a/tests/execution/default-value-config.md b/tests/execution/default-value-config.md new file mode 100644 index 0000000000..865a2a4ff7 --- /dev/null +++ b/tests/execution/default-value-config.md @@ -0,0 +1,16 @@ +# default value for input Type + +```graphql @config +schema @upstream(baseURL: "http://abc.com") { + query: Query +} + +type Query { + foo(input: Input!): Int @http(path: "/foo/{{.args.input.id}}") + bar(input: Input = {id: 3}): Int @http(path: "/foo/{{.args.input.id}}") +} + +input Input { + id: Int = 1 +} +```