A sandbox for learning Elixir and its environment
- Install Elixir, node, postgres
- mix archive.install https://github.com/phoenixframework/archives/raw/master/phx_new.ez
- In the github repo, there are Pull Requests made comparing each branch to the previous one
- Run the following command in the repo
mix new elixir_playground_umbrella --umbrella- This scaffolds an umbrella application.
- Similar to a .NET Solutions.
- Used for delineating logic or separating distince services (think micro services)
- Navigate into the
elixir_playground_umbrella/appsdirectory - Run the following command
mix new elvenhearth --sup- A --sup option can be given to generate an OTP application skeleton including a supervision tree. Normally an app is generated without a supervisor and without the app callback.
- Navigate into the
elixir_playground_umbrella/appsdirectory - Run the following command
mix phx.new elvenhearth_phx --no-ecto- When asked, select
Yor the default option for installing the dependencies. - The --no-ecto tells phoenix not to include the ecto wrapper or any of the files needed to interact with ecto.
- Navigate into
elixir_playground_umbrella/apps/elvenhearth_phx/assets
- Run
npm install
- Open the file
elixir_playground_umbrella/apps/elvenhearth/mix.exs
- Add the following dependencies found on hex.pm to the dependency block:
{:ecto, "~> 2.2"}or https://hex.pm/packages/ecto (for the latest){:postgrex, "~> 0.13.5"}or https://hex.pm/packages/postgrex (for the latest)
- Add
:ectoandpostgrexto the extra_applications list like so:extra_applications: [:logger, :postgrex, :ecto]
- Run
mix deps.get
- Open the file
elixir_playground_umbrella/apps/elvenhearth/config/config.exs
- Add the following config blocks to the bottom of the file:
config :elvenhearth, Elvenhearth.Repo, adapter: Ecto.Adapters.Postgres, database: "elvenhearth", username: "postgres", password: "postgres" config :elvenhearth, ecto_repos: [Elvenhearth.Repo]
- Create the file
elixir_playground_umbrella/apps/elvenhearth/lib/repo.ex
- Define the module as
Elvenhearth.Repo - Add this line:
use Ecto.Repo, otp_app: :elvenhearth- This defines this repo as an ecto repo.
- Open the file
elixir_playground_umbrella/apps/elvenhearth/lib/elvenhearth/application.ex
- Inside of the
def startfunction, add the following line as the first line inisde of it:import Supervisor.Spec
- Inside of the
children = []list in thedef startfunction, add in:worker(Elvenhearth.Repo, [])- This allows the repo to be restarted by a supervisor if it dies.
- Run
mix ecto.create
- This will create the database as configured in step 2 above.
- Running
mix ectowill tell you the other tasks that ecto can perform`
- Open the file
elixir_playground_umbrella/apps/elvenhearth_phx/mix.exs
- Add the following dependencies found on hex.pm to the dependency block:
{:absinthe, "~> 1.4"}or https://hex.pm/packages/absinthe for latest{:absinthe_plug, "~> 1.4"}or https://hex.pm/packages/absinthe_plug for latest{:absinthe_phoenix, "~> 1.4"}or https://hex.pm/packages/absinthe_phoenix for latest{:absinthe_relay, "~> 1.4"}or https://hex.pm/packages/absinthe_relay for latest
- Run
mix deps.get
- Open the file
elixir_playground_umbrella/apps/elvenhearth_phx/lib/elvenhearth_phx/application.ex
- Inside of the
children = []list in thedef startfunction, add in:supervisor(Absinthe.Subscription, [ElvenhearthPhxWeb.Endpoint])- This allows the repo to be restarted by a supervisor if it dies.
- Open the file
elixir_playground_umbrella/apps/elvenhearth_phx/lib/elvenhearth_phx_web/channels/user_socket.ex
- At the top of the module, inside of the
defmodule do, add:use Absinthe.Phoenix.Socket, schema: ElvenhearthPhxWeb.Schema- This allows subscriptions to work.
- Open the file
elixir_playground_umbrella/apps/elvenhearth_phx/lib/elvenhearth_phx_web/router.ex
- Add the graphiql route to the router:
1.
scope "/playground" do pipe_through :api forward("/graphiql", Absinthe.Plug.GraphiQL, schema: ElvenhearthPhxWeb.Schema, interface: :playground ) end
- Create the file
elixir_playground_umbrella/apps/elvenhearth_phx/lib/elvenhearth_phx_web/schema.ex
defmodule ElvenhearthPhxWeb.Schema do- Add
use Absinthe.Schemaat the top of the module - Add this simple query:
1.
query do field :health, :string do resolve(fn _, _, _ -> {:ok, "up"} end) end end
- From
elixir_playground_umbrella, run the following command:
mix phx.server- Now navigate to localhost:4000/playground/graphiql
- You can explore this interface that will come into play later.
- Create a
UserModel
- Create a new folder and file
users/user.exatelixir_playground_umbrella/apps/elvenhearth/lib/users/user.ex
defmodule Elvenhearth.Users.User do
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field :username, :string
field :password, :string
field :email, :string
field :age, :integer
has_many :characters, Elvenhearth.Characters.Character
timestamps()
end
@required_fields ~w(username password email)a
@optional_fields ~w(age)a
def changeset(user, params \\ %{}) do
user
|> cast(params, @required_fields ++ @optional_fields)
|> validate_required(@required_fields) #
end
end
- Create a
CharacterModel
- Create a new folder and file
users/user.exatelixir_playground_umbrella/apps/elvenhearth/lib/characters/character.ex
defmodule Elvenhearth.Characters.Character do
use Ecto.Schema
import Ecto.Changeset
schema "characters" do
field :name, :string
field :race, :string
belongs_to :user, Elvenhearth.Users.User
timestamps()
end
@required_fields ~w(name race)a
def changeset(character, params \\ %{}) do
character
|> cast(params, @required_fields)
|> validate_required(@required_fields)
end
end
- Create migrations to create the
usersandcharacterstables
- Navigate to
elixir_playground_umbrella/apps/elvenhearth- Run
mix ecto.gen.migration add_users_table:
- Add the following to the
def change doin the generated migration filecreate table(:users) do add :username, :string, size: 50 add :password, :string, size: 100 add :email, :string, size: 50 add :age, :integer timestamps() end
- Run
mix ecto.gen.migration add_characters_table
- Add the following to the
def change doin the generated migration filecreate table(:characters) do add :name, :string add :race, :string add :user_id, references(:users, on_delete: :nothing) timestamps() end
- Run
mix ecto.migrateto run create tables in the database.
- If you open postgres by running `psql -d [database name], you will see 3 tables:
charactersandusers
- By running
\d charactersyou will see that a Foreign key has been added for users.
schema_migrations
- This is how the app keeps track of what migrations have been run.
- Run
- Create queries for
usersandcharacters
- Create a file
user_queries.exatelixir_playground_umbrella/apps/elvenhearth/lib/users/user_queries.ex
defmodule Elvenhearth.Users.UserQueries do
import Ecto.Query
alias Elvenhearth.Repo
alias Elvenhearth.Users.User
def get_all do
Repo.all(from User)
end
def get_by_id(id) do
query =
from u in User,
where: u.id == ^id
Repo.all(query)
end
def any do
count =
Repo.one(
from u in User,
select: count(u.id)
)
count != 0
end
def create(user) do
Repo.insert(user)
end
end
- Create a file
character_queries.exatelixir_playground_umbrella/apps/elvenhearth/lib/characters/character_queries.ex1.defmodule Elvenhearth.Characters.CharacterQueries do import Ecto.Query alias Elvenhearth.Repo alias Elvenhearth.Characters.Character def get_all_for_user(user) do Repo.all( from c in Character, where: c.user_id == ^user.id ) end def any do count = Repo.one( from c in Character, select: count(c.id) ) count != 0 end def create(character) do Repo.insert(character) end end
- Seeds
- Create a file
seed.exslocated atelixir_playground_umbrella/apps/elvenhearth/priv/repo/seed.exs1.alias Elvenhearth.Users.{User, UserQueries} alias Elvenhearth.Characters.{Character, CharacterQueries} unless(UserQueries.any) do users = [ User.changeset(%User{}, %{ username: "test1", password: "1234", email: "test1@test.com" } ), User.changeset(%User{}, %{ username: "test2", password: "1234", email: "test2@test.com" } ), User.changeset(%User{}, %{ username: "test3", password: "1234", email: "test3@test.com" } ) ] inserted_users = Enum.map(users, fn(user) -> UserQueries.create(user) end) Enum.map(inserted_users, fn(inserted_user) -> {:ok, user} = inserted_user Character.changeset(%Character{}, %{ name: "Rand", race: "Human", user_id: user.id }) |> CharacterQueries.create() end) end- Run
mix run priv/repo/seed.exsfrom the logic app,elvenhearthto run the seeds. - Just play around with the seeds. You can see that there are a variety of topics that can be explored with what is happening in this file:
aliasallows you to use the last part of the path instead of having to type the whole thing out.unless(UserQueries.any) doallows for a simple way to only add seeds if there aren't already some in the databaseUser.changesetties to the user model that we created above. If you delete some of the fields, you an change the validity of the changeset. (pro tip, inspect the users list after making changes.)Enumis an elixir library that allows you to enumarate over lists in a variety of ways.
- Run
- In the
elixir_playground_umbrella/apps/elvenhearth_phx/lib/elvenhearth_phx_web/schema.exfile, add the following stuff:
- Create the user object on the schema
object :user do
field :username, :string
field :password, :string
field :email, non_null(:string)
end
- Add the users list field in the query block
field :users, list_of(:user) do
resolve &list_users/3
end
- Create a sample resolver method and something to populate it with
@users [
%{
username: "Rand",
password: "1234",
email: "test@test.com"
},
%{
username: "Perrin",
password: "5678"
}
]
def list_users(_, _, _) do
{:ok, @users}
end
- Break the Objects and the Query fields out into separate modules
- Create several modules in a new folder,
elixir_playground_umbrella/apps/elvenhearth_phx/lib/elvenhearth_phx_web/schema ObjectTypes- This is where you will put all of the absinthe objects such asobject :user,object :character.ObjectQueries- This is where you will put any of the queries that will go in theschema.query doblock.
- In the
elixir_playground_umbrella/apps/elvenhearth_phx/lib/elvenhearth_phx_web/schema.exmodule, you will add a few lines:
- At the top of the module, under the
use Absinthe.Schema, addimport_types ElvenhearthPhxWeb.Schema.ObjectTypesandimport_types ElvenhearthPhxWeb.Schema.ObjectQueries. This allows the broken out modules to be added to the schema. - At the bottom of the
query doblock, add the lineimport_fields :object_queries. This imports the query fields that we have implemented inObjectQueriesmodule.
- Let's add a couple of resolver modules. Create a folder,
elixir_playground_umbrella/apps/elvenhearth_phx/lib/elvenhearth_phx_web/resolvers. Create two modules in that folder:
user_resolver.excharacter_resolver.ex- These files are used as the intermediate logic layer between the actual logic application and the graphql schema.
- Create a couple of functions in these that you will reference instead of the anonymous functions in the schema fields.
- The following files should look like this:
-
The
user_resolver.exshould now look like this:defmodule ElvenhearthPhxWeb.Resolvers.UserResolver do alias Elvenhearth.Users.UserQueries def list_users(_, _, _) do {:ok, UserQueries.get_all()} end def anonymize_password(_, _, _) do {:ok, "Wouldn't you like to know"} end end -
The
character_resolver.exshould now look like this:defmodule ElvenhearthPhxWeb.Resolvers.CharacterResolver do alias Elvenhearth.Characters.CharacterQueries def list_characters(_, _, _) do {:ok, CharacterQueries.get_all()} end def get_characters_for_user(user, _, _) do {:ok, CharacterQueries.get_all_for_user(user)} end end -
The
schema.exfile should now look like this:defmodule ElvenhearthPhxWeb.Schema do use Absinthe.Schema import_types ElvenhearthPhxWeb.Schema.ObjectTypes import_types ElvenhearthPhxWeb.Schema.ObjectQueries query do field :health, :string do resolve(fn _, _, _ -> {:ok, "up"} end) end import_fields :object_queries end end -
The
object_types.exfile should look like this:defmodule ElvenhearthPhxWeb.Schema.ObjectTypes do use Absinthe.Schema.Notation alias ElvenhearthPhxWeb.Resolvers.{UserResolver, CharacterResolver} object :user do field :username, :string field :password, :string do resolve &UserResolver.anonymize_password/3 end field :email, non_null(:string) field :characters, list_of(:character) do resolve &CharacterResolver.get_characters_for_user/3 end end object :character do field :name, :string field :race, :string end end -
The
object_queries.exfile should look like this:
defmodule ElvenhearthPhxWeb.Schema.ObjectQueries do
use Absinthe.Schema.Notation
alias ElvenhearthPhxWeb.Resolvers.{UserResolver, CharacterResolver}
object :object_queries do
field :users, list_of(:user) do
resolve &UserResolver.list_users/3
end
field :characters, list_of(:character) do
resolve &CharacterResolver.list_characters/3
end
end
end
- We are going to restructure the
seed.exsfile for better functionality.
- We are doing this to show off
comprehensions, which are like for-loops on steroids - The file should look like this when you finish:
alias Elvenhearth.Users.{User, UserQueries}
alias Elvenhearth.Characters.{Character, CharacterQueries}
unless(UserQueries.any) do
users = [
User.changeset(%User{}, %{
username: "user1",
password: "1234",
email: "user1@test.com"
}
),
User.changeset(%User{}, %{
username: "user2",
password: "1234",
email: "user2@test.com"
}
),
User.changeset(%User{}, %{
username: "user3",
password: "1234",
email: "user3@test.com"
}
)
]
inserted_users =
Enum.map(users, fn(user) ->
UserQueries.create(user)
end)
[{_, user1}, {_, user2}, {_, user3}] = inserted_users
character_list = [
Character.changeset(%Character{}, %{
name: "Rand",
race: "Dragon",
user_id: user1.id
}),
Character.changeset(%Character{}, %{
name: "Matt",
race: "Fox",
user_id: user2.id
}),
Character.changeset(%Character{}, %{
name: "Perrin",
race: "Wolf",
user_id: user3.id
}),
Character.changeset(%Character{}, %{
name: "Trolloc",
race: "Shadow Spawn",
})
]
# This is called a Comprehension. It is like a loop on steroids
for character <- character_list do
CharacterQueries.create(character)
end
# You can pattern match using comprehensions.
for %Ecto.Changeset{valid?: true, changes: changes} = changeset <- character_list do
IO.inspect(changes)
end
end
- We are going to setup some mix aliases to run multiple mix commands with a single command.
- In the
elixir_playground_umbrella/mix.exsfile, we will add a couple of things:- Insde of the
def project doblock, add the linealiases: aliases()to the bottom of the list. - Create a function
defp aliases doand have it just contain and empty list[].
- Insde of the
- This will allow you to run commands like
mix setupthat will drop, create, and migrate the database. - The file should look like this at the end with several aliases setup:
defmodule ElixirPlaygroundUmbrella.MixProject do
use Mix.Project
def project do
[
apps_path: "apps",
start_permanent: Mix.env() == :prod,
deps: deps(),
aliases: aliases()
]
end
# Dependencies listed here are available only for this
# project and cannot be accessed from applications inside
# the apps folder.
#
# Run "mix help deps" for examples and options.
defp deps do
[]
end
defp aliases do
[
{:setup, ["ecto.drop", "ecto.create", "ecto.migrate"]},
{:reset, ["ecto.drop", "ecto.create", "ecto.migrate", "run apps/elvenhearth/priv/repo/seed.exs"]},
{:seed, "run apps/elvenhearth/priv/repo/seed.exs"}
]
end
end
- We are going to add an infinite loop to our queries to show that care needs to be taken.
- In the
user_queries.exmodule, change thedef get_by_id dofunction to returnRepo.oneinstead ofRepo.all. - In the
user_resolver.exmodule, add the following function:
def get_user_for_character(character, _, _) do
{:ok, UserQueries.get_by_id(character.user_id)}
end
- In the
object_types.exmodule, add a new field to the character object. That object should look like this afterwards:
object :character do
field :name, :string
field :race, :string
field :user, :user do
resolve &UserResolver.get_user_for_character/3
end
end
- Now, in the Graphiql interface, make your queries. You are now able to just query into the user and then character and then back into the user over and over again. Care needs to be taken to avoid things like this.
- We are going to add Apollo Tracing so that we can see the time each request takes in Graphiql
- Add the dependency to the phoenix
mix.exsfile, found here https://hex.pm/packages/apollo_tracing - At the top of the
schema.exmodule, adduse ApolloTracingright underuse Absinthe.Schema. - In the
router.exmodule, we need to add thepipeline: {ApolloTracing.Pipeline, :plug}to our forward route. It should look like this:
forward("/graphiql", Absinthe.Plug.GraphiQL,
schema: ElvenhearthPhxWeb.Schema,
interface: :playground,
pipeline: {ApolloTracing.Pipeline, :plug}
)
- Now, when you run the GraphQL query in Graphiql, the Tracing portion will now work and tell how long each field took to resolve.