@call
The @call
directive in GraphQL signifies a shift towards more efficient configuration management by introducing a methodology akin to function invocations in conventional programming. This directive is pivotal for developers navigating the intricacies of elaborate GraphQL schemas, where minimizing redundancy and adhering to the DRY (Don't Repeat Yourself) principle are paramount. Consider the following schema example:
schema {
query: Query
}
type Query {
user(id: Int!): User
@http(
url: "https://jsonplaceholder.typicode.com/users/{{.args.id}}"
)
posts: [Post]
@http(url: "https://jsonplaceholder.typicode.com/posts")
}
type Post {
id: Int!
userId: Int!
title: String!
body: String!
user: User
@http(
url: "https://jsonplaceholder.typicode.com/users/{{.value.userId}}"
)
}
type User {
id: Int!
name: String!
email: String!
}
In this schema, at lines 9
and 18
, a pattern of configuration duplication emerges when fetching user's data by its id, demonstrating a prime use case for the @call
directive. Through refactoring the Post
type to incorporate the @call
directive, we can eliminate this redundancy.
type Post {
id: Int!
userId: Int!
title: String!
body: String!
user: User
@call(
steps: [
{query: "user", args: {id: "{{.value.userId}}"}}
]
)
}
Here, the @call
directive invokes the user
query from the Query
type, leveraging the data-fetching process that's already defined in the root query
. The query
parameter specifies the target field, while the args
parameter delineates the arguments to be passed.
steps
@call
directive can compose together other resolvers, allowing to create a chain of resolvers that can be executed in sequence. This is done by using the steps
parameter, which is an array of objects that define the operations to be executed.
query
Specify the root query field to invoke, alongside the requisite arguments, using the @call
directive for a concise and efficient query structure.
type Post {
userId: Int!
user: User
@call(
steps: [
{query: "user", args: {id: "{{.value.userId}}"}}
]
)
}
mutation
Similarly, the @call
directive can facilitate calling a mutation from another mutation field, employing the mutation
parameter for field specification and the args
parameter for argument delineation.
type Mutation {
insertPost(input: PostInput, overwrite: Boolean): Post
@http(
body: "{{.args.input}}"
method: "POST"
url: "https://jsonplaceholder.typicode.com/posts"
query: {overwrite: "{{.args.overwrite}}"}
)
upsertPost(input: PostInput): Post
@call(
steps: [
{
mutation: "insertPost"
args: {input: "{{.args.input}}", overwrite: true}
}
]
)
}
args
The args
parameter in the @call
directive facilitates passing arguments to the targeted query or mutation, represented as a key-value mapping where each key corresponds to an argument name and its associated value.
type Post {
userId: Int!
user: User
@call(
steps: [
{query: "user", args: {id: "{{.value.userId}}"}}
]
)
}
The @call
directive is predominantly advantageous in complex, large-scale configurations. For those new to GraphQL or Tailcall, it may be beneficial to explore this directive after familiarizing yourself with the foundational aspects of GraphQL.
Composition
@call
directive provides the ability to express a sequence of steps that one might need to compose. These steps are executed such that the result of each step is passed as an argument to the next step. The query
and mutation
parameters are used to specify the target field, while the args
parameter is used to pass arguments to the target field.
Let's explain this with an example:
schema @server {
query: Query
}
type Query {
a(input: JSON): JSON
@expr(body: {value: "{{.args.input.a}}"})
b(input: JSON): JSON
@expr(body: {value: "{{.args.input.b}}"})
c(input: JSON): JSON
@expr(body: {value: "{{.args.input.c}}"})
}
Here we have defined there operations viz. a
, b
& c
each of them pluck their respective keys from the given input value. Let's run this query with some test input:
{
a(input: {a: 100})
b(input: {b: 200})
c(input: {c: 300})
}
Here is how the response would look like:
{
"data": {
"a": {
"value": 100
},
"b": {
"value": 200
},
"c": {
"value": 300
}
}
}
As you can see the @expr
directive plucks the inner value and returns the result. How about we implement an abc
operation that could leverage the existing operations and unwrap the following input value:
{"a": {"b": {"c": {"d": 1000}}}}
Given the above input if we wish to extract the last inner number 1000
then we could define a new operation as follows
schema @server {
query: Query
}
type Query {
a(input: JSON): JSON
@expr(body: {value: "{{.args.input.a}}"})
b(input: JSON): JSON
@expr(body: {value: "{{.args.input.b}}"})
c(input: JSON): JSON
@expr(body: {value: "{{.args.input.c}}"})
abc(input: JSON): JSON
@call(
steps: [
{query: "a", args: {input: "{{.args.input}}"}}
{query: "b", args: {input: "{{.args.value}}"}}
{query: "c", args: {input: "{{.args.value}}"}}
]
)
}
We use the @call
directive to compose the operations together. The args
specify how we would like to pass the arguments to the operation and the result of that operation is passed to the next step. We can test the new abc
operation with the following query:
query {
abc(input: {a: {b: {c: 1000}}})
}
The server returns the response that we expected:
{
"data": {
"abc": {
"value": 100
}
}
}
This way you can compose combine multiple operations can compose them together using the @call
directive.
We use JSON
scalar here because we don't care about the type safety of this option. In a real world example you might want to use proper input and output types.