Essa é a terceira parte do projeto Rockelivery. Esse projeto faz parte do Bootcamp da Rocketseat, ministrado pelo professor Rafael Camarda.
Conteúdo:
📇 Módulo para Leitura de Usuários
🔎 A Rota de Show dos Usuários
💀 A Rota de Deleção de Usuários
🔴 Centralizando as Mensagens de Erro
📝 Módulo para Update de Usuários
🔄 A Rota Update
🔌 Criando um Plug
📇 Módulo para Leitura de Usuários
Vamos agora fazer um módulo para pesquisar os usuários no banco de dados.
Lembrando que podemos pegar todos os usuários do banco de dados usando a função Rockelivery.Repo.all()
:
iex> Rockelivery.Repo.all(Rockelivery.User)
[debug] QUERY OK source="users" db=22.5ms decode=3.6ms queue=3.2ms idle=705.1ms
SELECT u0."id", u0."address", u0."age", u0."cep", u0."cpf", u0."email", u0."name", u0."password_hash", u0."inserted_at", u0."updated_at" FROM "users" AS u0 []
[
%Rockelivery.User{
__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
address: "Rua...",
age: 28,
cep: "12345678",
cpf: "12345678910",
email: "maiqui@teste.com",
id: "482f95a7-b447-42e9-ae67-aef72954c3f0",
inserted_at: ~N[2021-06-05 14:45:28],
name: "Maiqui",
password: nil,
password_hash: "$argon2id$v=19$m=131072,t=8,p=4$L3mKYnhn5ooDZRsyd7maaA$92xStD9oCV06HtJYnoYhGY4HBJK69bUvb6HpTv+IlPc",
updated_at: ~N[2021-06-05 14:45:28]
},
...
Ou buscar um usuário específico passando o ID desse usuário para a função Rockelivery.Repo.get()
. Note que quando o usuário é encontrado, um Schema
é retornado:
iex> Rockelivery.Repo.get(Rockelivery.User, "482f95a7-b447-42e9-ae67-aef72954c3f0")
[debug] QUERY OK source="users" db=2.9ms queue=2.4ms idle=1418.4ms
SELECT u0."id", u0."address", u0."age", u0."cep", u0."cpf", u0."email", u0."name", u0."password_hash", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE (u0."id" = $1) [<<72, 47, 149, 167, 180, 71, 66, 233, 174, 103, 174, 247, 41, 84, 195, 240>>]
%Rockelivery.User{
__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
address: "Rua...",
age: 28,
cep: "12345678",
cpf: "12345678910",
email: "maiqui@teste.com",
id: "482f95a7-b447-42e9-ae67-aef72954c3f0",
inserted_at: ~N[2021-06-05 14:45:28],
name: "Maiqui",
password: nil,
password_hash: "$argon2id$v=19$m=131072,t=8,p=4$L3mKYnhn5ooDZRsyd7maaA$92xStD9oCV06HtJYnoYhGY4HBJK69bUvb6HpTv+IlPc",
updated_at: ~N[2021-06-05 14:45:28]
}
Mas quando o usuário não é encontrado, um nil
é retornado:
Crie o arquivo lib/rockelivery/users/get.ex
:
defmodule Rockelivery.Users.Get do
alias Rockelivery.{Repo, User}
def by_id(id) do
case Repo.get(User, id) do
nil -> {:error, %{status: :not_found, result: "User not found"}}
user_schema -> {:ok, user_schema}
end
end
end
Testando com um ID inválido:
iex> Rockelivery.Users.Get.by_id("482f95a7-b447-42e9-ae67-aef72954c3f1")
[debug] QUERY OK source="users" db=4.6ms queue=0.1ms idle=1023.1ms
SELECT u0."id", u0."address", u0."age", u0."cep", u0."cpf", u0."email", u0."name", u0."password_hash", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE (u0."id" = $1) [<<72, 47, 149, 167, 180, 71, 66, 233, 174, 103, 174, 247, 41, 84, 195, 241>>]
{:error, %{result: "User not found", status: :not_found}}
Testando com um ID válido:
iex> Rockelivery.Users.Get.by_id("482f95a7-b447-42e9-ae67-aef72954c3f0")
[debug] QUERY OK source="users" db=2.0ms queue=0.1ms idle=1622.8ms
SELECT u0."id", u0."address", u0."age", u0."cep", u0."cpf", u0."email", u0."name", u0."password_hash", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE (u0."id" = $1) [<<72, 47, 149, 167, 180, 71, 66, 233, 174, 103, 174, 247, 41, 84, 195, 240>>]
{:ok,
%Rockelivery.User{
__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
address: "Rua...",
age: 28,
cep: "12345678",
cpf: "12345678910",
email: "maiqui@teste.com",
id: "482f95a7-b447-42e9-ae67-aef72954c3f0",
inserted_at: ~N[2021-06-05 14:45:28],
name: "Maiqui",
password: nil,
password_hash: "$argon2id$v=19$m=131072,t=8,p=4$L3mKYnhn5ooDZRsyd7maaA$92xStD9oCV06HtJYnoYhGY4HBJK69bUvb6HpTv+IlPc",
updated_at: ~N[2021-06-05 14:45:28]
}}
Se tentarmos usar um ID que não seja no formato UUID, receberemos uma exceção:
Então precisamos refatorar o nosso código para verificar se um ID é um UUID. Vamos antes só entender a função Ecto.UUID.cast/1
:
iex> Ecto.UUID.cast("123456")
:error
iex> Ecto.UUID.cast("482f95a7-b447-42e9-ae67-aef72954c3f0")
{:ok, "482f95a7-b447-42e9-ae67-aef72954c3f0"}
Em lib/rockelivery/users/get.ex
:
defmodule Rockelivery.Users.Get do
alias Ecto.UUID
alias Rockelivery.{Repo, User}
def by_id(id) do
case UUID.cast(id) do
:error -> {:error, %{status: :bad_request, result: "Invalid Format!"}}
{:ok, uuid} -> get(uuid)
end
end
defp get(id) do
case Repo.get(User, id) do
nil -> {:error, %{status: :not_found, result: "User not found!"}}
user_schema -> {:ok, user_schema}
end
end
end
🔎 A Rota de Show dos Usuários
A partir de agora vamos construir todo o código para que a rota show funcione. Lembrando que a rota já foi criada quando usamos o código resources
:
Fachada para a função by_id
Vamos construir a nossa fachada para a função Rockelivery.Users.Get.by_id/1
, já que precisaremos dela na action show
do controller RockeliveryWeb.UsersController
.
Em lib/rockelivery.ex
:
defmodule Rockelivery do
alias Rockelivery.Users.Create, as: UserCreate
# adicione
alias Rockelivery.Users.Get, as: UserGet
defdelegate create_user(params), to: UserCreate, as: :call
# adicione
defdelegate get_user_by_id(id), to: UserGet, as: :by_id
end
Construindo a action show
Em lib/rockelivery_web/controllers/user_controller.ex
adicione o código abaixo:
def show(conn, %{"id" => id}) do
with {:ok, %User{} = user} <- Rockelivery.get_user_by_id(id) do
conn
|> put_status(:ok)
|> render("show.json", user: user)
end
end
Ao contrário da action index
, invés de params
estamos fazendo Pattern Matching colocando %{"id" => id}
.
Adicionando "show.json" na view
Em lib/rockelivery_web/views/users_view.ex
adicione o código abaixo:
def render("show.json", %{user: %User{} = user}), do: %{user: user}
Testando a nossa rota show
Vamos passar um id
com formato inválido para ver a mensagem:
Ajustando a mensagem
Em lib/rockelivery_web/views/error_view.ex
adicione o código abaixo:
def render("error.json", %{result: %Changeset{} = changeset}) do
%{message: translate_errors(changeset)}
end
# abaixo do código acima
# adicione
def render("error.json", %{result: error_message}) do
%{message: error_message}
end
Formato válido, mas o ID não existe no banco:
Refatoração
Lembrando que antes do erro ir para a error_view
, ele passa no FallbackController
e nosso result
que recebia só um changeset
agora recebe também uma mensagem, então vamos alterar o nosso código. Podemos alterar o nome da variável changeset
para result
, ou para changeset_or_message
que fica mais claro ao ser lido:
💀 A Rota de Deleção de Usuários
Vamos começar com o módulo Delete
. Ele vai ser parecido com o módulo Get
.
Módulo Delete
Crie um arquivo em lib/rockelivery/users/delete.ex
, e o conteúdo deve ter essa aparência:
defmodule Rockelivery.Users.Delete do
alias Ecto.UUID
alias Rockelivery.{Repo, User}
def call(id) do
case UUID.cast(id) do
:error -> {:error, %{status: :bad_request, result: "Invalid Format!"}}
{:ok, uuid} -> delete(uuid)
end
end
defp delete(id) do
case Repo.get(User, id) do
nil -> {:error, %{status: :not_found, result: "User not found!"}}
user_schema -> Repo.delete(user_schema)
end
end
end
Vamos agora fazer a fachada para este módulo.
Delete Facade
Em lib/rockelivery.ex
adicione o código abaixo:
alias Rockelivery.Users.Delete, as: UserDelete
defdelegate delete_user(id), to: UserDelete, as: :call
Delete Action
Em lib/rockelivery_web/controllers/users_controller.ex
adicione o código abaixo:
def delete(conn, %{"id" => id}) do
with {:ok, %User{}} <- Rockelivery.delete_user(id) do
conn
|> put_status(:no_content)
|> text("")
end
end
Como estamos colocando um status :no_content
, não vamos devolver nenhum body
, por isso não vamos usar a função render, apenas devolveremos uma string vazia.
Testando a deleção
🔴 Centralizando as Mensagens de Erro
Agora existe uma oportunidade para refarorarmos o nosso código. Podemos observar que começamos a repetir as mensagens de erro e os status code:
Vamos então centralizar essas menssagens em um arquivo só.
Criando um arquivo para as mensagens de erro
Crie lib/rockelivery/error.ex
e o seu conteúdo deve ter essa aparência:
defmodule Rockelivery.Error do
@keys [:status, :result]
@enforce_keys @keys
defstruct @keys
def build(status, result) do
%__MODULE__{
status: status,
result: result
}
end
def build_user_not_found, do: build(:not_found, "User not found")
def build_invalid_id_format, do: build(:bad_request, "Invalid id format")
end
Refatorando
Em lib/rockelivery/users/get.ex
:
Faça o mesmo em lib/rockelivery/users/delete.ex
:
E, em lib/rockelivery/users/create.ex
use apenas a função Error.build/2
:
defp handle_insert({:error, changeset}) do
# ANTES
# {:error, %{status: :bad_request, result: changeset}}
# DEPOIS
{:error, Error.build(:bad_request, changeset)}
end
Vamos alterar também o FallbackController
e colocar agora a nossa struct error
. Com ela nosso código fica mais declarativo, ficando mais fácil entender o código.
Em lib/rockelivery_web/controllers/fallback_controller.ex
:
📝 Módulo para Update de Usuários
Primeiramente vamos verificar na documentação como funciona o callback Ecto.Repo.update
: https://hexdocs.pm/ecto/Ecto.Repo-callback-update.html
No exemplo da documentação podemos ver que precisaremos de um changeset
:
Vamos criar o arquivo lib/rockelivery/users/update.ex
:
defmodule Rockelivery.Users.Update do
alias Ecto.UUID
alias Rockelivery.{Error, Repo, User}
def call(%{"id" => id} = params) do
case UUID.cast(id) do
:error -> {:error, Error.build_invalid_id_format()}
{:ok, _uuid} -> update(params)
end
end
defp update(%{"id" => id} = params) do
case Repo.get(User, id) do
nil -> {:error, Error.build_user_not_found()}
user_schema -> do_update(user_schema, params)
end
end
defp do_update(%User{} = user, %{} = params) do
user
|> User.changeset(params)
|> Repo.update()
end
end
Precisamos alterar também lib/rockelivery/user.ex
para podermos passar um schema
como parâmetro:
Testando o Update
Buscando todos os usuários cadastrados com o comando:
iex> Rockelivery.Repo.all Rockelivery.User
Pegamos um ID e vamos tentar atualizar o nome:
- Criando os parâmetros:
iex> params = %{"id" => "482f95a7-b447-42e9-ae67-aef72954c3f0", "name" => "Maiqui Tomé"}
- Executando a função Update:
iex> Rockelivery.Users.Update.call params
- Resultado:
Quando passamos o schema %User{}
com os dados puxados do banco para a função User.changeset/2
estamos passando com o campo password nulo, pois o banco de dados não retorna os dados dele já que esse campo é virtual. A função User.changeset/2
faz a validação e diz que esse campo é obrigatório informar. Precisamos refatorar novamente o nosso código.
Em lib/rockelivery/user.ex
:
Em lib/rockelivery/users/update.ex
:
🔄 A Rota Update
Após o módulo de update criado, vamos partir para a criação de todo o código para a rota update funcionar.
Update Facade
Em lib/rockelivery.ex
adicione o código abaixo:
alias Rockelivery.Users.Update, as: UserUpdate
defdelegate update_user(params), to: UserUpdate, as: :call
Update Action
Em lib/rockelivery_web/controllers/user_controller.ex
adicione o código abaixo:
def show(conn, %{"id" => id}) do
with {:ok, %User{} = user} <- Rockelivery.get_user_by_id(id) do
conn
|> put_status(:ok)
# alterando "show.json" para "user.json"
|> render("user.json", user: user)
end
end
# adicione
def update(conn, %{} = params) do
with {:ok, %User{} = user} <- Rockelivery.update_user(params) do
conn
|> put_status(:ok)
|> render("user.json", user: user)
end
end
Como a action show
é praticamente igual a action update
vamos usar o template "user.json"
para os dois. Vamos alterar agora o código da view.
Em lib/rockelivery_web/views/users_view.ex
altere "show.json"
para "user.json"
:
Testando a Rota Update
Primeiramente, vamos pesquisar um usuário usando a action show
:
Para todos os campos aparecerem certifique-se que você acrescentou eles no Jason.Encoder
:
Agora vamos editar esse usuário usando a rota nova de update. Vou apenas trocar o nome de "Maiqui Tomé" para "Maiqui Pirolli Tomé":
Ao pesquisar novamente usando a rota Get:
🔌 Criando um Plug
Primeiramente, antes de entendermos melhor o que é um Plug, vamos entender porque vamos precisar de um. Podemos perceber na imagem abaixo, que em vários arquivos estamos repetindo o mesmo trecho de código para verificar se um ID é um UUID.
Para melhorar o nosso código precisamos construir um Plug. Vamos antes enteder ele melhor.
Plug é uma convenção para manipular a struct de conexão %Plug.Conn{}
. Mesmo não fazendo parte do núcleo de Elixir, Plug é um projeto oficial de Elixir. Você pode conferir a documentação oficial do Plug aqui: https://hexdocs.pm/plug/readme.html
A definição da documentação do Phoenix diz que o Plug é uma especificação para módulos combináveis entre aplicativos da web. É também uma camada de abstração para adaptadores de conexão de diferentes servidores da web. A ideia básica do Plug é unificar o conceito de uma "conexão" na qual operamos. Isso difere de outras camadas de middleware HTTP, como Rack do Ruby on Rails, onde a solicitação e a resposta são separadas na pilha de middleware. Essa definição e mais detalhes você encontra aqui: https://hexdocs.pm/phoenix/plug.html
Function Plugs
Para atuar como um Plug, uma função precisa aceitar uma estrutura de conexão (%Plug.Conn{}
) e opções. Ela também precisa retornar uma estrutura de conexão. Qualquer função que atenda a esses critérios servirá.
Veja mais detalhes aqui: https://hexdocs.pm/phoenix/plug.html#function-plugs
Module Plugs
Os Module Plugs são outro tipo de Plug que nos permite definir uma transformação de conexão em um módulo. O módulo só precisa implementar duas funções:
-
init/1
que inicializa quaisquer argumentos ou opções a serem passadas paracall/2
-
call/2
que realiza a transformação da conexão.call/2
é apenas uma Function Plug que vimos anteriormente.
Entendendo o fluxo
Um Plug executa antes da request chegar no controller. Vamos tentar entender melhor esse fluxo no arquivo de rotas.
Vamos pensar que um usuário fez um post. O navegador acessou a barra de endereço http://localhost:4000/api/users
, enviou um HTTP Request para a nossa aplicação que estava executando nesse endereço. O HTTP Request foi construído com um par do verbo POST
e o caminho /Users
, que foi mapeado para um par do controller UsersController
e a action create
.
Antes de chegar na action create
do UsersController
:
- Primeiramente vai para o
scope "/api"
; - depois para o
pipe_throw :api
que redireciona para o blocopipeline :api
; - depois para o
plug :accepts, ["json"]
dizendo que essa rota aceita o formatojson
, - depois no nosso plug
UUIDChecker
que criaremos ainda, - e só depois para o
UsersController
onde os dados serão moldados na action create, nesse caso.
O nosso primeiro plug
Crie um arquivo no diretório lib/rockelivery_web/plugs/uuid_checker.ex
. O conteúdo deste arquivo deve ter a seguinte aparência:
defmodule RockeliveryWeb.Plugs.UUIDChecker do
import Plug.Conn
alias Ecto.UUID
alias Plug.Conn
# init/1 inicializa quaisquer argumentos ou opções a serem passadas para call/2
def init(options), do: options
# call/2 é uma Function Plug que realiza a transformação da conexão.
# Toda Function Plug precisa aceitar uma estrutura de conexão %Plug.Conn{} e opções.
def call(%Conn{params: %{"id" => id}} = conn, _opts) do
case UUID.cast(id) do
:error -> render_error(conn)
# se o ID for um UUID a conexão (conn) pode ir para o controller:
{:ok, _uuid} -> conn
end
end
# Caso não tenha id nos paramêtros, como na rota create,
# continua enviando a conexão no fluxo normal
def call(conn, _opts), do: conn
defp render_error(conn) do
body = Jason.encode!(%{message: "Invalid UUID"})
conn
|> put_resp_content_type("application/json")
|> send_resp(:bad_request, body)
|> halt()
end
end
Vamos entender algumas funções usadas no código acima:
Plug.Conn.put_resp_content_type(conn, content_type)
informa que será devolvido uma resposta no formato json. Veja na documentação: https://hexdocs.pm/plug/Plug.Conn.html#put_resp_content_type/3Plug.Conn.send_resp(conn, status, body)
envia uma resposta com o status e o corpo fornecidos. Veja na documentação: https://hexdocs.pm/plug/Plug.Conn.html#send_resp/3Plug.Conn.halt(conn)
interrompe o pipeline de Plug, evitando que mais plugs downstream sejam invocados. Veja na documentação: https://hexdocs.pm/plug/Plug.Conn.html#halt/1
Em lib/rockelivery_web/router.ex
:
Refatorando
Em lib/rockelivery/error.view
podemos retirar a mensagem de erro do UUID, já que isso está sendo tratado agora no nosso Plug que criamos:
Podemos agora retirar a verificação do UUID nos 3 arquivos onde estava sendo usado:
Exemplo da refatoração no arquivo de update:
Faça essa refatoração nos outros 2 arquivos.
Agora nosso Plug está tratando os IDs inválidos :)
E assim terminamos a terceira parte da nossa aplicação :)
Top comments (0)