Each system has to handle cases when there are zero data (404), has one instance (viewing this particular post), has few instances (tags assigned to posts) and many instances (dev.to posts. Cases when one can safely assume there will be too many to show in one place everything). This post is about the latter and most practical way to deal with datasets with unbounded size: pagination. But let's start with an easier user case.
Works on my machine
- elixir: 1.8.1
- phoenix: 1.4.2
- absinthe: 1.4.16
Returning everything
defmodule MyAppWeb.Schema do
use Absinthe.Schema
@doc """
Defining query posts which returns a list of posts from the repository
"""
query do
field :posts, list_of(:post) do
resolve(&MyApp.PostsResolver.posts/3)
end
end
@doc """
Post structure
"""
object :post do
field :id, :integer
field :title, :string
end
end
defmodule MyApp.PostsRepository do
@moduledoc """
Represents the existing code boundary.
Can be seen as internal code, that won't even know there is a graphql consumer.
No graphql related code here
"""
defmodule Post do
@moduledoc """
Simulates ecto schema
"""
defstruct [:id, :title]
end
@doc """
Simulates values coming from db (for example)
"""
defp all_posts() do
[
%Post{id: 1, title: "1"},
%Post{id: 2, title: "2"},
%Post{id: 3, title: "3"},
%Post{id: 4, title: "4"}
]
end
@doc """
Returns all posts
"""
def posts() do
all_posts()
end
end
defmodule MyApp.PostsResolver do
@moduledoc """
Resolves graphql query.
Handles graphql call and maps it to necessary internal code.
"""
@doc """
Handles posts request and map it to internal posts repository to fetch results.
"""
def posts(_, _, _) do
{:ok, MyApp.PostsRepository.posts()}
end
end
Building from previous posts in this dev.to series seems reasonable. Now we can execute the following graphql query and expect a result that is returned from MyApp.PostsRepository
:
query {
posts {
id
title
}
}
Let's continue with paginating results and accessing further entries.
Pagination
defmodule MyAppWeb.Schema do
...
query do
field :posts, list_of(:post) do
# Added required argument to provide page to fetch.
arg(:page, non_null(:integer))
resolve(&MyApp.PostsResolver.posts/3)
end
end
end
defmodule MyApp.PostsRepository do
...
@doc """
Returns posts for selected page
"""
def posts(page, per_page) do
offset = (page - 1) * per_page
all_posts()
|> Enum.drop(offset)
|> Enum.take(per_page)
end
end
defmodule MyApp.PostsResolver do
@doc """
How many posts show in a single page
"""
@per_page 2
@doc """
Accept selected page and pass it to repository
"""
def posts(_, %{page: page}, _) do
{:ok, MyApp.PostsRepository.posts(page, @per_page)}
end
end
Above you can see code changes to support pagination. It shows how small and trivial those changes actually are. Now we can provide page argument to fallowing graphql query and get the expected output.
query {
posts(page: 3) {
id
title
}
}
Most paginated endpoints have some meta information. For example how many pages in total, there are. Let's implement that as well.
Paginated with metadata
defmodule MyAppWeb.Schema do
...
query do
# Changed to return paginated posts object
field :posts, :paginated_posts do
arg(:page, non_null(:integer))
resolve(&MyApp.PostsResolver.posts/3)
end
end
@doc """
Paginated posts object contains list of data and meta information
"""
object :paginated_posts do
field :results, list_of(:post)
field :meta, :page_info
end
@doc """
Meta information object structure
"""
object :page_info do
field :page, :integer
field :total_pages, :integer
end
end
defmodule MyApp.PostsRepository do
...
@doc """
How many posts there are
"""
def count() do
Enum.count(all_posts())
end
end
defmodule MyApp.PostsResolver do
...
@doc """
Returns custom data set to support graphql defined structure
"""
def posts(_, %{page: page}, _) do
results = MyApp.PostsRepository.posts(page, @per_page)
total_pages = Float.ceil(MyApp.PostsRepository.count() / @per_page)
{:ok, %{results: results, meta: %{page: page, total_pages: total_pages}}}
end
end
This is all needed changed code to implement simple pagination. And now graphql query looks like the following:
query {
posts(page: 1) {
results {
id
title
},
meta {
page,
totalPages
}
}
}
Conclusion
- Pagination seemed easy for me to grasp. Building upon an understanding of previous posts, it felt natural.
- Graphql methodology is to handle HTTP layer communication, and I like how unintrusive it is. They don't try to give some idiomatic pagination or something like that. Everything is a field, and you can resolve each field as you wish.
- Each project could implement and require different structures to support all needs. Extended meta information for example. So take this example with a grain of salt, as not this is the "best" way to do it, nor it tries to be.
P.S. If you have any comments or I haven't been clear enough about some parts, please let me know :)
Uncommented final code version
defmodule MyAppWeb.Schema do
use Absinthe.Schema
query do
field :posts, :paginated_posts do
arg(:page, non_null(:integer))
resolve(&MyApp.PostsResolver.posts/3)
end
end
object :paginated_posts do
field :results, list_of(:post)
field :meta, :page_info
end
object :page_info do
field :page, :integer
field :total_pages, :integer
end
object :post do
field :id, :integer
field :title, :string
end
end
defmodule MyApp.PostsRepository do
defmodule Post do
defstruct [:id, :title]
end
def all_posts() do
[
%Post{id: 1, title: "1"},
%Post{id: 2, title: "2"},
%Post{id: 3, title: "3"},
%Post{id: 4, title: "4"}
]
end
def posts(page \\ 1, per_page) do
offset = (page - 1) * per_page
all_posts()
|> Enum.drop(offset)
|> Enum.take(per_page)
end
def count() do
Enum.count(all_posts())
end
end
defmodule MyApp.PostsResolver do
@per_page 2
def posts(_, %{page: page}, _) do
results = MyApp.PostsRepository.posts(page, @per_page)
total_pages = Float.ceil(MyApp.PostsRepository.count() / @per_page)
{:ok, %{results: results, meta: %{page: page, total_pages: total_pages}}}
end
end
Top comments (0)