Skip to content

Commit

Permalink
feat: create @call operator (#1064)
Browse files Browse the repository at this point in the history
Co-authored-by: Tushar Mathur <[email protected]>
  • Loading branch information
ologbonowiwi and tusharmath authored Mar 4, 2024
1 parent 64f0f71 commit 607d336
Show file tree
Hide file tree
Showing 52 changed files with 1,717 additions and 80 deletions.
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}}"})
}
21 changes: 21 additions & 0 deletions generated/.tailcallrc.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,26 @@ directive @cache(
maxAge: Int!
) on FIELD_DEFINITION

"""
Provides the ability to refer to a field defined in the root Query or Mutation.
"""
directive @call(
"""
The arguments of the field on the `Query` or `Mutation` 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 +438,7 @@ input ExprBody {
http: Http
grpc: Grpc
graphQL: GraphQL
call: Call
const: JSON
if: ExprIf
and: [ExprBody]
Expand Down
49 changes: 49 additions & 0 deletions generated/.tailcallrc.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,31 @@
}
}
},
"Call": {
"description": "Provides the ability to refer to a field defined in the root Query or Mutation.",
"type": "object",
"properties": {
"args": {
"description": "The arguments of the field on the `Query` or `Mutation` 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 +251,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 +965,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::btree_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

1 comment on commit 607d336

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Running 30s test @ http://localhost:8000/graphql

4 threads and 100 connections

Thread Stats Avg Stdev Max +/- Stdev
Latency 17.57ms 49.96ms 1.58s 99.24%
Req/Sec 1.78k 309.70 2.09k 77.92%

212032 requests in 30.01s, 0.91GB read

Socket errors: connect 0, read 0, write 0, timeout 3

Requests/sec: 7065.41

Transfer/sec: 31.09MB

Please sign in to comment.