Buscando as informações no banco de dados e Endpoint para consulta

5 minute read

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.

Projeto no Github