View Source Query Arguments
Our GraphQL API would be pretty boring (and useless) if clients couldn't retrieve filtered data.
Let's assume that our API needs to add the ability to look-up users by their ID and get the posts that they've authored. Here's what a basic query to do that might look like:
{
user(id: "1") {
name
posts {
id
title
}
}
}
The query includes a field argument, id
, contained within the
parentheses after the user
field name. To make this all work, we need to modify
our schema a bit.
Defining Arguments
First, let's create a :user
type and define its relationship to
:post
while we're at it. We'll create a new module for the
account-related types and put it there; in
blog_web/schema/account_types.ex
:
defmodule BlogWeb.Schema.AccountTypes do
use Absinthe.Schema.Notation
@desc "A user of the blog"
object :user do
field :id, :id
field :name, :string
field :email, :string
field :posts, list_of(:post)
end
end
The :posts
field points to a list of :post
results. (This matches
up with what we have on the Ecto side, where Blog.Accounts.User
defines a has_many
association with Blog.Content.Post
.)
We've already defined the :post
type, but let's go ahead and add an
:author
field that points back to our :user
type. In
blog_web/schema/content_types.ex
:
object :post do
# post fields we defined earlier...
field :author, :user
end
Now let's add the :user
field to our query root object in our
schema, defining a mandatory :id
argument and using the
Resolvers.Accounts.find_user/3
resolver function. We also need to
make sure we import the types from BlogWeb.Schema.AccountTypes
so
that :user
is available.
In blog_web/schema.ex
:
defmodule BlogWeb.Schema do
use Absinthe.Schema
import_types Absinthe.Type.Custom
# Add this `import_types`:
import_types BlogWeb.Schema.AccountTypes
import_types BlogWeb.Schema.ContentTypes
alias BlogWeb.Resolvers
query do
@desc "Get all posts"
field :posts, list_of(:post) do
resolve &Resolvers.Content.list_posts/3
end
# Add this field:
@desc "Get a user of the blog"
field :user, :user do
arg :id, non_null(:id)
resolve &Resolvers.Accounts.find_user/3
end
end
end
Now let's use the argument in our resolver. In blog_web/resolvers/accounts.ex
:
defmodule BlogWeb.Resolvers.Accounts do
def find_user(_parent, %{id: id}, _resolution) do
case Blog.Accounts.find_user(id) do
nil ->
{:error, "User ID #{id} not found"}
user ->
{:ok, user}
end
end
end
Our schema marks the :id
argument as non_null
, so we can be
certain we will receive it. If :id
is left out of the query,
Absinthe will return an informative error to the user, and the resolve
function will not be called.
If you have experience writing Phoenix controller actions, you might wonder why we can match incoming arguments with atoms instead of having to use strings.
The answer is simple: you've defined the arguments in the schema using atom identifiers, so Absinthe knows what arguments will be used ahead of time, and will coerce as appropriate---culling any extraneous arguments given to a query. This means that all arguments can be supplied to the resolve functions with atom keys.
Finally you'll see that we can handle the possibility that the query, while valid from GraphQL's perspective, may still ask for a user that does not exist. We've decided to return an error in that case.
There's a valid argument for just returning
{:ok, nil}
when a record can't be found. Whether the absence of data constitutes an error is a decision you get to make.
Arguments and Non-Root Fields
Let's assume we want to query all posts from a user published within a
given time range. First, let's add a new field to our :post
object
type, :published_at
.
The GraphQL specification doesn't define any official date or time
types, but it does support custom scalar types (you can read more
about them in the related guide), and
Absinthe ships with several built-in scalar types. We'll use
:naive_datetime
(which doesn't include timezone information) here.
Edit blog_web/schema/content_types.ex
:
defmodule BlogWeb.Schema.ContentTypes do
use Absinthe.Schema.Notation
@desc "A blog post"
object :post do
field :id, :id
field :title, :string
field :body, :string
field :author, :user
# Add this:
field :published_at, :naive_datetime
end
end
To make the :naive_datetime
type available, add an import_types
line to
your blog_web/schema.ex
:
import_types Absinthe.Type.Custom
For more information about how types are imported, read the guide on the topic.
For now, just remember that
import_types
should only be used in top-level schema module. (Think of it like a manifest.)
Here's the query we'd like to be able to use, getting the posts for a user on a given date:
{
user(id: "1") {
name
posts(date: "2017-01-01") {
title
body
publishedAt
}
}
}
To use the passed date, we need to update our :user
object type and
make some changes to its :posts
field; it needs to support a :date
argument and use a custom resolver. In blog_web/schema/account_types.ex
:
defmodule BlogWeb.Schema.AccountTypes do
use Absinthe.Schema.Notation
alias BlogWeb.Resolvers
object :user do
field :id, :id
field :name, :string
field :email, :string
# Add the block here:
field :posts, list_of(:post) do
arg :date, :date
resolve &Resolvers.Content.list_posts/3
end
end
end
For the resolver, we've added another function head to
Resolvers.Content.list_posts/3
. This illustrates how you can use the
first argument to a resolver to match the parent object of a field. In
this case, that parent object would be a Blog.Accounts.User
Ecto
schema:
# Add this:
def list_posts(%Blog.Accounts.User{} = author, args, _resolution) do
{:ok, Blog.Content.list_posts(author, args)}
end
# Before this:
def list_posts(_parent, _args, _resolution) do
{:ok, Blog.Content.list_posts()}
end
Here we pass on the user and arguments to the domain logic function,
Blog.Content.list_posts/3
, which will find the posts for the user
and date (if it's provided; the :date
argument is optional). The
resolver, just as when it's used for the top level query :posts
,
returns the posts in an :ok
tuple.
Check out the full implementation of logic for
Blog.Content.list_posts/3
--and some simple seed data--in the absinthe_tutorial repository.
If you've done everything correctly (and have some data handy), if you
start up your server with mix phx.server
and head over
to http://localhost:4000/api/graphiql, you should be able to play
with the query.
It should look something like this:
Next Step
Next up, we look at how to modify our data using mutations.