From 3a35b99d32f0244a725723dfaf89a49034ec018c Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Tue, 2 Jan 2024 23:41:54 -0300 Subject: [PATCH 01/92] feat(call operator): add basic types --- src/blueprint/from_config/definitions.rs | 2 ++ src/blueprint/from_config/operators/mod.rs | 2 ++ src/config/config.rs | 11 +++++++++++ src/config/from_document.rs | 6 ++++-- 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/blueprint/from_config/definitions.rs b/src/blueprint/from_config/definitions.rs index c56bce953a..3cac51bda1 100644 --- a/src/blueprint/from_config/definitions.rs +++ b/src/blueprint/from_config/definitions.rs @@ -345,6 +345,7 @@ fn to_fields(object_name: &str, type_of: &config::Type, config: &Config) -> Vali .and(update_const_field().trace(config::Const::trace_name().as_str())) .and(update_graphql(&operation_type).trace(config::GraphQL::trace_name().as_str())) .and(update_modify().trace(config::Modify::trace_name().as_str())) + .and(update_call(&operation_type).trace(config::Call::trace_name().as_str())) .and(update_nested_resolvers()) .try_fold(&(config, field, type_of, name), FieldDefinition::default()) }; @@ -383,6 +384,7 @@ fn to_fields(object_name: &str, type_of: &config::Type, config: &Config) -> Vali const_field: source_field.const_field.clone(), graphql: source_field.graphql.clone(), cache: source_field.cache.clone(), + call: source_field.call.clone(), }; to_field(&add_field.name, &new_field) .and_then(|field_definition| { diff --git a/src/blueprint/from_config/operators/mod.rs b/src/blueprint/from_config/operators/mod.rs index b831c2a3ad..add9130dde 100644 --- a/src/blueprint/from_config/operators/mod.rs +++ b/src/blueprint/from_config/operators/mod.rs @@ -1,3 +1,4 @@ +mod call; mod const_field; mod graphql; mod grpc; @@ -5,6 +6,7 @@ mod http; mod modify; mod unsafe_field; +pub use call::*; pub use const_field::*; pub use graphql::*; pub use grpc::*; diff --git a/src/config/config.rs b/src/config/config.rs index 88158f2db0..3440c6af77 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -251,6 +251,8 @@ pub struct Field { #[serde(default, skip_serializing_if = "is_default")] pub http: Option, #[serde(default, skip_serializing_if = "is_default")] + pub call: Option, + #[serde(default, skip_serializing_if = "is_default")] pub grpc: Option, #[serde(rename = "unsafe", default, skip_serializing_if = "is_default")] pub unsafe_operation: Option, @@ -387,6 +389,15 @@ pub struct Http { pub group_by: Vec, } +#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq)] +pub struct Call { + #[serde(default, skip_serializing_if = "is_default")] + pub query: Option, + #[serde(default, skip_serializing_if = "is_default")] + pub mutation: Option, + pub args: KeyValues, +} + #[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct Grpc { diff --git a/src/config/from_document.rs b/src/config/from_document.rs index 4f0853a270..cce81fe771 100644 --- a/src/config/from_document.rs +++ b/src/config/from_document.rs @@ -8,7 +8,7 @@ use async_graphql::parser::Positioned; use async_graphql::Name; use super::Cache; -use crate::config::{self, Config, GraphQL, Grpc, RootSchema, Server, Union, Upstream}; +use crate::config::{self, Call, Config, GraphQL, Grpc, RootSchema, Server, Union, Upstream}; use crate::directive::DirectiveCodec; use crate::valid::Valid; @@ -222,7 +222,8 @@ where .zip(GraphQL::from_directives(directives.iter())) .zip(Cache::from_directives(directives.iter())) .zip(Grpc::from_directives(directives.iter())) - .map(|(((http, graphql), cache), grpc)| { + .zip(Call::from_directives(directives.iter())) + .map(|((((http, graphql), cache), grpc), call)| { let unsafe_operation = to_unsafe_operation(directives); let const_field = to_const_field(directives); config::Field { @@ -239,6 +240,7 @@ where const_field, graphql, cache: cache.or(parent_cache), + call, } }) } From 56b731e1dfffeba322cb1f5700d79e569dffc354 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Tue, 2 Jan 2024 23:43:46 -0300 Subject: [PATCH 02/92] feat: implement most of call operator --- src/blueprint/from_config/operators/call.rs | 59 +++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 src/blueprint/from_config/operators/call.rs diff --git a/src/blueprint/from_config/operators/call.rs b/src/blueprint/from_config/operators/call.rs new file mode 100644 index 0000000000..c3126ec492 --- /dev/null +++ b/src/blueprint/from_config/operators/call.rs @@ -0,0 +1,59 @@ +use crate::blueprint::*; +use crate::config; +use crate::config::{Config, Field, GraphQLOperationType}; +use crate::directive::DirectiveCodec; +use crate::try_fold::TryFold; +use crate::valid::Valid; + +pub fn update_call( + operation_type: &GraphQLOperationType, +) -> TryFold<'_, (&Config, &Field, &config::Type, &str), FieldDefinition, String> { + TryFold::<(&Config, &Field, &config::Type, &str), FieldDefinition, String>::new( + move |(config, field, type_of, name), b_field| { + let Some(call) = &field.call else { + return Valid::succeed(b_field); + }; + + let type_and_field = if let Some(mutation) = &call.mutation { + Valid::succeed(("Mutation", mutation.as_str())) + } else if let Some(query) = &call.query { + Valid::succeed(("Query", query.as_str())) + } else { + Valid::fail("call must have one of mutation or query".to_string()) + }; + + type_and_field + .and_then(|(type_name, field_name)| { + Valid::from_option( + config.find_type(type_name), + format!("{} type not found on config", type_name), + ) + .zip(Valid::succeed(field_name)) + }) + .and_then(|(query_type, field_name)| { + Valid::from_option( + query_type.fields.get(field_name), + format!("{} field not found", field_name), + ) + .and_then(|field| { + if !field.has_resolver() { + return Valid::fail(format!("{} field has no resolver", field_name)); + } + + Valid::succeed(field) + }) + }) + .and_then(|field| { + // TO-DO: parse call.args into a way that `update_http`, `update_grpc` and `update_graphql` can use + + TryFold::<(&Config, &Field, &config::Type, &str), FieldDefinition, String>::new(|_, b_field| { + Valid::succeed(b_field) + }) + .and(update_http().trace(config::Http::trace_name().as_str())) + .and(update_grpc(operation_type).trace(config::Grpc::trace_name().as_str())) + .and(update_graphql(operation_type).trace(config::GraphQL::trace_name().as_str())) + .try_fold(&(config, field, type_of, name), b_field) + }) + }, + ) +} From b2c54a3b269661e8ec7828551da5f912d48c38a9 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Tue, 2 Jan 2024 23:44:02 -0300 Subject: [PATCH 03/92] chore(jsonplaceholder): use `@call` directive --- examples/jsonplaceholder.graphql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/jsonplaceholder.graphql b/examples/jsonplaceholder.graphql index fa92976fd5..d6400d1b2f 100644 --- a/examples/jsonplaceholder.graphql +++ b/examples/jsonplaceholder.graphql @@ -23,5 +23,5 @@ type Post { userId: Int! title: String! body: String! - user: User @http(path: "/users/{{value.userId}}") + user: User @call(query: "user", args: [{key: "id", value: "{{value.userId}}"}]) } From 9ed145ea8e2b768945c78de68de5dd7cd580b3e7 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 3 Jan 2024 01:42:35 -0300 Subject: [PATCH 04/92] feat(call): override http path args --- src/blueprint/from_config/operators/call.rs | 29 ++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/blueprint/from_config/operators/call.rs b/src/blueprint/from_config/operators/call.rs index c3126ec492..6b21f44dac 100644 --- a/src/blueprint/from_config/operators/call.rs +++ b/src/blueprint/from_config/operators/call.rs @@ -14,6 +14,13 @@ pub fn update_call( return Valid::succeed(b_field); }; + if validate_field_has_resolver(name, field, &config.types).is_succeed() { + return Valid::fail(format!( + "@call directive is not allowed on field {} because it already has a resolver", + name + )); + } + let type_and_field = if let Some(mutation) = &call.mutation { Valid::succeed(("Mutation", mutation.as_str())) } else if let Some(query) = &call.query { @@ -43,16 +50,32 @@ pub fn update_call( Valid::succeed(field) }) }) - .and_then(|field| { - // TO-DO: parse call.args into a way that `update_http`, `update_grpc` and `update_graphql` can use + .zip(Valid::succeed(call.args.iter())) + .and_then(|(field, args)| { + args.fold(Valid::succeed(field.clone()), |field, (key, value)| { + field.and_then(|field| { + let value = value.replace("{{", "").replace("}}", ""); + + if let Some(http) = field.clone().http.as_mut() { + http.path = http.path.replace(format!("args.{}", key).as_str(), value.as_str()); + + let field = Field { http: Some(http.clone()), ..field.clone() }; + return Valid::succeed(field); + } + + Valid::succeed(field) + }) + }) + }) + .and_then(|_field| { TryFold::<(&Config, &Field, &config::Type, &str), FieldDefinition, String>::new(|_, b_field| { Valid::succeed(b_field) }) .and(update_http().trace(config::Http::trace_name().as_str())) .and(update_grpc(operation_type).trace(config::Grpc::trace_name().as_str())) .and(update_graphql(operation_type).trace(config::GraphQL::trace_name().as_str())) - .try_fold(&(config, field, type_of, name), b_field) + .try_fold(&(config, &_field, type_of, name), b_field) }) }, ) From 6eca588f25dfb5ef8ebc55d1d50d6579467affa6 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 3 Jan 2024 01:54:29 -0300 Subject: [PATCH 05/92] test(call): add test for query operation --- tests/http/call-operator.yml | 30 ++++++++++++++++++++++++++++++ tests/http/config/call.graphql | 27 +++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 tests/http/call-operator.yml create mode 100644 tests/http/config/call.graphql diff --git a/tests/http/call-operator.yml b/tests/http/call-operator.yml new file mode 100644 index 0000000000..026a26e395 --- /dev/null +++ b/tests/http/call-operator.yml @@ -0,0 +1,30 @@ +--- +name: With call operator +config: !file tests/http/config/call.graphql + +mock: + - request: + url: http://jsonplaceholder.typicode.com/users/1 + response: + body: + id: 1 + name: foo + - request: + url: http://jsonplaceholder.typicode.com/posts + response: + body: + - id: 1 + userId: 1 + +assert: + - request: + method: POST + url: http://localhost:8080/graphql + body: + query: "query { posts { user { name } } }" + response: + body: + data: + posts: + - user: + name: foo diff --git a/tests/http/config/call.graphql b/tests/http/config/call.graphql new file mode 100644 index 0000000000..d6400d1b2f --- /dev/null +++ b/tests/http/config/call.graphql @@ -0,0 +1,27 @@ +schema + @server(port: 8000, graphiql: true, hostname: "0.0.0.0") + @upstream(baseURL: "http://jsonplaceholder.typicode.com", httpCache: true) { + query: Query +} + +type Query { + posts: [Post] @http(path: "/posts") + user(id: Int!): User @http(path: "/users/{{args.id}}") +} + +type User { + id: Int! + name: String! + username: String! + email: String! + phone: String + website: String +} + +type Post { + id: Int! + userId: Int! + title: String! + body: String! + user: User @call(query: "user", args: [{key: "id", value: "{{value.userId}}"}]) +} From 3e6f6f7541924a9c9bd41a371c8cff0ae20a6858 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 3 Jan 2024 02:08:44 -0300 Subject: [PATCH 06/92] test(call): add test cases for errors --- .../graphql/errors/test-call-operator.graphql | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 tests/graphql/errors/test-call-operator.graphql diff --git a/tests/graphql/errors/test-call-operator.graphql b/tests/graphql/errors/test-call-operator.graphql new file mode 100644 index 0000000000..110df70374 --- /dev/null +++ b/tests/graphql/errors/test-call-operator.graphql @@ -0,0 +1,25 @@ +#> server-sdl +schema @server @upstream(baseURL: "http://localhost:3000") { + query: Query +} + +type Query { + posts: [Post] @http(path: "/posts") + userWithoutResolver(id: Int!): User + user(id: Int!): User @http(path: "/users/{{args.id}}") +} + +type User { + id: Int! +} + +type Post { + userId: Int! + withoutResolver: User @call(query: "userWithoutResolver", args: [{key: "id", value: "{{value.userId}}"}]) + multipleResolvers: User @http(path: "/users/{{value.userId}}") @call(query: "user", args: [{key: "id", value: "{{value.userId}}"}]) +} + +#> client-sdl +type Failure @error(message: "No resolver has been found in the schema", trace: ["Query", "userWithoutResolver"]) +type Failure @error(message: "@call directive is not allowed on field multipleResolvers because it already has a resolver", trace: ["Post", "multipleResolvers", "@call"]) +type Failure @error(message: "userWithoutResolver field has no resolver", trace: ["Post", "withoutResolver", "@call"]) From 9df332b6a5762a31167387efcc87375bbde77f06 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 3 Jan 2024 02:13:16 -0300 Subject: [PATCH 07/92] test(call): add test for lack of operator --- tests/graphql/errors/test-call-operator.graphql | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/graphql/errors/test-call-operator.graphql b/tests/graphql/errors/test-call-operator.graphql index 110df70374..b8de3c6cb1 100644 --- a/tests/graphql/errors/test-call-operator.graphql +++ b/tests/graphql/errors/test-call-operator.graphql @@ -17,9 +17,11 @@ type Post { userId: Int! withoutResolver: User @call(query: "userWithoutResolver", args: [{key: "id", value: "{{value.userId}}"}]) multipleResolvers: User @http(path: "/users/{{value.userId}}") @call(query: "user", args: [{key: "id", value: "{{value.userId}}"}]) + withoutOperator: User @call(args: [{key: "id", value: "{{value.userId}}"}]) } #> client-sdl type Failure @error(message: "No resolver has been found in the schema", trace: ["Query", "userWithoutResolver"]) type Failure @error(message: "@call directive is not allowed on field multipleResolvers because it already has a resolver", trace: ["Post", "multipleResolvers", "@call"]) +type Failure @error(message: "call must have one of mutation or query", trace: ["Post", "withoutOperator", "@call",]) type Failure @error(message: "userWithoutResolver field has no resolver", trace: ["Post", "withoutResolver", "@call"]) From 14fb87888d56cfee4a5b50da416f2e72ec13885c Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 3 Jan 2024 02:15:41 -0300 Subject: [PATCH 08/92] style: run `./lint.sh --mode=fix` --- tests/graphql/errors/test-call-operator.graphql | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/graphql/errors/test-call-operator.graphql b/tests/graphql/errors/test-call-operator.graphql index b8de3c6cb1..7b87818235 100644 --- a/tests/graphql/errors/test-call-operator.graphql +++ b/tests/graphql/errors/test-call-operator.graphql @@ -16,12 +16,18 @@ type User { type Post { userId: Int! withoutResolver: User @call(query: "userWithoutResolver", args: [{key: "id", value: "{{value.userId}}"}]) - multipleResolvers: User @http(path: "/users/{{value.userId}}") @call(query: "user", args: [{key: "id", value: "{{value.userId}}"}]) + multipleResolvers: User + @http(path: "/users/{{value.userId}}") + @call(query: "user", args: [{key: "id", value: "{{value.userId}}"}]) withoutOperator: User @call(args: [{key: "id", value: "{{value.userId}}"}]) } #> client-sdl type Failure @error(message: "No resolver has been found in the schema", trace: ["Query", "userWithoutResolver"]) -type Failure @error(message: "@call directive is not allowed on field multipleResolvers because it already has a resolver", trace: ["Post", "multipleResolvers", "@call"]) -type Failure @error(message: "call must have one of mutation or query", trace: ["Post", "withoutOperator", "@call",]) +type Failure + @error( + message: "@call directive is not allowed on field multipleResolvers because it already has a resolver" + trace: ["Post", "multipleResolvers", "@call"] + ) +type Failure @error(message: "call must have one of mutation or query", trace: ["Post", "withoutOperator", "@call"]) type Failure @error(message: "userWithoutResolver field has no resolver", trace: ["Post", "withoutResolver", "@call"]) From 397e06a5def2673c35370baa7b62dd1f2ac15c2e Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 3 Jan 2024 13:07:43 -0300 Subject: [PATCH 09/92] refactor(call): remove mutation for lack of use case --- src/blueprint/from_config/operators/call.rs | 6 ++---- src/config/config.rs | 2 -- tests/graphql/errors/test-call-operator.graphql | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/blueprint/from_config/operators/call.rs b/src/blueprint/from_config/operators/call.rs index 6b21f44dac..339dab7722 100644 --- a/src/blueprint/from_config/operators/call.rs +++ b/src/blueprint/from_config/operators/call.rs @@ -21,12 +21,10 @@ pub fn update_call( )); } - let type_and_field = if let Some(mutation) = &call.mutation { - Valid::succeed(("Mutation", mutation.as_str())) - } else if let Some(query) = &call.query { + let type_and_field = if let Some(query) = &call.query { Valid::succeed(("Query", query.as_str())) } else { - Valid::fail("call must have one of mutation or query".to_string()) + Valid::fail("call must have query".to_string()) }; type_and_field diff --git a/src/config/config.rs b/src/config/config.rs index 3440c6af77..e51509f92e 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -393,8 +393,6 @@ pub struct Http { pub struct Call { #[serde(default, skip_serializing_if = "is_default")] pub query: Option, - #[serde(default, skip_serializing_if = "is_default")] - pub mutation: Option, pub args: KeyValues, } diff --git a/tests/graphql/errors/test-call-operator.graphql b/tests/graphql/errors/test-call-operator.graphql index 7b87818235..0123a371da 100644 --- a/tests/graphql/errors/test-call-operator.graphql +++ b/tests/graphql/errors/test-call-operator.graphql @@ -29,5 +29,5 @@ type Failure message: "@call directive is not allowed on field multipleResolvers because it already has a resolver" trace: ["Post", "multipleResolvers", "@call"] ) -type Failure @error(message: "call must have one of mutation or query", trace: ["Post", "withoutOperator", "@call"]) +type Failure @error(message: "call must have query", trace: ["Post", "withoutOperator", "@call"]) type Failure @error(message: "userWithoutResolver field has no resolver", trace: ["Post", "withoutResolver", "@call"]) From 0e5bed68ee47741184a0500ff6b5767caa206630 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 3 Jan 2024 13:17:46 -0300 Subject: [PATCH 10/92] fix: fail on lack of http resolver --- src/blueprint/from_config/operators/call.rs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/blueprint/from_config/operators/call.rs b/src/blueprint/from_config/operators/call.rs index 339dab7722..63a0d1c908 100644 --- a/src/blueprint/from_config/operators/call.rs +++ b/src/blueprint/from_config/operators/call.rs @@ -41,15 +41,14 @@ pub fn update_call( format!("{} field not found", field_name), ) .and_then(|field| { - if !field.has_resolver() { - return Valid::fail(format!("{} field has no resolver", field_name)); + if field.has_resolver() { + Valid::succeed((field, field_name, call.args.iter())) + } else { + Valid::fail(format!("{} field has no resolver", field_name)) } - - Valid::succeed(field) }) }) - .zip(Valid::succeed(call.args.iter())) - .and_then(|(field, args)| { + .and_then(|(field, field_name, args)| { args.fold(Valid::succeed(field.clone()), |field, (key, value)| { field.and_then(|field| { let value = value.replace("{{", "").replace("}}", ""); @@ -59,10 +58,10 @@ pub fn update_call( let field = Field { http: Some(http.clone()), ..field.clone() }; - return Valid::succeed(field); + Valid::succeed(field) + } else { + Valid::fail(format!("{} field does not have an http resolver", field_name)) } - - Valid::succeed(field) }) }) }) From e46de03ed5d0b69c71149e251fbac8363be32f4b Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 3 Jan 2024 13:17:56 -0300 Subject: [PATCH 11/92] test: add test for invalid resolver --- tests/graphql/errors/test-call-operator.graphql | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/graphql/errors/test-call-operator.graphql b/tests/graphql/errors/test-call-operator.graphql index 0123a371da..03f020970c 100644 --- a/tests/graphql/errors/test-call-operator.graphql +++ b/tests/graphql/errors/test-call-operator.graphql @@ -7,6 +7,8 @@ type Query { posts: [Post] @http(path: "/posts") userWithoutResolver(id: Int!): User user(id: Int!): User @http(path: "/users/{{args.id}}") + userWithGraphQLResolver(id: Int): User + @graphQL(baseURL: "http://upstream/graphql", name: "user", args: [{key: "id", value: "{{args.id}}"}]) } type User { @@ -20,10 +22,12 @@ type Post { @http(path: "/users/{{value.userId}}") @call(query: "user", args: [{key: "id", value: "{{value.userId}}"}]) withoutOperator: User @call(args: [{key: "id", value: "{{value.userId}}"}]) + invalidResolver: User @call(query: "userWithGraphQLResolver", args: [{key: "id", value: "{{value.userId}}"}]) } #> client-sdl type Failure @error(message: "No resolver has been found in the schema", trace: ["Query", "userWithoutResolver"]) +type Failure @error(message: "userWithGraphQLResolver field does not have an http resolver", trace: ["Post", "invalidResolver", "@call"]) type Failure @error( message: "@call directive is not allowed on field multipleResolvers because it already has a resolver" From 63a3d5243bf8a7b51e820820ddbb3173d840ffae Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 3 Jan 2024 13:35:03 -0300 Subject: [PATCH 12/92] style: run `./lint.sh --mode=fix` --- tests/graphql/errors/test-call-operator.graphql | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/graphql/errors/test-call-operator.graphql b/tests/graphql/errors/test-call-operator.graphql index 03f020970c..b2c36c68de 100644 --- a/tests/graphql/errors/test-call-operator.graphql +++ b/tests/graphql/errors/test-call-operator.graphql @@ -27,7 +27,11 @@ type Post { #> client-sdl type Failure @error(message: "No resolver has been found in the schema", trace: ["Query", "userWithoutResolver"]) -type Failure @error(message: "userWithGraphQLResolver field does not have an http resolver", trace: ["Post", "invalidResolver", "@call"]) +type Failure + @error( + message: "userWithGraphQLResolver field does not have an http resolver" + trace: ["Post", "invalidResolver", "@call"] + ) type Failure @error( message: "@call directive is not allowed on field multipleResolvers because it already has a resolver" From debceeedb1ad701ffcd01b376eb9db084e32dad6 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 3 Jan 2024 13:35:44 -0300 Subject: [PATCH 13/92] docs(tailcallrc): add `@call` operator directive --- examples/.tailcallrc.graphql | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/examples/.tailcallrc.graphql b/examples/.tailcallrc.graphql index 153fbbca1f..fa10b2d1c2 100644 --- a/examples/.tailcallrc.graphql +++ b/examples/.tailcallrc.graphql @@ -300,6 +300,20 @@ directive @cache( maxAge: Int! ) on FIELD_DEFINITION +""" +The @call operator is used to reference an `@http` operator. +""" +directive @call( + """ + The name of the field that has the `@http` resolver to be called. + """ + query: String! + """ + The arguments to be replace the `@http` resolver args. + """ + args: [KeyValue] +) + enum Method { GET POST From 000af1c775829808424344be04f5547a325231c8 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 3 Jan 2024 16:54:35 -0300 Subject: [PATCH 14/92] refactor(call): use `Valid::from_option` instead of `if let Some` --- src/blueprint/from_config/operators/call.rs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/blueprint/from_config/operators/call.rs b/src/blueprint/from_config/operators/call.rs index 63a0d1c908..a79b0ae867 100644 --- a/src/blueprint/from_config/operators/call.rs +++ b/src/blueprint/from_config/operators/call.rs @@ -21,23 +21,17 @@ pub fn update_call( )); } - let type_and_field = if let Some(query) = &call.query { - Valid::succeed(("Query", query.as_str())) - } else { - Valid::fail("call must have query".to_string()) - }; - - type_and_field - .and_then(|(type_name, field_name)| { + Valid::from_option(call.query.clone(), "call must have query".to_string()) + .and_then(|field_name| { Valid::from_option( - config.find_type(type_name), - format!("{} type not found on config", type_name), + config.find_type("Query"), + format!("Query type not found on config"), ) .zip(Valid::succeed(field_name)) }) .and_then(|(query_type, field_name)| { Valid::from_option( - query_type.fields.get(field_name), + query_type.fields.get(&field_name), format!("{} field not found", field_name), ) .and_then(|field| { From b0d82b0d3f632f0292e940b4fd285eb6945490cf Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 3 Jan 2024 17:19:38 -0300 Subject: [PATCH 15/92] refactor(call): remove useless format --- src/blueprint/from_config/operators/call.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/blueprint/from_config/operators/call.rs b/src/blueprint/from_config/operators/call.rs index a79b0ae867..d5d0de9d5d 100644 --- a/src/blueprint/from_config/operators/call.rs +++ b/src/blueprint/from_config/operators/call.rs @@ -23,11 +23,8 @@ pub fn update_call( Valid::from_option(call.query.clone(), "call must have query".to_string()) .and_then(|field_name| { - Valid::from_option( - config.find_type("Query"), - format!("Query type not found on config"), - ) - .zip(Valid::succeed(field_name)) + Valid::from_option(config.find_type("Query"), "Query type not found on config".to_string()) + .zip(Valid::succeed(field_name)) }) .and_then(|(query_type, field_name)| { Valid::from_option( From 52dc898e91051333d533c9ff74ec860123afe777 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 3 Jan 2024 17:21:16 -0300 Subject: [PATCH 16/92] fix(tailcallrc): add `on FIELD_DEFINITION` for `@call` directive --- examples/.tailcallrc.graphql | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/.tailcallrc.graphql b/examples/.tailcallrc.graphql index fa10b2d1c2..9f3b3b1a69 100644 --- a/examples/.tailcallrc.graphql +++ b/examples/.tailcallrc.graphql @@ -312,7 +312,8 @@ directive @call( The arguments to be replace the `@http` resolver args. """ args: [KeyValue] -) +) on FIELD_DEFINITION + enum Method { GET From e27963e6fd91c36b43f6f9be1109b5b7b607d95f Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 3 Jan 2024 17:30:35 -0300 Subject: [PATCH 17/92] style: run `./lint.sh --mode=fix` --- examples/.tailcallrc.graphql | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/.tailcallrc.graphql b/examples/.tailcallrc.graphql index 9f3b3b1a69..448913bc8b 100644 --- a/examples/.tailcallrc.graphql +++ b/examples/.tailcallrc.graphql @@ -314,7 +314,6 @@ directive @call( args: [KeyValue] ) on FIELD_DEFINITION - enum Method { GET POST From 0488069e30db2f60944917beb142d7d3db004da4 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 3 Jan 2024 18:16:23 -0300 Subject: [PATCH 18/92] style: run `./lint.sh --mode=fix` --- .github/ISSUE_TEMPLATE/guide.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/guide.md b/.github/ISSUE_TEMPLATE/guide.md index ba5d897665..41b57c655d 100644 --- a/.github/ISSUE_TEMPLATE/guide.md +++ b/.github/ISSUE_TEMPLATE/guide.md @@ -8,20 +8,25 @@ labels: "type: docs" ## Content Creation Requirements + To maintain the quality of our content, please adhere to the following guidelines: ### 1. Accuracy in Language + - **Grammar and Spelling:** Ensure your content is free from grammatical and spelling errors. ### 2. Tone and Style + - **Neutral Tone:** Maintain a neutral, non-emotional tone throughout the content. - **Engaging Style:** Write in a free-flowing and engaging manner, keeping the reader's interest. ### 3. Content Integrity + - **Fact-Checking:** Verify all information. If unsure, use Discord to clarify. - **Relevance:** Ensure all content is cohesive, to the point, and directly related to the title. ### 4. Originality + - **Avoid Low-Effort Content:** Content should be original and not solely generated by AI tools like ChatGPT. **PS: Adherence to these guidelines is crucial. Content not meeting these standards may require revision or may not be accepted.** From 152d0511d9535f12136c3ad55bd031e57f6a01ad Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Tue, 2 Jan 2024 23:41:54 -0300 Subject: [PATCH 19/92] feat(call operator): add basic types --- src/blueprint/from_config/definitions.rs | 2 ++ src/blueprint/from_config/operators/mod.rs | 2 ++ src/config/config.rs | 11 +++++++++++ src/config/from_document.rs | 6 ++++-- 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/blueprint/from_config/definitions.rs b/src/blueprint/from_config/definitions.rs index c56bce953a..3cac51bda1 100644 --- a/src/blueprint/from_config/definitions.rs +++ b/src/blueprint/from_config/definitions.rs @@ -345,6 +345,7 @@ fn to_fields(object_name: &str, type_of: &config::Type, config: &Config) -> Vali .and(update_const_field().trace(config::Const::trace_name().as_str())) .and(update_graphql(&operation_type).trace(config::GraphQL::trace_name().as_str())) .and(update_modify().trace(config::Modify::trace_name().as_str())) + .and(update_call(&operation_type).trace(config::Call::trace_name().as_str())) .and(update_nested_resolvers()) .try_fold(&(config, field, type_of, name), FieldDefinition::default()) }; @@ -383,6 +384,7 @@ fn to_fields(object_name: &str, type_of: &config::Type, config: &Config) -> Vali const_field: source_field.const_field.clone(), graphql: source_field.graphql.clone(), cache: source_field.cache.clone(), + call: source_field.call.clone(), }; to_field(&add_field.name, &new_field) .and_then(|field_definition| { diff --git a/src/blueprint/from_config/operators/mod.rs b/src/blueprint/from_config/operators/mod.rs index b831c2a3ad..add9130dde 100644 --- a/src/blueprint/from_config/operators/mod.rs +++ b/src/blueprint/from_config/operators/mod.rs @@ -1,3 +1,4 @@ +mod call; mod const_field; mod graphql; mod grpc; @@ -5,6 +6,7 @@ mod http; mod modify; mod unsafe_field; +pub use call::*; pub use const_field::*; pub use graphql::*; pub use grpc::*; diff --git a/src/config/config.rs b/src/config/config.rs index 88158f2db0..3440c6af77 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -251,6 +251,8 @@ pub struct Field { #[serde(default, skip_serializing_if = "is_default")] pub http: Option, #[serde(default, skip_serializing_if = "is_default")] + pub call: Option, + #[serde(default, skip_serializing_if = "is_default")] pub grpc: Option, #[serde(rename = "unsafe", default, skip_serializing_if = "is_default")] pub unsafe_operation: Option, @@ -387,6 +389,15 @@ pub struct Http { pub group_by: Vec, } +#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq)] +pub struct Call { + #[serde(default, skip_serializing_if = "is_default")] + pub query: Option, + #[serde(default, skip_serializing_if = "is_default")] + pub mutation: Option, + pub args: KeyValues, +} + #[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct Grpc { diff --git a/src/config/from_document.rs b/src/config/from_document.rs index 4f0853a270..cce81fe771 100644 --- a/src/config/from_document.rs +++ b/src/config/from_document.rs @@ -8,7 +8,7 @@ use async_graphql::parser::Positioned; use async_graphql::Name; use super::Cache; -use crate::config::{self, Config, GraphQL, Grpc, RootSchema, Server, Union, Upstream}; +use crate::config::{self, Call, Config, GraphQL, Grpc, RootSchema, Server, Union, Upstream}; use crate::directive::DirectiveCodec; use crate::valid::Valid; @@ -222,7 +222,8 @@ where .zip(GraphQL::from_directives(directives.iter())) .zip(Cache::from_directives(directives.iter())) .zip(Grpc::from_directives(directives.iter())) - .map(|(((http, graphql), cache), grpc)| { + .zip(Call::from_directives(directives.iter())) + .map(|((((http, graphql), cache), grpc), call)| { let unsafe_operation = to_unsafe_operation(directives); let const_field = to_const_field(directives); config::Field { @@ -239,6 +240,7 @@ where const_field, graphql, cache: cache.or(parent_cache), + call, } }) } From 6b78f788879f5d9dec3588b7433c6296a923bb48 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Tue, 2 Jan 2024 23:43:46 -0300 Subject: [PATCH 20/92] feat: implement most of call operator --- src/blueprint/from_config/operators/call.rs | 59 +++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 src/blueprint/from_config/operators/call.rs diff --git a/src/blueprint/from_config/operators/call.rs b/src/blueprint/from_config/operators/call.rs new file mode 100644 index 0000000000..c3126ec492 --- /dev/null +++ b/src/blueprint/from_config/operators/call.rs @@ -0,0 +1,59 @@ +use crate::blueprint::*; +use crate::config; +use crate::config::{Config, Field, GraphQLOperationType}; +use crate::directive::DirectiveCodec; +use crate::try_fold::TryFold; +use crate::valid::Valid; + +pub fn update_call( + operation_type: &GraphQLOperationType, +) -> TryFold<'_, (&Config, &Field, &config::Type, &str), FieldDefinition, String> { + TryFold::<(&Config, &Field, &config::Type, &str), FieldDefinition, String>::new( + move |(config, field, type_of, name), b_field| { + let Some(call) = &field.call else { + return Valid::succeed(b_field); + }; + + let type_and_field = if let Some(mutation) = &call.mutation { + Valid::succeed(("Mutation", mutation.as_str())) + } else if let Some(query) = &call.query { + Valid::succeed(("Query", query.as_str())) + } else { + Valid::fail("call must have one of mutation or query".to_string()) + }; + + type_and_field + .and_then(|(type_name, field_name)| { + Valid::from_option( + config.find_type(type_name), + format!("{} type not found on config", type_name), + ) + .zip(Valid::succeed(field_name)) + }) + .and_then(|(query_type, field_name)| { + Valid::from_option( + query_type.fields.get(field_name), + format!("{} field not found", field_name), + ) + .and_then(|field| { + if !field.has_resolver() { + return Valid::fail(format!("{} field has no resolver", field_name)); + } + + Valid::succeed(field) + }) + }) + .and_then(|field| { + // TO-DO: parse call.args into a way that `update_http`, `update_grpc` and `update_graphql` can use + + TryFold::<(&Config, &Field, &config::Type, &str), FieldDefinition, String>::new(|_, b_field| { + Valid::succeed(b_field) + }) + .and(update_http().trace(config::Http::trace_name().as_str())) + .and(update_grpc(operation_type).trace(config::Grpc::trace_name().as_str())) + .and(update_graphql(operation_type).trace(config::GraphQL::trace_name().as_str())) + .try_fold(&(config, field, type_of, name), b_field) + }) + }, + ) +} From 0607dd529fb333cb60dc49caf601eb06de20f267 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Tue, 2 Jan 2024 23:44:02 -0300 Subject: [PATCH 21/92] chore(jsonplaceholder): use `@call` directive --- examples/jsonplaceholder.graphql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/jsonplaceholder.graphql b/examples/jsonplaceholder.graphql index fa92976fd5..d6400d1b2f 100644 --- a/examples/jsonplaceholder.graphql +++ b/examples/jsonplaceholder.graphql @@ -23,5 +23,5 @@ type Post { userId: Int! title: String! body: String! - user: User @http(path: "/users/{{value.userId}}") + user: User @call(query: "user", args: [{key: "id", value: "{{value.userId}}"}]) } From 8ca2ba546f4858cd2c8492589b93136da63b2cee Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 3 Jan 2024 01:42:35 -0300 Subject: [PATCH 22/92] feat(call): override http path args --- src/blueprint/from_config/operators/call.rs | 29 ++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/blueprint/from_config/operators/call.rs b/src/blueprint/from_config/operators/call.rs index c3126ec492..6b21f44dac 100644 --- a/src/blueprint/from_config/operators/call.rs +++ b/src/blueprint/from_config/operators/call.rs @@ -14,6 +14,13 @@ pub fn update_call( return Valid::succeed(b_field); }; + if validate_field_has_resolver(name, field, &config.types).is_succeed() { + return Valid::fail(format!( + "@call directive is not allowed on field {} because it already has a resolver", + name + )); + } + let type_and_field = if let Some(mutation) = &call.mutation { Valid::succeed(("Mutation", mutation.as_str())) } else if let Some(query) = &call.query { @@ -43,16 +50,32 @@ pub fn update_call( Valid::succeed(field) }) }) - .and_then(|field| { - // TO-DO: parse call.args into a way that `update_http`, `update_grpc` and `update_graphql` can use + .zip(Valid::succeed(call.args.iter())) + .and_then(|(field, args)| { + args.fold(Valid::succeed(field.clone()), |field, (key, value)| { + field.and_then(|field| { + let value = value.replace("{{", "").replace("}}", ""); + + if let Some(http) = field.clone().http.as_mut() { + http.path = http.path.replace(format!("args.{}", key).as_str(), value.as_str()); + + let field = Field { http: Some(http.clone()), ..field.clone() }; + return Valid::succeed(field); + } + + Valid::succeed(field) + }) + }) + }) + .and_then(|_field| { TryFold::<(&Config, &Field, &config::Type, &str), FieldDefinition, String>::new(|_, b_field| { Valid::succeed(b_field) }) .and(update_http().trace(config::Http::trace_name().as_str())) .and(update_grpc(operation_type).trace(config::Grpc::trace_name().as_str())) .and(update_graphql(operation_type).trace(config::GraphQL::trace_name().as_str())) - .try_fold(&(config, field, type_of, name), b_field) + .try_fold(&(config, &_field, type_of, name), b_field) }) }, ) From 1303d09f530628e62c02462b6ac954b3a7398285 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 3 Jan 2024 01:54:29 -0300 Subject: [PATCH 23/92] test(call): add test for query operation --- tests/http/call-operator.yml | 30 ++++++++++++++++++++++++++++++ tests/http/config/call.graphql | 27 +++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 tests/http/call-operator.yml create mode 100644 tests/http/config/call.graphql diff --git a/tests/http/call-operator.yml b/tests/http/call-operator.yml new file mode 100644 index 0000000000..026a26e395 --- /dev/null +++ b/tests/http/call-operator.yml @@ -0,0 +1,30 @@ +--- +name: With call operator +config: !file tests/http/config/call.graphql + +mock: + - request: + url: http://jsonplaceholder.typicode.com/users/1 + response: + body: + id: 1 + name: foo + - request: + url: http://jsonplaceholder.typicode.com/posts + response: + body: + - id: 1 + userId: 1 + +assert: + - request: + method: POST + url: http://localhost:8080/graphql + body: + query: "query { posts { user { name } } }" + response: + body: + data: + posts: + - user: + name: foo diff --git a/tests/http/config/call.graphql b/tests/http/config/call.graphql new file mode 100644 index 0000000000..d6400d1b2f --- /dev/null +++ b/tests/http/config/call.graphql @@ -0,0 +1,27 @@ +schema + @server(port: 8000, graphiql: true, hostname: "0.0.0.0") + @upstream(baseURL: "http://jsonplaceholder.typicode.com", httpCache: true) { + query: Query +} + +type Query { + posts: [Post] @http(path: "/posts") + user(id: Int!): User @http(path: "/users/{{args.id}}") +} + +type User { + id: Int! + name: String! + username: String! + email: String! + phone: String + website: String +} + +type Post { + id: Int! + userId: Int! + title: String! + body: String! + user: User @call(query: "user", args: [{key: "id", value: "{{value.userId}}"}]) +} From 8a7daaf186c811c9475e28dc8427ec822f1f5b9a Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 3 Jan 2024 02:08:44 -0300 Subject: [PATCH 24/92] test(call): add test cases for errors --- .../graphql/errors/test-call-operator.graphql | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 tests/graphql/errors/test-call-operator.graphql diff --git a/tests/graphql/errors/test-call-operator.graphql b/tests/graphql/errors/test-call-operator.graphql new file mode 100644 index 0000000000..110df70374 --- /dev/null +++ b/tests/graphql/errors/test-call-operator.graphql @@ -0,0 +1,25 @@ +#> server-sdl +schema @server @upstream(baseURL: "http://localhost:3000") { + query: Query +} + +type Query { + posts: [Post] @http(path: "/posts") + userWithoutResolver(id: Int!): User + user(id: Int!): User @http(path: "/users/{{args.id}}") +} + +type User { + id: Int! +} + +type Post { + userId: Int! + withoutResolver: User @call(query: "userWithoutResolver", args: [{key: "id", value: "{{value.userId}}"}]) + multipleResolvers: User @http(path: "/users/{{value.userId}}") @call(query: "user", args: [{key: "id", value: "{{value.userId}}"}]) +} + +#> client-sdl +type Failure @error(message: "No resolver has been found in the schema", trace: ["Query", "userWithoutResolver"]) +type Failure @error(message: "@call directive is not allowed on field multipleResolvers because it already has a resolver", trace: ["Post", "multipleResolvers", "@call"]) +type Failure @error(message: "userWithoutResolver field has no resolver", trace: ["Post", "withoutResolver", "@call"]) From 13df7cf05cf097606eaa551cb23f11403533001a Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 3 Jan 2024 02:13:16 -0300 Subject: [PATCH 25/92] test(call): add test for lack of operator --- tests/graphql/errors/test-call-operator.graphql | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/graphql/errors/test-call-operator.graphql b/tests/graphql/errors/test-call-operator.graphql index 110df70374..b8de3c6cb1 100644 --- a/tests/graphql/errors/test-call-operator.graphql +++ b/tests/graphql/errors/test-call-operator.graphql @@ -17,9 +17,11 @@ type Post { userId: Int! withoutResolver: User @call(query: "userWithoutResolver", args: [{key: "id", value: "{{value.userId}}"}]) multipleResolvers: User @http(path: "/users/{{value.userId}}") @call(query: "user", args: [{key: "id", value: "{{value.userId}}"}]) + withoutOperator: User @call(args: [{key: "id", value: "{{value.userId}}"}]) } #> client-sdl type Failure @error(message: "No resolver has been found in the schema", trace: ["Query", "userWithoutResolver"]) type Failure @error(message: "@call directive is not allowed on field multipleResolvers because it already has a resolver", trace: ["Post", "multipleResolvers", "@call"]) +type Failure @error(message: "call must have one of mutation or query", trace: ["Post", "withoutOperator", "@call",]) type Failure @error(message: "userWithoutResolver field has no resolver", trace: ["Post", "withoutResolver", "@call"]) From aa698795665a9242501889de1601c8fc24cd660d Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 3 Jan 2024 02:15:41 -0300 Subject: [PATCH 26/92] style: run `./lint.sh --mode=fix` --- tests/graphql/errors/test-call-operator.graphql | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/graphql/errors/test-call-operator.graphql b/tests/graphql/errors/test-call-operator.graphql index b8de3c6cb1..7b87818235 100644 --- a/tests/graphql/errors/test-call-operator.graphql +++ b/tests/graphql/errors/test-call-operator.graphql @@ -16,12 +16,18 @@ type User { type Post { userId: Int! withoutResolver: User @call(query: "userWithoutResolver", args: [{key: "id", value: "{{value.userId}}"}]) - multipleResolvers: User @http(path: "/users/{{value.userId}}") @call(query: "user", args: [{key: "id", value: "{{value.userId}}"}]) + multipleResolvers: User + @http(path: "/users/{{value.userId}}") + @call(query: "user", args: [{key: "id", value: "{{value.userId}}"}]) withoutOperator: User @call(args: [{key: "id", value: "{{value.userId}}"}]) } #> client-sdl type Failure @error(message: "No resolver has been found in the schema", trace: ["Query", "userWithoutResolver"]) -type Failure @error(message: "@call directive is not allowed on field multipleResolvers because it already has a resolver", trace: ["Post", "multipleResolvers", "@call"]) -type Failure @error(message: "call must have one of mutation or query", trace: ["Post", "withoutOperator", "@call",]) +type Failure + @error( + message: "@call directive is not allowed on field multipleResolvers because it already has a resolver" + trace: ["Post", "multipleResolvers", "@call"] + ) +type Failure @error(message: "call must have one of mutation or query", trace: ["Post", "withoutOperator", "@call"]) type Failure @error(message: "userWithoutResolver field has no resolver", trace: ["Post", "withoutResolver", "@call"]) From 28420f5a16ce25b40cafb3faa2a7dec85e3ba52b Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 3 Jan 2024 13:07:43 -0300 Subject: [PATCH 27/92] refactor(call): remove mutation for lack of use case --- src/blueprint/from_config/operators/call.rs | 6 ++---- src/config/config.rs | 2 -- tests/graphql/errors/test-call-operator.graphql | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/blueprint/from_config/operators/call.rs b/src/blueprint/from_config/operators/call.rs index 6b21f44dac..339dab7722 100644 --- a/src/blueprint/from_config/operators/call.rs +++ b/src/blueprint/from_config/operators/call.rs @@ -21,12 +21,10 @@ pub fn update_call( )); } - let type_and_field = if let Some(mutation) = &call.mutation { - Valid::succeed(("Mutation", mutation.as_str())) - } else if let Some(query) = &call.query { + let type_and_field = if let Some(query) = &call.query { Valid::succeed(("Query", query.as_str())) } else { - Valid::fail("call must have one of mutation or query".to_string()) + Valid::fail("call must have query".to_string()) }; type_and_field diff --git a/src/config/config.rs b/src/config/config.rs index 3440c6af77..e51509f92e 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -393,8 +393,6 @@ pub struct Http { pub struct Call { #[serde(default, skip_serializing_if = "is_default")] pub query: Option, - #[serde(default, skip_serializing_if = "is_default")] - pub mutation: Option, pub args: KeyValues, } diff --git a/tests/graphql/errors/test-call-operator.graphql b/tests/graphql/errors/test-call-operator.graphql index 7b87818235..0123a371da 100644 --- a/tests/graphql/errors/test-call-operator.graphql +++ b/tests/graphql/errors/test-call-operator.graphql @@ -29,5 +29,5 @@ type Failure message: "@call directive is not allowed on field multipleResolvers because it already has a resolver" trace: ["Post", "multipleResolvers", "@call"] ) -type Failure @error(message: "call must have one of mutation or query", trace: ["Post", "withoutOperator", "@call"]) +type Failure @error(message: "call must have query", trace: ["Post", "withoutOperator", "@call"]) type Failure @error(message: "userWithoutResolver field has no resolver", trace: ["Post", "withoutResolver", "@call"]) From feb6ba1f927056d404910fad8a8868e9145c0792 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 3 Jan 2024 13:17:46 -0300 Subject: [PATCH 28/92] fix: fail on lack of http resolver --- src/blueprint/from_config/operators/call.rs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/blueprint/from_config/operators/call.rs b/src/blueprint/from_config/operators/call.rs index 339dab7722..63a0d1c908 100644 --- a/src/blueprint/from_config/operators/call.rs +++ b/src/blueprint/from_config/operators/call.rs @@ -41,15 +41,14 @@ pub fn update_call( format!("{} field not found", field_name), ) .and_then(|field| { - if !field.has_resolver() { - return Valid::fail(format!("{} field has no resolver", field_name)); + if field.has_resolver() { + Valid::succeed((field, field_name, call.args.iter())) + } else { + Valid::fail(format!("{} field has no resolver", field_name)) } - - Valid::succeed(field) }) }) - .zip(Valid::succeed(call.args.iter())) - .and_then(|(field, args)| { + .and_then(|(field, field_name, args)| { args.fold(Valid::succeed(field.clone()), |field, (key, value)| { field.and_then(|field| { let value = value.replace("{{", "").replace("}}", ""); @@ -59,10 +58,10 @@ pub fn update_call( let field = Field { http: Some(http.clone()), ..field.clone() }; - return Valid::succeed(field); + Valid::succeed(field) + } else { + Valid::fail(format!("{} field does not have an http resolver", field_name)) } - - Valid::succeed(field) }) }) }) From f81a9722891eb98c1d8bf1be0c4b79b7888c137e Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 3 Jan 2024 13:17:56 -0300 Subject: [PATCH 29/92] test: add test for invalid resolver --- tests/graphql/errors/test-call-operator.graphql | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/graphql/errors/test-call-operator.graphql b/tests/graphql/errors/test-call-operator.graphql index 0123a371da..03f020970c 100644 --- a/tests/graphql/errors/test-call-operator.graphql +++ b/tests/graphql/errors/test-call-operator.graphql @@ -7,6 +7,8 @@ type Query { posts: [Post] @http(path: "/posts") userWithoutResolver(id: Int!): User user(id: Int!): User @http(path: "/users/{{args.id}}") + userWithGraphQLResolver(id: Int): User + @graphQL(baseURL: "http://upstream/graphql", name: "user", args: [{key: "id", value: "{{args.id}}"}]) } type User { @@ -20,10 +22,12 @@ type Post { @http(path: "/users/{{value.userId}}") @call(query: "user", args: [{key: "id", value: "{{value.userId}}"}]) withoutOperator: User @call(args: [{key: "id", value: "{{value.userId}}"}]) + invalidResolver: User @call(query: "userWithGraphQLResolver", args: [{key: "id", value: "{{value.userId}}"}]) } #> client-sdl type Failure @error(message: "No resolver has been found in the schema", trace: ["Query", "userWithoutResolver"]) +type Failure @error(message: "userWithGraphQLResolver field does not have an http resolver", trace: ["Post", "invalidResolver", "@call"]) type Failure @error( message: "@call directive is not allowed on field multipleResolvers because it already has a resolver" From 9b591a9afd2b898d471ee51b4164db7e3edd36c6 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 3 Jan 2024 13:35:03 -0300 Subject: [PATCH 30/92] style: run `./lint.sh --mode=fix` --- tests/graphql/errors/test-call-operator.graphql | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/graphql/errors/test-call-operator.graphql b/tests/graphql/errors/test-call-operator.graphql index 03f020970c..b2c36c68de 100644 --- a/tests/graphql/errors/test-call-operator.graphql +++ b/tests/graphql/errors/test-call-operator.graphql @@ -27,7 +27,11 @@ type Post { #> client-sdl type Failure @error(message: "No resolver has been found in the schema", trace: ["Query", "userWithoutResolver"]) -type Failure @error(message: "userWithGraphQLResolver field does not have an http resolver", trace: ["Post", "invalidResolver", "@call"]) +type Failure + @error( + message: "userWithGraphQLResolver field does not have an http resolver" + trace: ["Post", "invalidResolver", "@call"] + ) type Failure @error( message: "@call directive is not allowed on field multipleResolvers because it already has a resolver" From 1fb8a8ea0df18de0ae44f9479d11065f95ef21bb Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 3 Jan 2024 13:35:44 -0300 Subject: [PATCH 31/92] docs(tailcallrc): add `@call` operator directive --- examples/.tailcallrc.graphql | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/examples/.tailcallrc.graphql b/examples/.tailcallrc.graphql index 153fbbca1f..fa10b2d1c2 100644 --- a/examples/.tailcallrc.graphql +++ b/examples/.tailcallrc.graphql @@ -300,6 +300,20 @@ directive @cache( maxAge: Int! ) on FIELD_DEFINITION +""" +The @call operator is used to reference an `@http` operator. +""" +directive @call( + """ + The name of the field that has the `@http` resolver to be called. + """ + query: String! + """ + The arguments to be replace the `@http` resolver args. + """ + args: [KeyValue] +) + enum Method { GET POST From 6305bd6a37ade868fb366c7105726b9e4ee009a5 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 3 Jan 2024 16:54:35 -0300 Subject: [PATCH 32/92] refactor(call): use `Valid::from_option` instead of `if let Some` --- src/blueprint/from_config/operators/call.rs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/blueprint/from_config/operators/call.rs b/src/blueprint/from_config/operators/call.rs index 63a0d1c908..a79b0ae867 100644 --- a/src/blueprint/from_config/operators/call.rs +++ b/src/blueprint/from_config/operators/call.rs @@ -21,23 +21,17 @@ pub fn update_call( )); } - let type_and_field = if let Some(query) = &call.query { - Valid::succeed(("Query", query.as_str())) - } else { - Valid::fail("call must have query".to_string()) - }; - - type_and_field - .and_then(|(type_name, field_name)| { + Valid::from_option(call.query.clone(), "call must have query".to_string()) + .and_then(|field_name| { Valid::from_option( - config.find_type(type_name), - format!("{} type not found on config", type_name), + config.find_type("Query"), + format!("Query type not found on config"), ) .zip(Valid::succeed(field_name)) }) .and_then(|(query_type, field_name)| { Valid::from_option( - query_type.fields.get(field_name), + query_type.fields.get(&field_name), format!("{} field not found", field_name), ) .and_then(|field| { From f8adc1ec33dd063487bddd45e98b77f23be510af Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 3 Jan 2024 17:19:38 -0300 Subject: [PATCH 33/92] refactor(call): remove useless format --- src/blueprint/from_config/operators/call.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/blueprint/from_config/operators/call.rs b/src/blueprint/from_config/operators/call.rs index a79b0ae867..d5d0de9d5d 100644 --- a/src/blueprint/from_config/operators/call.rs +++ b/src/blueprint/from_config/operators/call.rs @@ -23,11 +23,8 @@ pub fn update_call( Valid::from_option(call.query.clone(), "call must have query".to_string()) .and_then(|field_name| { - Valid::from_option( - config.find_type("Query"), - format!("Query type not found on config"), - ) - .zip(Valid::succeed(field_name)) + Valid::from_option(config.find_type("Query"), "Query type not found on config".to_string()) + .zip(Valid::succeed(field_name)) }) .and_then(|(query_type, field_name)| { Valid::from_option( From 40467b532e3f308475cb728d4c01908f9a8d4998 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 3 Jan 2024 17:21:16 -0300 Subject: [PATCH 34/92] fix(tailcallrc): add `on FIELD_DEFINITION` for `@call` directive --- examples/.tailcallrc.graphql | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/.tailcallrc.graphql b/examples/.tailcallrc.graphql index fa10b2d1c2..9f3b3b1a69 100644 --- a/examples/.tailcallrc.graphql +++ b/examples/.tailcallrc.graphql @@ -312,7 +312,8 @@ directive @call( The arguments to be replace the `@http` resolver args. """ args: [KeyValue] -) +) on FIELD_DEFINITION + enum Method { GET From 380d80521e677ac97868a3f582588df3674cb149 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 3 Jan 2024 17:30:35 -0300 Subject: [PATCH 35/92] style: run `./lint.sh --mode=fix` --- examples/.tailcallrc.graphql | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/.tailcallrc.graphql b/examples/.tailcallrc.graphql index 9f3b3b1a69..448913bc8b 100644 --- a/examples/.tailcallrc.graphql +++ b/examples/.tailcallrc.graphql @@ -314,7 +314,6 @@ directive @call( args: [KeyValue] ) on FIELD_DEFINITION - enum Method { GET POST From fba92c1467eb3458aae41bcf2660db4114f9a5c6 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Sat, 6 Jan 2024 09:22:40 -0300 Subject: [PATCH 36/92] docs(call): add WIP call docs --- docs/operators/call.md | 53 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 docs/operators/call.md diff --git a/docs/operators/call.md b/docs/operators/call.md new file mode 100644 index 0000000000..cdf6a6c248 --- /dev/null +++ b/docs/operators/call.md @@ -0,0 +1,53 @@ +--- +title: "@call" +--- + +The **@call** operator is used to reference an `@http` operator. It is useful when you have multiple fields that resolves from the same HTTP endpoint. + +```graphql showLineNumbers +schema { + query: Query +} + +type Query { + posts: [Post] @http(path: "/posts") + user(id: Int!): User @http(path: "/users/{{args.id}}") +} + +type User { + id: Int! + name: String! + username: String! + email: String! +} + +type Post { + id: Int! + userId: Int! + title: String! + body: String! + user: User @call(query: "user", args: [{key: "id", value: "{{value.userId}}"}]) +} +``` + +## query + +The name of the field that has the `@http` resolver to be called. It is required. + +```graphql showLineNumbers +type Post { + userId: Int! + user: User @call(query: "user", args: [{key: "id", value: "{{value.userId}}"}]) +} +``` + +## args + +The arguments to be passed to the `@http` resolver. It is optional. + +```graphql showLineNumbers +type Post { + userId: Int! + user: User @call(query: "user", args: [{key: "id", value: "{{value.userId}}"}]) +} +``` From a167b49ef83e9a11116c658091b0c01b1e65a63a Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Sun, 7 Jan 2024 23:10:18 -0300 Subject: [PATCH 37/92] refactor: update `call.args` into `HashMap` --- examples/.tailcallrc.graphql | 2 +- examples/jsonplaceholder.graphql | 2 +- src/config/config.rs | 4 ++-- tests/graphql/errors/test-call-operator.graphql | 8 ++++---- tests/http/config/call.graphql | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/examples/.tailcallrc.graphql b/examples/.tailcallrc.graphql index 448913bc8b..ddfbf32a1d 100644 --- a/examples/.tailcallrc.graphql +++ b/examples/.tailcallrc.graphql @@ -311,7 +311,7 @@ directive @call( """ The arguments to be replace the `@http` resolver args. """ - args: [KeyValue] + args: JSON ) on FIELD_DEFINITION enum Method { diff --git a/examples/jsonplaceholder.graphql b/examples/jsonplaceholder.graphql index d6400d1b2f..71da2e1fa4 100644 --- a/examples/jsonplaceholder.graphql +++ b/examples/jsonplaceholder.graphql @@ -23,5 +23,5 @@ type Post { userId: Int! title: String! body: String! - user: User @call(query: "user", args: [{key: "id", value: "{{value.userId}}"}]) + user: User @call(query: "user", args: {id: "{{value.userId}}"}) } diff --git a/src/config/config.rs b/src/config/config.rs index e51509f92e..4d44938dba 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -1,4 +1,4 @@ -use std::collections::{BTreeMap, BTreeSet, HashSet}; +use std::collections::{BTreeMap, BTreeSet, HashSet, HashMap}; use std::fmt::{self, Display}; use std::num::NonZeroU64; @@ -393,7 +393,7 @@ pub struct Http { pub struct Call { #[serde(default, skip_serializing_if = "is_default")] pub query: Option, - pub args: KeyValues, + pub args: HashMap, } #[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq)] diff --git a/tests/graphql/errors/test-call-operator.graphql b/tests/graphql/errors/test-call-operator.graphql index b2c36c68de..da626ac214 100644 --- a/tests/graphql/errors/test-call-operator.graphql +++ b/tests/graphql/errors/test-call-operator.graphql @@ -17,12 +17,12 @@ type User { type Post { userId: Int! - withoutResolver: User @call(query: "userWithoutResolver", args: [{key: "id", value: "{{value.userId}}"}]) + withoutResolver: User @call(query: "userWithoutResolver", args: {id: "{{value.userId}}"}) multipleResolvers: User @http(path: "/users/{{value.userId}}") - @call(query: "user", args: [{key: "id", value: "{{value.userId}}"}]) - withoutOperator: User @call(args: [{key: "id", value: "{{value.userId}}"}]) - invalidResolver: User @call(query: "userWithGraphQLResolver", args: [{key: "id", value: "{{value.userId}}"}]) + @call(query: "user", args: {id: "{{value.userId}}"}) + withoutOperator: User @call(args: {id: "{{value.userId}}"}) + invalidResolver: User @call(query: "userWithGraphQLResolver", args: {id: "{{value.userId}}"}) } #> client-sdl diff --git a/tests/http/config/call.graphql b/tests/http/config/call.graphql index d6400d1b2f..71da2e1fa4 100644 --- a/tests/http/config/call.graphql +++ b/tests/http/config/call.graphql @@ -23,5 +23,5 @@ type Post { userId: Int! title: String! body: String! - user: User @call(query: "user", args: [{key: "id", value: "{{value.userId}}"}]) + user: User @call(query: "user", args: {id: "{{value.userId}}"}) } From 98fed0e73db5bf2d5a2aad34d5d0858db0825a10 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Sun, 7 Jan 2024 23:28:38 -0300 Subject: [PATCH 38/92] style: run `./lint.sh --mode=fix` --- src/config/config.rs | 2 +- tests/graphql/errors/test-call-operator.graphql | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/config/config.rs b/src/config/config.rs index 4d44938dba..5d6914df2f 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -1,4 +1,4 @@ -use std::collections::{BTreeMap, BTreeSet, HashSet, HashMap}; +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::fmt::{self, Display}; use std::num::NonZeroU64; diff --git a/tests/graphql/errors/test-call-operator.graphql b/tests/graphql/errors/test-call-operator.graphql index da626ac214..bca095d295 100644 --- a/tests/graphql/errors/test-call-operator.graphql +++ b/tests/graphql/errors/test-call-operator.graphql @@ -18,9 +18,7 @@ type User { type Post { userId: Int! withoutResolver: User @call(query: "userWithoutResolver", args: {id: "{{value.userId}}"}) - multipleResolvers: User - @http(path: "/users/{{value.userId}}") - @call(query: "user", args: {id: "{{value.userId}}"}) + multipleResolvers: User @http(path: "/users/{{value.userId}}") @call(query: "user", args: {id: "{{value.userId}}"}) withoutOperator: User @call(args: {id: "{{value.userId}}"}) invalidResolver: User @call(query: "userWithGraphQLResolver", args: {id: "{{value.userId}}"}) } From c9c53f9cffa98cc3200dd36e77bb2eefb8800d2c Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Thu, 11 Jan 2024 20:18:05 -0300 Subject: [PATCH 39/92] test: add tests to `@graphQL` resolver on call operator --- .../graphql/errors/test-call-operator.graphql | 9 +-- tests/http/call-graphql-datasource.yml | 61 +++++++++++++++++++ .../config/call-graphql-datasource.graphql | 27 ++++++++ 3 files changed, 89 insertions(+), 8 deletions(-) create mode 100644 tests/http/call-graphql-datasource.yml create mode 100644 tests/http/config/call-graphql-datasource.graphql diff --git a/tests/graphql/errors/test-call-operator.graphql b/tests/graphql/errors/test-call-operator.graphql index bca095d295..6558b26bec 100644 --- a/tests/graphql/errors/test-call-operator.graphql +++ b/tests/graphql/errors/test-call-operator.graphql @@ -7,8 +7,6 @@ type Query { posts: [Post] @http(path: "/posts") userWithoutResolver(id: Int!): User user(id: Int!): User @http(path: "/users/{{args.id}}") - userWithGraphQLResolver(id: Int): User - @graphQL(baseURL: "http://upstream/graphql", name: "user", args: [{key: "id", value: "{{args.id}}"}]) } type User { @@ -20,16 +18,11 @@ type Post { withoutResolver: User @call(query: "userWithoutResolver", args: {id: "{{value.userId}}"}) multipleResolvers: User @http(path: "/users/{{value.userId}}") @call(query: "user", args: {id: "{{value.userId}}"}) withoutOperator: User @call(args: {id: "{{value.userId}}"}) - invalidResolver: User @call(query: "userWithGraphQLResolver", args: {id: "{{value.userId}}"}) + # invalidResolver: User @call(query: "userWithGraphQLResolver", args: {id: "{{value.userId}}"}) } #> client-sdl type Failure @error(message: "No resolver has been found in the schema", trace: ["Query", "userWithoutResolver"]) -type Failure - @error( - message: "userWithGraphQLResolver field does not have an http resolver" - trace: ["Post", "invalidResolver", "@call"] - ) type Failure @error( message: "@call directive is not allowed on field multipleResolvers because it already has a resolver" diff --git a/tests/http/call-graphql-datasource.yml b/tests/http/call-graphql-datasource.yml new file mode 100644 index 0000000000..763dc5eed0 --- /dev/null +++ b/tests/http/call-graphql-datasource.yml @@ -0,0 +1,61 @@ +--- +name: Call operator with graphQL datasource +config: !file tests/http/config/call-graphql-datasource.graphql +mock: + - request: + url: http://jsonplaceholder.typicode.com/posts + response: + body: + - id: 1 + title: "a" + userId: 1 + - id: 2 + title: "b" + userId: 1 + - id: 3 + title: "c" + userId: 2 + - id: 4 + title: "d" + userId: 2 + - request: + url: http://upstream/graphql + method: POST + body: '{ "query": "query { user(id: 1) { name } }" }' + response: + body: + data: + user: + name: "Leanne Graham" + - request: + url: http://upstream/graphql + method: POST + body: '{ "query": "query { user(id: 2) { name } }" }' + response: + body: + data: + user: + name: "Ervin Howell" + +assert: + - request: + method: POST + url: http://localhost:8080/graphql + body: + query: "query { posts { title user { name } } }" + response: + body: + data: + posts: + - title: "a" + user: + name: "Leanne Graham" + - title: "b" + user: + name: "Leanne Graham" + - title: "c" + user: + name: "Ervin Howell" + - title: "d" + user: + name: "Ervin Howell" diff --git a/tests/http/config/call-graphql-datasource.graphql b/tests/http/config/call-graphql-datasource.graphql new file mode 100644 index 0000000000..d9918c44ee --- /dev/null +++ b/tests/http/config/call-graphql-datasource.graphql @@ -0,0 +1,27 @@ +schema + @server(port: 8000, graphiql: true, hostname: "0.0.0.0") + @upstream(baseURL: "http://jsonplaceholder.typicode.com", httpCache: true) { + query: Query +} + +type Query { + posts: [Post] @http(path: "/posts") + user(id: Int!): User @graphQL(baseURL: "http://upstream/graphql", name: "user", args: [{key: "id", value: "{{args.id}}"}]) +} + +type User { + id: Int! + name: String! + username: String! + email: String! + phone: String + website: String +} + +type Post { + id: Int! + userId: Int! + title: String! + body: String! + user: User @call(query: "user", args: {id: "{{value.userId}}"}) +} From f6e2d529318afffa1e19d2be1c37fe0553e527b8 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Thu, 11 Jan 2024 20:18:19 -0300 Subject: [PATCH 40/92] feat: implement graphql operator --- src/blueprint/from_config/operators/call.rs | 29 ++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/blueprint/from_config/operators/call.rs b/src/blueprint/from_config/operators/call.rs index d5d0de9d5d..1475f2b047 100644 --- a/src/blueprint/from_config/operators/call.rs +++ b/src/blueprint/from_config/operators/call.rs @@ -1,9 +1,10 @@ -use crate::blueprint::*; -use crate::config; +use crate::{blueprint::*, config}; use crate::config::{Config, Field, GraphQLOperationType}; use crate::directive::DirectiveCodec; +// use crate::mustache::Mustache; use crate::try_fold::TryFold; use crate::valid::Valid; +// use crate::valid::ValidationError; pub fn update_call( operation_type: &GraphQLOperationType, @@ -42,14 +43,36 @@ pub fn update_call( .and_then(|(field, field_name, args)| { args.fold(Valid::succeed(field.clone()), |field, (key, value)| { field.and_then(|field| { - let value = value.replace("{{", "").replace("}}", ""); + // not sure if the code below will be useful + // TO-DO: remove if not needed + // let mustache = Mustache::parse(value.as_str()).map_err(|e| ValidationError::new(e.to_string())).unwrap(); + // println!("mustache: {:?}", mustache); + // println!("field: {:?}", field); if let Some(http) = field.clone().http.as_mut() { + let value = value.replace("{{", "").replace("}}", ""); + http.path = http.path.replace(format!("args.{}", key).as_str(), value.as_str()); let field = Field { http: Some(http.clone()), ..field.clone() }; Valid::succeed(field) + } else if let Some(graphql) = field.clone().graphql.as_mut() { + graphql.args = graphql.args.clone().map(|mut args| { + args.0.iter_mut().for_each(|(k, v)| { + if k == key { + *v = value.clone(); + } + }); + + args + }); + + let field = Field { graphql: Some(graphql.clone()), ..field.clone() }; + + Valid::succeed(field) + } else if let Some(grpc) = field.clone().grpc.as_mut() { + todo!("grpc not implemented yet"); } else { Valid::fail(format!("{} field does not have an http resolver", field_name)) } From ecb469b73207799fdba8dff809faa86a5dd7627c Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Thu, 11 Jan 2024 20:26:40 -0300 Subject: [PATCH 41/92] style: run `./lint.sh --mode=fix` --- src/blueprint/from_config/operators/call.rs | 7 ++++--- tests/http/config/call-graphql-datasource.graphql | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/blueprint/from_config/operators/call.rs b/src/blueprint/from_config/operators/call.rs index 1475f2b047..49a39f0e21 100644 --- a/src/blueprint/from_config/operators/call.rs +++ b/src/blueprint/from_config/operators/call.rs @@ -1,4 +1,5 @@ -use crate::{blueprint::*, config}; +use crate::blueprint::*; +use crate::config; use crate::config::{Config, Field, GraphQLOperationType}; use crate::directive::DirectiveCodec; // use crate::mustache::Mustache; @@ -51,7 +52,7 @@ pub fn update_call( if let Some(http) = field.clone().http.as_mut() { let value = value.replace("{{", "").replace("}}", ""); - + http.path = http.path.replace(format!("args.{}", key).as_str(), value.as_str()); let field = Field { http: Some(http.clone()), ..field.clone() }; @@ -71,7 +72,7 @@ pub fn update_call( let field = Field { graphql: Some(graphql.clone()), ..field.clone() }; Valid::succeed(field) - } else if let Some(grpc) = field.clone().grpc.as_mut() { + } else if let Some(_grpc) = field.clone().grpc.as_mut() { todo!("grpc not implemented yet"); } else { Valid::fail(format!("{} field does not have an http resolver", field_name)) diff --git a/tests/http/config/call-graphql-datasource.graphql b/tests/http/config/call-graphql-datasource.graphql index d9918c44ee..39713588b4 100644 --- a/tests/http/config/call-graphql-datasource.graphql +++ b/tests/http/config/call-graphql-datasource.graphql @@ -6,7 +6,8 @@ schema type Query { posts: [Post] @http(path: "/posts") - user(id: Int!): User @graphQL(baseURL: "http://upstream/graphql", name: "user", args: [{key: "id", value: "{{args.id}}"}]) + user(id: Int!): User + @graphQL(baseURL: "http://upstream/graphql", name: "user", args: [{key: "id", value: "{{args.id}}"}]) } type User { From 8b4e5e2fc1e3e660ab8c48c84f90c4fc74f61149 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Sun, 14 Jan 2024 14:22:52 -0300 Subject: [PATCH 42/92] test: add tests for argument mismatch --- tests/graphql/errors/test-call-operator.graphql | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/graphql/errors/test-call-operator.graphql b/tests/graphql/errors/test-call-operator.graphql index 6558b26bec..402022a16b 100644 --- a/tests/graphql/errors/test-call-operator.graphql +++ b/tests/graphql/errors/test-call-operator.graphql @@ -18,11 +18,13 @@ type Post { withoutResolver: User @call(query: "userWithoutResolver", args: {id: "{{value.userId}}"}) multipleResolvers: User @http(path: "/users/{{value.userId}}") @call(query: "user", args: {id: "{{value.userId}}"}) withoutOperator: User @call(args: {id: "{{value.userId}}"}) + brokenMustache: User @call(query: "user", args: {}) # invalidResolver: User @call(query: "userWithGraphQLResolver", args: {id: "{{value.userId}}"}) } #> client-sdl type Failure @error(message: "No resolver has been found in the schema", trace: ["Query", "userWithoutResolver"]) +type Failure @error(message: "no argument 'id' found", trace: ["Post", "brokenMustache", "@call", "@http", "path"]) type Failure @error( message: "@call directive is not allowed on field multipleResolvers because it already has a resolver" From df14c219fbf0b7445c8027343bd415ae39220a9b Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Mon, 15 Jan 2024 05:20:18 -0300 Subject: [PATCH 43/92] test: include test for http operator args mismatch --- tests/graphql/errors/test-call-operator.graphql | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/graphql/errors/test-call-operator.graphql b/tests/graphql/errors/test-call-operator.graphql index 402022a16b..38dc68c3d2 100644 --- a/tests/graphql/errors/test-call-operator.graphql +++ b/tests/graphql/errors/test-call-operator.graphql @@ -7,6 +7,8 @@ type Query { posts: [Post] @http(path: "/posts") userWithoutResolver(id: Int!): User user(id: Int!): User @http(path: "/users/{{args.id}}") + userWithGraphQLResolver(id: Int!): User + @graphQL(name: "user", args: [{key: "id", value: "{{args.id}}"}]) } type User { @@ -18,13 +20,13 @@ type Post { withoutResolver: User @call(query: "userWithoutResolver", args: {id: "{{value.userId}}"}) multipleResolvers: User @http(path: "/users/{{value.userId}}") @call(query: "user", args: {id: "{{value.userId}}"}) withoutOperator: User @call(args: {id: "{{value.userId}}"}) - brokenMustache: User @call(query: "user", args: {}) + argumentMismatchHttp: User @call(query: "user", args: {}) # invalidResolver: User @call(query: "userWithGraphQLResolver", args: {id: "{{value.userId}}"}) } #> client-sdl type Failure @error(message: "No resolver has been found in the schema", trace: ["Query", "userWithoutResolver"]) -type Failure @error(message: "no argument 'id' found", trace: ["Post", "brokenMustache", "@call", "@http", "path"]) +type Failure @error(message: "no argument 'id' found", trace: ["Post", "argumentMismatchHttp", "@call", "@http", "path"]) type Failure @error( message: "@call directive is not allowed on field multipleResolvers because it already has a resolver" From 4bcff13a3d97d149055a10f6c7cc10375aa8be6d Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Tue, 16 Jan 2024 13:26:46 -0300 Subject: [PATCH 44/92] chore: checkpoint --- src/blueprint/from_config/definitions.rs | 7 +- src/blueprint/from_config/operators/call.rs | 85 +++++++++++---------- 2 files changed, 49 insertions(+), 43 deletions(-) diff --git a/src/blueprint/from_config/definitions.rs b/src/blueprint/from_config/definitions.rs index 3cac51bda1..89192cc3b3 100644 --- a/src/blueprint/from_config/definitions.rs +++ b/src/blueprint/from_config/definitions.rs @@ -345,9 +345,14 @@ fn to_fields(object_name: &str, type_of: &config::Type, config: &Config) -> Vali .and(update_const_field().trace(config::Const::trace_name().as_str())) .and(update_graphql(&operation_type).trace(config::GraphQL::trace_name().as_str())) .and(update_modify().trace(config::Modify::trace_name().as_str())) - .and(update_call(&operation_type).trace(config::Call::trace_name().as_str())) + .and(validate_call(&operation_type).trace(config::Call::trace_name().as_str())) .and(update_nested_resolvers()) .try_fold(&(config, field, type_of, name), FieldDefinition::default()) + .and_then(|b_field| { + println!("b_field.resolver: {:?}", b_field.resolver); + + Valid::succeed(b_field) + }) }; let fields = Valid::from_iter( diff --git a/src/blueprint/from_config/operators/call.rs b/src/blueprint/from_config/operators/call.rs index 49a39f0e21..b75bedf555 100644 --- a/src/blueprint/from_config/operators/call.rs +++ b/src/blueprint/from_config/operators/call.rs @@ -7,7 +7,7 @@ use crate::try_fold::TryFold; use crate::valid::Valid; // use crate::valid::ValidationError; -pub fn update_call( +pub fn validate_call( operation_type: &GraphQLOperationType, ) -> TryFold<'_, (&Config, &Field, &config::Type, &str), FieldDefinition, String> { TryFold::<(&Config, &Field, &config::Type, &str), FieldDefinition, String>::new( @@ -41,54 +41,55 @@ pub fn update_call( } }) }) - .and_then(|(field, field_name, args)| { - args.fold(Valid::succeed(field.clone()), |field, (key, value)| { - field.and_then(|field| { - // not sure if the code below will be useful - // TO-DO: remove if not needed - // let mustache = Mustache::parse(value.as_str()).map_err(|e| ValidationError::new(e.to_string())).unwrap(); - // println!("mustache: {:?}", mustache); - // println!("field: {:?}", field); + .map_to(b_field) + // .and_then(|(field, field_name, args)| { + // args.fold(Valid::succeed(field.clone()), |field, (key, value)| { + // field.and_then(|field| { + // // not sure if the code below will be useful + // // TO-DO: remove if not needed + // // let mustache = Mustache::parse(value.as_str()).map_err(|e| ValidationError::new(e.to_string())).unwrap(); + // // println!("mustache: {:?}", mustache); + // // println!("field: {:?}", field); - if let Some(http) = field.clone().http.as_mut() { - let value = value.replace("{{", "").replace("}}", ""); + // if let Some(http) = field.clone().http.as_mut() { + // let value = value.replace("{{", "").replace("}}", ""); - http.path = http.path.replace(format!("args.{}", key).as_str(), value.as_str()); + // http.path = http.path.replace(format!("args.{}", key).as_str(), value.as_str()); - let field = Field { http: Some(http.clone()), ..field.clone() }; + // let field = Field { http: Some(http.clone()), ..field.clone() }; - Valid::succeed(field) - } else if let Some(graphql) = field.clone().graphql.as_mut() { - graphql.args = graphql.args.clone().map(|mut args| { - args.0.iter_mut().for_each(|(k, v)| { - if k == key { - *v = value.clone(); - } - }); + // Valid::succeed(field) + // } else if let Some(graphql) = field.clone().graphql.as_mut() { + // graphql.args = graphql.args.clone().map(|mut args| { + // args.0.iter_mut().for_each(|(k, v)| { + // if k == key { + // *v = value.clone(); + // } + // }); - args - }); + // args + // }); - let field = Field { graphql: Some(graphql.clone()), ..field.clone() }; + // let field = Field { graphql: Some(graphql.clone()), ..field.clone() }; - Valid::succeed(field) - } else if let Some(_grpc) = field.clone().grpc.as_mut() { - todo!("grpc not implemented yet"); - } else { - Valid::fail(format!("{} field does not have an http resolver", field_name)) - } - }) - }) - }) - .and_then(|_field| { - TryFold::<(&Config, &Field, &config::Type, &str), FieldDefinition, String>::new(|_, b_field| { - Valid::succeed(b_field) - }) - .and(update_http().trace(config::Http::trace_name().as_str())) - .and(update_grpc(operation_type).trace(config::Grpc::trace_name().as_str())) - .and(update_graphql(operation_type).trace(config::GraphQL::trace_name().as_str())) - .try_fold(&(config, &_field, type_of, name), b_field) - }) + // Valid::succeed(field) + // } else if let Some(_grpc) = field.clone().grpc.as_mut() { + // todo!("grpc not implemented yet"); + // } else { + // Valid::fail(format!("{} field does not have an http resolver", field_name)) + // } + // }) + // }) + // }) + // .and_then(|_field| { + // TryFold::<(&Config, &Field, &config::Type, &str), FieldDefinition, String>::new(|_, b_field| { + // Valid::succeed(b_field) + // }) + // .and(update_http().trace(config::Http::trace_name().as_str())) + // .and(update_grpc(operation_type).trace(config::Grpc::trace_name().as_str())) + // .and(update_graphql(operation_type).trace(config::GraphQL::trace_name().as_str())) + // .try_fold(&(config, &_field, type_of, name), b_field) + // }) }, ) } From 0e2dd40ba57df851cd237b296c8aca936b6b2a2f Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Tue, 16 Jan 2024 13:37:33 -0300 Subject: [PATCH 45/92] refactor: make validate return the `b_field` itself --- src/blueprint/from_config/operators/graphql.rs | 2 +- src/blueprint/from_config/operators/grpc.rs | 2 +- src/blueprint/from_config/operators/http.rs | 2 +- src/blueprint/validate.rs | 3 ++- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/blueprint/from_config/operators/graphql.rs b/src/blueprint/from_config/operators/graphql.rs index 04e0ce505e..160040ca27 100644 --- a/src/blueprint/from_config/operators/graphql.rs +++ b/src/blueprint/from_config/operators/graphql.rs @@ -40,7 +40,7 @@ pub fn update_graphql<'a>( compile_graphql(config, operation_type, graphql) .map(|resolver| b_field.resolver(Some(resolver))) - .and_then(|b_field| b_field.validate_field(type_of, config).map_to(b_field)) + .and_then(|b_field| b_field.validate_field(type_of, config)) }, ) } diff --git a/src/blueprint/from_config/operators/grpc.rs b/src/blueprint/from_config/operators/grpc.rs index 19ecc656de..4733bff4e4 100644 --- a/src/blueprint/from_config/operators/grpc.rs +++ b/src/blueprint/from_config/operators/grpc.rs @@ -158,7 +158,7 @@ pub fn update_grpc<'a>( compile_grpc(CompileGrpc { config, operation_type, field, grpc, validate_with_schema: true }) .map(|resolver| b_field.resolver(Some(resolver))) - .and_then(|b_field| b_field.validate_field(type_of, config).map_to(b_field)) + .and_then(|b_field| b_field.validate_field(type_of, config)) }, ) } diff --git a/src/blueprint/from_config/operators/http.rs b/src/blueprint/from_config/operators/http.rs index ee7935da3b..9c757f3404 100644 --- a/src/blueprint/from_config/operators/http.rs +++ b/src/blueprint/from_config/operators/http.rs @@ -63,7 +63,7 @@ pub fn update_http<'a>() -> TryFold<'a, (&'a Config, &'a Field, &'a config::Type compile_http(config, field, http) .map(|resolver| b_field.resolver(Some(resolver))) - .and_then(|b_field| b_field.validate_field(type_of, config).map_to(b_field)) + .and_then(|b_field| b_field.validate_field(type_of, config)) }, ) } diff --git a/src/blueprint/validate.rs b/src/blueprint/validate.rs index 3f4eb88024..6aa209bcbe 100644 --- a/src/blueprint/validate.rs +++ b/src/blueprint/validate.rs @@ -101,7 +101,7 @@ impl<'a> MustachePartsValidator<'a> { } impl FieldDefinition { - pub fn validate_field(&self, type_of: &config::Type, config: &Config) -> Valid<(), String> { + pub fn validate_field(&self, type_of: &config::Type, config: &Config) -> Valid { // XXX we could use `Mustache`'s `render` method with a mock // struct implementing the `PathString` trait encapsulating `validation_map` // but `render` simply falls back to the default value for a given @@ -168,5 +168,6 @@ impl FieldDefinition { } _ => Valid::succeed(()), } + .map_to(self.clone()) } } From c2a72c7fea913221a175bcfee2efd79e0a573f96 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Tue, 16 Jan 2024 13:38:24 -0300 Subject: [PATCH 46/92] refactor: move ownership to `validate_field` method --- src/blueprint/validate.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/blueprint/validate.rs b/src/blueprint/validate.rs index 6aa209bcbe..349b27df8a 100644 --- a/src/blueprint/validate.rs +++ b/src/blueprint/validate.rs @@ -101,14 +101,14 @@ impl<'a> MustachePartsValidator<'a> { } impl FieldDefinition { - pub fn validate_field(&self, type_of: &config::Type, config: &Config) -> Valid { + pub fn validate_field(self, type_of: &config::Type, config: &Config) -> Valid { // XXX we could use `Mustache`'s `render` method with a mock // struct implementing the `PathString` trait encapsulating `validation_map` // but `render` simply falls back to the default value for a given // type if it doesn't exist, so we wouldn't be able to get enough // context from that method alone // So we must duplicate some of that logic here :( - let parts_validator = MustachePartsValidator::new(type_of, config, self); + let parts_validator = MustachePartsValidator::new(type_of, config, &self); match &self.resolver { Some(Expression::Unsafe(Unsafe::Http { req_template, .. })) => { From 7817ee39487ebaaeebeee4b750afbf0826bd984c Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 17 Jan 2024 10:49:07 -0300 Subject: [PATCH 47/92] test: add mutation breaking tests --- tests/http/config/mutation-nested.graphql | 35 +++++++++++++++++++++++ tests/http/mutation-nested.yml | 34 ++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 tests/http/config/mutation-nested.graphql create mode 100644 tests/http/mutation-nested.yml diff --git a/tests/http/config/mutation-nested.graphql b/tests/http/config/mutation-nested.graphql new file mode 100644 index 0000000000..b4eca01099 --- /dev/null +++ b/tests/http/config/mutation-nested.graphql @@ -0,0 +1,35 @@ +schema @server @upstream(baseURL: "http://jsonplaceholder.typicode.com") { + query: Query + mutation: Mutation +} + +input PostInput { + body: String + title: String +} + +input UserInput { + name: String + email: String + updatePost(input: PostInput): Post @http(body: "{{args.input}}", method: "POST", path: "/posts") +} + +type Mutation { + insertUser(input: UserInput): User @http(body: "{{args.input}}", method: "POST", path: "/users") +} + +type User { + id: Int + name: String + post: Post @http(body: "{{args}}", method: "POST", path: "/posts") +} + +type Post { + body: String + id: Int + title: String +} + +type Query { + firstUser: User @http(method: "GET", path: "/users/1") +} diff --git a/tests/http/mutation-nested.yml b/tests/http/mutation-nested.yml new file mode 100644 index 0000000000..af30aff397 --- /dev/null +++ b/tests/http/mutation-nested.yml @@ -0,0 +1,34 @@ +--- +config: !file tests/http/config/mutation-nested.graphql +name: Mutation nested + +mock: + - request: + method: POST + url: http://jsonplaceholder.typicode.com/posts + body: '{"body":"post-body","title":"post-title"}' + response: + body: + title: "post-title" + body: "post-body" + userId: 1 + - request: + method: POST + url: http://jsonplaceholder.typicode.com/users + body: '{"email":"user-email","name":"user-name","post":{"body":"post-body","title":"post-title"}}' + response: + body: + name: "user-name" + email: "user-email" + +assert: + - request: + method: POST + url: http://localhost:8080/graphql + body: + query: 'mutation { insertUser(input: { name: "user-name", email: "user-email" }) { updatePost({ body: "post-body", title: "post-title" }) { body } } }' + response: + body: + data: + insertUser: + name: "user-namee" From ac8c49e8c1f5e6614374d508981d60e38e10c239 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 17 Jan 2024 10:49:38 -0300 Subject: [PATCH 48/92] docs: update args typings --- examples/.tailcallrc.graphql | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/examples/.tailcallrc.graphql b/examples/.tailcallrc.graphql index 7d8114330c..3830901ca1 100644 --- a/examples/.tailcallrc.graphql +++ b/examples/.tailcallrc.graphql @@ -315,15 +315,29 @@ The @call operator is used to reference a resolver operator (available resolvers """ directive @call( """ - The name of the field that has the `@http` resolver to be called. + The name of the field that has the resolver to be called. """ query: String! + # """ + # The name of the field that has the resolver to be called. + # """ + # mutation: String! + # """ + The arguments to be replace the values on the actual resolver. """ - The arguments to be replace the `@http` resolver args. + args: Args """ - args: JSON ) on FIELD_DEFINITION +"""An union between JSON and ExprArgs""" +union Args = JSON | ExprArgs + +input ExprArgs { + http: ExprHttp + grpc: ExprGrpc + graphQL: ExprGraphQL +} + """ Allows composing operators as simple expressions """ @@ -378,6 +392,8 @@ input ExprIf { else: ExprBody! } +input + """ Arguments for the http-node in expression AST. Same as the @http directive. """ From 41a70be3c215025f7a79f6b44abf4c8b1b2b38da Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 17 Jan 2024 10:50:03 -0300 Subject: [PATCH 49/92] chore: uncomment simple working solution --- src/blueprint/from_config/definitions.rs | 6 +- src/blueprint/from_config/operators/call.rs | 162 ++++++++++++++------ 2 files changed, 114 insertions(+), 54 deletions(-) diff --git a/src/blueprint/from_config/definitions.rs b/src/blueprint/from_config/definitions.rs index 1c4ae1ddc4..55bcea6f4c 100644 --- a/src/blueprint/from_config/definitions.rs +++ b/src/blueprint/from_config/definitions.rs @@ -9,6 +9,7 @@ use crate::blueprint::*; use crate::config; use crate::config::{Config, Field, GraphQLOperationType, Union}; use crate::directive::DirectiveCodec; +use crate::lambda::Unsafe; use crate::lambda::{Expression, Lambda}; use crate::try_fold::TryFold; use crate::valid::Valid; @@ -349,11 +350,6 @@ fn to_fields(object_name: &str, type_of: &config::Type, config: &Config) -> Vali .and(validate_call(&operation_type).trace(config::Call::trace_name().as_str())) .and(update_nested_resolvers()) .try_fold(&(config, field, type_of, name), FieldDefinition::default()) - .and_then(|b_field| { - println!("b_field.resolver: {:?}", b_field.resolver); - - Valid::succeed(b_field) - }) }; // Process fields that are not marked as `omit` diff --git a/src/blueprint/from_config/operators/call.rs b/src/blueprint/from_config/operators/call.rs index b75bedf555..e7a8800777 100644 --- a/src/blueprint/from_config/operators/call.rs +++ b/src/blueprint/from_config/operators/call.rs @@ -2,11 +2,75 @@ use crate::blueprint::*; use crate::config; use crate::config::{Config, Field, GraphQLOperationType}; use crate::directive::DirectiveCodec; +use crate::lambda::Expression; +use crate::lambda::Unsafe; // use crate::mustache::Mustache; use crate::try_fold::TryFold; use crate::valid::Valid; // use crate::valid::ValidationError; +// fn fail_if_has_call(field: &Field, b_field: FieldDefinition) -> Valid { +// if field.call.is_some().clone() { +// Valid::fail("Resolver is not defined".to_string()) +// } else { +// Valid::succeed(b_field) +// } +// .trace(config::Call::trace_name().as_str()) +// } + +// pub fn build_call<'a>( +// config: &'a Config, +// field: &'a Field, +// type_of: &'a config::Type, +// ) -> impl Fn(FieldDefinition) -> Valid + 'a { +// |b_field: FieldDefinition| { +// let Some(resolver) = b_field.resolver.clone() else { +// return fail_if_has_call(field, b_field); +// }; + +// field +// .call +// .clone() +// .unwrap() +// .args +// .iter() +// .fold(resolver.clone(), |resolver, (key, value)| match resolver.clone() { +// Expression::Unsafe(Unsafe::Http { req_template, .. }) => { +// todo!() +// } +// Expression::Unsafe(Unsafe::GraphQLEndpoint { req_template, .. }) => todo!(), +// Expression::Unsafe(Unsafe::Grpc { .. }) => todo!("grpc not implemented yet"), +// _ => resolver, +// }); + +// Valid::succeed(b_field) + +// // match resolver { +// // Expression::Unsafe(Unsafe::Http { req_template, .. }) => { +// // req_template. +// // } +// // _ => fail_if_has_call(field, b_field), +// // } + +// // .and_then(|b_field| { +// // let Some(resolver) = b_field.resolver else { +// // return Valid::fail("Resolver is not defined".to_string()); +// // }; + +// // match resolver { +// // Expression::Unsafe(Unsafe::Http { req_template, .. }) => { + +// // } +// // } +// // }) +// // .and_then(|b_field| { +// // b_field +// // .validate_field(type_of, config) +// // .trace(config::Call::trace_name().as_str()) +// // }) +// } +// } + pub fn validate_call( operation_type: &GraphQLOperationType, ) -> TryFold<'_, (&Config, &Field, &config::Type, &str), FieldDefinition, String> { @@ -41,55 +105,55 @@ pub fn validate_call( } }) }) - .map_to(b_field) - // .and_then(|(field, field_name, args)| { - // args.fold(Valid::succeed(field.clone()), |field, (key, value)| { - // field.and_then(|field| { - // // not sure if the code below will be useful - // // TO-DO: remove if not needed - // // let mustache = Mustache::parse(value.as_str()).map_err(|e| ValidationError::new(e.to_string())).unwrap(); - // // println!("mustache: {:?}", mustache); - // // println!("field: {:?}", field); - - // if let Some(http) = field.clone().http.as_mut() { - // let value = value.replace("{{", "").replace("}}", ""); - - // http.path = http.path.replace(format!("args.{}", key).as_str(), value.as_str()); - - // let field = Field { http: Some(http.clone()), ..field.clone() }; - - // Valid::succeed(field) - // } else if let Some(graphql) = field.clone().graphql.as_mut() { - // graphql.args = graphql.args.clone().map(|mut args| { - // args.0.iter_mut().for_each(|(k, v)| { - // if k == key { - // *v = value.clone(); - // } - // }); - - // args - // }); - - // let field = Field { graphql: Some(graphql.clone()), ..field.clone() }; - - // Valid::succeed(field) - // } else if let Some(_grpc) = field.clone().grpc.as_mut() { - // todo!("grpc not implemented yet"); - // } else { - // Valid::fail(format!("{} field does not have an http resolver", field_name)) - // } - // }) - // }) - // }) - // .and_then(|_field| { - // TryFold::<(&Config, &Field, &config::Type, &str), FieldDefinition, String>::new(|_, b_field| { - // Valid::succeed(b_field) - // }) - // .and(update_http().trace(config::Http::trace_name().as_str())) - // .and(update_grpc(operation_type).trace(config::Grpc::trace_name().as_str())) - // .and(update_graphql(operation_type).trace(config::GraphQL::trace_name().as_str())) - // .try_fold(&(config, &_field, type_of, name), b_field) - // }) + // .map_to(b_field) + .and_then(|(field, field_name, args)| { + args.fold(Valid::succeed(field.clone()), |field, (key, value)| { + field.and_then(|field| { + // not sure if the code below will be useful + // TO-DO: remove if not needed + // let mustache = Mustache::parse(value.as_str()).map_err(|e| ValidationError::new(e.to_string())).unwrap(); + // println!("mustache: {:?}", mustache); + // println!("field: {:?}", field); + + if let Some(http) = field.clone().http.as_mut() { + let value = value.replace("{{", "").replace("}}", ""); + + http.path = http.path.replace(format!("args.{}", key).as_str(), value.as_str()); + + let field = Field { http: Some(http.clone()), ..field.clone() }; + + Valid::succeed(field) + } else if let Some(graphql) = field.clone().graphql.as_mut() { + graphql.args = graphql.args.clone().map(|mut args| { + args.0.iter_mut().for_each(|(k, v)| { + if k == key { + *v = value.clone(); + } + }); + + args + }); + + let field = Field { graphql: Some(graphql.clone()), ..field.clone() }; + + Valid::succeed(field) + } else if let Some(_grpc) = field.clone().grpc.as_mut() { + todo!("grpc not implemented yet"); + } else { + Valid::fail(format!("{} field does not have an http resolver", field_name)) + } + }) + }) + }) + .and_then(|_field| { + TryFold::<(&Config, &Field, &config::Type, &str), FieldDefinition, String>::new(|_, b_field| { + Valid::succeed(b_field) + }) + .and(update_http().trace(config::Http::trace_name().as_str())) + .and(update_grpc(operation_type).trace(config::Grpc::trace_name().as_str())) + .and(update_graphql(operation_type).trace(config::GraphQL::trace_name().as_str())) + .try_fold(&(config, &_field, type_of, name), b_field) + }) }, ) } From 305e84ca2fa9a3fefe3efaa784a8602ba3872884 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Thu, 18 Jan 2024 01:11:25 -0300 Subject: [PATCH 50/92] feat: fail with "no argument found" message --- src/blueprint/from_config/definitions.rs | 141 ++++++++++++++++++++++- 1 file changed, 140 insertions(+), 1 deletion(-) diff --git a/src/blueprint/from_config/definitions.rs b/src/blueprint/from_config/definitions.rs index 55bcea6f4c..d93922df91 100644 --- a/src/blueprint/from_config/definitions.rs +++ b/src/blueprint/from_config/definitions.rs @@ -11,6 +11,7 @@ use crate::config::{Config, Field, GraphQLOperationType, Union}; use crate::directive::DirectiveCodec; use crate::lambda::Unsafe; use crate::lambda::{Expression, Lambda}; +use crate::mustache::Mustache; use crate::try_fold::TryFold; use crate::valid::Valid; @@ -347,9 +348,147 @@ fn to_fields(object_name: &str, type_of: &config::Type, config: &Config) -> Vali .and(update_graphql(&operation_type).trace(config::GraphQL::trace_name().as_str())) .and(update_expr(&operation_type).trace(config::Expr::trace_name().as_str())) .and(update_modify().trace(config::Modify::trace_name().as_str())) - .and(validate_call(&operation_type).trace(config::Call::trace_name().as_str())) + // .and(validate_call(&operation_type).trace(config::Call::trace_name().as_str())) .and(update_nested_resolvers()) .try_fold(&(config, field, type_of, name), FieldDefinition::default()) + .and_then(|b_field| { + let Some(call) = &field.call else { + return Valid::succeed(b_field); + }; + + if validate_field_has_resolver(name, field, &config.types).is_succeed() { + return Valid::fail(format!( + "@call directive is not allowed on field {} because it already has a resolver", + name + )) + .trace(config::Call::trace_name().as_str()); + } + + Valid::from_option(call.query.clone(), "call must have query".to_string()) + .and_then(|field_name| { + Valid::from_option(config.find_type("Query"), "Query type not found on config".to_string()) + .zip(Valid::succeed(field_name)) + }) + .and_then(|(query_type, field_name)| { + Valid::from_option( + query_type.fields.get(&field_name), + format!("{} field not found", field_name), + ) + .zip(Valid::succeed(field_name)) + // .and_then(|field| { + // if field.has_resolver() { + // Valid::succeed((field, field_name, call.args.iter())) + // } else { + // Valid::fail(format!("{} field has no resolver", field_name)) + // } + // }) + }) + .zip(Valid::succeed((call.args.iter(), b_field.resolver.clone()))) + .and_then(|((_field, field_name), (args, resolver))| { + let empties: Vec<(&String, &config::Arg)> = _field + .args + .iter() + .filter(|(k, _)| args.clone().find(|(k1, _)| k == k1).is_none()) + .collect(); + + if empties.len().gt(&0) { + return Valid::fail(format!( + "no argument {} found", + empties + .iter() + .map(|(k, _)| format!("'{}'", k)) + .collect::>() + .join(", ") + )); + } + + let Some(resolver) = resolver else { + return Valid::fail(format!("{} field has no resolver", field_name)); + }; + + Valid::succeed(b_field.clone()) + }) + .map_to(b_field) + // .zip(Valid::succeed(resolver)) + // .and_then(|(data , other)| {}) + // .and_then(|((field, field_name, args), resolver)| { + // match resolver { + // Expression::Unsafe(Unsafe::Http { req_template, .. }) => { + // todo!() + // } + // Expression::Unsafe(Unsafe::GraphQLEndpoint { mut req_template, batch, field_name, dl_id }) => { + // // req_template.operation_arguments = Some( + // // req_template + // // .operation_arguments + // // .iter() + // // .map(|data| { + // // data.clone() + // // // data + // // // .iter() + // // // .map(|(k, v)| { + // // // if k == key { + // // // (k.clone(), Mustache::parse(&value).unwrap()) + // // // } else { + // // // (k.clone(), v.clone()) + // // // } + // // // }) + // // // .collect::>() + // // }) + // // .collect(), + // if let Some(operation_arguments) = &mut req_template.operation_arguments { + // todo!("graphql endpoint") + // // Valid::succeed(operation_arguments).and_then(|operation_arguments| { + // // Valid::succeed(operation_arguments.iter()) + // // .and_then(|operation_arguments| operation_arguments.map(|(key, value)| (key, value))) + // // .map_to(b_field) + // // operation_arguments.iter_mut().for_each(|(key, value)| { + // // let arg = args.clone().find(|(k, _)| k == &key); + // // if let Some((_, _value)) = arg { + // // *value = Mustache::parse(&_value).unwrap(); + // // req_template.operation_arguments = Some(operation_arguments.clone()); + // // } else { + // // return Valid::fail(format!("{} argument not found", key)); + // // } + // // }); + // // }) + // // Valid::succeed(operation_arguments.iter_mut()) + // // .and_then(|operation_arguments| { + // // Valid::succeed(operation_arguments.map(|(key, value)| { + // // let arg = args.clone().find(|(k, _)| k == &key); + // // if let Some((_, _value)) = arg { + // // *value = Mustache::parse(&_value).unwrap(); + // // Valid::succeed(operation_arguments.clone()) + // // } else { + // // return Valid::fail(format!("{} argument not found", key)); + // // } + // // })) + // // }) + // // .map_to(b_field) + // // Valid::succeed( + // // operation_arguments + // // .iter() + // // .map(|(key, value)| { + // // let arg = args.clone().find(|(k, _)| k == &key); + // // if let Some((_, _value)) = arg { + // // Valid::succeed((key.clone(), Mustache::parse(&_value).unwrap())) + // // } else { + // // Valid::fail(format!("{} argument not found", key)) + // // } + // // }) + // // .collect::>>(), + // // ) + // // .and_then(|operation_arguments| {}) + // // .map_to(b_field.clone()) + // } else { + // Valid::succeed(b_field.clone()) + // } + // } + // Expression::Unsafe(Unsafe::Grpc { .. }) => todo!("grpc not implemented yet"), + // _ => Valid::succeed(b_field.clone()), + // } + // }) + .trace(config::Call::trace_name().as_str()) + }) }; // Process fields that are not marked as `omit` From ad4730f602cd8ecd42e0a547284884412baa0e99 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Thu, 18 Jan 2024 01:15:21 -0300 Subject: [PATCH 51/92] feat: add field name on trace --- src/blueprint/from_config/definitions.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/blueprint/from_config/definitions.rs b/src/blueprint/from_config/definitions.rs index d93922df91..eebbd50ecd 100644 --- a/src/blueprint/from_config/definitions.rs +++ b/src/blueprint/from_config/definitions.rs @@ -399,7 +399,8 @@ fn to_fields(object_name: &str, type_of: &config::Type, config: &Config) -> Vali .map(|(k, _)| format!("'{}'", k)) .collect::>() .join(", ") - )); + )) + .trace(field_name.as_str()); } let Some(resolver) = resolver else { From 79e2d9dd4a0d81d1acb734303b3d62410053546b Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Thu, 18 Jan 2024 01:16:26 -0300 Subject: [PATCH 52/92] test: add mismatch graphql args test --- tests/graphql/errors/test-call-operator.graphql | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/graphql/errors/test-call-operator.graphql b/tests/graphql/errors/test-call-operator.graphql index 38dc68c3d2..f40e8c44f5 100644 --- a/tests/graphql/errors/test-call-operator.graphql +++ b/tests/graphql/errors/test-call-operator.graphql @@ -7,8 +7,7 @@ type Query { posts: [Post] @http(path: "/posts") userWithoutResolver(id: Int!): User user(id: Int!): User @http(path: "/users/{{args.id}}") - userWithGraphQLResolver(id: Int!): User - @graphQL(name: "user", args: [{key: "id", value: "{{args.id}}"}]) + userWithGraphQLResolver(id: Int!): User @graphQL(name: "user", args: [{key: "id", value: "{{args.id}}"}]) } type User { @@ -21,12 +20,17 @@ type Post { multipleResolvers: User @http(path: "/users/{{value.userId}}") @call(query: "user", args: {id: "{{value.userId}}"}) withoutOperator: User @call(args: {id: "{{value.userId}}"}) argumentMismatchHttp: User @call(query: "user", args: {}) - # invalidResolver: User @call(query: "userWithGraphQLResolver", args: {id: "{{value.userId}}"}) + argumentMismatchGraphQL: User @call(query: "userWithGraphQLResolver", args: {}) } #> client-sdl type Failure @error(message: "No resolver has been found in the schema", trace: ["Query", "userWithoutResolver"]) -type Failure @error(message: "no argument 'id' found", trace: ["Post", "argumentMismatchHttp", "@call", "@http", "path"]) +type Failure + @error( + message: "no argument 'id' found" + trace: ["Post", "argumentMismatchGraphQL", "@call", "userWithGraphQLResolver"] + ) +type Failure @error(message: "no argument 'id' found", trace: ["Post", "argumentMismatchHttp", "@call", "user"]) type Failure @error( message: "@call directive is not allowed on field multipleResolvers because it already has a resolver" From cf8af4921ffb10cfc0aaaa8e961dcdba05ce5b94 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Thu, 18 Jan 2024 03:08:28 -0300 Subject: [PATCH 53/92] feat: move implementation to inside call operator and implement graphql/http --- src/blueprint/from_config/definitions.rs | 143 +------------- src/blueprint/from_config/operators/call.rs | 202 +++++++++----------- src/mustache.rs | 6 + 3 files changed, 95 insertions(+), 256 deletions(-) diff --git a/src/blueprint/from_config/definitions.rs b/src/blueprint/from_config/definitions.rs index eebbd50ecd..3a008ca768 100644 --- a/src/blueprint/from_config/definitions.rs +++ b/src/blueprint/from_config/definitions.rs @@ -9,9 +9,7 @@ use crate::blueprint::*; use crate::config; use crate::config::{Config, Field, GraphQLOperationType, Union}; use crate::directive::DirectiveCodec; -use crate::lambda::Unsafe; use crate::lambda::{Expression, Lambda}; -use crate::mustache::Mustache; use crate::try_fold::TryFold; use crate::valid::Valid; @@ -348,148 +346,9 @@ fn to_fields(object_name: &str, type_of: &config::Type, config: &Config) -> Vali .and(update_graphql(&operation_type).trace(config::GraphQL::trace_name().as_str())) .and(update_expr(&operation_type).trace(config::Expr::trace_name().as_str())) .and(update_modify().trace(config::Modify::trace_name().as_str())) - // .and(validate_call(&operation_type).trace(config::Call::trace_name().as_str())) + .and(update_call(&operation_type).trace(config::Call::trace_name().as_str())) .and(update_nested_resolvers()) .try_fold(&(config, field, type_of, name), FieldDefinition::default()) - .and_then(|b_field| { - let Some(call) = &field.call else { - return Valid::succeed(b_field); - }; - - if validate_field_has_resolver(name, field, &config.types).is_succeed() { - return Valid::fail(format!( - "@call directive is not allowed on field {} because it already has a resolver", - name - )) - .trace(config::Call::trace_name().as_str()); - } - - Valid::from_option(call.query.clone(), "call must have query".to_string()) - .and_then(|field_name| { - Valid::from_option(config.find_type("Query"), "Query type not found on config".to_string()) - .zip(Valid::succeed(field_name)) - }) - .and_then(|(query_type, field_name)| { - Valid::from_option( - query_type.fields.get(&field_name), - format!("{} field not found", field_name), - ) - .zip(Valid::succeed(field_name)) - // .and_then(|field| { - // if field.has_resolver() { - // Valid::succeed((field, field_name, call.args.iter())) - // } else { - // Valid::fail(format!("{} field has no resolver", field_name)) - // } - // }) - }) - .zip(Valid::succeed((call.args.iter(), b_field.resolver.clone()))) - .and_then(|((_field, field_name), (args, resolver))| { - let empties: Vec<(&String, &config::Arg)> = _field - .args - .iter() - .filter(|(k, _)| args.clone().find(|(k1, _)| k == k1).is_none()) - .collect(); - - if empties.len().gt(&0) { - return Valid::fail(format!( - "no argument {} found", - empties - .iter() - .map(|(k, _)| format!("'{}'", k)) - .collect::>() - .join(", ") - )) - .trace(field_name.as_str()); - } - - let Some(resolver) = resolver else { - return Valid::fail(format!("{} field has no resolver", field_name)); - }; - - Valid::succeed(b_field.clone()) - }) - .map_to(b_field) - // .zip(Valid::succeed(resolver)) - // .and_then(|(data , other)| {}) - // .and_then(|((field, field_name, args), resolver)| { - // match resolver { - // Expression::Unsafe(Unsafe::Http { req_template, .. }) => { - // todo!() - // } - // Expression::Unsafe(Unsafe::GraphQLEndpoint { mut req_template, batch, field_name, dl_id }) => { - // // req_template.operation_arguments = Some( - // // req_template - // // .operation_arguments - // // .iter() - // // .map(|data| { - // // data.clone() - // // // data - // // // .iter() - // // // .map(|(k, v)| { - // // // if k == key { - // // // (k.clone(), Mustache::parse(&value).unwrap()) - // // // } else { - // // // (k.clone(), v.clone()) - // // // } - // // // }) - // // // .collect::>() - // // }) - // // .collect(), - // if let Some(operation_arguments) = &mut req_template.operation_arguments { - // todo!("graphql endpoint") - // // Valid::succeed(operation_arguments).and_then(|operation_arguments| { - // // Valid::succeed(operation_arguments.iter()) - // // .and_then(|operation_arguments| operation_arguments.map(|(key, value)| (key, value))) - // // .map_to(b_field) - // // operation_arguments.iter_mut().for_each(|(key, value)| { - // // let arg = args.clone().find(|(k, _)| k == &key); - // // if let Some((_, _value)) = arg { - // // *value = Mustache::parse(&_value).unwrap(); - // // req_template.operation_arguments = Some(operation_arguments.clone()); - // // } else { - // // return Valid::fail(format!("{} argument not found", key)); - // // } - // // }); - // // }) - // // Valid::succeed(operation_arguments.iter_mut()) - // // .and_then(|operation_arguments| { - // // Valid::succeed(operation_arguments.map(|(key, value)| { - // // let arg = args.clone().find(|(k, _)| k == &key); - // // if let Some((_, _value)) = arg { - // // *value = Mustache::parse(&_value).unwrap(); - // // Valid::succeed(operation_arguments.clone()) - // // } else { - // // return Valid::fail(format!("{} argument not found", key)); - // // } - // // })) - // // }) - // // .map_to(b_field) - // // Valid::succeed( - // // operation_arguments - // // .iter() - // // .map(|(key, value)| { - // // let arg = args.clone().find(|(k, _)| k == &key); - // // if let Some((_, _value)) = arg { - // // Valid::succeed((key.clone(), Mustache::parse(&_value).unwrap())) - // // } else { - // // Valid::fail(format!("{} argument not found", key)) - // // } - // // }) - // // .collect::>>(), - // // ) - // // .and_then(|operation_arguments| {}) - // // .map_to(b_field.clone()) - // } else { - // Valid::succeed(b_field.clone()) - // } - // } - // Expression::Unsafe(Unsafe::Grpc { .. }) => todo!("grpc not implemented yet"), - // _ => Valid::succeed(b_field.clone()), - // } - // }) - .trace(config::Call::trace_name().as_str()) - }) }; // Process fields that are not marked as `omit` diff --git a/src/blueprint/from_config/operators/call.rs b/src/blueprint/from_config/operators/call.rs index e7a8800777..203450d8eb 100644 --- a/src/blueprint/from_config/operators/call.rs +++ b/src/blueprint/from_config/operators/call.rs @@ -1,81 +1,21 @@ +use std::collections::BTreeMap; + use crate::blueprint::*; use crate::config; +use crate::config::KeyValues; use crate::config::{Config, Field, GraphQLOperationType}; -use crate::directive::DirectiveCodec; use crate::lambda::Expression; use crate::lambda::Unsafe; -// use crate::mustache::Mustache; +use crate::mustache::Mustache; +use crate::mustache::Segment; use crate::try_fold::TryFold; use crate::valid::Valid; -// use crate::valid::ValidationError; - -// fn fail_if_has_call(field: &Field, b_field: FieldDefinition) -> Valid { -// if field.call.is_some().clone() { -// Valid::fail("Resolver is not defined".to_string()) -// } else { -// Valid::succeed(b_field) -// } -// .trace(config::Call::trace_name().as_str()) -// } - -// pub fn build_call<'a>( -// config: &'a Config, -// field: &'a Field, -// type_of: &'a config::Type, -// ) -> impl Fn(FieldDefinition) -> Valid + 'a { -// |b_field: FieldDefinition| { -// let Some(resolver) = b_field.resolver.clone() else { -// return fail_if_has_call(field, b_field); -// }; - -// field -// .call -// .clone() -// .unwrap() -// .args -// .iter() -// .fold(resolver.clone(), |resolver, (key, value)| match resolver.clone() { -// Expression::Unsafe(Unsafe::Http { req_template, .. }) => { -// todo!() -// } -// Expression::Unsafe(Unsafe::GraphQLEndpoint { req_template, .. }) => todo!(), -// Expression::Unsafe(Unsafe::Grpc { .. }) => todo!("grpc not implemented yet"), -// _ => resolver, -// }); - -// Valid::succeed(b_field) - -// // match resolver { -// // Expression::Unsafe(Unsafe::Http { req_template, .. }) => { -// // req_template. -// // } -// // _ => fail_if_has_call(field, b_field), -// // } -// // .and_then(|b_field| { -// // let Some(resolver) = b_field.resolver else { -// // return Valid::fail("Resolver is not defined".to_string()); -// // }; - -// // match resolver { -// // Expression::Unsafe(Unsafe::Http { req_template, .. }) => { - -// // } -// // } -// // }) -// // .and_then(|b_field| { -// // b_field -// // .validate_field(type_of, config) -// // .trace(config::Call::trace_name().as_str()) -// // }) -// } -// } - -pub fn validate_call( +pub fn update_call( operation_type: &GraphQLOperationType, ) -> TryFold<'_, (&Config, &Field, &config::Type, &str), FieldDefinition, String> { TryFold::<(&Config, &Field, &config::Type, &str), FieldDefinition, String>::new( - move |(config, field, type_of, name), b_field| { + move |(config, field, _type_of, name), b_field| { let Some(call) = &field.call else { return Valid::succeed(b_field); }; @@ -97,7 +37,8 @@ pub fn validate_call( query_type.fields.get(&field_name), format!("{} field not found", field_name), ) - .and_then(|field| { + .zip(Valid::succeed(field_name)) + .and_then(|(field, field_name)| { if field.has_resolver() { Valid::succeed((field, field_name, call.args.iter())) } else { @@ -105,54 +46,87 @@ pub fn validate_call( } }) }) - // .map_to(b_field) - .and_then(|(field, field_name, args)| { - args.fold(Valid::succeed(field.clone()), |field, (key, value)| { - field.and_then(|field| { - // not sure if the code below will be useful - // TO-DO: remove if not needed - // let mustache = Mustache::parse(value.as_str()).map_err(|e| ValidationError::new(e.to_string())).unwrap(); - // println!("mustache: {:?}", mustache); - // println!("field: {:?}", field); - - if let Some(http) = field.clone().http.as_mut() { - let value = value.replace("{{", "").replace("}}", ""); - - http.path = http.path.replace(format!("args.{}", key).as_str(), value.as_str()); - - let field = Field { http: Some(http.clone()), ..field.clone() }; - - Valid::succeed(field) - } else if let Some(graphql) = field.clone().graphql.as_mut() { - graphql.args = graphql.args.clone().map(|mut args| { - args.0.iter_mut().for_each(|(k, v)| { - if k == key { - *v = value.clone(); - } - }); - - args - }); - - let field = Field { graphql: Some(graphql.clone()), ..field.clone() }; - - Valid::succeed(field) - } else if let Some(_grpc) = field.clone().grpc.as_mut() { - todo!("grpc not implemented yet"); - } else { - Valid::fail(format!("{} field does not have an http resolver", field_name)) + .and_then(|(_field, field_name, args)| { + let empties: Vec<(&String, &config::Arg)> = _field + .args + .iter() + .filter(|(k, _)| args.clone().find(|(k1, _)| k == k1).is_none()) + .collect(); + + if empties.len().gt(&0) { + return Valid::fail(format!( + "no argument {} found", + empties + .iter() + .map(|(k, _)| format!("'{}'", k)) + .collect::>() + .join(", ") + )) + .trace(field_name.as_str()); + } + + if let Some(http) = _field.http.clone() { + compile_http(config, field, &http).and_then(|expr| match expr.clone() { + Expression::Unsafe(Unsafe::Http { mut req_template, group_by, dl_id }) => { + req_template = req_template.clone().root_url( + req_template + .root_url + .get_segments() + .iter() + .map(|segment| { + match segment { + Segment::Literal(literal) => Segment::Literal(literal.clone()), + Segment::Expression(expression) => { + if expression[0] == "args" { + // this value will always be present because we already checked it + let (_, value) = args.clone().find(|(k, _)| **k == expression[1]).unwrap(); + let item = Mustache::parse(value).unwrap(); + + let expression = item.get_segments().first().unwrap().to_owned().to_owned(); + + expression + } else { + Segment::Expression(expression.clone()) + } + } + } + }) + .collect::>() + .into(), + ); + + Valid::succeed(Expression::Unsafe(Unsafe::Http { req_template, group_by, dl_id })) } + _ => Valid::succeed(expr), }) - }) - }) - .and_then(|_field| { - TryFold::<(&Config, &Field, &config::Type, &str), FieldDefinition, String>::new(|_, b_field| { - Valid::succeed(b_field) - }) - .and(update_http().trace(config::Http::trace_name().as_str())) - .and(update_grpc(operation_type).trace(config::Grpc::trace_name().as_str())) - .and(update_graphql(operation_type).trace(config::GraphQL::trace_name().as_str())) - .try_fold(&(config, &_field, type_of, name), b_field) + } else if let Some(mut graphql) = _field.graphql.clone() { + if let Some(mut _args) = graphql.args { + let mut updated: BTreeMap = BTreeMap::new(); + + for (key, value) in _args.0 { + let found = args + .clone() + .into_iter() + .find_map(|(k, v)| if *k == key { Some(v) } else { None }); + + if let Some(v) = found { + updated.insert(key, v.clone().to_string()); + } else { + updated.insert(key, value); + } + } + + graphql.args = Some(KeyValues(updated)); + } + compile_graphql(config, operation_type, &graphql) + } else if let Some(grpc) = _field.grpc.clone() { + let inputs: CompileGrpc<'_> = + CompileGrpc { config, operation_type, field, grpc: &grpc, validate_with_schema: false }; + compile_grpc(inputs) + } else { + return Valid::fail(format!("{} field has no resolver", field_name)); + } + .and_then(|resolver| Valid::succeed(b_field.resolver(Some(resolver)))) }) }, ) diff --git a/src/mustache.rs b/src/mustache.rs index c8d24dacb1..8168db354c 100644 --- a/src/mustache.rs +++ b/src/mustache.rs @@ -64,6 +64,12 @@ impl Mustache { } } + pub fn get_segments(&self) -> Vec<&Segment> { + match self { + Mustache(segments) => segments.iter().collect(), + } + } + pub fn expression_segments(&self) -> Vec<&Vec> { match self { Mustache(segments) => segments From 0f0cbc70e2e9c3298c120f18ef3bf9196a6ac652 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Thu, 18 Jan 2024 03:10:22 -0300 Subject: [PATCH 54/92] test: delete broken mutation nested tests --- tests/http/config/mutation-nested.graphql | 35 ----------------------- tests/http/mutation-nested.yml | 34 ---------------------- 2 files changed, 69 deletions(-) delete mode 100644 tests/http/config/mutation-nested.graphql delete mode 100644 tests/http/mutation-nested.yml diff --git a/tests/http/config/mutation-nested.graphql b/tests/http/config/mutation-nested.graphql deleted file mode 100644 index b4eca01099..0000000000 --- a/tests/http/config/mutation-nested.graphql +++ /dev/null @@ -1,35 +0,0 @@ -schema @server @upstream(baseURL: "http://jsonplaceholder.typicode.com") { - query: Query - mutation: Mutation -} - -input PostInput { - body: String - title: String -} - -input UserInput { - name: String - email: String - updatePost(input: PostInput): Post @http(body: "{{args.input}}", method: "POST", path: "/posts") -} - -type Mutation { - insertUser(input: UserInput): User @http(body: "{{args.input}}", method: "POST", path: "/users") -} - -type User { - id: Int - name: String - post: Post @http(body: "{{args}}", method: "POST", path: "/posts") -} - -type Post { - body: String - id: Int - title: String -} - -type Query { - firstUser: User @http(method: "GET", path: "/users/1") -} diff --git a/tests/http/mutation-nested.yml b/tests/http/mutation-nested.yml deleted file mode 100644 index af30aff397..0000000000 --- a/tests/http/mutation-nested.yml +++ /dev/null @@ -1,34 +0,0 @@ ---- -config: !file tests/http/config/mutation-nested.graphql -name: Mutation nested - -mock: - - request: - method: POST - url: http://jsonplaceholder.typicode.com/posts - body: '{"body":"post-body","title":"post-title"}' - response: - body: - title: "post-title" - body: "post-body" - userId: 1 - - request: - method: POST - url: http://jsonplaceholder.typicode.com/users - body: '{"email":"user-email","name":"user-name","post":{"body":"post-body","title":"post-title"}}' - response: - body: - name: "user-name" - email: "user-email" - -assert: - - request: - method: POST - url: http://localhost:8080/graphql - body: - query: 'mutation { insertUser(input: { name: "user-name", email: "user-email" }) { updatePost({ body: "post-body", title: "post-title" }) { body } } }' - response: - body: - data: - insertUser: - name: "user-namee" From 802abafc77a2ec8e4a97362ef1f9d91583ec56cb Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Thu, 18 Jan 2024 03:27:49 -0300 Subject: [PATCH 55/92] style: run `./lint.sh --mode=fix` --- examples/.tailcallrc.graphql | 3 +-- src/blueprint/from_config/operators/call.rs | 9 +++------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/examples/.tailcallrc.graphql b/examples/.tailcallrc.graphql index 3830901ca1..a71feaadaf 100644 --- a/examples/.tailcallrc.graphql +++ b/examples/.tailcallrc.graphql @@ -322,11 +322,10 @@ directive @call( # The name of the field that has the resolver to be called. # """ # mutation: String! - # """ + """ The arguments to be replace the values on the actual resolver. """ args: Args - """ ) on FIELD_DEFINITION """An union between JSON and ExprArgs""" diff --git a/src/blueprint/from_config/operators/call.rs b/src/blueprint/from_config/operators/call.rs index 203450d8eb..e15058be27 100644 --- a/src/blueprint/from_config/operators/call.rs +++ b/src/blueprint/from_config/operators/call.rs @@ -2,12 +2,9 @@ use std::collections::BTreeMap; use crate::blueprint::*; use crate::config; -use crate::config::KeyValues; -use crate::config::{Config, Field, GraphQLOperationType}; -use crate::lambda::Expression; -use crate::lambda::Unsafe; -use crate::mustache::Mustache; -use crate::mustache::Segment; +use crate::config::{Config, Field, GraphQLOperationType, KeyValues}; +use crate::lambda::{Expression, Unsafe}; +use crate::mustache::{Mustache, Segment}; use crate::try_fold::TryFold; use crate::valid::Valid; From 9185fb7e5916b5a5063b9cbc40ee33f3064b0a5a Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Thu, 18 Jan 2024 03:51:31 -0300 Subject: [PATCH 56/92] refactor: create method to find value --- src/blueprint/from_config/operators/call.rs | 43 ++++++++++----------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/src/blueprint/from_config/operators/call.rs b/src/blueprint/from_config/operators/call.rs index e15058be27..f944c6320d 100644 --- a/src/blueprint/from_config/operators/call.rs +++ b/src/blueprint/from_config/operators/call.rs @@ -1,3 +1,4 @@ +use std::collections::hash_map::Iter; use std::collections::BTreeMap; use crate::blueprint::*; @@ -8,6 +9,12 @@ use crate::mustache::{Mustache, Segment}; use crate::try_fold::TryFold; use crate::valid::Valid; +fn find_value<'a>(args: &'a Iter<'a, String, String>, key: &'a String) -> Option<&'a String> { + args + .clone() + .find_map(|(k, value)| if k == key { Some(value) } else { None }) +} + pub fn update_call( operation_type: &GraphQLOperationType, ) -> TryFold<'_, (&Config, &Field, &config::Type, &str), FieldDefinition, String> { @@ -70,21 +77,18 @@ pub fn update_call( .root_url .get_segments() .iter() - .map(|segment| { - match segment { - Segment::Literal(literal) => Segment::Literal(literal.clone()), - Segment::Expression(expression) => { - if expression[0] == "args" { - // this value will always be present because we already checked it - let (_, value) = args.clone().find(|(k, _)| **k == expression[1]).unwrap(); - let item = Mustache::parse(value).unwrap(); + .map(|segment| match segment { + Segment::Literal(literal) => Segment::Literal(literal.clone()), + Segment::Expression(expression) => { + if expression[0] == "args" { + let value = find_value(&args, &expression[1]).unwrap(); + let item = Mustache::parse(value).unwrap(); - let expression = item.get_segments().first().unwrap().to_owned().to_owned(); + let expression = item.get_segments().first().unwrap().to_owned().to_owned(); - expression - } else { - Segment::Expression(expression.clone()) - } + expression + } else { + Segment::Expression(expression.clone()) } } }) @@ -100,17 +104,10 @@ pub fn update_call( if let Some(mut _args) = graphql.args { let mut updated: BTreeMap = BTreeMap::new(); - for (key, value) in _args.0 { - let found = args - .clone() - .into_iter() - .find_map(|(k, v)| if *k == key { Some(v) } else { None }); + for (key, _) in _args.0 { + let value = find_value(&args, &key).unwrap(); - if let Some(v) = found { - updated.insert(key, v.clone().to_string()); - } else { - updated.insert(key, value); - } + updated.insert(key.clone(), value.to_string()); } graphql.args = Some(KeyValues(updated)); From f820d8972c519c054bc79730208ad572b90351ac Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Thu, 18 Jan 2024 03:54:21 -0300 Subject: [PATCH 57/92] docs: remove broken stuff from docs --- examples/.tailcallrc.graphql | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/.tailcallrc.graphql b/examples/.tailcallrc.graphql index a71feaadaf..8b4f0e219a 100644 --- a/examples/.tailcallrc.graphql +++ b/examples/.tailcallrc.graphql @@ -328,7 +328,9 @@ directive @call( args: Args ) on FIELD_DEFINITION -"""An union between JSON and ExprArgs""" +""" +An union between JSON and ExprArgs +""" union Args = JSON | ExprArgs input ExprArgs { @@ -391,8 +393,6 @@ input ExprIf { else: ExprBody! } -input - """ Arguments for the http-node in expression AST. Same as the @http directive. """ From e77fbc64dacc3d29c33722606378047be31e52c7 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Thu, 18 Jan 2024 03:57:07 -0300 Subject: [PATCH 58/92] refactor: use `!_.any()` instead of `_.find().is_none()` --- src/blueprint/from_config/operators/call.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blueprint/from_config/operators/call.rs b/src/blueprint/from_config/operators/call.rs index f944c6320d..430a975487 100644 --- a/src/blueprint/from_config/operators/call.rs +++ b/src/blueprint/from_config/operators/call.rs @@ -54,7 +54,7 @@ pub fn update_call( let empties: Vec<(&String, &config::Arg)> = _field .args .iter() - .filter(|(k, _)| args.clone().find(|(k1, _)| k == k1).is_none()) + .filter(|(k, _)| !args.clone().any(|(k1, _)| k1.eq(*k))) .collect(); if empties.len().gt(&0) { From 4fa7bf34aa7fd3b777c145c7d33fb3bf04699741 Mon Sep 17 00:00:00 2001 From: ologbonowiwi <100464352+ologbonowiwi@users.noreply.github.com> Date: Thu, 18 Jan 2024 04:02:07 -0300 Subject: [PATCH 59/92] Update examples/.tailcallrc.graphql --- examples/.tailcallrc.graphql | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/examples/.tailcallrc.graphql b/examples/.tailcallrc.graphql index 8b4f0e219a..3aa3ae213f 100644 --- a/examples/.tailcallrc.graphql +++ b/examples/.tailcallrc.graphql @@ -325,20 +325,9 @@ directive @call( """ The arguments to be replace the values on the actual resolver. """ - args: Args + args: JSON ) on FIELD_DEFINITION -""" -An union between JSON and ExprArgs -""" -union Args = JSON | ExprArgs - -input ExprArgs { - http: ExprHttp - grpc: ExprGrpc - graphQL: ExprGraphQL -} - """ Allows composing operators as simple expressions """ From 0fc8b69ef184e7ce0e51fce361955419c1014879 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Thu, 18 Jan 2024 04:08:35 -0300 Subject: [PATCH 60/92] test: add test case for header mismatch on graphql --- tests/graphql/errors/test-call-operator.graphql | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/graphql/errors/test-call-operator.graphql b/tests/graphql/errors/test-call-operator.graphql index f40e8c44f5..5ab2c5556e 100644 --- a/tests/graphql/errors/test-call-operator.graphql +++ b/tests/graphql/errors/test-call-operator.graphql @@ -8,6 +8,7 @@ type Query { userWithoutResolver(id: Int!): User user(id: Int!): User @http(path: "/users/{{args.id}}") userWithGraphQLResolver(id: Int!): User @graphQL(name: "user", args: [{key: "id", value: "{{args.id}}"}]) + userWithGraphQLHeaders(id: Int!): User @graphQL(name: "user", headers: [{key: "id", value: "{{args.id}}"}]) } type User { @@ -21,6 +22,7 @@ type Post { withoutOperator: User @call(args: {id: "{{value.userId}}"}) argumentMismatchHttp: User @call(query: "user", args: {}) argumentMismatchGraphQL: User @call(query: "userWithGraphQLResolver", args: {}) + headersMismatchGraphQL: User @call(query: "userWithGraphQLResolver", args: {}) } #> client-sdl @@ -31,6 +33,11 @@ type Failure trace: ["Post", "argumentMismatchGraphQL", "@call", "userWithGraphQLResolver"] ) type Failure @error(message: "no argument 'id' found", trace: ["Post", "argumentMismatchHttp", "@call", "user"]) +type Failure + @error( + message: "no argument 'id' found" + trace: ["Post", "headersMismatchGraphQL", "@call", "userWithGraphQLResolver"] + ) type Failure @error( message: "@call directive is not allowed on field multipleResolvers because it already has a resolver" From 8c8c93fbf14dbfc18b2426bf1e0b88bb4f3a6a95 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Thu, 18 Jan 2024 04:10:32 -0300 Subject: [PATCH 61/92] test: rename url mismatch test case name --- tests/graphql/errors/test-call-operator.graphql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/graphql/errors/test-call-operator.graphql b/tests/graphql/errors/test-call-operator.graphql index 5ab2c5556e..5be819d48e 100644 --- a/tests/graphql/errors/test-call-operator.graphql +++ b/tests/graphql/errors/test-call-operator.graphql @@ -20,7 +20,7 @@ type Post { withoutResolver: User @call(query: "userWithoutResolver", args: {id: "{{value.userId}}"}) multipleResolvers: User @http(path: "/users/{{value.userId}}") @call(query: "user", args: {id: "{{value.userId}}"}) withoutOperator: User @call(args: {id: "{{value.userId}}"}) - argumentMismatchHttp: User @call(query: "user", args: {}) + urlMismatchHttp: User @call(query: "user", args: {}) argumentMismatchGraphQL: User @call(query: "userWithGraphQLResolver", args: {}) headersMismatchGraphQL: User @call(query: "userWithGraphQLResolver", args: {}) } @@ -32,7 +32,6 @@ type Failure message: "no argument 'id' found" trace: ["Post", "argumentMismatchGraphQL", "@call", "userWithGraphQLResolver"] ) -type Failure @error(message: "no argument 'id' found", trace: ["Post", "argumentMismatchHttp", "@call", "user"]) type Failure @error( message: "no argument 'id' found" @@ -43,5 +42,6 @@ type Failure message: "@call directive is not allowed on field multipleResolvers because it already has a resolver" trace: ["Post", "multipleResolvers", "@call"] ) +type Failure @error(message: "no argument 'id' found", trace: ["Post", "urlMismatchHttp", "@call", "user"]) type Failure @error(message: "call must have query", trace: ["Post", "withoutOperator", "@call"]) type Failure @error(message: "userWithoutResolver field has no resolver", trace: ["Post", "withoutResolver", "@call"]) From ca46089bdd57c0da9f98574dfafc1d15f2bd7182 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Fri, 19 Jan 2024 02:13:41 -0300 Subject: [PATCH 62/92] style: run `./lint.sh --mode=fix` --- examples/.tailcallrc.schema.json | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/examples/.tailcallrc.schema.json b/examples/.tailcallrc.schema.json index 1b5c7703e8..60c39d6899 100644 --- a/examples/.tailcallrc.schema.json +++ b/examples/.tailcallrc.schema.json @@ -86,6 +86,21 @@ "required": ["maxAge"], "type": "object" }, + "Call": { + "properties": { + "args": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "query": { + "type": ["string", "null"] + } + }, + "required": ["args"], + "type": "object" + }, "Const": { "description": "The `@const` operators allows us to embed a constant response for the schema.", "properties": { @@ -775,6 +790,17 @@ ], "description": "Sets the cache configuration for a field" }, + "call": { + "anyOf": [ + { + "$ref": "#/definitions/Call" + }, + { + "type": "null" + } + ], + "description": "Inserts a call resolver for the field." + }, "const": { "anyOf": [ { From dc9b982e0cf542ae0e56d493fbf289e5f72dc17a Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Fri, 19 Jan 2024 03:08:32 -0300 Subject: [PATCH 63/92] docs: add docs for `Call` operator --- examples/.tailcallrc.schema.json | 477 ++++++++++++++++++++++++------- src/config/config.rs | 6 + 2 files changed, 382 insertions(+), 101 deletions(-) diff --git a/examples/.tailcallrc.schema.json b/examples/.tailcallrc.schema.json index 60c39d6899..71fe0b3a05 100644 --- a/examples/.tailcallrc.schema.json +++ b/examples/.tailcallrc.schema.json @@ -16,14 +16,20 @@ "type": "array" } }, - "required": ["name", "path"], + "required": [ + "name", + "path" + ], "type": "object" }, "Arg": { "properties": { "default_value": true, "doc": { - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "list": { "type": "boolean" @@ -45,7 +51,9 @@ "type": "string" } }, - "required": ["type"], + "required": [ + "type" + ], "type": "object" }, "Batch": { @@ -83,22 +91,32 @@ "type": "integer" } }, - "required": ["maxAge"], + "required": [ + "maxAge" + ], "type": "object" }, "Call": { + "description": "For instance, if you have a `user(id: Int!): User @http(path: \"/users/{{args.id}}\")` field on the `Query` type, you can reference it from another field on the `Query` type using the `@call` operator. So, on `Post.user` you can declare `user: User @call(query: \"user\", args: {id: \"{{value.userId}}\"})`, and this will replace the `{{args.id}}` used in the `@http` operator with the value of `userId` from the `Post` type.", "properties": { "args": { "additionalProperties": { "type": "string" }, + "description": "The arguments of the field on the `Query` type that you want to call. For instance `{id: \"{{value.userId}}\"}`.", "type": "object" }, "query": { - "type": ["string", "null"] + "description": "The name of the field on the `Query` type that you want to call. For instance `user`.", + "type": [ + "string", + "null" + ] } }, - "required": ["args"], + "required": [ + "args" + ], "type": "object" }, "Const": { @@ -106,11 +124,16 @@ "properties": { "data": true }, - "required": ["data"], + "required": [ + "data" + ], "type": "object" }, "Encoding": { - "enum": ["ApplicationJson", "ApplicationXWwwFormUrlencoded"], + "enum": [ + "ApplicationJson", + "ApplicationXWwwFormUrlencoded" + ], "type": "string" }, "Expr": { @@ -125,7 +148,9 @@ "description": "Root of the expression AST" } }, - "required": ["body"], + "required": [ + "body" + ], "type": "object" }, "ExprBody": { @@ -138,7 +163,9 @@ "$ref": "#/definitions/Http" } }, - "required": ["http"], + "required": [ + "http" + ], "type": "object" }, { @@ -149,7 +176,9 @@ "$ref": "#/definitions/Grpc" } }, - "required": ["grpc"], + "required": [ + "grpc" + ], "type": "object" }, { @@ -160,7 +189,22 @@ "$ref": "#/definitions/GraphQL" } }, - "required": ["graphQL"], + "required": [ + "graphQL" + ], + "type": "object" + }, + { + "additionalProperties": false, + "description": "Reuses a resolver pre-defined on type `Query`", + "properties": { + "call": { + "$ref": "#/definitions/Call" + } + }, + "required": [ + "call" + ], "type": "object" }, { @@ -169,7 +213,9 @@ "properties": { "const": true }, - "required": ["const"], + "required": [ + "const" + ], "type": "object" }, { @@ -203,11 +249,17 @@ "description": "Expression to evaluate if the condition is true" } }, - "required": ["cond", "else", "then"], + "required": [ + "cond", + "else", + "then" + ], "type": "object" } }, - "required": ["if"], + "required": [ + "if" + ], "type": "object" }, { @@ -220,7 +272,9 @@ "type": "array" } }, - "required": ["and"], + "required": [ + "and" + ], "type": "object" }, { @@ -233,7 +287,9 @@ "type": "array" } }, - "required": ["or"], + "required": [ + "or" + ], "type": "object" }, { @@ -266,7 +322,9 @@ "type": "array" } }, - "required": ["cond"], + "required": [ + "cond" + ], "type": "object" }, { @@ -286,7 +344,9 @@ "type": "array" } }, - "required": ["defaultTo"], + "required": [ + "defaultTo" + ], "type": "object" }, { @@ -296,7 +356,9 @@ "$ref": "#/definitions/ExprBody" } }, - "required": ["isEmpty"], + "required": [ + "isEmpty" + ], "type": "object" }, { @@ -306,7 +368,9 @@ "$ref": "#/definitions/ExprBody" } }, - "required": ["not"], + "required": [ + "not" + ], "type": "object" }, { @@ -319,7 +383,9 @@ "type": "array" } }, - "required": ["concat"], + "required": [ + "concat" + ], "type": "object" }, { @@ -332,7 +398,9 @@ "type": "array" } }, - "required": ["intersection"], + "required": [ + "intersection" + ], "type": "object" }, { @@ -358,7 +426,9 @@ "type": "array" } }, - "required": ["difference"], + "required": [ + "difference" + ], "type": "object" }, { @@ -378,7 +448,9 @@ "type": "array" } }, - "required": ["eq"], + "required": [ + "eq" + ], "type": "object" }, { @@ -398,7 +470,9 @@ "type": "array" } }, - "required": ["gt"], + "required": [ + "gt" + ], "type": "object" }, { @@ -418,7 +492,9 @@ "type": "array" } }, - "required": ["gte"], + "required": [ + "gte" + ], "type": "object" }, { @@ -438,7 +514,9 @@ "type": "array" } }, - "required": ["lt"], + "required": [ + "lt" + ], "type": "object" }, { @@ -458,7 +536,9 @@ "type": "array" } }, - "required": ["lte"], + "required": [ + "lte" + ], "type": "object" }, { @@ -471,7 +551,9 @@ "type": "array" } }, - "required": ["max"], + "required": [ + "max" + ], "type": "object" }, { @@ -484,7 +566,9 @@ "type": "array" } }, - "required": ["min"], + "required": [ + "min" + ], "type": "object" }, { @@ -510,7 +594,9 @@ "type": "array" } }, - "required": ["pathEq"], + "required": [ + "pathEq" + ], "type": "object" }, { @@ -533,7 +619,9 @@ "type": "array" } }, - "required": ["propEq"], + "required": [ + "propEq" + ], "type": "object" }, { @@ -556,7 +644,9 @@ "type": "array" } }, - "required": ["sortPath"], + "required": [ + "sortPath" + ], "type": "object" }, { @@ -582,7 +672,9 @@ "type": "array" } }, - "required": ["symmetricDifference"], + "required": [ + "symmetricDifference" + ], "type": "object" }, { @@ -608,7 +700,9 @@ "type": "array" } }, - "required": ["union"], + "required": [ + "union" + ], "type": "object" }, { @@ -628,7 +722,9 @@ "type": "array" } }, - "required": ["mod"], + "required": [ + "mod" + ], "type": "object" }, { @@ -648,7 +744,9 @@ "type": "array" } }, - "required": ["add"], + "required": [ + "add" + ], "type": "object" }, { @@ -658,7 +756,9 @@ "$ref": "#/definitions/ExprBody" } }, - "required": ["dec"], + "required": [ + "dec" + ], "type": "object" }, { @@ -678,7 +778,9 @@ "type": "array" } }, - "required": ["divide"], + "required": [ + "divide" + ], "type": "object" }, { @@ -688,7 +790,9 @@ "$ref": "#/definitions/ExprBody" } }, - "required": ["inc"], + "required": [ + "inc" + ], "type": "object" }, { @@ -708,7 +812,9 @@ "type": "array" } }, - "required": ["multiply"], + "required": [ + "multiply" + ], "type": "object" }, { @@ -718,7 +824,9 @@ "$ref": "#/definitions/ExprBody" } }, - "required": ["negate"], + "required": [ + "negate" + ], "type": "object" }, { @@ -731,7 +839,9 @@ "type": "array" } }, - "required": ["product"], + "required": [ + "product" + ], "type": "object" }, { @@ -751,7 +861,9 @@ "type": "array" } }, - "required": ["subtract"], + "required": [ + "subtract" + ], "type": "object" }, { @@ -764,7 +876,9 @@ "type": "array" } }, - "required": ["sum"], + "required": [ + "sum" + ], "type": "object" } ] @@ -814,7 +928,10 @@ }, "doc": { "description": "Publicly visible documentation for the field.", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "expr": { "anyOf": [ @@ -928,7 +1045,10 @@ }, "baseURL": { "description": "This refers to the base URL of the API. If not specified, the default base URL is the one specified in the `@upstream` operator.", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "batch": { "description": "If the upstream GraphQL server supports request batching, you can specify the 'batch' argument to batch several requests into a single batch request.\n\nMake sure you have also specified batch settings to the `@upstream` and to the `@graphQL` operator.", @@ -947,7 +1067,9 @@ "type": "string" } }, - "required": ["name"], + "required": [ + "name" + ], "type": "object" }, "Grpc": { @@ -955,11 +1077,17 @@ "properties": { "baseURL": { "description": "This refers to the base URL of the API. If not specified, the default base URL is the one specified in the `@upstream` operator", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "body": { "description": "This refers to the arguments of your gRPC call. You can pass it as a static object or use Mustache template for dynamic parameters. These parameters will be added in the body in `protobuf` format.", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "groupBy": { "description": "The key path in the response which should be used to group multiple requests. For instance `[\"news\",\"id\"]`. For more details please refer out [n + 1 guide](https://tailcall.run/docs/guides/n+1#solving-using-batching).", @@ -989,7 +1117,11 @@ "type": "string" } }, - "required": ["method", "protoPath", "service"], + "required": [ + "method", + "protoPath", + "service" + ], "type": "object" }, "Http": { @@ -997,11 +1129,17 @@ "properties": { "baseURL": { "description": "This refers to the base URL of the API. If not specified, the default base URL is the one specified in the `@upstream` operator", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "body": { "description": "The body of the API call. It's used for methods like POST or PUT that send data to the server. You can pass it as a static object or use a Mustache template to substitute variables from the GraphQL variables.", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "encoding": { "allOf": [ @@ -1069,11 +1207,16 @@ "description": "This represents the query parameters of your API call. You can pass it as a static object or use Mustache template for dynamic parameters. These parameters will be added to the URL." } }, - "required": ["path"], + "required": [ + "path" + ], "type": "object" }, "HttpVersion": { - "enum": ["HTTP1", "HTTP2"], + "enum": [ + "HTTP1", + "HTTP2" + ], "type": "string" }, "JS": { @@ -1082,7 +1225,9 @@ "type": "string" } }, - "required": ["script"], + "required": [ + "script" + ], "type": "object" }, "KeyValues": { @@ -1092,13 +1237,26 @@ "type": "object" }, "Method": { - "enum": ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "CONNECT", "TRACE"], + "enum": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE", + "HEAD", + "OPTIONS", + "CONNECT", + "TRACE" + ], "type": "string" }, "Modify": { "properties": { "name": { - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "omit": { "type": "boolean" @@ -1115,19 +1273,30 @@ "type": "string" } }, - "required": ["url"], + "required": [ + "url" + ], "type": "object" }, "RootSchema": { "properties": { "mutation": { - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "query": { - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "subscription": { - "type": ["string", "null"] + "type": [ + "string", + "null" + ] } }, "type": "object" @@ -1137,53 +1306,89 @@ "properties": { "apolloTracing": { "description": "`apolloTracing` exposes GraphQL query performance data, including execution time of queries and individual resolvers.", - "type": ["boolean", "null"] + "type": [ + "boolean", + "null" + ] }, "batchRequests": { "description": "`batchRequests` combines multiple requests into one, improving performance but potentially introducing latency and complicating debugging. Use judiciously. @default `false`", - "type": ["boolean", "null"] + "type": [ + "boolean", + "null" + ] }, "cacheControlHeader": { "description": "`cacheControlHeader` sends `Cache-Control` headers in responses when activated. The `max-age` value is the least of the values received from upstream services. @default `false`.", - "type": ["boolean", "null"] + "type": [ + "boolean", + "null" + ] }, "cert": { "description": "`cert` sets the path to certificate(s) for running the server over HTTP2 (HTTPS). @default `null`.", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "globalResponseTimeout": { "description": "`globalResponseTimeout` sets the maximum query duration before termination, acting as a safeguard against long-running queries.", "format": "int64", - "type": ["integer", "null"] + "type": [ + "integer", + "null" + ] }, "graphiql": { "description": "`graphiql` activates the GraphiQL IDE at the root path within Tailcall, a tool for query development and testing. @default `false`.", - "type": ["boolean", "null"] + "type": [ + "boolean", + "null" + ] }, "hostname": { "description": "`hostname` sets the server hostname.", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "introspection": { "description": "`introspection` allows clients to fetch schema information directly, aiding tools and applications in understanding available types, fields, and operations. @default `true`.", - "type": ["boolean", "null"] + "type": [ + "boolean", + "null" + ] }, "key": { "description": "`key` sets the path to key for running the server over HTTP2 (HTTPS). @default `null`.", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "pipelineFlush": { - "type": ["boolean", "null"] + "type": [ + "boolean", + "null" + ] }, "port": { "description": "`port` sets the Tailcall running port. @default `8000`.", "format": "uint16", "minimum": 0.0, - "type": ["integer", "null"] + "type": [ + "integer", + "null" + ] }, "queryValidation": { "description": "`queryValidation` checks incoming GraphQL queries against the schema, preventing errors from invalid queries. Can be disabled for performance. @default `false`.", - "type": ["boolean", "null"] + "type": [ + "boolean", + "null" + ] }, "responseHeaders": { "allOf": [ @@ -1195,7 +1400,10 @@ }, "responseValidation": { "description": "`responseValidation` Tailcall automatically validates responses from upstream services using inferred schema. @default `false`.", - "type": ["boolean", "null"] + "type": [ + "boolean", + "null" + ] }, "vars": { "allOf": [ @@ -1220,7 +1428,10 @@ "description": "`workers` sets the number of worker threads. @default the number of system cores.", "format": "uint", "minimum": 0.0, - "type": ["integer", "null"] + "type": [ + "integer", + "null" + ] } }, "type": "object" @@ -1249,14 +1460,20 @@ }, "doc": { "description": "Documentation for the type that is publicly visible.", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "enum": { "description": "Variants for the type if it's an enum", "items": { "type": "string" }, - "type": ["array", "null"], + "type": [ + "array", + "null" + ], "uniqueItems": true }, "fields": { @@ -1283,13 +1500,18 @@ "type": "boolean" } }, - "required": ["fields"], + "required": [ + "fields" + ], "type": "object" }, "Union": { "properties": { "doc": { - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "types": { "items": { @@ -1299,7 +1521,9 @@ "uniqueItems": true } }, - "required": ["types"], + "required": [ + "types" + ], "type": "object" }, "Upstream": { @@ -1310,12 +1534,18 @@ "items": { "type": "string" }, - "type": ["array", "null"], + "type": [ + "array", + "null" + ], "uniqueItems": true }, "baseURL": { "description": "This refers to the default base URL for your APIs. If it's not explicitly mentioned in the `@upstream` operator, then each [@http](#http) operator must specify its own `baseURL`. If neither `@upstream` nor [@http](#http) provides a `baseURL`, it results in a compilation error.", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "batch": { "anyOf": [ @@ -1332,43 +1562,67 @@ "description": "The time in seconds that the connection will wait for a response before timing out.", "format": "uint64", "minimum": 0.0, - "type": ["integer", "null"] + "type": [ + "integer", + "null" + ] }, "http2Only": { "description": "The `http2Only` setting allows you to specify whether the client should always issue HTTP2 requests, without checking if the server supports it or not. By default it is set to `false` for all HTTP requests made by the server, but is automatically set to true for GRPC.", - "type": ["boolean", "null"] + "type": [ + "boolean", + "null" + ] }, "httpCache": { "description": "Activating this enables Tailcall's HTTP caching, adhering to the [HTTP Caching RFC](https://tools.ietf.org/html/rfc7234), to enhance performance by minimizing redundant data fetches. Defaults to `false` if unspecified.", - "type": ["boolean", "null"] + "type": [ + "boolean", + "null" + ] }, "keepAliveInterval": { "description": "The time in seconds between each keep-alive message sent to maintain the connection.", "format": "uint64", "minimum": 0.0, - "type": ["integer", "null"] + "type": [ + "integer", + "null" + ] }, "keepAliveTimeout": { "description": "The time in seconds that the connection will wait for a keep-alive message before closing.", "format": "uint64", "minimum": 0.0, - "type": ["integer", "null"] + "type": [ + "integer", + "null" + ] }, "keepAliveWhileIdle": { "description": "A boolean value that determines whether keep-alive messages should be sent while the connection is idle.", - "type": ["boolean", "null"] + "type": [ + "boolean", + "null" + ] }, "poolIdleTimeout": { "description": "The time in seconds that the connection pool will wait before closing idle connections.", "format": "uint64", "minimum": 0.0, - "type": ["integer", "null"] + "type": [ + "integer", + "null" + ] }, "poolMaxIdlePerHost": { "description": "The maximum number of idle connections that will be maintained per host.", "format": "uint", "minimum": 0.0, - "type": ["integer", "null"] + "type": [ + "integer", + "null" + ] }, "proxy": { "anyOf": [ @@ -1385,17 +1639,26 @@ "description": "The time in seconds between each TCP keep-alive message sent to maintain the connection.", "format": "uint64", "minimum": 0.0, - "type": ["integer", "null"] + "type": [ + "integer", + "null" + ] }, "timeout": { "description": "The maximum time in seconds that the connection will wait for a response.", "format": "uint64", "minimum": 0.0, - "type": ["integer", "null"] + "type": [ + "integer", + "null" + ] }, "userAgent": { "description": "The User-Agent header value to be used in HTTP requests. @default `Tailcall/1.0`", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] } }, "type": "object" @@ -1403,7 +1666,11 @@ "schema": { "oneOf": [ { - "enum": ["Str", "Num", "Bool"], + "enum": [ + "Str", + "Num", + "Bool" + ], "type": "string" }, { @@ -1416,7 +1683,9 @@ "type": "object" } }, - "required": ["Obj"], + "required": [ + "Obj" + ], "type": "object" }, { @@ -1426,7 +1695,9 @@ "$ref": "#/definitions/schema" } }, - "required": ["Arr"], + "required": [ + "Arr" + ], "type": "object" }, { @@ -1436,7 +1707,9 @@ "$ref": "#/definitions/schema" } }, - "required": ["Opt"], + "required": [ + "Opt" + ], "type": "object" } ] @@ -1485,7 +1758,9 @@ "description": "Dictates how tailcall should handle upstream requests/responses. Tuning upstream can improve performance and reliability for connections." } }, - "required": ["schema"], + "required": [ + "schema" + ], "title": "Config", "type": "object" -} +} \ No newline at end of file diff --git a/src/config/config.rs b/src/config/config.rs index c2246cfdbf..f041a768ed 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -545,9 +545,15 @@ pub struct Http { } #[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq, schemars::JsonSchema)] +// The @call operator is used to reference a resolver operator (`@http`, `@grpc`, `@graphQL`) from a field on `Query` type. +/// +/// For instance, if you have a `user(id: Int!): User @http(path: "/users/{{args.id}}")` field on the `Query` type, you can reference it from another field on the `Query` type using the `@call` operator. +/// So, on `Post.user` you can declare `user: User @call(query: "user", args: {id: "{{value.userId}}"})`, and this will replace the `{{args.id}}` used in the `@http` operator with the value of `userId` from the `Post` type. pub struct Call { #[serde(default, skip_serializing_if = "is_default")] + /// The name of the field on the `Query` type that you want to call. For instance `user`. pub query: Option, + /// The arguments of the field on the `Query` type that you want to call. For instance `{id: "{{value.userId}}"}`. pub args: HashMap, } From cf2598c88cd4254ed99f01e8cf75224cb74e5270 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Fri, 19 Jan 2024 03:10:08 -0300 Subject: [PATCH 64/92] refactor: create `compile_call` method --- src/blueprint/from_config/operators/call.rs | 188 +++++++++--------- .../graphql/errors/test-call-operator.graphql | 2 +- 2 files changed, 98 insertions(+), 92 deletions(-) diff --git a/src/blueprint/from_config/operators/call.rs b/src/blueprint/from_config/operators/call.rs index 5fc2b9c90b..448c7cff5a 100644 --- a/src/blueprint/from_config/operators/call.rs +++ b/src/blueprint/from_config/operators/call.rs @@ -19,109 +19,115 @@ pub fn update_call( operation_type: &GraphQLOperationType, ) -> TryFold<'_, (&Config, &Field, &config::Type, &str), FieldDefinition, String> { TryFold::<(&Config, &Field, &config::Type, &str), FieldDefinition, String>::new( - move |(config, field, _type_of, name), b_field| { + move |(config, field, _, _), b_field| { let Some(call) = &field.call else { return Valid::succeed(b_field); }; - if validate_field_has_resolver(name, field, &config.types).is_succeed() { - return Valid::fail(format!( - "@call directive is not allowed on field {} because it already has a resolver", - name - )); - } + compile_call(field, config, call, operation_type) + .and_then(|resolver| Valid::succeed(b_field.resolver(Some(resolver)))) + }, + ) +} - Valid::from_option(call.query.clone(), "call must have query".to_string()) - .and_then(|field_name| { - Valid::from_option(config.find_type("Query"), "Query type not found on config".to_string()) - .zip(Valid::succeed(field_name)) - }) - .and_then(|(query_type, field_name)| { - Valid::from_option( - query_type.fields.get(&field_name), - format!("{} field not found", field_name), - ) - .zip(Valid::succeed(field_name)) - .and_then(|(field, field_name)| { - if field.has_resolver() { - Valid::succeed((field, field_name, call.args.iter())) - } else { - Valid::fail(format!("{} field has no resolver", field_name)) - } - }) - }) - .and_then(|(_field, field_name, args)| { - let empties: Vec<(&String, &config::Arg)> = _field - .args - .iter() - .filter(|(k, _)| !args.clone().any(|(k1, _)| k1.eq(*k))) - .collect(); +pub fn compile_call( + field: &Field, + config: &Config, + call: &config::Call, + operation_type: &GraphQLOperationType, +) -> Valid { + if validate_field_has_resolver(field.name(), field, &config.types).is_succeed() { + return Valid::fail("@call directive is not allowed on field because it already has a resolver".to_string()); + } - if empties.len().gt(&0) { - return Valid::fail(format!( - "no argument {} found", - empties - .iter() - .map(|(k, _)| format!("'{}'", k)) - .collect::>() - .join(", ") - )) - .trace(field_name.as_str()); - } + Valid::from_option(call.query.clone(), "call must have query".to_string()) + .and_then(|field_name| { + Valid::from_option(config.find_type("Query"), "Query type not found on config".to_string()) + .zip(Valid::succeed(field_name)) + }) + .and_then(|(query_type, field_name)| { + Valid::from_option( + query_type.fields.get(&field_name), + format!("{} field not found", field_name), + ) + .zip(Valid::succeed(field_name)) + .and_then(|(field, field_name)| { + if field.has_resolver() { + Valid::succeed((field, field_name, call.args.iter())) + } else { + Valid::fail(format!("{} field has no resolver", field_name)) + } + }) + }) + .and_then(|(_field, field_name, args)| { + let empties: Vec<(&String, &config::Arg)> = _field + .args + .iter() + .filter(|(k, _)| !args.clone().any(|(k1, _)| k1.eq(*k))) + .collect(); - if let Some(http) = _field.http.clone() { - compile_http(config, field, &http).and_then(|expr| match expr.clone() { - Expression::IO(IO::Http { mut req_template, group_by, dl_id }) => { - req_template = req_template.clone().root_url( - req_template - .root_url - .get_segments() - .iter() - .map(|segment| match segment { - Segment::Literal(literal) => Segment::Literal(literal.clone()), - Segment::Expression(expression) => { - if expression[0] == "args" { - let value = find_value(&args, &expression[1]).unwrap(); - let item = Mustache::parse(value).unwrap(); + if empties.len().gt(&0) { + return Valid::fail(format!( + "no argument {} found", + empties + .iter() + .map(|(k, _)| format!("'{}'", k)) + .collect::>() + .join(", ") + )) + .trace(field_name.as_str()); + } - let expression = item.get_segments().first().unwrap().to_owned().to_owned(); + if let Some(http) = _field.http.clone() { + compile_http(config, field, &http).and_then(|expr| match expr.clone() { + Expression::IO(IO::Http { mut req_template, group_by, dl_id }) => { + req_template = req_template.clone().root_url( + req_template + .root_url + .get_segments() + .iter() + .map(|segment| match segment { + Segment::Literal(literal) => Segment::Literal(literal.clone()), + Segment::Expression(expression) => { + if expression[0] == "args" { + let value = find_value(&args, &expression[1]).unwrap(); + let item = Mustache::parse(value).unwrap(); - expression - } else { - Segment::Expression(expression.clone()) - } - } - }) - .collect::>() - .into(), - ); + let expression = item.get_segments().first().unwrap().to_owned().to_owned(); - Valid::succeed(Expression::IO(IO::Http { req_template, group_by, dl_id })) - } - _ => Valid::succeed(expr), - }) - } else if let Some(mut graphql) = _field.graphql.clone() { - if let Some(mut _args) = graphql.args { - let mut updated: BTreeMap = BTreeMap::new(); + expression + } else { + Segment::Expression(expression.clone()) + } + } + }) + .collect::>() + .into(), + ); - for (key, _) in _args.0 { - let value = find_value(&args, &key).unwrap(); + Valid::succeed(Expression::IO(IO::Http { req_template, group_by, dl_id })) + } + _ => Valid::succeed(expr), + }) + } else if let Some(mut graphql) = _field.graphql.clone() { + if let Some(mut _args) = graphql.args { + let mut updated: BTreeMap = BTreeMap::new(); - updated.insert(key.clone(), value.to_string()); - } + for (key, _) in _args.0 { + let value = find_value(&args, &key).unwrap(); - graphql.args = Some(KeyValues(updated)); - } - compile_graphql(config, operation_type, &graphql) - } else if let Some(grpc) = _field.grpc.clone() { - let inputs: CompileGrpc<'_> = - CompileGrpc { config, operation_type, field, grpc: &grpc, validate_with_schema: false }; - compile_grpc(inputs) - } else { - return Valid::fail(format!("{} field has no resolver", field_name)); + updated.insert(key.clone(), value.to_string()); } - .and_then(|resolver| Valid::succeed(b_field.resolver(Some(resolver)))) - }) - }, - ) + + graphql.args = Some(KeyValues(updated)); + } + compile_graphql(config, operation_type, &graphql) + } else if let Some(grpc) = _field.grpc.clone() { + let inputs: CompileGrpc<'_> = + CompileGrpc { config, operation_type, field, grpc: &grpc, validate_with_schema: false }; + compile_grpc(inputs) + } else { + return Valid::fail(format!("{} field has no resolver", field_name)); + } + }) } diff --git a/tests/graphql/errors/test-call-operator.graphql b/tests/graphql/errors/test-call-operator.graphql index 5be819d48e..0d46cb34d2 100644 --- a/tests/graphql/errors/test-call-operator.graphql +++ b/tests/graphql/errors/test-call-operator.graphql @@ -39,7 +39,7 @@ type Failure ) type Failure @error( - message: "@call directive is not allowed on field multipleResolvers because it already has a resolver" + message: "@call directive is not allowed on field because it already has a resolver" trace: ["Post", "multipleResolvers", "@call"] ) type Failure @error(message: "no argument 'id' found", trace: ["Post", "urlMismatchHttp", "@call", "user"]) From 2741014ada5f284be533e45cbf62ea041201fcc0 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Fri, 19 Jan 2024 03:10:17 -0300 Subject: [PATCH 65/92] feat: add call on `expr` --- src/blueprint/from_config/operators/expr.rs | 1 + src/config/expr.rs | 7 ++++++- tests/graphql/errors/test-expr-errors.graphql | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/blueprint/from_config/operators/expr.rs b/src/blueprint/from_config/operators/expr.rs index b6be79cd75..68a4db6867 100644 --- a/src/blueprint/from_config/operators/expr.rs +++ b/src/blueprint/from_config/operators/expr.rs @@ -54,6 +54,7 @@ fn compile(ctx: &CompilationContext, expr: ExprBody) -> Valid compile_graphql(config, operation_type, &gql), + ExprBody::Call(call) => compile_call(field, config, &call, operation_type), // Safe Expr ExprBody::Const(value) => compile_const(CompileConst { config, field, value: &value, validate: false }), diff --git a/src/config/expr.rs b/src/config/expr.rs index 7727a26a14..35d94bc9ad 100644 --- a/src/config/expr.rs +++ b/src/config/expr.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; -use super::{GraphQL, Grpc, Http}; +use super::{Call, GraphQL, Grpc, Http}; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, schemars::JsonSchema)] /// Allows composing operators as simple expressions @@ -24,6 +24,10 @@ pub enum ExprBody { #[serde(rename = "graphQL")] GraphQL(GraphQL), + /// Reuses a resolver pre-defined on type `Query` + #[serde(rename = "call")] + Call(Call), + /// Evaluate to constant data #[serde(rename = "const")] Const(Value), @@ -121,6 +125,7 @@ impl ExprBody { ExprBody::Http(_) => true, ExprBody::Grpc(_) => true, ExprBody::GraphQL(_) => true, + ExprBody::Call(_) => true, ExprBody::Const(_) => false, ExprBody::If { cond, on_true, on_false } => cond.has_io() || on_true.has_io() || on_false.has_io(), ExprBody::And(l) => l.iter().any(|e| e.has_io()), diff --git a/tests/graphql/errors/test-expr-errors.graphql b/tests/graphql/errors/test-expr-errors.graphql index 6359851059..958c143ea7 100644 --- a/tests/graphql/errors/test-expr-errors.graphql +++ b/tests/graphql/errors/test-expr-errors.graphql @@ -12,6 +12,6 @@ type Query { type Failure @error(message: "Parsing failed because of missing field `body`", trace: ["@expr"]) @error( - message: "Parsing failed because of unknown variant `unsupported`, expected one of `http`, `grpc`, `graphQL`, `const`, `if`, `and`, `or`, `cond`, `defaultTo`, `isEmpty`, `not`, `concat`, `intersection`, `difference`, `eq`, `gt`, `gte`, `lt`, `lte`, `max`, `min`, `pathEq`, `propEq`, `sortPath`, `symmetricDifference`, `union`, `mod`, `add`, `dec`, `divide`, `inc`, `multiply`, `negate`, `product`, `subtract`, `sum`" + message: "Parsing failed because of unknown variant `unsupported`, expected one of `http`, `grpc`, `graphQL`, `call`, `const`, `if`, `and`, `or`, `cond`, `defaultTo`, `isEmpty`, `not`, `concat`, `intersection`, `difference`, `eq`, `gt`, `gte`, `lt`, `lte`, `max`, `min`, `pathEq`, `propEq`, `sortPath`, `symmetricDifference`, `union`, `mod`, `add`, `dec`, `divide`, `inc`, `multiply`, `negate`, `product`, `subtract`, `sum`" trace: ["@expr", "body"] ) From ac956f4494371b7f2d4afaefd423402e4a8bd2d2 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Fri, 19 Jan 2024 03:13:15 -0300 Subject: [PATCH 66/92] feat: add call on resolvable directives --- src/config/config.rs | 3 +++ tests/graphql/errors/test-call-operator.graphql | 6 ------ .../test-multiple-resolvable-directives-on-field.graphql | 2 ++ 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/config/config.rs b/src/config/config.rs index f041a768ed..9ad63a171b 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -420,6 +420,9 @@ impl Field { if self.grpc.is_some() { directives.push(Grpc::trace_name()); } + if self.call.is_some() { + directives.push(Call::trace_name()); + } directives } pub fn has_batched_resolver(&self) -> bool { diff --git a/tests/graphql/errors/test-call-operator.graphql b/tests/graphql/errors/test-call-operator.graphql index 0d46cb34d2..45d598653a 100644 --- a/tests/graphql/errors/test-call-operator.graphql +++ b/tests/graphql/errors/test-call-operator.graphql @@ -18,7 +18,6 @@ type User { type Post { userId: Int! withoutResolver: User @call(query: "userWithoutResolver", args: {id: "{{value.userId}}"}) - multipleResolvers: User @http(path: "/users/{{value.userId}}") @call(query: "user", args: {id: "{{value.userId}}"}) withoutOperator: User @call(args: {id: "{{value.userId}}"}) urlMismatchHttp: User @call(query: "user", args: {}) argumentMismatchGraphQL: User @call(query: "userWithGraphQLResolver", args: {}) @@ -37,11 +36,6 @@ type Failure message: "no argument 'id' found" trace: ["Post", "headersMismatchGraphQL", "@call", "userWithGraphQLResolver"] ) -type Failure - @error( - message: "@call directive is not allowed on field because it already has a resolver" - trace: ["Post", "multipleResolvers", "@call"] - ) type Failure @error(message: "no argument 'id' found", trace: ["Post", "urlMismatchHttp", "@call", "user"]) type Failure @error(message: "call must have query", trace: ["Post", "withoutOperator", "@call"]) type Failure @error(message: "userWithoutResolver field has no resolver", trace: ["Post", "withoutResolver", "@call"]) diff --git a/tests/graphql/errors/test-multiple-resolvable-directives-on-field.graphql b/tests/graphql/errors/test-multiple-resolvable-directives-on-field.graphql index 4591e52afa..2d06c70d8c 100644 --- a/tests/graphql/errors/test-multiple-resolvable-directives-on-field.graphql +++ b/tests/graphql/errors/test-multiple-resolvable-directives-on-field.graphql @@ -12,6 +12,7 @@ type Query { user1: User @const(data: {name: "John"}) @http(path: "/users/1") user2: User @const(data: {name: "John"}) @js(script: "{id: 1, name: 'foo'}") user3: User @http(path: "/users/1") @js(script: "{id: 1, name: 'foo'}") + user4: User @http(path: "/users/{{value.userId}}") @call(query: "user", args: {id: "{{value.userId}}"}) } #> client-sdl @@ -19,3 +20,4 @@ type Failure @error(message: "Multiple resolvers detected [@http, @const]", trace: ["Query", "user1"]) @error(message: "Multiple resolvers detected [@js, @const]", trace: ["Query", "user2"]) @error(message: "Multiple resolvers detected [@http, @js]", trace: ["Query", "user3"]) + @error(message: "Multiple resolvers detected [@http, @call]", trace: ["Query", "user4"]) From d14550f6334a452496e4efb8ccff4e64490f5e14 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Sat, 20 Jan 2024 23:45:54 -0300 Subject: [PATCH 67/92] refactor: change `req_template` without `mut` --- src/blueprint/from_config/operators/call.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/blueprint/from_config/operators/call.rs b/src/blueprint/from_config/operators/call.rs index 448c7cff5a..c61fbf09f4 100644 --- a/src/blueprint/from_config/operators/call.rs +++ b/src/blueprint/from_config/operators/call.rs @@ -80,8 +80,8 @@ pub fn compile_call( if let Some(http) = _field.http.clone() { compile_http(config, field, &http).and_then(|expr| match expr.clone() { - Expression::IO(IO::Http { mut req_template, group_by, dl_id }) => { - req_template = req_template.clone().root_url( + Expression::IO(IO::Http { req_template, group_by, dl_id }) => Valid::succeed( + req_template.clone().root_url( req_template .root_url .get_segments() @@ -103,10 +103,9 @@ pub fn compile_call( }) .collect::>() .into(), - ); - - Valid::succeed(Expression::IO(IO::Http { req_template, group_by, dl_id })) - } + ), + ) + .map(|req_template| Expression::IO(IO::Http { req_template, group_by, dl_id })), _ => Valid::succeed(expr), }) } else if let Some(mut graphql) = _field.graphql.clone() { From 792a59264e90fac5996c8a58ea806b7831692d88 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Mon, 22 Jan 2024 15:11:35 -0300 Subject: [PATCH 68/92] checkpoint query support --- src/blueprint/from_config/operators/call.rs | 81 +++++++++++++++++++++ tests/http/call-operator.yml | 42 +++++++++++ tests/http/config/call.graphql | 13 +++- 3 files changed, 133 insertions(+), 3 deletions(-) diff --git a/src/blueprint/from_config/operators/call.rs b/src/blueprint/from_config/operators/call.rs index c61fbf09f4..ab21e8694a 100644 --- a/src/blueprint/from_config/operators/call.rs +++ b/src/blueprint/from_config/operators/call.rs @@ -4,6 +4,7 @@ use std::collections::BTreeMap; use crate::blueprint::*; use crate::config; use crate::config::{Config, Field, GraphQLOperationType, KeyValues}; +use crate::endpoint::Endpoint; use crate::lambda::{Expression, IO}; use crate::mustache::{Mustache, Segment}; use crate::try_fold::TryFold; @@ -105,6 +106,86 @@ pub fn compile_call( .into(), ), ) + .and_then(|req_template| { + Valid::succeed( + req_template.clone().query( + req_template + .query + .iter() + .map(|(key, value)| { + let segments = value.get_segments(); + + let segments = segments + .iter() + .map(|segment| match segment { + Segment::Literal(literal) => Segment::Literal(literal.clone()), + Segment::Expression(expression) => { + if expression[0] == "args" { + let value = find_value(&args, &expression[1]).unwrap(); + let item = Mustache::parse(value).unwrap(); + + let expression = item.get_segments().first().unwrap().to_owned().to_owned(); + + expression + } else { + Segment::Expression(expression.clone()) + } + } + }) + .collect::>(); + + let value = Mustache::from(segments); + + (key.to_owned(), value) + }) + .collect(), + ), + ) + .map(|req_template| { + let query = req_template + .endpoint + .query + .iter() + .map(|(key, value)| { + let mustache = Mustache::parse(value).unwrap(); + + let segments = mustache.get_segments(); + + let value = segments + .iter() + .map(|segment| match segment { + Segment::Literal(literal) => literal, + Segment::Expression(expression) => { + if expression[0] == "args" { + find_value(&args, &expression[1]).unwrap() + } else { + value + } + } + }) + .collect::>() + .first() + .unwrap() + .to_owned(); + + println!("value: {:?}", value); + + (key.to_owned(), value.to_owned()) + }) + .collect(); + + println!("query: {:?}", query); + + let endpoint = req_template.endpoint.clone().query(query); + + req_template.endpoint(endpoint) + }) + }) + .and_then(|req_template| { + println!("req_template_h: {:?}", req_template); + + Valid::succeed(req_template) + }) .map(|req_template| Expression::IO(IO::Http { req_template, group_by, dl_id })), _ => Valid::succeed(expr), }) diff --git a/tests/http/call-operator.yml b/tests/http/call-operator.yml index 026a26e395..d8260d5055 100644 --- a/tests/http/call-operator.yml +++ b/tests/http/call-operator.yml @@ -1,6 +1,7 @@ --- name: With call operator config: !file tests/http/config/call.graphql +runner: only mock: - request: @@ -15,6 +16,24 @@ mock: body: - id: 1 userId: 1 + - request: + url: http://jsonplaceholder.typicode.com/users + response: + body: + - id: 1 + name: foo + # - request: + # url: http://jsonplaceholder.typicode.com/posts?userId=1 + # response: + # body: + # - id: 1 + # userId: 1 + # title: bar + # body: baz + # - id: 2 + # userId: 1 + # title: qux + # body: quux assert: - request: @@ -28,3 +47,26 @@ assert: posts: - user: name: foo + # - request: + # method: POST + # url: http://localhost:8080/graphql + # body: + # query: "query { userPosts(id: 1) { title } }" + # response: + # body: + # data: + # userPosts: + # - title: bar + # - title: qux + - request: + method: POST + url: http://localhost:8080/graphql + body: + query: "query { userWithPosts { posts { title } } }" + response: + body: + data: + userWithPosts: + posts: + - title: bar + - title: qux diff --git a/tests/http/config/call.graphql b/tests/http/config/call.graphql index 71da2e1fa4..663aac8599 100644 --- a/tests/http/config/call.graphql +++ b/tests/http/config/call.graphql @@ -7,6 +7,13 @@ schema type Query { posts: [Post] @http(path: "/posts") user(id: Int!): User @http(path: "/users/{{args.id}}") + userPosts(id: ID!): [Post] @http(path: "/posts", query: [{key: "userId", value: "{{args.id}}"}]) +} + +type UserWithPosts { + id: Int! + name: String! + posts: [Post] @call(query: "userPosts", args: {id: "{{value.id}}"}) } type User { @@ -19,9 +26,9 @@ type User { } type Post { - id: Int! + id: Int userId: Int! - title: String! - body: String! + title: String + body: String user: User @call(query: "user", args: {id: "{{value.userId}}"}) } From f4d8c4150e979009379c8f7cb22ce780d9821595 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 24 Jan 2024 13:41:45 -0300 Subject: [PATCH 69/92] feat: support mock based on headers --- tests/http_spec.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/tests/http_spec.rs b/tests/http_spec.rs index 9bd5081d80..9ed690fc32 100644 --- a/tests/http_spec.rs +++ b/tests/http_spec.rs @@ -276,7 +276,20 @@ impl HttpIO for MockHttpClient { None => Value::Null, }; let body_match = req_body == mock_req.0.body; - method_match && url_match && (body_match || is_grpc) + let headers_match = req + .headers() + .iter() + .filter(|(key, _)| key.to_string() != "content-type") + .all(|(key, value)| { + let header_name = key.to_string(); + + let header_value = value.to_str().unwrap(); + let mock_header_value = "".to_string(); + let mock_header_value = mock_req.0.headers.get(&header_name).unwrap_or(&mock_header_value); + header_value == mock_header_value + }); + + method_match && url_match && headers_match && (body_match || is_grpc) }) .ok_or(anyhow!( "No mock found for request: {:?} {} in {}", From d732830017feb23753bfbd395549e5d6d2c6fbe5 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 24 Jan 2024 13:42:12 -0300 Subject: [PATCH 70/92] test(`@graphQL`): create tests for call resolver --- tests/http/call-operator.yml | 74 +++++++++++++++++++++++++++------- tests/http/config/call.graphql | 6 +++ 2 files changed, 65 insertions(+), 15 deletions(-) diff --git a/tests/http/call-operator.yml b/tests/http/call-operator.yml index d8260d5055..ab94eed77b 100644 --- a/tests/http/call-operator.yml +++ b/tests/http/call-operator.yml @@ -17,11 +17,31 @@ mock: - id: 1 userId: 1 - request: - url: http://jsonplaceholder.typicode.com/users + url: http://upstream/graphql + method: POST + body: '{ "query": "query { user(id: 1) { name } }" }' response: body: - - id: 1 - name: foo + data: + user: + name: "Leanne Graham" + - request: + url: http://upstream/graphql + method: POST + body: '{ "query": "query { user { name } }" }' + headers: + id: 1 + response: + body: + data: + user: + name: "Leanne Graham" + # - request: + # url: http://jsonplaceholder.typicode.com/users + # response: + # body: + # - id: 1 + # name: foo # - request: # url: http://jsonplaceholder.typicode.com/posts?userId=1 # response: @@ -47,6 +67,30 @@ assert: posts: - user: name: foo + - request: + method: POST + url: http://localhost:8080/graphql + body: + query: "query { posts { userGraphQLHeaders { name } } }" + headers: + id: 1 + response: + body: + data: + posts: + - userGraphQLHeaders: + name: "Leanne Graham" + - request: + method: POST + url: http://localhost:8080/graphql + body: + query: "query { posts { userGraphQLHeaders { name } } }" + response: + body: + data: + posts: + - userGraphQLHeaders: + name: "Leanne Graham" # - request: # method: POST # url: http://localhost:8080/graphql @@ -58,15 +102,15 @@ assert: # userPosts: # - title: bar # - title: qux - - request: - method: POST - url: http://localhost:8080/graphql - body: - query: "query { userWithPosts { posts { title } } }" - response: - body: - data: - userWithPosts: - posts: - - title: bar - - title: qux + # - request: + # method: POST + # url: http://localhost:8080/graphql + # body: + # query: "query { userWithPosts { posts { title } } }" + # response: + # body: + # data: + # userWithPosts: + # posts: + # - title: bar + # - title: qux diff --git a/tests/http/config/call.graphql b/tests/http/config/call.graphql index 663aac8599..f0e5943052 100644 --- a/tests/http/config/call.graphql +++ b/tests/http/config/call.graphql @@ -8,6 +8,10 @@ type Query { posts: [Post] @http(path: "/posts") user(id: Int!): User @http(path: "/users/{{args.id}}") userPosts(id: ID!): [Post] @http(path: "/posts", query: [{key: "userId", value: "{{args.id}}"}]) + userGraphQL(id: Int): User + @graphQL(baseURL: "http://upstream/graphql", name: "user", args: [{key: "id", value: "{{args.id}}"}]) + userGraphQLHeaders(id: Int!): User + @graphQL(baseURL: "http://upstream/graphql", name: "user", headers: [{key: "id", value: "{{args.id}}"}]) } type UserWithPosts { @@ -31,4 +35,6 @@ type Post { title: String body: String user: User @call(query: "user", args: {id: "{{value.userId}}"}) + userGraphQL: User @call(query: "userGraphQL", args: {id: "{{value.userId}}"}) + userGraphQLHeaders: User @call(query: "userGraphQLHeaders", args: {id: "{{value.userId}}"}) } From f8c45f7a95508c64e953aeb48f4d2d6f96f539ed Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 24 Jan 2024 13:43:40 -0300 Subject: [PATCH 71/92] feat: implement `@call` for `graphql.headers` --- src/blueprint/from_config/operators/call.rs | 23 ++++++++++++--------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/blueprint/from_config/operators/call.rs b/src/blueprint/from_config/operators/call.rs index ab21e8694a..3d35c8c103 100644 --- a/src/blueprint/from_config/operators/call.rs +++ b/src/blueprint/from_config/operators/call.rs @@ -4,7 +4,6 @@ use std::collections::BTreeMap; use crate::blueprint::*; use crate::config; use crate::config::{Config, Field, GraphQLOperationType, KeyValues}; -use crate::endpoint::Endpoint; use crate::lambda::{Expression, IO}; use crate::mustache::{Mustache, Segment}; use crate::try_fold::TryFold; @@ -168,24 +167,15 @@ pub fn compile_call( .unwrap() .to_owned(); - println!("value: {:?}", value); - (key.to_owned(), value.to_owned()) }) .collect(); - println!("query: {:?}", query); - let endpoint = req_template.endpoint.clone().query(query); req_template.endpoint(endpoint) }) }) - .and_then(|req_template| { - println!("req_template_h: {:?}", req_template); - - Valid::succeed(req_template) - }) .map(|req_template| Expression::IO(IO::Http { req_template, group_by, dl_id })), _ => Valid::succeed(expr), }) @@ -201,6 +191,19 @@ pub fn compile_call( graphql.args = Some(KeyValues(updated)); } + + if graphql.headers.0.len() > 0 { + let mut headers = graphql.headers.0.clone(); + + for (key, _) in graphql.headers.0.iter() { + let value = find_value(&args, &key).unwrap(); + + headers.insert(key.clone(), value.to_string()); + } + + graphql.headers = KeyValues(headers); + } + compile_graphql(config, operation_type, &graphql) } else if let Some(grpc) = _field.grpc.clone() { let inputs: CompileGrpc<'_> = From 623d6faa4d364a76cce1c3a7534ee5ee7224ffc7 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 24 Jan 2024 13:49:32 -0300 Subject: [PATCH 72/92] test(`@http`): create tests mocking headers value --- tests/http/call-operator.yml | 19 +++++++++++++++++++ tests/http/config/call.graphql | 2 ++ 2 files changed, 21 insertions(+) diff --git a/tests/http/call-operator.yml b/tests/http/call-operator.yml index ab94eed77b..6387ef127b 100644 --- a/tests/http/call-operator.yml +++ b/tests/http/call-operator.yml @@ -10,6 +10,14 @@ mock: body: id: 1 name: foo + - request: + url: http://jsonplaceholder.typicode.com/users + headers: + id: 1 + response: + body: + id: 1 + name: "Ervin Howell" - request: url: http://jsonplaceholder.typicode.com/posts response: @@ -91,6 +99,17 @@ assert: posts: - userGraphQLHeaders: name: "Leanne Graham" + - request: + method: POST + url: http://localhost:8080/graphql + body: + query: "query { posts { userHttpHeaders { name } } }" + response: + body: + data: + posts: + - userHttpHeaders: + name: "Ervin Howell" # - request: # method: POST # url: http://localhost:8080/graphql diff --git a/tests/http/config/call.graphql b/tests/http/config/call.graphql index f0e5943052..af38415cda 100644 --- a/tests/http/config/call.graphql +++ b/tests/http/config/call.graphql @@ -8,6 +8,7 @@ type Query { posts: [Post] @http(path: "/posts") user(id: Int!): User @http(path: "/users/{{args.id}}") userPosts(id: ID!): [Post] @http(path: "/posts", query: [{key: "userId", value: "{{args.id}}"}]) + userHttpHeaders(id: ID!): User @http(path: "/users", headers: [{key: "id", value: "{{args.id}}"}]) userGraphQL(id: Int): User @graphQL(baseURL: "http://upstream/graphql", name: "user", args: [{key: "id", value: "{{args.id}}"}]) userGraphQLHeaders(id: Int!): User @@ -35,6 +36,7 @@ type Post { title: String body: String user: User @call(query: "user", args: {id: "{{value.userId}}"}) + userHttpHeaders: User @call(query: "userHttpHeaders", args: {id: "{{value.userId}}"}) userGraphQL: User @call(query: "userGraphQL", args: {id: "{{value.userId}}"}) userGraphQLHeaders: User @call(query: "userGraphQLHeaders", args: {id: "{{value.userId}}"}) } From 338f7483c5930dba6a1ef3147c360e2b9a5f9867 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 24 Jan 2024 13:56:45 -0300 Subject: [PATCH 73/92] feat(`@call`): fill `http.headers` with `args` --- src/blueprint/from_config/operators/call.rs | 32 +++++++++++++-------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/blueprint/from_config/operators/call.rs b/src/blueprint/from_config/operators/call.rs index 3d35c8c103..0bcd8eeffb 100644 --- a/src/blueprint/from_config/operators/call.rs +++ b/src/blueprint/from_config/operators/call.rs @@ -78,7 +78,9 @@ pub fn compile_call( .trace(field_name.as_str()); } - if let Some(http) = _field.http.clone() { + if let Some(mut http) = _field.http.clone() { + http.headers = update_headers(http.headers.0.clone(), &args); + compile_http(config, field, &http).and_then(|expr| match expr.clone() { Expression::IO(IO::Http { req_template, group_by, dl_id }) => Valid::succeed( req_template.clone().root_url( @@ -192,17 +194,7 @@ pub fn compile_call( graphql.args = Some(KeyValues(updated)); } - if graphql.headers.0.len() > 0 { - let mut headers = graphql.headers.0.clone(); - - for (key, _) in graphql.headers.0.iter() { - let value = find_value(&args, &key).unwrap(); - - headers.insert(key.clone(), value.to_string()); - } - - graphql.headers = KeyValues(headers); - } + graphql.headers = update_headers(graphql.headers.0.clone(), &args); compile_graphql(config, operation_type, &graphql) } else if let Some(grpc) = _field.grpc.clone() { @@ -214,3 +206,19 @@ pub fn compile_call( } }) } + +fn update_headers(headers: BTreeMap, args: &Iter) -> KeyValues { + if headers.len() == 0 { + return KeyValues(headers); + } + + let mut headers = headers; + + for (key, _) in headers.clone().into_iter() { + let value = find_value(&args, &key).unwrap(); + + headers.insert(key.clone(), value.to_string()); + } + + KeyValues(headers) +} From 5a3cf6e14c065b89ef513bb06618f96e4d11f85a Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 24 Jan 2024 15:08:43 -0300 Subject: [PATCH 74/92] test(`@http`): create tests replacing `http.query` by `call.args` --- tests/http/call-operator.yml | 21 +++++++++++++++++++-- tests/http/config/call.graphql | 6 ++++-- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/tests/http/call-operator.yml b/tests/http/call-operator.yml index 6387ef127b..d5bae629c2 100644 --- a/tests/http/call-operator.yml +++ b/tests/http/call-operator.yml @@ -17,13 +17,19 @@ mock: response: body: id: 1 - name: "Ervin Howell" + name: "Leanne Graham http headers" - request: url: http://jsonplaceholder.typicode.com/posts response: body: - id: 1 userId: 1 + - request: + url: http://jsonplaceholder.typicode.com/users?id=1 + response: + body: + id: 1 + name: "Leanne Graham http query" - request: url: http://upstream/graphql method: POST @@ -109,7 +115,18 @@ assert: data: posts: - userHttpHeaders: - name: "Ervin Howell" + name: "Leanne Graham http headers" + - request: + method: POST + url: http://localhost:8080/graphql + body: + query: "query { posts { userHttpQuery { name } } }" + response: + body: + data: + posts: + - userHttpQuery: + name: "Leanne Graham http query" # - request: # method: POST # url: http://localhost:8080/graphql diff --git a/tests/http/config/call.graphql b/tests/http/config/call.graphql index af38415cda..b7d158f9d1 100644 --- a/tests/http/config/call.graphql +++ b/tests/http/config/call.graphql @@ -7,8 +7,9 @@ schema type Query { posts: [Post] @http(path: "/posts") user(id: Int!): User @http(path: "/users/{{args.id}}") - userPosts(id: ID!): [Post] @http(path: "/posts", query: [{key: "userId", value: "{{args.id}}"}]) + # userPosts(id: ID!): [Post] @http(path: "/posts", query: [{key: "userId", value: "{{args.id}}"}]) userHttpHeaders(id: ID!): User @http(path: "/users", headers: [{key: "id", value: "{{args.id}}"}]) + userHttpQuery(id: ID!): User @http(path: "/users", query: [{key: "id", value: "{{args.id}}"}]) userGraphQL(id: Int): User @graphQL(baseURL: "http://upstream/graphql", name: "user", args: [{key: "id", value: "{{args.id}}"}]) userGraphQLHeaders(id: Int!): User @@ -18,7 +19,7 @@ type Query { type UserWithPosts { id: Int! name: String! - posts: [Post] @call(query: "userPosts", args: {id: "{{value.id}}"}) + # posts: [Post] @call(query: "userPosts", args: {id: "{{value.id}}"}) } type User { @@ -37,6 +38,7 @@ type Post { body: String user: User @call(query: "user", args: {id: "{{value.userId}}"}) userHttpHeaders: User @call(query: "userHttpHeaders", args: {id: "{{value.userId}}"}) + userHttpQuery: User @call(query: "userHttpQuery", args: {id: "{{value.userId}}"}) userGraphQL: User @call(query: "userGraphQL", args: {id: "{{value.userId}}"}) userGraphQLHeaders: User @call(query: "userGraphQLHeaders", args: {id: "{{value.userId}}"}) } From d1bf6c43a1a9137dab7f76d880ea04674072c4a7 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 24 Jan 2024 15:08:58 -0300 Subject: [PATCH 75/92] feat: implement `query` replacement on `http` --- src/blueprint/from_config/operators/call.rs | 160 ++++++++------------ 1 file changed, 60 insertions(+), 100 deletions(-) diff --git a/src/blueprint/from_config/operators/call.rs b/src/blueprint/from_config/operators/call.rs index 0bcd8eeffb..d48bf82bbc 100644 --- a/src/blueprint/from_config/operators/call.rs +++ b/src/blueprint/from_config/operators/call.rs @@ -1,9 +1,8 @@ use std::collections::hash_map::Iter; -use std::collections::BTreeMap; use crate::blueprint::*; use crate::config; -use crate::config::{Config, Field, GraphQLOperationType, KeyValues}; +use crate::config::{Config, Field, GraphQLOperationType}; use crate::lambda::{Expression, IO}; use crate::mustache::{Mustache, Segment}; use crate::try_fold::TryFold; @@ -78,9 +77,7 @@ pub fn compile_call( .trace(field_name.as_str()); } - if let Some(mut http) = _field.http.clone() { - http.headers = update_headers(http.headers.0.clone(), &args); - + if let Some(http) = _field.http.clone() { compile_http(config, field, &http).and_then(|expr| match expr.clone() { Expression::IO(IO::Http { req_template, group_by, dl_id }) => Valid::succeed( req_template.clone().root_url( @@ -107,96 +104,51 @@ pub fn compile_call( .into(), ), ) - .and_then(|req_template| { - Valid::succeed( - req_template.clone().query( - req_template - .query - .iter() - .map(|(key, value)| { - let segments = value.get_segments(); - - let segments = segments - .iter() - .map(|segment| match segment { - Segment::Literal(literal) => Segment::Literal(literal.clone()), - Segment::Expression(expression) => { - if expression[0] == "args" { - let value = find_value(&args, &expression[1]).unwrap(); - let item = Mustache::parse(value).unwrap(); - - let expression = item.get_segments().first().unwrap().to_owned().to_owned(); - - expression - } else { - Segment::Expression(expression.clone()) - } - } - }) - .collect::>(); - - let value = Mustache::from(segments); - - (key.to_owned(), value) - }) - .collect(), - ), - ) - .map(|req_template| { - let query = req_template - .endpoint - .query - .iter() - .map(|(key, value)| { - let mustache = Mustache::parse(value).unwrap(); - - let segments = mustache.get_segments(); - - let value = segments - .iter() - .map(|segment| match segment { - Segment::Literal(literal) => literal, - Segment::Expression(expression) => { - if expression[0] == "args" { - find_value(&args, &expression[1]).unwrap() - } else { - value - } - } - }) - .collect::>() - .first() - .unwrap() - .to_owned(); - - (key.to_owned(), value.to_owned()) - }) - .collect(); - - let endpoint = req_template.endpoint.clone().query(query); - - req_template.endpoint(endpoint) - }) + .map(|req_template| { + req_template + .clone() + .query(req_template.clone().query.iter().map(replace_mustache(&args)).collect()) + }) + .map(|req_template| { + req_template + .clone() + .headers(req_template.headers.iter().map(replace_mustache(&args)).collect()) }) .map(|req_template| Expression::IO(IO::Http { req_template, group_by, dl_id })), _ => Valid::succeed(expr), }) - } else if let Some(mut graphql) = _field.graphql.clone() { - if let Some(mut _args) = graphql.args { - let mut updated: BTreeMap = BTreeMap::new(); - - for (key, _) in _args.0 { - let value = find_value(&args, &key).unwrap(); - - updated.insert(key.clone(), value.to_string()); - } - - graphql.args = Some(KeyValues(updated)); - } - - graphql.headers = update_headers(graphql.headers.0.clone(), &args); + } else if let Some(graphql) = _field.graphql.clone() { + compile_graphql(config, operation_type, &graphql).and_then(|expr| match expr { + Expression::IO(IO::GraphQLEndpoint { req_template, field_name, batch, dl_id }) => Valid::succeed( + req_template + .clone() + .headers(req_template.headers.iter().map(replace_mustache(&args)).collect()), + ) + .map(|req_template| { + if req_template.operation_arguments.is_some() { + let operation_arguments = req_template + .clone() + .operation_arguments + .unwrap() + .iter() + .map(replace_mustache(&args)) + .collect::>(); - compile_graphql(config, operation_type, &graphql) + req_template.operation_arguments(Some(operation_arguments)) + } else { + req_template + } + }) + .and_then(|req_template| { + Valid::succeed(Expression::IO(IO::GraphQLEndpoint { + req_template, + field_name, + batch, + dl_id, + })) + }), + _ => Valid::succeed(expr), + }) } else if let Some(grpc) = _field.grpc.clone() { let inputs: CompileGrpc<'_> = CompileGrpc { config, operation_type, field, grpc: &grpc, validate_with_schema: false }; @@ -207,18 +159,26 @@ pub fn compile_call( }) } -fn update_headers(headers: BTreeMap, args: &Iter) -> KeyValues { - if headers.len() == 0 { - return KeyValues(headers); - } +fn replace_mustache<'a, T: Clone>(args: &'a Iter<'a, String, String>) -> impl Fn(&(T, Mustache)) -> (T, Mustache) + 'a { + |(key, value)| { + let value: Mustache = value + .expression_segments() + .iter() + .map(|expression| { + if expression[0] == "args" { + let value = find_value(args, &expression[1]).unwrap(); + let item = Mustache::parse(value).unwrap(); - let mut headers = headers; + let expression = item.get_segments().first().unwrap().to_owned().to_owned(); - for (key, _) in headers.clone().into_iter() { - let value = find_value(&args, &key).unwrap(); + expression + } else { + Segment::Expression(expression.to_owned().to_owned()) + } + }) + .collect::>() + .into(); - headers.insert(key.clone(), value.to_string()); + (key.clone().to_owned(), value) } - - KeyValues(headers) } From 9e9187774b9a04257c9b956ce32903f6d05a4220 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 24 Jan 2024 15:21:23 -0300 Subject: [PATCH 76/92] test: uncomment `userWithPosts` test --- tests/http/call-operator.yml | 82 +++++++++++++++++----------------- tests/http/config/call.graphql | 5 ++- 2 files changed, 44 insertions(+), 43 deletions(-) diff --git a/tests/http/call-operator.yml b/tests/http/call-operator.yml index d5bae629c2..ff852290e5 100644 --- a/tests/http/call-operator.yml +++ b/tests/http/call-operator.yml @@ -50,24 +50,24 @@ mock: data: user: name: "Leanne Graham" - # - request: - # url: http://jsonplaceholder.typicode.com/users - # response: - # body: - # - id: 1 - # name: foo - # - request: - # url: http://jsonplaceholder.typicode.com/posts?userId=1 - # response: - # body: - # - id: 1 - # userId: 1 - # title: bar - # body: baz - # - id: 2 - # userId: 1 - # title: qux - # body: quux + - request: + url: http://jsonplaceholder.typicode.com/users + response: + body: + - id: 1 + name: foo + - request: + url: http://jsonplaceholder.typicode.com/posts?userId=1 + response: + body: + - id: 1 + userId: 1 + title: bar + body: baz + - id: 2 + userId: 1 + title: qux + body: quux assert: - request: @@ -127,26 +127,26 @@ assert: posts: - userHttpQuery: name: "Leanne Graham http query" - # - request: - # method: POST - # url: http://localhost:8080/graphql - # body: - # query: "query { userPosts(id: 1) { title } }" - # response: - # body: - # data: - # userPosts: - # - title: bar - # - title: qux - # - request: - # method: POST - # url: http://localhost:8080/graphql - # body: - # query: "query { userWithPosts { posts { title } } }" - # response: - # body: - # data: - # userWithPosts: - # posts: - # - title: bar - # - title: qux + - request: + method: POST + url: http://localhost:8080/graphql + body: + query: "query { userPosts(id: 1) { title } }" + response: + body: + data: + userPosts: + - title: bar + - title: qux + - request: + method: POST + url: http://localhost:8080/graphql + body: + query: "query { userWithPosts { posts { title } } }" + response: + body: + data: + userWithPosts: + posts: + - title: bar + - title: qux diff --git a/tests/http/config/call.graphql b/tests/http/config/call.graphql index b7d158f9d1..c02b879faf 100644 --- a/tests/http/config/call.graphql +++ b/tests/http/config/call.graphql @@ -7,19 +7,20 @@ schema type Query { posts: [Post] @http(path: "/posts") user(id: Int!): User @http(path: "/users/{{args.id}}") - # userPosts(id: ID!): [Post] @http(path: "/posts", query: [{key: "userId", value: "{{args.id}}"}]) + userPosts(id: ID!): [Post] @http(path: "/posts", query: [{key: "userId", value: "{{args.id}}"}]) userHttpHeaders(id: ID!): User @http(path: "/users", headers: [{key: "id", value: "{{args.id}}"}]) userHttpQuery(id: ID!): User @http(path: "/users", query: [{key: "id", value: "{{args.id}}"}]) userGraphQL(id: Int): User @graphQL(baseURL: "http://upstream/graphql", name: "user", args: [{key: "id", value: "{{args.id}}"}]) userGraphQLHeaders(id: Int!): User @graphQL(baseURL: "http://upstream/graphql", name: "user", headers: [{key: "id", value: "{{args.id}}"}]) + userWithPosts: UserWithPosts @http(path: "/users/1") } type UserWithPosts { id: Int! name: String! - # posts: [Post] @call(query: "userPosts", args: {id: "{{value.id}}"}) + posts: [Post] @call(query: "userPosts", args: {id: "{{value.id}}"}) } type User { From c3b77d1624aa4ef4ad2a33ce638b2973a9b0c9ac Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 24 Jan 2024 15:42:43 -0300 Subject: [PATCH 77/92] feat: create basic test for `@grpc` operator --- tests/http/call-operator.yml | 30 ++++++++++++++++++++++++++++++ tests/http/config/call.graphql | 19 +++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/tests/http/call-operator.yml b/tests/http/call-operator.yml index ff852290e5..e500ad1274 100644 --- a/tests/http/call-operator.yml +++ b/tests/http/call-operator.yml @@ -68,6 +68,11 @@ mock: userId: 1 title: qux body: quux + - request: + url: http://localhost:50051/NewsService/GetAllNews + method: POST + response: + body: \0\0\0\0t\n#\x08\x01\x12\x06Note 1\x1a\tContent 1\"\x0cPost image 1\n#\x08\x02\x12\x06Note 2\x1a\tContent 2\"\x0cPost image 2 assert: - request: @@ -150,3 +155,28 @@ assert: posts: - title: bar - title: qux + - request: + method: POST + url: http://localhost:8080/graphql + body: + query: "query { news { news{ id }} }" + response: + body: + data: + news: + news: + - id: 1 + - id: 2 + - request: + method: POST + url: http://localhost:8080/graphql + body: + query: "query { posts { news { news { id } } } }" + response: + body: + data: + posts: + - news: + news: + - id: 1 + - id: 2 diff --git a/tests/http/config/call.graphql b/tests/http/config/call.graphql index c02b879faf..047501f725 100644 --- a/tests/http/config/call.graphql +++ b/tests/http/config/call.graphql @@ -15,6 +15,24 @@ type Query { userGraphQLHeaders(id: Int!): User @graphQL(baseURL: "http://upstream/graphql", name: "user", headers: [{key: "id", value: "{{args.id}}"}]) userWithPosts: UserWithPosts @http(path: "/users/1") + news: NewsData! + @grpc( + service: "NewsService" + method: "GetAllNews" + baseURL: "http://localhost:50051" + protoPath: "src/grpc/tests/news.proto" + ) +} + +type NewsData { + news: [News]! +} + +type News { + id: Int + title: String + body: String + postImage: String } type UserWithPosts { @@ -42,4 +60,5 @@ type Post { userHttpQuery: User @call(query: "userHttpQuery", args: {id: "{{value.userId}}"}) userGraphQL: User @call(query: "userGraphQL", args: {id: "{{value.userId}}"}) userGraphQLHeaders: User @call(query: "userGraphQLHeaders", args: {id: "{{value.userId}}"}) + news: NewsData! @call(query: "news", args: {}) } From 77789489bbcbbe4d2dc0f16983e383f0e7923197 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 24 Jan 2024 16:00:58 -0300 Subject: [PATCH 78/92] test: add grpc arg test --- tests/http/call-operator.yml | 25 +++++++++++++++++++++++++ tests/http/config/call.graphql | 8 ++++++++ 2 files changed, 33 insertions(+) diff --git a/tests/http/call-operator.yml b/tests/http/call-operator.yml index e500ad1274..f55623d693 100644 --- a/tests/http/call-operator.yml +++ b/tests/http/call-operator.yml @@ -180,3 +180,28 @@ assert: news: - id: 1 - id: 2 + - request: + method: POST + url: http://localhost:8080/graphql + body: + query: "query { posts { newsWithPortArg { news { id } } } }" + response: + body: + data: + posts: + - newsWithPortArg: + news: + - id: 1 + - id: 2 + - request: + method: POST + url: http://localhost:8080/graphql + body: + query: "query { newsWithPortArg(port: 50051) { news { id } } }" + response: + body: + data: + newsWithPortArg: + news: + - id: 1 + - id: 2 diff --git a/tests/http/config/call.graphql b/tests/http/config/call.graphql index 047501f725..db07e36ce1 100644 --- a/tests/http/config/call.graphql +++ b/tests/http/config/call.graphql @@ -22,6 +22,13 @@ type Query { baseURL: "http://localhost:50051" protoPath: "src/grpc/tests/news.proto" ) + newsWithPortArg(port: Int!): NewsData! + @grpc( + service: "NewsService" + method: "GetAllNews" + baseURL: "http://localhost:{{args.port}}" + protoPath: "src/grpc/tests/news.proto" + ) } type NewsData { @@ -61,4 +68,5 @@ type Post { userGraphQL: User @call(query: "userGraphQL", args: {id: "{{value.userId}}"}) userGraphQLHeaders: User @call(query: "userGraphQLHeaders", args: {id: "{{value.userId}}"}) news: NewsData! @call(query: "news", args: {}) + newsWithPortArg: NewsData! @call(query: "news", args: {port: "50051"}) } From 182969ddbd6d8fa04cee079161846a324243a392 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 24 Jan 2024 16:01:07 -0300 Subject: [PATCH 79/92] feat: support `@grpc` on `@call` --- src/blueprint/from_config/operators/call.rs | 72 ++++++++++++++------- 1 file changed, 47 insertions(+), 25 deletions(-) diff --git a/src/blueprint/from_config/operators/call.rs b/src/blueprint/from_config/operators/call.rs index d48bf82bbc..a9199d643f 100644 --- a/src/blueprint/from_config/operators/call.rs +++ b/src/blueprint/from_config/operators/call.rs @@ -80,29 +80,9 @@ pub fn compile_call( if let Some(http) = _field.http.clone() { compile_http(config, field, &http).and_then(|expr| match expr.clone() { Expression::IO(IO::Http { req_template, group_by, dl_id }) => Valid::succeed( - req_template.clone().root_url( - req_template - .root_url - .get_segments() - .iter() - .map(|segment| match segment { - Segment::Literal(literal) => Segment::Literal(literal.clone()), - Segment::Expression(expression) => { - if expression[0] == "args" { - let value = find_value(&args, &expression[1]).unwrap(); - let item = Mustache::parse(value).unwrap(); - - let expression = item.get_segments().first().unwrap().to_owned().to_owned(); - - expression - } else { - Segment::Expression(expression.clone()) - } - } - }) - .collect::>() - .into(), - ), + req_template + .clone() + .root_url(replace_url(&req_template.root_url, &args)), ) .map(|req_template| { req_template @@ -132,7 +112,7 @@ pub fn compile_call( .unwrap() .iter() .map(replace_mustache(&args)) - .collect::>(); + .collect(); req_template.operation_arguments(Some(operation_arguments)) } else { @@ -150,15 +130,57 @@ pub fn compile_call( _ => Valid::succeed(expr), }) } else if let Some(grpc) = _field.grpc.clone() { + // todo!("needs to be implemented"); let inputs: CompileGrpc<'_> = CompileGrpc { config, operation_type, field, grpc: &grpc, validate_with_schema: false }; - compile_grpc(inputs) + compile_grpc(inputs).and_then(|expr| match expr { + Expression::IO(IO::Grpc { req_template, group_by, dl_id }) => { + Valid::succeed(req_template.clone().url(replace_url(&req_template.url, &args))) + .map(|req_template| { + req_template + .clone() + .headers(req_template.headers.iter().map(replace_mustache(&args)).collect()) + }) + .map(|req_template| { + if let Some(body) = req_template.clone().body { + req_template.clone().body(Some(replace_url(&body, &args))) + } else { + req_template + } + }) + .map(|req_template| Expression::IO(IO::Grpc { req_template, group_by, dl_id })) + } + _ => Valid::succeed(expr), + }) } else { return Valid::fail(format!("{} field has no resolver", field_name)); } }) } +fn replace_url(url: &Mustache, args: &Iter<'_, String, String>) -> Mustache { + url + .get_segments() + .iter() + .map(|segment| match segment { + Segment::Literal(literal) => Segment::Literal(literal.clone()), + Segment::Expression(expression) => { + if expression[0] == "args" { + let value = find_value(&args, &expression[1]).unwrap(); + let item = Mustache::parse(value).unwrap(); + + let expression = item.get_segments().first().unwrap().to_owned().to_owned(); + + expression + } else { + Segment::Expression(expression.clone()) + } + } + }) + .collect::>() + .into() +} + fn replace_mustache<'a, T: Clone>(args: &'a Iter<'a, String, String>) -> impl Fn(&(T, Mustache)) -> (T, Mustache) + 'a { |(key, value)| { let value: Mustache = value From 2481c986b29be8a5a4e77ec76bb8c108e0dfc0e7 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 24 Jan 2024 16:05:58 -0300 Subject: [PATCH 80/92] refactor: delete unused file --- src/blueprint/from_config/operators/call.rs | 206 -------------------- 1 file changed, 206 deletions(-) delete mode 100644 src/blueprint/from_config/operators/call.rs diff --git a/src/blueprint/from_config/operators/call.rs b/src/blueprint/from_config/operators/call.rs deleted file mode 100644 index a9199d643f..0000000000 --- a/src/blueprint/from_config/operators/call.rs +++ /dev/null @@ -1,206 +0,0 @@ -use std::collections::hash_map::Iter; - -use crate::blueprint::*; -use crate::config; -use crate::config::{Config, Field, GraphQLOperationType}; -use crate::lambda::{Expression, IO}; -use crate::mustache::{Mustache, Segment}; -use crate::try_fold::TryFold; -use crate::valid::Valid; - -fn find_value<'a>(args: &'a Iter<'a, String, String>, key: &'a String) -> Option<&'a String> { - args - .clone() - .find_map(|(k, value)| if k == key { Some(value) } else { None }) -} - -pub fn update_call( - operation_type: &GraphQLOperationType, -) -> TryFold<'_, (&Config, &Field, &config::Type, &str), FieldDefinition, String> { - TryFold::<(&Config, &Field, &config::Type, &str), FieldDefinition, String>::new( - move |(config, field, _, _), b_field| { - let Some(call) = &field.call else { - return Valid::succeed(b_field); - }; - - compile_call(field, config, call, operation_type) - .and_then(|resolver| Valid::succeed(b_field.resolver(Some(resolver)))) - }, - ) -} - -pub fn compile_call( - field: &Field, - config: &Config, - call: &config::Call, - operation_type: &GraphQLOperationType, -) -> Valid { - if validate_field_has_resolver(field.name(), field, &config.types).is_succeed() { - return Valid::fail("@call directive is not allowed on field because it already has a resolver".to_string()); - } - - Valid::from_option(call.query.clone(), "call must have query".to_string()) - .and_then(|field_name| { - Valid::from_option(config.find_type("Query"), "Query type not found on config".to_string()) - .zip(Valid::succeed(field_name)) - }) - .and_then(|(query_type, field_name)| { - Valid::from_option( - query_type.fields.get(&field_name), - format!("{} field not found", field_name), - ) - .zip(Valid::succeed(field_name)) - .and_then(|(field, field_name)| { - if field.has_resolver() { - Valid::succeed((field, field_name, call.args.iter())) - } else { - Valid::fail(format!("{} field has no resolver", field_name)) - } - }) - }) - .and_then(|(_field, field_name, args)| { - let empties: Vec<(&String, &config::Arg)> = _field - .args - .iter() - .filter(|(k, _)| !args.clone().any(|(k1, _)| k1.eq(*k))) - .collect(); - - if empties.len().gt(&0) { - return Valid::fail(format!( - "no argument {} found", - empties - .iter() - .map(|(k, _)| format!("'{}'", k)) - .collect::>() - .join(", ") - )) - .trace(field_name.as_str()); - } - - if let Some(http) = _field.http.clone() { - compile_http(config, field, &http).and_then(|expr| match expr.clone() { - Expression::IO(IO::Http { req_template, group_by, dl_id }) => Valid::succeed( - req_template - .clone() - .root_url(replace_url(&req_template.root_url, &args)), - ) - .map(|req_template| { - req_template - .clone() - .query(req_template.clone().query.iter().map(replace_mustache(&args)).collect()) - }) - .map(|req_template| { - req_template - .clone() - .headers(req_template.headers.iter().map(replace_mustache(&args)).collect()) - }) - .map(|req_template| Expression::IO(IO::Http { req_template, group_by, dl_id })), - _ => Valid::succeed(expr), - }) - } else if let Some(graphql) = _field.graphql.clone() { - compile_graphql(config, operation_type, &graphql).and_then(|expr| match expr { - Expression::IO(IO::GraphQLEndpoint { req_template, field_name, batch, dl_id }) => Valid::succeed( - req_template - .clone() - .headers(req_template.headers.iter().map(replace_mustache(&args)).collect()), - ) - .map(|req_template| { - if req_template.operation_arguments.is_some() { - let operation_arguments = req_template - .clone() - .operation_arguments - .unwrap() - .iter() - .map(replace_mustache(&args)) - .collect(); - - req_template.operation_arguments(Some(operation_arguments)) - } else { - req_template - } - }) - .and_then(|req_template| { - Valid::succeed(Expression::IO(IO::GraphQLEndpoint { - req_template, - field_name, - batch, - dl_id, - })) - }), - _ => Valid::succeed(expr), - }) - } else if let Some(grpc) = _field.grpc.clone() { - // todo!("needs to be implemented"); - let inputs: CompileGrpc<'_> = - CompileGrpc { config, operation_type, field, grpc: &grpc, validate_with_schema: false }; - compile_grpc(inputs).and_then(|expr| match expr { - Expression::IO(IO::Grpc { req_template, group_by, dl_id }) => { - Valid::succeed(req_template.clone().url(replace_url(&req_template.url, &args))) - .map(|req_template| { - req_template - .clone() - .headers(req_template.headers.iter().map(replace_mustache(&args)).collect()) - }) - .map(|req_template| { - if let Some(body) = req_template.clone().body { - req_template.clone().body(Some(replace_url(&body, &args))) - } else { - req_template - } - }) - .map(|req_template| Expression::IO(IO::Grpc { req_template, group_by, dl_id })) - } - _ => Valid::succeed(expr), - }) - } else { - return Valid::fail(format!("{} field has no resolver", field_name)); - } - }) -} - -fn replace_url(url: &Mustache, args: &Iter<'_, String, String>) -> Mustache { - url - .get_segments() - .iter() - .map(|segment| match segment { - Segment::Literal(literal) => Segment::Literal(literal.clone()), - Segment::Expression(expression) => { - if expression[0] == "args" { - let value = find_value(&args, &expression[1]).unwrap(); - let item = Mustache::parse(value).unwrap(); - - let expression = item.get_segments().first().unwrap().to_owned().to_owned(); - - expression - } else { - Segment::Expression(expression.clone()) - } - } - }) - .collect::>() - .into() -} - -fn replace_mustache<'a, T: Clone>(args: &'a Iter<'a, String, String>) -> impl Fn(&(T, Mustache)) -> (T, Mustache) + 'a { - |(key, value)| { - let value: Mustache = value - .expression_segments() - .iter() - .map(|expression| { - if expression[0] == "args" { - let value = find_value(args, &expression[1]).unwrap(); - let item = Mustache::parse(value).unwrap(); - - let expression = item.get_segments().first().unwrap().to_owned().to_owned(); - - expression - } else { - Segment::Expression(expression.to_owned().to_owned()) - } - }) - .collect::>() - .into(); - - (key.clone().to_owned(), value) - } -} From b6ac5c36fe4ea89e98af15b153c2a8c7b6d6a38e Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 24 Jan 2024 16:08:05 -0300 Subject: [PATCH 81/92] style: fix needless borrow error --- src/blueprint/operators/call.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blueprint/operators/call.rs b/src/blueprint/operators/call.rs index a9199d643f..9644fb2d9c 100644 --- a/src/blueprint/operators/call.rs +++ b/src/blueprint/operators/call.rs @@ -166,7 +166,7 @@ fn replace_url(url: &Mustache, args: &Iter<'_, String, String>) -> Mustache { Segment::Literal(literal) => Segment::Literal(literal.clone()), Segment::Expression(expression) => { if expression[0] == "args" { - let value = find_value(&args, &expression[1]).unwrap(); + let value = find_value(args, &expression[1]).unwrap(); let item = Mustache::parse(value).unwrap(); let expression = item.get_segments().first().unwrap().to_owned().to_owned(); From ce5b63bc4b53da761a6cebbd47295d2e99c25999 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 24 Jan 2024 16:13:13 -0300 Subject: [PATCH 82/92] style: fix `cmp-owned` rule --- tests/http_spec.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/http_spec.rs b/tests/http_spec.rs index 3cf75a578b..cddf4f9c6e 100644 --- a/tests/http_spec.rs +++ b/tests/http_spec.rs @@ -280,7 +280,7 @@ impl HttpIO for MockHttpClient { let headers_match = req .headers() .iter() - .filter(|(key, _)| key.to_string() != "content-type") + .filter(|(key, _)| *key != "content-type") .all(|(key, value)| { let header_name = key.to_string(); From 46ed159f48feeec38eafdfb021b234d57fedd84b Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Wed, 24 Jan 2024 16:19:03 -0300 Subject: [PATCH 83/92] refactor: remove unreachable code --- src/blueprint/operators/call.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/blueprint/operators/call.rs b/src/blueprint/operators/call.rs index 9644fb2d9c..6c387807bd 100644 --- a/src/blueprint/operators/call.rs +++ b/src/blueprint/operators/call.rs @@ -35,10 +35,6 @@ pub fn compile_call( call: &config::Call, operation_type: &GraphQLOperationType, ) -> Valid { - if validate_field_has_resolver(field.name(), field, &config.types).is_succeed() { - return Valid::fail("@call directive is not allowed on field because it already has a resolver".to_string()); - } - Valid::from_option(call.query.clone(), "call must have query".to_string()) .and_then(|field_name| { Valid::from_option(config.find_type("Query"), "Query type not found on config".to_string()) From 2add539acc4e5d93d0c2f10687ac194bb526c014 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Mon, 29 Jan 2024 07:43:49 -0300 Subject: [PATCH 84/92] feat: add call directive on `into_document` --- src/config/into_document.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/config/into_document.rs b/src/config/into_document.rs index 3545d43e82..1063d5b703 100644 --- a/src/config/into_document.rs +++ b/src/config/into_document.rs @@ -188,6 +188,7 @@ fn get_directives(field: &crate::config::Field) -> Vec Date: Mon, 29 Jan 2024 08:29:21 -0300 Subject: [PATCH 85/92] style: run `./lint.sh --mode=fix` --- src/blueprint/operators/call.rs | 360 +++++++++++++++++--------------- 1 file changed, 196 insertions(+), 164 deletions(-) diff --git a/src/blueprint/operators/call.rs b/src/blueprint/operators/call.rs index 6c387807bd..bc360e1291 100644 --- a/src/blueprint/operators/call.rs +++ b/src/blueprint/operators/call.rs @@ -9,194 +9,226 @@ use crate::try_fold::TryFold; use crate::valid::Valid; fn find_value<'a>(args: &'a Iter<'a, String, String>, key: &'a String) -> Option<&'a String> { - args - .clone() - .find_map(|(k, value)| if k == key { Some(value) } else { None }) + args.clone() + .find_map(|(k, value)| if k == key { Some(value) } else { None }) } pub fn update_call( - operation_type: &GraphQLOperationType, + operation_type: &GraphQLOperationType, ) -> TryFold<'_, (&Config, &Field, &config::Type, &str), FieldDefinition, String> { - TryFold::<(&Config, &Field, &config::Type, &str), FieldDefinition, String>::new( - move |(config, field, _, _), b_field| { - let Some(call) = &field.call else { - return Valid::succeed(b_field); - }; + TryFold::<(&Config, &Field, &config::Type, &str), FieldDefinition, String>::new( + move |(config, field, _, _), b_field| { + let Some(call) = &field.call else { + return Valid::succeed(b_field); + }; - compile_call(field, config, call, operation_type) - .and_then(|resolver| Valid::succeed(b_field.resolver(Some(resolver)))) - }, - ) + compile_call(field, config, call, operation_type) + .and_then(|resolver| Valid::succeed(b_field.resolver(Some(resolver)))) + }, + ) } pub fn compile_call( - field: &Field, - config: &Config, - call: &config::Call, - operation_type: &GraphQLOperationType, + field: &Field, + config: &Config, + call: &config::Call, + operation_type: &GraphQLOperationType, ) -> Valid { - Valid::from_option(call.query.clone(), "call must have query".to_string()) - .and_then(|field_name| { - Valid::from_option(config.find_type("Query"), "Query type not found on config".to_string()) - .zip(Valid::succeed(field_name)) - }) - .and_then(|(query_type, field_name)| { - Valid::from_option( - query_type.fields.get(&field_name), - format!("{} field not found", field_name), - ) - .zip(Valid::succeed(field_name)) - .and_then(|(field, field_name)| { - if field.has_resolver() { - Valid::succeed((field, field_name, call.args.iter())) - } else { - Valid::fail(format!("{} field has no resolver", field_name)) - } - }) - }) - .and_then(|(_field, field_name, args)| { - let empties: Vec<(&String, &config::Arg)> = _field - .args - .iter() - .filter(|(k, _)| !args.clone().any(|(k1, _)| k1.eq(*k))) - .collect(); - - if empties.len().gt(&0) { - return Valid::fail(format!( - "no argument {} found", - empties - .iter() - .map(|(k, _)| format!("'{}'", k)) - .collect::>() - .join(", ") - )) - .trace(field_name.as_str()); - } - - if let Some(http) = _field.http.clone() { - compile_http(config, field, &http).and_then(|expr| match expr.clone() { - Expression::IO(IO::Http { req_template, group_by, dl_id }) => Valid::succeed( - req_template - .clone() - .root_url(replace_url(&req_template.root_url, &args)), - ) - .map(|req_template| { - req_template - .clone() - .query(req_template.clone().query.iter().map(replace_mustache(&args)).collect()) - }) - .map(|req_template| { - req_template - .clone() - .headers(req_template.headers.iter().map(replace_mustache(&args)).collect()) - }) - .map(|req_template| Expression::IO(IO::Http { req_template, group_by, dl_id })), - _ => Valid::succeed(expr), + Valid::from_option(call.query.clone(), "call must have query".to_string()) + .and_then(|field_name| { + Valid::from_option( + config.find_type("Query"), + "Query type not found on config".to_string(), + ) + .zip(Valid::succeed(field_name)) }) - } else if let Some(graphql) = _field.graphql.clone() { - compile_graphql(config, operation_type, &graphql).and_then(|expr| match expr { - Expression::IO(IO::GraphQLEndpoint { req_template, field_name, batch, dl_id }) => Valid::succeed( - req_template - .clone() - .headers(req_template.headers.iter().map(replace_mustache(&args)).collect()), - ) - .map(|req_template| { - if req_template.operation_arguments.is_some() { - let operation_arguments = req_template - .clone() - .operation_arguments - .unwrap() + .and_then(|(query_type, field_name)| { + Valid::from_option( + query_type.fields.get(&field_name), + format!("{} field not found", field_name), + ) + .zip(Valid::succeed(field_name)) + .and_then(|(field, field_name)| { + if field.has_resolver() { + Valid::succeed((field, field_name, call.args.iter())) + } else { + Valid::fail(format!("{} field has no resolver", field_name)) + } + }) + }) + .and_then(|(_field, field_name, args)| { + let empties: Vec<(&String, &config::Arg)> = _field + .args .iter() - .map(replace_mustache(&args)) + .filter(|(k, _)| !args.clone().any(|(k1, _)| k1.eq(*k))) .collect(); - req_template.operation_arguments(Some(operation_arguments)) + if empties.len().gt(&0) { + return Valid::fail(format!( + "no argument {} found", + empties + .iter() + .map(|(k, _)| format!("'{}'", k)) + .collect::>() + .join(", ") + )) + .trace(field_name.as_str()); + } + + if let Some(http) = _field.http.clone() { + compile_http(config, field, &http).and_then(|expr| match expr.clone() { + Expression::IO(IO::Http { req_template, group_by, dl_id }) => Valid::succeed( + req_template + .clone() + .root_url(replace_url(&req_template.root_url, &args)), + ) + .map(|req_template| { + req_template.clone().query( + req_template + .clone() + .query + .iter() + .map(replace_mustache(&args)) + .collect(), + ) + }) + .map(|req_template| { + req_template.clone().headers( + req_template + .headers + .iter() + .map(replace_mustache(&args)) + .collect(), + ) + }) + .map(|req_template| Expression::IO(IO::Http { req_template, group_by, dl_id })), + _ => Valid::succeed(expr), + }) + } else if let Some(graphql) = _field.graphql.clone() { + compile_graphql(config, operation_type, &graphql).and_then(|expr| match expr { + Expression::IO(IO::GraphQLEndpoint { + req_template, + field_name, + batch, + dl_id, + }) => Valid::succeed( + req_template.clone().headers( + req_template + .headers + .iter() + .map(replace_mustache(&args)) + .collect(), + ), + ) + .map(|req_template| { + if req_template.operation_arguments.is_some() { + let operation_arguments = req_template + .clone() + .operation_arguments + .unwrap() + .iter() + .map(replace_mustache(&args)) + .collect(); + + req_template.operation_arguments(Some(operation_arguments)) + } else { + req_template + } + }) + .and_then(|req_template| { + Valid::succeed(Expression::IO(IO::GraphQLEndpoint { + req_template, + field_name, + batch, + dl_id, + })) + }), + _ => Valid::succeed(expr), + }) + } else if let Some(grpc) = _field.grpc.clone() { + // todo!("needs to be implemented"); + let inputs: CompileGrpc<'_> = CompileGrpc { + config, + operation_type, + field, + grpc: &grpc, + validate_with_schema: false, + }; + compile_grpc(inputs).and_then(|expr| match expr { + Expression::IO(IO::Grpc { req_template, group_by, dl_id }) => Valid::succeed( + req_template + .clone() + .url(replace_url(&req_template.url, &args)), + ) + .map(|req_template| { + req_template.clone().headers( + req_template + .headers + .iter() + .map(replace_mustache(&args)) + .collect(), + ) + }) + .map(|req_template| { + if let Some(body) = req_template.clone().body { + req_template.clone().body(Some(replace_url(&body, &args))) + } else { + req_template + } + }) + .map(|req_template| Expression::IO(IO::Grpc { req_template, group_by, dl_id })), + _ => Valid::succeed(expr), + }) } else { - req_template + return Valid::fail(format!("{} field has no resolver", field_name)); } - }) - .and_then(|req_template| { - Valid::succeed(Expression::IO(IO::GraphQLEndpoint { - req_template, - field_name, - batch, - dl_id, - })) - }), - _ => Valid::succeed(expr), }) - } else if let Some(grpc) = _field.grpc.clone() { - // todo!("needs to be implemented"); - let inputs: CompileGrpc<'_> = - CompileGrpc { config, operation_type, field, grpc: &grpc, validate_with_schema: false }; - compile_grpc(inputs).and_then(|expr| match expr { - Expression::IO(IO::Grpc { req_template, group_by, dl_id }) => { - Valid::succeed(req_template.clone().url(replace_url(&req_template.url, &args))) - .map(|req_template| { - req_template - .clone() - .headers(req_template.headers.iter().map(replace_mustache(&args)).collect()) - }) - .map(|req_template| { - if let Some(body) = req_template.clone().body { - req_template.clone().body(Some(replace_url(&body, &args))) - } else { - req_template - } - }) - .map(|req_template| Expression::IO(IO::Grpc { req_template, group_by, dl_id })) - } - _ => Valid::succeed(expr), - }) - } else { - return Valid::fail(format!("{} field has no resolver", field_name)); - } - }) } fn replace_url(url: &Mustache, args: &Iter<'_, String, String>) -> Mustache { - url - .get_segments() - .iter() - .map(|segment| match segment { - Segment::Literal(literal) => Segment::Literal(literal.clone()), - Segment::Expression(expression) => { - if expression[0] == "args" { - let value = find_value(args, &expression[1]).unwrap(); - let item = Mustache::parse(value).unwrap(); + url.get_segments() + .iter() + .map(|segment| match segment { + Segment::Literal(literal) => Segment::Literal(literal.clone()), + Segment::Expression(expression) => { + if expression[0] == "args" { + let value = find_value(args, &expression[1]).unwrap(); + let item = Mustache::parse(value).unwrap(); - let expression = item.get_segments().first().unwrap().to_owned().to_owned(); + let expression = item.get_segments().first().unwrap().to_owned().to_owned(); - expression - } else { - Segment::Expression(expression.clone()) - } - } - }) - .collect::>() - .into() + expression + } else { + Segment::Expression(expression.clone()) + } + } + }) + .collect::>() + .into() } -fn replace_mustache<'a, T: Clone>(args: &'a Iter<'a, String, String>) -> impl Fn(&(T, Mustache)) -> (T, Mustache) + 'a { - |(key, value)| { - let value: Mustache = value - .expression_segments() - .iter() - .map(|expression| { - if expression[0] == "args" { - let value = find_value(args, &expression[1]).unwrap(); - let item = Mustache::parse(value).unwrap(); +fn replace_mustache<'a, T: Clone>( + args: &'a Iter<'a, String, String>, +) -> impl Fn(&(T, Mustache)) -> (T, Mustache) + 'a { + |(key, value)| { + let value: Mustache = value + .expression_segments() + .iter() + .map(|expression| { + if expression[0] == "args" { + let value = find_value(args, &expression[1]).unwrap(); + let item = Mustache::parse(value).unwrap(); - let expression = item.get_segments().first().unwrap().to_owned().to_owned(); + let expression = item.get_segments().first().unwrap().to_owned().to_owned(); - expression - } else { - Segment::Expression(expression.to_owned().to_owned()) - } - }) - .collect::>() - .into(); + expression + } else { + Segment::Expression(expression.to_owned().to_owned()) + } + }) + .collect::>() + .into(); - (key.clone().to_owned(), value) - } + (key.clone().to_owned(), value) + } } From 7993df687b7ce1186f41c32a87e6ae3d4f4900bb Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Mon, 29 Jan 2024 08:33:05 -0300 Subject: [PATCH 86/92] refactor: return field on `validate_field` --- src/blueprint/mustache.rs | 5 +++-- src/blueprint/operators/graphql.rs | 2 +- src/blueprint/operators/grpc.rs | 2 +- src/blueprint/operators/http.rs | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/blueprint/mustache.rs b/src/blueprint/mustache.rs index 2f5aaccea2..1c58dfafb6 100644 --- a/src/blueprint/mustache.rs +++ b/src/blueprint/mustache.rs @@ -101,14 +101,14 @@ impl<'a> MustachePartsValidator<'a> { } impl FieldDefinition { - pub fn validate_field(&self, type_of: &config::Type, config: &Config) -> Valid<(), String> { + pub fn validate_field(self, type_of: &config::Type, config: &Config) -> Valid { // XXX we could use `Mustache`'s `render` method with a mock // struct implementing the `PathString` trait encapsulating `validation_map` // but `render` simply falls back to the default value for a given // type if it doesn't exist, so we wouldn't be able to get enough // context from that method alone // So we must duplicate some of that logic here :( - let parts_validator = MustachePartsValidator::new(type_of, config, self); + let parts_validator = MustachePartsValidator::new(type_of, config, &self); match &self.resolver { Some(Expression::IO(IO::Http { req_template, .. })) => { @@ -168,5 +168,6 @@ impl FieldDefinition { } _ => Valid::succeed(()), } + .map_to(self) } } diff --git a/src/blueprint/operators/graphql.rs b/src/blueprint/operators/graphql.rs index 4be5959e7d..0094c159fb 100644 --- a/src/blueprint/operators/graphql.rs +++ b/src/blueprint/operators/graphql.rs @@ -49,7 +49,7 @@ pub fn update_graphql<'a>( compile_graphql(config, operation_type, graphql) .map(|resolver| b_field.resolver(Some(resolver))) - .and_then(|b_field| b_field.validate_field(type_of, config).map_to(b_field)) + .and_then(|b_field| b_field.validate_field(type_of, config)) }, ) } diff --git a/src/blueprint/operators/grpc.rs b/src/blueprint/operators/grpc.rs index f974d34d39..01b9a89541 100644 --- a/src/blueprint/operators/grpc.rs +++ b/src/blueprint/operators/grpc.rs @@ -178,7 +178,7 @@ pub fn update_grpc<'a>( validate_with_schema: true, }) .map(|resolver| b_field.resolver(Some(resolver))) - .and_then(|b_field| b_field.validate_field(type_of, config).map_to(b_field)) + .and_then(|b_field| b_field.validate_field(type_of, config)) }, ) } diff --git a/src/blueprint/operators/http.rs b/src/blueprint/operators/http.rs index ce0990548b..02ae5ac211 100644 --- a/src/blueprint/operators/http.rs +++ b/src/blueprint/operators/http.rs @@ -78,7 +78,7 @@ pub fn update_http<'a>( compile_http(config, field, http) .map(|resolver| b_field.resolver(Some(resolver))) - .and_then(|b_field| b_field.validate_field(type_of, config).map_to(b_field)) + .and_then(|b_field| b_field.validate_field(type_of, config)) }, ) } From 0c05f71759d933fbb06fcfed78443660c4fdc231 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Mon, 29 Jan 2024 08:39:42 -0300 Subject: [PATCH 87/92] test: remove character breaking tests --- .../errors/test-multiple-resolvable-directives-on-field.graphql | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/graphql/errors/test-multiple-resolvable-directives-on-field.graphql b/tests/graphql/errors/test-multiple-resolvable-directives-on-field.graphql index 5af470dc18..22df130f3e 100644 --- a/tests/graphql/errors/test-multiple-resolvable-directives-on-field.graphql +++ b/tests/graphql/errors/test-multiple-resolvable-directives-on-field.graphql @@ -16,4 +16,3 @@ type Query { #> client-sdl type Failure @error(message: "Multiple resolvers detected [@http, @const]", trace: ["Query", "user1"]) type Failure @error(message: "Multiple resolvers detected [@http, @call]", trace: ["Query", "user2"]) -``` From a867c58e0d239542d5862919ca02423f2084fb82 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Mon, 29 Jan 2024 11:16:42 -0300 Subject: [PATCH 88/92] fix: delete doc for call operator --- docs/operators/call.md | 53 ------------------------------------------ 1 file changed, 53 deletions(-) delete mode 100644 docs/operators/call.md diff --git a/docs/operators/call.md b/docs/operators/call.md deleted file mode 100644 index cdf6a6c248..0000000000 --- a/docs/operators/call.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: "@call" ---- - -The **@call** operator is used to reference an `@http` operator. It is useful when you have multiple fields that resolves from the same HTTP endpoint. - -```graphql showLineNumbers -schema { - query: Query -} - -type Query { - posts: [Post] @http(path: "/posts") - user(id: Int!): User @http(path: "/users/{{args.id}}") -} - -type User { - id: Int! - name: String! - username: String! - email: String! -} - -type Post { - id: Int! - userId: Int! - title: String! - body: String! - user: User @call(query: "user", args: [{key: "id", value: "{{value.userId}}"}]) -} -``` - -## query - -The name of the field that has the `@http` resolver to be called. It is required. - -```graphql showLineNumbers -type Post { - userId: Int! - user: User @call(query: "user", args: [{key: "id", value: "{{value.userId}}"}]) -} -``` - -## args - -The arguments to be passed to the `@http` resolver. It is optional. - -```graphql showLineNumbers -type Post { - userId: Int! - user: User @call(query: "user", args: [{key: "id", value: "{{value.userId}}"}]) -} -``` From e26a9b3e40827eb0efa4067183a95d9c22f00363 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Mon, 29 Jan 2024 12:08:51 -0300 Subject: [PATCH 89/92] refactor: use `try_from` to avoid unreachable code --- src/blueprint/operators/call.rs | 158 ++++++++++++++++++++++++++------ 1 file changed, 132 insertions(+), 26 deletions(-) diff --git a/src/blueprint/operators/call.rs b/src/blueprint/operators/call.rs index bc360e1291..a17cf1d21b 100644 --- a/src/blueprint/operators/call.rs +++ b/src/blueprint/operators/call.rs @@ -2,7 +2,12 @@ use std::collections::hash_map::Iter; use crate::blueprint::*; use crate::config; +use crate::config::group_by::GroupBy; use crate::config::{Config, Field, GraphQLOperationType}; +use crate::graphql; +use crate::grpc; +use crate::http; +use crate::lambda::DataLoaderId; use crate::lambda::{Expression, IO}; use crate::mustache::{Mustache, Segment}; use crate::try_fold::TryFold; @@ -28,6 +33,64 @@ pub fn update_call( ) } +struct Http { + pub req_template: http::RequestTemplate, + pub group_by: Option, + pub dl_id: Option, +} + +struct GraphQLEndpoint { + pub req_template: graphql::RequestTemplate, + pub field_name: String, + pub batch: bool, + pub dl_id: Option, +} + +struct Grpc { + pub req_template: grpc::RequestTemplate, + pub group_by: Option, + pub dl_id: Option, +} + +impl TryFrom for Http { + type Error = String; + + fn try_from(expr: Expression) -> Result { + match expr { + Expression::IO(IO::Http { req_template, group_by, dl_id }) => { + Ok(Http { req_template, group_by, dl_id }) + } + _ => Err("not an http expression".to_string()), + } + } +} + +impl TryFrom for GraphQLEndpoint { + type Error = String; + + fn try_from(expr: Expression) -> Result { + match expr { + Expression::IO(IO::GraphQLEndpoint { req_template, field_name, batch, dl_id }) => { + Ok(GraphQLEndpoint { req_template, field_name, batch, dl_id }) + } + _ => Err("not a graphql expression".to_string()), + } + } +} + +impl TryFrom for Grpc { + type Error = String; + + fn try_from(expr: Expression) -> Result { + match expr { + Expression::IO(IO::Grpc { req_template, group_by, dl_id }) => { + Ok(Grpc { req_template, group_by, dl_id }) + } + _ => Err("not a grpc expression".to_string()), + } + } +} + pub fn compile_call( field: &Field, config: &Config, @@ -76,11 +139,13 @@ pub fn compile_call( } if let Some(http) = _field.http.clone() { - compile_http(config, field, &http).and_then(|expr| match expr.clone() { - Expression::IO(IO::Http { req_template, group_by, dl_id }) => Valid::succeed( - req_template + compile_http(config, field, &http).and_then(|expr| { + let http = Http::try_from(expr).unwrap(); + + Valid::succeed( + http.req_template .clone() - .root_url(replace_url(&req_template.root_url, &args)), + .root_url(replace_url(&http.req_template.root_url, &args)), ) .map(|req_template| { req_template.clone().query( @@ -101,19 +166,22 @@ pub fn compile_call( .collect(), ) }) - .map(|req_template| Expression::IO(IO::Http { req_template, group_by, dl_id })), - _ => Valid::succeed(expr), + .map(|req_template| { + Expression::IO(IO::Http { + req_template: req_template, + dl_id: http.dl_id, + group_by: http.group_by, + }) + }) }) } else if let Some(graphql) = _field.graphql.clone() { - compile_graphql(config, operation_type, &graphql).and_then(|expr| match expr { - Expression::IO(IO::GraphQLEndpoint { - req_template, - field_name, - batch, - dl_id, - }) => Valid::succeed( - req_template.clone().headers( - req_template + compile_graphql(config, operation_type, &graphql).and_then(|expr| { + let graphql = GraphQLEndpoint::try_from(expr).unwrap(); + + Valid::succeed( + graphql.req_template.clone().headers( + graphql + .req_template .headers .iter() .map(replace_mustache(&args)) @@ -138,12 +206,11 @@ pub fn compile_call( .and_then(|req_template| { Valid::succeed(Expression::IO(IO::GraphQLEndpoint { req_template, - field_name, - batch, - dl_id, + field_name: graphql.field_name, + batch: graphql.batch, + dl_id: graphql.dl_id, })) - }), - _ => Valid::succeed(expr), + }) }) } else if let Some(grpc) = _field.grpc.clone() { // todo!("needs to be implemented"); @@ -154,11 +221,13 @@ pub fn compile_call( grpc: &grpc, validate_with_schema: false, }; - compile_grpc(inputs).and_then(|expr| match expr { - Expression::IO(IO::Grpc { req_template, group_by, dl_id }) => Valid::succeed( - req_template + compile_grpc(inputs).and_then(|expr| { + let grpc = Grpc::try_from(expr).unwrap(); + + Valid::succeed( + grpc.req_template .clone() - .url(replace_url(&req_template.url, &args)), + .url(replace_url(&grpc.req_template.url, &args)), ) .map(|req_template| { req_template.clone().headers( @@ -176,8 +245,13 @@ pub fn compile_call( req_template } }) - .map(|req_template| Expression::IO(IO::Grpc { req_template, group_by, dl_id })), - _ => Valid::succeed(expr), + .map(|req_template| { + Expression::IO(IO::Grpc { + req_template, + group_by: grpc.group_by, + dl_id: grpc.dl_id, + }) + }) }) } else { return Valid::fail(format!("{} field has no resolver", field_name)); @@ -232,3 +306,35 @@ fn replace_mustache<'a, T: Clone>( (key.clone().to_owned(), value) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn try_from_http_fail() { + let expr = Expression::Literal("test".into()); + + let http = Http::try_from(expr); + + assert!(http.is_err()); + } + + #[test] + fn try_from_graphql_fail() { + let expr = Expression::Literal("test".into()); + + let graphql = GraphQLEndpoint::try_from(expr); + + assert!(graphql.is_err()); + } + + #[test] + fn try_from_grpc_fail() { + let expr = Expression::Literal("test".into()); + + let grpc = Grpc::try_from(expr); + + assert!(grpc.is_err()); + } +} From 4bffd937bf038f9d519378e5f0d51bab5dec5b85 Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Mon, 29 Jan 2024 13:18:24 -0300 Subject: [PATCH 90/92] style: run `./lint.sh --mode=fix` --- src/blueprint/operators/call.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/blueprint/operators/call.rs b/src/blueprint/operators/call.rs index a17cf1d21b..57def4d539 100644 --- a/src/blueprint/operators/call.rs +++ b/src/blueprint/operators/call.rs @@ -1,17 +1,13 @@ use std::collections::hash_map::Iter; use crate::blueprint::*; -use crate::config; use crate::config::group_by::GroupBy; use crate::config::{Config, Field, GraphQLOperationType}; -use crate::graphql; -use crate::grpc; -use crate::http; -use crate::lambda::DataLoaderId; -use crate::lambda::{Expression, IO}; +use crate::lambda::{DataLoaderId, Expression, IO}; use crate::mustache::{Mustache, Segment}; use crate::try_fold::TryFold; use crate::valid::Valid; +use crate::{config, graphql, grpc, http}; fn find_value<'a>(args: &'a Iter<'a, String, String>, key: &'a String) -> Option<&'a String> { args.clone() @@ -168,7 +164,7 @@ pub fn compile_call( }) .map(|req_template| { Expression::IO(IO::Http { - req_template: req_template, + req_template, dl_id: http.dl_id, group_by: http.group_by, }) From ec7c9f48fd69bb79add20147a7246563f124dffb Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Mon, 29 Jan 2024 13:25:18 -0300 Subject: [PATCH 91/92] feat: support mutation on call --- src/blueprint/operators/call.rs | 309 +++++++++--------- src/config/config.rs | 8 + .../graphql/errors/test-call-operator.graphql | 2 +- 3 files changed, 170 insertions(+), 149 deletions(-) diff --git a/src/blueprint/operators/call.rs b/src/blueprint/operators/call.rs index 57def4d539..5bc503877a 100644 --- a/src/blueprint/operators/call.rs +++ b/src/blueprint/operators/call.rs @@ -87,172 +87,185 @@ impl TryFrom for Grpc { } } +fn get_type_and_field(call: &config::Call) -> Option<(String, String)> { + if let Some(query) = &call.query { + Some(("Query".to_string(), query.clone())) + } else if let Some(mutation) = &call.mutation { + Some(("Mutation".to_string(), mutation.clone())) + } else { + None + } +} + pub fn compile_call( field: &Field, config: &Config, call: &config::Call, operation_type: &GraphQLOperationType, ) -> Valid { - Valid::from_option(call.query.clone(), "call must have query".to_string()) - .and_then(|field_name| { - Valid::from_option( - config.find_type("Query"), - "Query type not found on config".to_string(), - ) - .zip(Valid::succeed(field_name)) - }) - .and_then(|(query_type, field_name)| { - Valid::from_option( - query_type.fields.get(&field_name), - format!("{} field not found", field_name), - ) - .zip(Valid::succeed(field_name)) - .and_then(|(field, field_name)| { - if field.has_resolver() { - Valid::succeed((field, field_name, call.args.iter())) - } else { - Valid::fail(format!("{} field has no resolver", field_name)) - } - }) - }) - .and_then(|(_field, field_name, args)| { - let empties: Vec<(&String, &config::Arg)> = _field - .args - .iter() - .filter(|(k, _)| !args.clone().any(|(k1, _)| k1.eq(*k))) - .collect(); - - if empties.len().gt(&0) { - return Valid::fail(format!( - "no argument {} found", - empties - .iter() - .map(|(k, _)| format!("'{}'", k)) - .collect::>() - .join(", ") - )) - .trace(field_name.as_str()); + Valid::from_option( + get_type_and_field(call), + "call must have query or mutation".to_string(), + ) + .and_then(|(type_name, field_name)| { + Valid::from_option( + config.find_type(&type_name), + format!("{} type not found on config", type_name), + ) + .zip(Valid::succeed(field_name)) + }) + .and_then(|(query_type, field_name)| { + Valid::from_option( + query_type.fields.get(&field_name), + format!("{} field not found", field_name), + ) + .zip(Valid::succeed(field_name)) + .and_then(|(field, field_name)| { + if field.has_resolver() { + Valid::succeed((field, field_name, call.args.iter())) + } else { + Valid::fail(format!("{} field has no resolver", field_name)) } + }) + }) + .and_then(|(_field, field_name, args)| { + let empties: Vec<(&String, &config::Arg)> = _field + .args + .iter() + .filter(|(k, _)| !args.clone().any(|(k1, _)| k1.eq(*k))) + .collect(); + + if empties.len().gt(&0) { + return Valid::fail(format!( + "no argument {} found", + empties + .iter() + .map(|(k, _)| format!("'{}'", k)) + .collect::>() + .join(", ") + )) + .trace(field_name.as_str()); + } - if let Some(http) = _field.http.clone() { - compile_http(config, field, &http).and_then(|expr| { - let http = Http::try_from(expr).unwrap(); - - Valid::succeed( - http.req_template + if let Some(http) = _field.http.clone() { + compile_http(config, field, &http).and_then(|expr| { + let http = Http::try_from(expr).unwrap(); + + Valid::succeed( + http.req_template + .clone() + .root_url(replace_url(&http.req_template.root_url, &args)), + ) + .map(|req_template| { + req_template.clone().query( + req_template .clone() - .root_url(replace_url(&http.req_template.root_url, &args)), + .query + .iter() + .map(replace_mustache(&args)) + .collect(), ) - .map(|req_template| { - req_template.clone().query( - req_template - .clone() - .query - .iter() - .map(replace_mustache(&args)) - .collect(), - ) - }) - .map(|req_template| { - req_template.clone().headers( - req_template - .headers - .iter() - .map(replace_mustache(&args)) - .collect(), - ) - }) - .map(|req_template| { - Expression::IO(IO::Http { - req_template, - dl_id: http.dl_id, - group_by: http.group_by, - }) - }) }) - } else if let Some(graphql) = _field.graphql.clone() { - compile_graphql(config, operation_type, &graphql).and_then(|expr| { - let graphql = GraphQLEndpoint::try_from(expr).unwrap(); - - Valid::succeed( - graphql.req_template.clone().headers( - graphql - .req_template - .headers - .iter() - .map(replace_mustache(&args)) - .collect(), - ), + .map(|req_template| { + req_template.clone().headers( + req_template + .headers + .iter() + .map(replace_mustache(&args)) + .collect(), ) - .map(|req_template| { - if req_template.operation_arguments.is_some() { - let operation_arguments = req_template - .clone() - .operation_arguments - .unwrap() - .iter() - .map(replace_mustache(&args)) - .collect(); - - req_template.operation_arguments(Some(operation_arguments)) - } else { - req_template - } - }) - .and_then(|req_template| { - Valid::succeed(Expression::IO(IO::GraphQLEndpoint { - req_template, - field_name: graphql.field_name, - batch: graphql.batch, - dl_id: graphql.dl_id, - })) + }) + .map(|req_template| { + Expression::IO(IO::Http { + req_template, + dl_id: http.dl_id, + group_by: http.group_by, }) }) - } else if let Some(grpc) = _field.grpc.clone() { - // todo!("needs to be implemented"); - let inputs: CompileGrpc<'_> = CompileGrpc { - config, - operation_type, - field, - grpc: &grpc, - validate_with_schema: false, - }; - compile_grpc(inputs).and_then(|expr| { - let grpc = Grpc::try_from(expr).unwrap(); - - Valid::succeed( - grpc.req_template + }) + } else if let Some(graphql) = _field.graphql.clone() { + compile_graphql(config, operation_type, &graphql).and_then(|expr| { + let graphql = GraphQLEndpoint::try_from(expr).unwrap(); + + Valid::succeed( + graphql.req_template.clone().headers( + graphql + .req_template + .headers + .iter() + .map(replace_mustache(&args)) + .collect(), + ), + ) + .map(|req_template| { + if req_template.operation_arguments.is_some() { + let operation_arguments = req_template .clone() - .url(replace_url(&grpc.req_template.url, &args)), + .operation_arguments + .unwrap() + .iter() + .map(replace_mustache(&args)) + .collect(); + + req_template.operation_arguments(Some(operation_arguments)) + } else { + req_template + } + }) + .and_then(|req_template| { + Valid::succeed(Expression::IO(IO::GraphQLEndpoint { + req_template, + field_name: graphql.field_name, + batch: graphql.batch, + dl_id: graphql.dl_id, + })) + }) + }) + } else if let Some(grpc) = _field.grpc.clone() { + // todo!("needs to be implemented"); + let inputs: CompileGrpc<'_> = CompileGrpc { + config, + operation_type, + field, + grpc: &grpc, + validate_with_schema: false, + }; + compile_grpc(inputs).and_then(|expr| { + let grpc = Grpc::try_from(expr).unwrap(); + + Valid::succeed( + grpc.req_template + .clone() + .url(replace_url(&grpc.req_template.url, &args)), + ) + .map(|req_template| { + req_template.clone().headers( + req_template + .headers + .iter() + .map(replace_mustache(&args)) + .collect(), ) - .map(|req_template| { - req_template.clone().headers( - req_template - .headers - .iter() - .map(replace_mustache(&args)) - .collect(), - ) - }) - .map(|req_template| { - if let Some(body) = req_template.clone().body { - req_template.clone().body(Some(replace_url(&body, &args))) - } else { - req_template - } - }) - .map(|req_template| { - Expression::IO(IO::Grpc { - req_template, - group_by: grpc.group_by, - dl_id: grpc.dl_id, - }) + }) + .map(|req_template| { + if let Some(body) = req_template.clone().body { + req_template.clone().body(Some(replace_url(&body, &args))) + } else { + req_template + } + }) + .map(|req_template| { + Expression::IO(IO::Grpc { + req_template, + group_by: grpc.group_by, + dl_id: grpc.dl_id, }) }) - } else { - return Valid::fail(format!("{} field has no resolver", field_name)); - } - }) + }) + } else { + return Valid::fail(format!("{} field has no resolver", field_name)); + } + }) } fn replace_url(url: &Mustache, args: &Iter<'_, String, String>) -> Mustache { diff --git a/src/config/config.rs b/src/config/config.rs index 0562edd65b..f6cbc722ce 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -569,10 +569,18 @@ pub struct Http { /// /// For instance, if you have a `user(id: Int!): User @http(path: "/users/{{args.id}}")` field on the `Query` type, you can reference it from another field on the `Query` type using the `@call` operator. /// So, on `Post.user` you can declare `user: User @call(query: "user", args: {id: "{{value.userId}}"})`, and this will replace the `{{args.id}}` used in the `@http` operator with the value of `userId` from the `Post` type. +/// +/// In case you have a `user(input: UserInput!): User @http(path: "/users")` field on the `Mutation` type, you can reference it from another field on the `Mutation` type. +/// So, on `Post.user` you can declare `user: User @call(mutation: "user", args: {input: "{{value.userInput}}"})`, and this will replace the `{{args.input}}` used in the `@http` operator with the value of `userInput` from the `Post` type. pub struct Call { #[serde(default, skip_serializing_if = "is_default")] /// The name of the field on the `Query` type that you want to call. For instance `user`. pub query: Option, + + #[serde(default, skip_serializing_if = "is_default")] + /// The name of the field on the `Mutation` type that you want to call. For instance `createUser`. + pub mutation: Option, + /// The arguments of the field on the `Query` type that you want to call. For instance `{id: "{{value.userId}}"}`. pub args: HashMap, } diff --git a/tests/graphql/errors/test-call-operator.graphql b/tests/graphql/errors/test-call-operator.graphql index 45d598653a..4f99bbfaf4 100644 --- a/tests/graphql/errors/test-call-operator.graphql +++ b/tests/graphql/errors/test-call-operator.graphql @@ -37,5 +37,5 @@ type Failure trace: ["Post", "headersMismatchGraphQL", "@call", "userWithGraphQLResolver"] ) type Failure @error(message: "no argument 'id' found", trace: ["Post", "urlMismatchHttp", "@call", "user"]) -type Failure @error(message: "call must have query", trace: ["Post", "withoutOperator", "@call"]) +type Failure @error(message: "call must have query or mutation", trace: ["Post", "withoutOperator", "@call"]) type Failure @error(message: "userWithoutResolver field has no resolver", trace: ["Post", "withoutResolver", "@call"]) From 0c56c4f3ccad1322ea074f53c9734fb625b9d64f Mon Sep 17 00:00:00 2001 From: Wesley Matos Date: Mon, 29 Jan 2024 14:08:38 -0300 Subject: [PATCH 92/92] style: run `./lint.sh --mode=fix` --- generated/.tailcallrc.schema.json | 9 ++++++++- src/blueprint/operators/call.rs | 6 +----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/generated/.tailcallrc.schema.json b/generated/.tailcallrc.schema.json index b135039fc5..3c1fd68d44 100644 --- a/generated/.tailcallrc.schema.json +++ b/generated/.tailcallrc.schema.json @@ -97,7 +97,7 @@ "type": "object" }, "Call": { - "description": "For instance, if you have a `user(id: Int!): User @http(path: \"/users/{{args.id}}\")` field on the `Query` type, you can reference it from another field on the `Query` type using the `@call` operator. So, on `Post.user` you can declare `user: User @call(query: \"user\", args: {id: \"{{value.userId}}\"})`, and this will replace the `{{args.id}}` used in the `@http` operator with the value of `userId` from the `Post` type.", + "description": "For instance, if you have a `user(id: Int!): User @http(path: \"/users/{{args.id}}\")` field on the `Query` type, you can reference it from another field on the `Query` type using the `@call` operator. So, on `Post.user` you can declare `user: User @call(query: \"user\", args: {id: \"{{value.userId}}\"})`, and this will replace the `{{args.id}}` used in the `@http` operator with the value of `userId` from the `Post` type.\n\nIn case you have a `user(input: UserInput!): User @http(path: \"/users\")` field on the `Mutation` type, you can reference it from another field on the `Mutation` type. So, on `Post.user` you can declare `user: User @call(mutation: \"user\", args: {input: \"{{value.userInput}}\"})`, and this will replace the `{{args.input}}` used in the `@http` operator with the value of `userInput` from the `Post` type.", "properties": { "args": { "additionalProperties": { @@ -106,6 +106,13 @@ "description": "The arguments of the field on the `Query` type that you want to call. For instance `{id: \"{{value.userId}}\"}`.", "type": "object" }, + "mutation": { + "description": "The name of the field on the `Mutation` type that you want to call. For instance `createUser`.", + "type": [ + "string", + "null" + ] + }, "query": { "description": "The name of the field on the `Query` type that you want to call. For instance `user`.", "type": [ diff --git a/src/blueprint/operators/call.rs b/src/blueprint/operators/call.rs index 5bc503877a..82dd9c3db7 100644 --- a/src/blueprint/operators/call.rs +++ b/src/blueprint/operators/call.rs @@ -90,11 +90,7 @@ impl TryFrom for Grpc { fn get_type_and_field(call: &config::Call) -> Option<(String, String)> { if let Some(query) = &call.query { Some(("Query".to_string(), query.clone())) - } else if let Some(mutation) = &call.mutation { - Some(("Mutation".to_string(), mutation.clone())) - } else { - None - } + } else { call.mutation.as_ref().map(|mutation| ("Mutation".to_string(), mutation.clone())) } } pub fn compile_call(