Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: create @call operator #1064

Merged
Merged
Show file tree
Hide file tree
Changes from 61 commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
7dc62fd
feat: create `@call` operator
ologbonowiwi Jan 29, 2024
35d5548
style: run `./lint.sh --mode=fix`
ologbonowiwi Jan 29, 2024
fe0f59d
refactor: reuse mustache replacer
ologbonowiwi Jan 29, 2024
820a3f3
refactor: remove duplicated check
ologbonowiwi Jan 29, 2024
e133676
test: add test case of user without args
ologbonowiwi Jan 29, 2024
c2b9a96
test: add test for `@call` with mutation
ologbonowiwi Jan 29, 2024
8867519
test: add test of resolver not using args
ologbonowiwi Jan 29, 2024
164546f
docs: uncomment `mutation` from tailcallrc
ologbonowiwi Jan 29, 2024
655f1c1
test: polish tests given runner changes
ologbonowiwi Jan 29, 2024
2388efd
Merge branch 'main' of github.com:tailcallhq/tailcall into feat/#846/…
ologbonowiwi Jan 31, 2024
bde932d
style: run `./lint.sh --mode=fix`
ologbonowiwi Jan 31, 2024
ddfa3ee
Merge branch 'main' into feat/#846/create-call-operator
ologbonowiwi Jan 31, 2024
887aed3
Merge branch 'main' into feat/#846/create-call-operator
ologbonowiwi Feb 2, 2024
90418d4
fix: import validator
ologbonowiwi Feb 2, 2024
ee822d1
refactor(`call.rs`): improve implementation readability
ologbonowiwi Feb 3, 2024
da34b1b
Merge branch 'main' of github.com:tailcallhq/tailcall into feat/#846/…
ologbonowiwi Feb 9, 2024
888c0a2
fix: use `package_and_service` to find package and `service` to build…
ologbonowiwi Feb 9, 2024
d94c74d
test: move changes to new setup
ologbonowiwi Feb 9, 2024
996ddca
docs: add `@call` to docs
ologbonowiwi Feb 9, 2024
14f875f
test: move tests to execution
ologbonowiwi Feb 9, 2024
92fa697
test: delete duplicated test
ologbonowiwi Feb 9, 2024
b806cf9
test: delete unnecessary snapshots
ologbonowiwi Feb 9, 2024
1198bb8
test: delete `call-operator.yml`
ologbonowiwi Feb 9, 2024
9159a1b
chore: delete unused snapshots
ologbonowiwi Feb 9, 2024
ca6ab5c
Merge branch 'main' into feat/#846/create-call-operator
ologbonowiwi Feb 10, 2024
f58a71c
Merge branch 'main' into feat/#846/create-call-operator
ologbonowiwi Feb 17, 2024
d7fa2db
Merge branch 'feat/#846/create-call-operator' of github.com:ologbonow…
ologbonowiwi Feb 17, 2024
429a8e0
refactor: rename `config_set` to `config_module`
ologbonowiwi Feb 17, 2024
8db7cd4
Merge branch 'main' of github.com:tailcallhq/tailcall into feat/#846/…
ologbonowiwi Feb 17, 2024
e1789f5
Merge branch 'main' into feat/#846/create-call-operator
ologbonowiwi Feb 19, 2024
c54fbd6
Merge branch 'feat/#846/create-call-operator' of github.com:ologbonow…
ologbonowiwi Feb 19, 2024
68722d5
Revert "fix: use `package_and_service` to find package and `service` …
ologbonowiwi Feb 19, 2024
d161971
fix: re-add `news.` prefix on URL
ologbonowiwi Feb 19, 2024
c7bbfc9
Merge branch 'main' into feat/#846/create-call-operator
ologbonowiwi Feb 23, 2024
998d49c
Merge branch 'feat/#846/create-call-operator' of github.com:ologbonow…
ologbonowiwi Feb 23, 2024
c5709da
test(call): add `DynamicValue` for `Expression::Literal`
ologbonowiwi Feb 23, 2024
72abef3
Merge branch 'main' into feat/#846/create-call-operator
ologbonowiwi Feb 25, 2024
08ad768
Merge branch 'feat/#846/create-call-operator' of github.com:ologbonow…
ologbonowiwi Feb 25, 2024
172ec98
feat(mustache): implement `ToString` for `Mustache`
ologbonowiwi Feb 25, 2024
34fe0cc
refactor(call): change `b_field.http` instead of transforming http `R…
ologbonowiwi Feb 25, 2024
4e88208
refactor(call): change `b_field.graphql` instead of transforming grap…
ologbonowiwi Feb 25, 2024
942df65
refactor(call): create `KeyValues` replacer
ologbonowiwi Feb 25, 2024
17bfd9b
refactor(call): invoke replacer once and reuse in variables
ologbonowiwi Feb 25, 2024
0ff793f
refactor: delete `FromIterator` impl for `KeyValues`
ologbonowiwi Feb 25, 2024
6726280
Merge branch 'main' into feat/#846/create-call-operator
ologbonowiwi Feb 26, 2024
c723983
docs: mention `Mutation` usage
ologbonowiwi Feb 27, 2024
d8da185
Merge branch 'feat/#846/create-call-operator' of github.com:ologbonow…
ologbonowiwi Feb 27, 2024
25eac84
chore: run `./lint.sh --mode=fix`
ologbonowiwi Feb 27, 2024
48477ae
Merge branch 'main' of github.com:tailcallhq/tailcall into feat/#846/…
ologbonowiwi Feb 27, 2024
4b8f9d1
Merge branch 'main' into feat/#846/create-call-operator
ologbonowiwi Feb 27, 2024
f5dfdb9
test: add more tests for call with mutation
ologbonowiwi Feb 28, 2024
1ba4fea
fix: add call to the resolver list
ologbonowiwi Feb 28, 2024
4bf9baf
docs: add `@call` example
ologbonowiwi Feb 28, 2024
b86c063
Merge branch 'feat/#846/create-call-operator' of github.com:ologbonow…
ologbonowiwi Feb 28, 2024
017f61c
Merge branch 'main' of github.com:tailcallhq/tailcall into feat/#846/…
ologbonowiwi Feb 28, 2024
8c05477
chore: delete unused test configs
ologbonowiwi Feb 28, 2024
5ec60e3
refactor: avoid code duplication when using `compile_const`
ologbonowiwi Feb 28, 2024
8c12d4a
feat: use `@call` with `@const`
ologbonowiwi Feb 28, 2024
b2eb6dd
refactor: make `@call.args` receive `Value` instead of `String`
ologbonowiwi Feb 28, 2024
4372297
docs(generated): run `./lint.sh --mode=fix`
ologbonowiwi Feb 28, 2024
8560cfd
refactor: revert change on validator return
ologbonowiwi Feb 29, 2024
f4834be
Merge branch 'main' into feat/#846/create-call-operator
ologbonowiwi Feb 29, 2024
3dc808b
test: fix call-mutation test
ologbonowiwi Feb 29, 2024
47c8c69
Merge branch 'feat/#846/create-call-operator' of github.com:ologbonow…
ologbonowiwi Feb 29, 2024
281b17c
refactor: remove test flakiness by using `BTreeMap`
ologbonowiwi Feb 29, 2024
202c9fd
Merge branch 'main' into feat/#846/create-call-operator
tusharmath Mar 2, 2024
8ac7335
Merge branch 'feat/#846/create-call-operator' of github.com:ologbonow…
ologbonowiwi Mar 2, 2024
b199a05
refactor(`@call`): make `args` optional
ologbonowiwi Mar 2, 2024
15248f0
docs: update docs via `lint.sh --mode=fix`
ologbonowiwi Mar 2, 2024
c2b99ce
Merge branch 'main' of github.com:tailcallhq/tailcall into feat/#846/…
ologbonowiwi Mar 2, 2024
6b31712
update doc
tusharmath Mar 4, 2024
045d9bd
Merge branch 'main' into feat/#846/create-call-operator
tusharmath Mar 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion autogen/src/gen_gql_schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ use schemars::schema::{
use tailcall::config;

static GRAPHQL_SCHEMA_FILE: &str = "generated/.tailcallrc.graphql";
static DIRECTIVE_ALLOW_LIST: [(&str, Entity, bool); 13] = [
static DIRECTIVE_ALLOW_LIST: [(&str, Entity, bool); 14] = [
("server", Entity::Schema, false),
("link", Entity::Schema, true),
("upstream", Entity::Schema, false),
("http", Entity::FieldDefinition, false),
("call", Entity::FieldDefinition, false),
("grpc", Entity::FieldDefinition, false),
("addField", Entity::Object, true),
("modify", Entity::FieldDefinition, false),
Expand Down
22 changes: 22 additions & 0 deletions examples/call.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
schema @server(graphiql: true) @upstream(baseURL: "http://jsonplaceholder.typicode.com") {
query: Query
}

type Post {
body: String
id: Int
title: String
userId: Int
}

type Query {
user(id: Int!): User @http(path: "/users/{{args.id}}")
firstUser: User @call(query: "user", args: {id: 1})
postFromUser(userId: Int!): [Post] @http(path: "/posts?userId={{args.userId}}")
}

type User {
id: Int
name: String
posts: [Post] @call(query: "postFromUser", args: {userId: "{{value.id}}"})
}
2 changes: 1 addition & 1 deletion examples/jsonplaceholder.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,5 @@ type Post {
userId: Int!
title: String!
body: String!
user: User @http(path: "/users/{{value.userId}}")
user: User @call(query: "user", args: {id: "{{value.userId}}"})
}
30 changes: 30 additions & 0 deletions generated/.tailcallrc.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,35 @@ directive @cache(
maxAge: Int!
) on FIELD_DEFINITION

"""
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.
"""
directive @call(
"""
The arguments of the field on the `Query` type that you want to call. For instance
`{id: "{{value.userId}}"}`.
"""
args: JSON
"""
The name of the field on the `Mutation` type that you want to call. For instance
`createUser`.
"""
mutation: String
"""
The name of the field on the `Query` type that you want to call. For instance `user`.
"""
query: String
) on FIELD_DEFINITION

"""
The `@const` operators allows us to embed a constant response for the schema.
"""
Expand Down Expand Up @@ -418,6 +447,7 @@ input ExprBody {
http: Http
grpc: Grpc
graphQL: GraphQL
call: Call
const: JSON
if: ExprIf
and: [ExprBody]
Expand Down
52 changes: 52 additions & 0 deletions generated/.tailcallrc.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,34 @@
}
}
},
"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.\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.",
"type": "object",
"required": [
"args"
],
"properties": {
"args": {
"description": "The arguments of the field on the `Query` type that you want to call. For instance `{id: \"{{value.userId}}\"}`.",
"type": "object",
"additionalProperties": true
},
"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": [
"string",
"null"
]
}
}
},
"Const": {
"description": "The `@const` operators allows us to embed a constant response for the schema.",
"type": "object",
Expand Down Expand Up @@ -226,6 +254,19 @@
},
"additionalProperties": false
},
{
"description": "Reuses a resolver pre-defined on `Query` or `Mutation`",
"type": "object",
"required": [
"call"
],
"properties": {
"call": {
"$ref": "#/definitions/Call"
}
},
"additionalProperties": false
},
{
"description": "Evaluate to constant data",
"type": "object",
Expand Down Expand Up @@ -927,6 +968,17 @@
}
]
},
"call": {
"description": "Inserts a call resolver for the field.",
"anyOf": [
{
"$ref": "#/definitions/Call"
},
{
"type": "null"
}
]
},
"const": {
"description": "Inserts a constant resolver for the field.",
"anyOf": [
Expand Down
1 change: 1 addition & 0 deletions src/blueprint/definitions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,7 @@ fn to_fields(
.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(update_call(&operation_type).trace(config::Call::trace_name().as_str()))
.and(update_nested_resolvers())
.and(update_cache_resolvers())
.try_fold(
Expand Down
177 changes: 177 additions & 0 deletions src/blueprint/operators/call.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
use std::collections::hash_map::Iter;

use serde_json::Value;

use crate::blueprint::*;
use crate::config;
use crate::config::{Field, GraphQLOperationType, KeyValues};
use crate::lambda::Expression;
use crate::mustache::{Mustache, Segment};
use crate::try_fold::TryFold;
use crate::valid::{Valid, Validator};

fn find_value<'a>(args: &'a Iter<'a, String, Value>, key: &'a String) -> Option<&'a Value> {
args.clone()
.find_map(|(k, value)| if k == key { Some(value) } else { None })
}

pub fn update_call(
operation_type: &GraphQLOperationType,
) -> TryFold<'_, (&ConfigModule, &Field, &config::Type, &str), FieldDefinition, String> {
TryFold::<(&ConfigModule, &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)
.map(|resolver| b_field.resolver(Some(resolver)))
},
)
}

pub fn compile_call(
field: &Field,
config_module: &ConfigModule,
call: &config::Call,
operation_type: &GraphQLOperationType,
) -> Valid<Expression, String> {
get_field_and_field_name(call, config_module).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::<Vec<String>>()
.join(", ")
))
.trace(field_name.as_str());
}

let string_replacer = replace_string(&args);
let key_value_replacer = replace_key_values(&args);

if let Some(mut http) = _field.http.clone() {
http.path = string_replacer(http.path.clone());
http.body = http.body.clone().map(string_replacer);
http.query = key_value_replacer(http.query);
http.headers = key_value_replacer(http.headers);

compile_http(config_module, field, &http)
} else if let Some(mut graphql) = _field.graphql.clone() {
graphql.headers = key_value_replacer(graphql.headers);
graphql.args = graphql.args.clone().map(key_value_replacer);

compile_graphql(config_module, operation_type, &graphql)
} else if let Some(mut grpc) = _field.grpc.clone() {
grpc.base_url = grpc.base_url.clone().map(&string_replacer);
grpc.headers = key_value_replacer(grpc.headers);
grpc.body = grpc.body.clone().map(string_replacer);

compile_grpc(CompileGrpc {
config_module,
operation_type,
field,
grpc: &grpc,
validate_with_schema: true,
})
} else if let Some(const_field) = _field.const_field.clone() {
compile_const(CompileConst {
config_module,
field: _field,
value: &const_field.data,
validate: true,
})
} else {
Valid::fail(format!("{} field has no resolver", field_name))
}
})
}

fn replace_key_values<'a>(
args: &'a Iter<'a, String, Value>,
) -> impl Fn(KeyValues) -> KeyValues + 'a {
|key_values| {
KeyValues(
key_values
.iter()
.map(|(k, v)| (k.clone(), replace_string(args)(v.clone())))
.collect(),
)
}
}

fn replace_string<'a>(args: &'a Iter<'a, String, Value>) -> impl Fn(String) -> String + 'a {
|str| {
let mustache = Mustache::parse(&str).unwrap();

let mustache = replace_mustache_value(&mustache, args);

mustache.to_string()
}
}

fn get_type_and_field(call: &config::Call) -> Option<(String, String)> {
if let Some(query) = &call.query {
Some(("Query".to_string(), query.clone()))
} else {
call.mutation
.as_ref()
.map(|mutation| ("Mutation".to_string(), mutation.clone()))
}
}

fn get_field_and_field_name<'a>(
call: &'a config::Call,
config_module: &'a ConfigModule,
) -> Valid<(&'a Field, String, Iter<'a, String, Value>), String> {
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_module.config.find_type(&type_name),
format!("{} type not found on config", type_name),
)
.and_then(|query_type| {
Valid::from_option(
query_type.fields.get(&field_name),
format!("{} field not found", field_name),
)
})
.fuse(Valid::succeed(field_name))
.fuse(Valid::succeed(call.args.iter()))
.into()
})
}

fn replace_mustache_value(value: &Mustache, args: &Iter<'_, String, Value>) -> Mustache {
value
.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.to_string().as_str()).unwrap();

let expression = item.get_segments().first().unwrap().to_owned().to_owned();

expression
} else {
Segment::Expression(expression.clone())
}
}
})
.collect::<Vec<Segment>>()
.into()
}
Loading
Loading