Buscando as informações no banco de dados e Endpoint para consulta
Módulo para busca dos dados
Agora que já conseguimos extrair, parsear, tratar e guardar todos os dados importantes, só falta desenvolver uma forma de buscar esses dados para concluirmos nossa lógica.
Para isso agreguei dentro do módulo Pep.Get as funções que serão utilizadas para esse fim.
Buscar por cpf:
def get_by_cpf(partial_cpf) do
ultima_fonte = get_last_source()
query =
from(PepSchema, where: [cpf: ^partial_cpf, source_id: ^ultima_fonte], preload: [:source])
pep = Repo.all(query)
{:ok, pep}
end
A função get_last_source/0 retorna o id do período mais atualizado da tabela de pessoas politicamente expostas. Essa informação é necessárias pois a tabela é atualizada mensalmente e todas elas ficam gravadas no banco, entretanto, apenas a última é interessante para o problema.
Agora definimos a query a ser utilizada para buscar as informações utilizando o Ecto.Query.
defp get_last_source() do
query = from s in Pep.Source, select: [s.ano_mes, s.id]
[_ano_mes, id] =
Repo.all(query)
|> Enum.map(fn each -> List.update_at(each, 0, &String.to_integer/1) end)
|> Enum.max()
id
end
Busca por nome:
def get_by_nome(nome) do
nome = "%" <> nome <> "%"
ultima_fonte = get_last_source()
query =
from p in PepSchema,
where: ilike(p.nome, ^nome) and p.source_id == ^ultima_fonte,
preload: [:source]
pep = Repo.all(query)
{:ok, pep}
end
Muito semelhante á função acima, entretanto, retornará dados que baterem, mesmo que parcialmente e case-insentive, com o nome que o usúario inserir.
Problema com buscas utilizando ‘ilike’
Note que estamos fazendo o uso do ilike na query, o que abre espaço para ações mal intencionadas principalmente com relação à performace e tráfego de dados. Como exemplo, se o usuário pesquisasse apenas com o nome “a”, o banco retornaria mais de 30MB.
Pra evitar isso, construí um plug que impede que consultas por nome com menos de 3 caracteres ou que possuam % sejam realizadas.
defmodule PepWeb.Plugs.QueryParams do
@moduledoc """
Plug para evitar a sobrecarga má intencionada do banco de dados atraves do query utilizando "ilike"
"""
import Plug.Conn
def init(opts), do: opts
def call(%{params: %{"partial_cpf" => partial_cpf}} = conn, _opts) do
case valid_string?(partial_cpf) && String.length(partial_cpf) >= 3 do
true -> conn
false -> render_error(conn)
end
end
def call(%{params: %{"nome" => nome}} = conn, _opts) do
case valid_string?(nome) && String.length(nome) >= 3 do
true -> conn
false -> render_error(conn)
end
end
def call(conn, _opts), do: conn
defp valid_string?(string) do
valid_char? = fn char -> char not in ["%"] end
string
|> String.graphemes()
|> Enum.all?(&valid_char?.(&1))
end
defp render_error(conn) do
body = Jason.encode!(%{message: "Characteres invalidos ou parametro curto (menor que tres)"})
conn
|> put_resp_content_type("application/json")
|> send_resp(:bad_request, body)
|> halt()
end
end
Controller
O controller das buscas são simples como devem ser
defmodule PepWeb.PepsController do
use PepWeb, :controller
alias PepWeb.FallbackController
action_fallback FallbackController
def show(conn, %{"partial_cpf" => partial_cpf} = _params) do
with {:ok, peps} <- Pep.get_by_cpf(partial_cpf) do
conn
|> put_status(:ok)
|> render("show.json", pep: peps)
end
end
def show(conn, %{"nome" => nome} = _params) do
with {:ok, peps} <- Pep.get_by_nome(nome) do
conn
|> put_status(:ok)
|> render("show.json", pep: peps)
end
end
end
View
Com a PepView a situação já é um pouco diferente, podemos observar que existe um pouco de lógica dentro desse módulo, porém, são lógicas de aprensentação/visualização. Sei que há formas de entregar os dados em json (protocols) e formatados (máscaras) para a View, entretanto, eu prefiro manter lógica de apresentação na camada de apresentação.
defmodule PepWeb.PepsView do
use PepWeb, :view
alias Pep.Pep, as: PepStruct
def render("show.json", %{pep: peps}) do
Enum.map(peps, &json_pep/1)
end
defp json_pep(%PepStruct{} = pep) do
%{
nome: pep.nome,
cpf_parcial: pep.cpf,
sigla: pep.sigla,
regiao: pep.regiao,
data_inicio: pep.data_inicio,
data_fim: pep.data_fim,
data_carencia: pep.data_carencia,
fonte: %{
ano_mes: pep.source.ano_mes,
data_de_insercao: naive_to_utc_sp(pep.source.inserted_at)
}
}
end
defp naive_to_utc_sp(naive_datetime) do
DateTime.from_naive!(naive_datetime, "Etc/UTC")
|> DateTime.shift_zone!("America/Sao_Paulo", Tzdata.TimeZoneDatabase)
end
end
Router
Para terminar de ligar os pontos, o Router será responsável de disponibilizar um Endpoint para acessar essas funções.
defmodule PepWeb.Router do
use PepWeb, :router
pipeline :api do
plug :accepts, ["json"]
plug PepWeb.Plugs.QueryParams
end
scope "/api", PepWeb do
pipe_through :api
post "/sources/:ano_mes", SourcesController, :create
get "/sources", SourcesController, :show
get "/pep/:partial_cpf", PepsController, :show
get "/pep/nome/:nome", PepsController, :show
end
end
De diferente só há a adição do Plug que criamos anteriormente no pipeline de apis, o Plugs.QueryParams.
Conclusão
Já temos uma api “funcionando” em mãos, porém, ainda faltam componentes essenciais para um software de qualidade, que são:
- Testes
- Documentação
- Mais testes
- Forma fácil de fazer deploy, de preferencia obedecendo os The Twelve-Factor App
- Refactor
- Adivinha? Testes
Por isso coloquei funcionando entre aspas, apesar da API ter a alta capacidade de processamento graças ao Elixir/Beam/Phoenix, ainda há muito há ser feito para garantir sua longevidade e manutenibilidade.