The goal of this repo is to be a robust example of Absinthe and Dataloder to supplement a getting started guide.
Browser games and leaderboards. You play them, you love them, let's make a server to store some of those scores so you can provide persistence for your players.
To start your Phoenix server:
- Install dependencies with
mix deps.get
- Create and migrate your database with
mix ecto.setup
- Start Phoenix endpoint with
mix phx.server
- Open graphiql to
http://localhost:4000/
These are the steps that I went through building this app. They each focus on one chunk of work, but not exactly a single feature. They try to introduce libraries one at a time.
Table of Contents
- Generate an App
- Ecto Schemas
- Absinthe Setup
- Dataloader
- Mutations
- Limit & filter Scores
- Phoenix Routing & Graphiql
Make a basic Phoenix app to serve an API only and use UUIDs instead of int Ids.
mix phx.new ./scoreboard --no-html --no-brunch --binary-id
Very neat, Adds in config: generators: [binary_id: true]
More info.
mix help phx.new
We will auto generate a context to access these Ecto Schemas
mix help phx.gen.context
Player
and Game
are many to many, using the Score
to map them together.
mix phx.gen.context Games Player players name:string
mix phx.gen.context Games Game games name:string
mix phx.gen.context Games Score scores total:integer player_id:references:players game_id:references:games
Let's make sure it works
mix test
This is nice, but I want to have the associations available on my Structs.
Updating this is pretty easy, we can just replace the foreign binary_ids with the [has_*, belongs_*]
macros.
In Scoreboard.Games.Score
Replace
field :player_id, :binary_id
field :game_id, :binary_id
With
belongs_to(:game, Game)
belongs_to(:player, Player)
I added the associations to the Game and Player Schemas schemas as well.
Your API will revolve around your Absinthe Schema
. To get this started we will define some types, eerily similary to Ecto.
The Game Type
@desc "A Game"
object :game do
field(:id, non_null(:id))
field(:name, non_null(:string))
end
This will define your API and how your incoming document maps to elixir functions.
Your Graph doesn't have to be anything like your DB, but in this case, it is.
This is the defintion for the API. Everything that will be exposed and explorable is defined in our schema.ex
.
query do
field :game, :game do
arg(:id, non_null(:id))
resolve(fn %{id: game_id}, _ ->
Games.get_game(game_id)
end)
end
end
There are some informative tests Here.
Dataloader takes care of batching our queries for us. It dramatically reduces code length and complexity too.
When we change data via Absinthe, these are called Mutations. Much like the "root query", we have a "root mutation". After the mutation, you can explore the graph and resolve the same way we do in queries.
mutation do
@desc "Submit a score"
field :submit_score, type: :score do
arg(:game_id, non_null(:id))
arg(:player_id, non_null(:id))
arg(:total, non_null(:integer))
resolve(&Resolvers.Games.submit_score/2)
end
end
Allow optional args on the scores
key of our game
type.
field :scores, list_of(:score) do
arg(:limit, :integer)
arg(:player_id, :id)
resolve(dataloader(:games))
end
And update Scoreboard.Games.query/2
to handle params
def query(Score, params) do
params
|> Map.to_list()
|> Enum.reduce(Score, &apply_param/2)
end
def apply_param({:limit, num}, queryable), do: queryable |> limit(^num)
Now that we can provide something useful, let's try and running the server. We just need to add a route that goes to our Absinthe schema.
ScoreboardWeb.Router
forward(
"/",
Absinthe.Plug.GraphiQL,
schema: ScoreboardWeb.Schema,
interface: :simple
)
forward("/api", Absinthe.Plug, schema: ScoreboardWeb.Schema)
Once the router is updated we can explore our absinthe schema using Graphiql. It's a UI tool that you can view schemas and write queries with. There are download docs in the repo, but I installed it through brew
.
Start the Server
mix phx.server
heroku config:set x="y" # Set env Vars for runtime (not compile-time)
git push heroku master # Deploy
heroku open #Open browser to app graphiql interface!
heroku run "POOL_SIZE=2 mix hello.task" #Run a mix task, & limit db connections
# Postgres stuff
heroku pg:info # get db_name from add-on field.
heroku pg:reset DB_NAME # Didn't need
heroku run MIX_ENV=prod mix ecto.migrate
heroku run MIX_ENV=prod mix run priv/repo/seeds.exs
Code specific resources
Talk resources
- Talk guidelines
- Elixir Conf Proposal Form
- Chad Fowlwer Quote
- Spotify Talk Example
- Evan on Storytelling
The fun stuff