# PhoenixBricks
A set of proposed patterns to improve code organization for [Phoenix](https://phoenixframework.org) application
## Installation
To install PhoenixBricks, add it to your list of dependencies in `mix.exs`.
```elixir
def deps do
[
{:phoenix_bricks, "~> 0.3.0"}
]
end
```
Once you've added PhoenixBricks to your list, update your dependencies by running:
```
$ mix deps.get
```
## Getting started
**PhoenixBricks** it is intented as a set of proposed pattern to organize code into a Phoenix application in a modular way.
### Queries
A query is a module that receives a list of atom/keywords and returns an Ecto.Query improved with provided scopes
```
ProductQuery.scope([title_matches: "a value", price_is_greater_than: 1000])
=> #Ecto.Query<from p0 in PhoenixBricks.Catalogue.Product,
where: ilike(p0.title, ^"%a value%"), where: p0.price >= ^1000
```
The idea is to have a query composer that could be easy to use like [ActiveRecord](https://github.com/rails/rails/tree/main/activerecord).
To generate a query for the `Catalogue.Product` schema with some additional custom scopes, simply run
```
$ mix phx.bricks.gen.query Catalogue.Product name:matches:string price:lte:integer
```
The generated module will contain a set of default scopes for simple column matchers and the additional scopes provided in the command.
```
defmodule PhoenixBricks.Catalogue.ProductQuery do
@moduledoc false
import Ecto.Query, warn: false
def starting_scope do
PhoenixBricks.Catalogue.Product
end
def scope(scopes, starting_scope) do
scopes
|> Enum.reduce(starting_scope, fn scope, query ->
apply_scope(query, scope)
end)
end
def scope(scopes) do
scope(scopes, starting_scope())
end
def scope do
starting_scope()
end
defp apply_scope(query, {column, {:eq, value}}) do
where(query, [q], field(q, ^column) == ^value)
end
defp apply_scope(query, {column, {:neq, value}}) do
where(query, [q], field(q, ^column) != ^value)
end
defp apply_scope(query, {column, {:lte, value}}) do
where(query, [q], field(q, ^column) <= ^value)
end
defp apply_scope(query, {column, {:lt, value}}) do
where(query, [q], field(q, ^column) < ^value)
end
defp apply_scope(query, {column, {:gte, value}}) do
where(query, [q], field(q, ^column) >= ^value)
end
defp apply_scope(query, {column, {:gt, value}}) do
where(query, [q], field(q, ^column) > ^value)
end
defp apply_scope(query, {column, {:matches, value}}) do
value = "%#{value}%"
where(query, [q], ilike(field(q, ^column), ^value))
end
defp apply_scope(query, {:title_matches, value}) do
apply_scope(query, {:title, {:matches, value}})
end
defp apply_scope(query, {:price_gte, value}) do
apply_scope(query, {:price, {:gte, value}})
end
end
```
Using the `ProductQuery` module it's possible to rewrite the `list_products/0` method of the context this way:
```
defmodule PhoenixBricks.Catalogue do
...
def list_products(scopes \\ []) do
ProductQuery.scope(scopes)
|> Repo.all()
end
...
end
[title_matches: "a value"]
|> Catalogue.list_products()
```
If you wanto to add custom scopes you simply have to add new definitions of the `apply_scope/2` method:
```
defp apply_scope(query, :active) do
from(q in query, where: q.active == true)
end
defp apply_scope(query, {:some_scope_name, value}) do
from(query in q, ...)
end
...
Catalogue.list_products([
:active,
title_matches: "a value",
some_scope_name: "some value"
])
```
### Filters
A filter is a module that receives a map of filters (for instance coming from a search form) and returns a list of allowed filters.
```
%{"filters" => %{"title_matches" => "a value", "not_allowed" => "xxx"}}
|> ProductFilter.params_to_scopes()
=> [title_matches: "a value"]
```
To generate a module filter for the `Catalogue.Product` schema you simply have to run
```
$ mix phx.bricks.gen.filters Catalogue.Product title:matches:string price:gte:integer
```
that will create a schema that we could use if a form for records filtering.
```
defmodule CrudExample.Catalogue.ProductFilter do
@moduledoc false
use Ecto.Schema
import Ecto.Changeset
alias CrudExample.Catalogue.ProductFilter
embedded_schema do
field :title_matches, :string
field :price_gte, :integer
end
def changeset(filter, attrs) do
filter
|> cast(attrs, [:title_matches, :price_gte])
end
def params_to_scopes(params) do
filters = Map.get(params, "filters", %{})
filter_changeset = changeset(%ProductFilter{}, filters)
filter_changeset.changes
|> Enum.map(fn {name, value} -> {name, value} end)
end
end
```
```
# lib/phoenix_bricks/catalogue.ex
def filter_products(params) do
ProductFilter.changeset(%ProductFilter{}, params)
end
# lib/phoenix_bricks_web/controllers/products_controller.ex
def index(conn, params) do
filter_changeset =
ProductFilter.changeset(%ProductFilter{}, params)
products =
params
|> ProductFilter.params_to_scopes()
|> ProductsQuery.scope()
|> Repo.all()
# or
# params
# |> ProductsFilter.params_to_scopes()
# |> Products.list_products()
render(
conn,
"index.html",
products: products,
filter_changeset: filter_changeset
)
end
# lib/phoenix_bricks_web/templates/products/index.html.eex
<%= form_for @filter_changeset, Routes.product_index_path(@conn, :index) method: :get do %>
<%= label :title_matches %>
<%= text_input :title_matches %>
<% end %>
<%= for product <- @products do %>
...
<% end %>
```
### Query e Filters
The 2 modules can be used in pipeline since the output format for filters are the input format of queries.
```
%{"filters" => %{"title_matches" => "title", "price_gte" => "32"}}
|> ProductFilter.params_to_scopes()
|> ProductQuery.scope()
|> Repo.all()
```