sb logoToday I Learned

8 posts by herminiotorres @herminiotorres

How to grasp the business logic with unique_index

How do I ensure a unique index when only a user tries to get more than one ticket for a paid conference? And be more flexible in accepting such users get more than one ticket for a free conference?

How are our goals here?

  • sent paid conference to Ticket.changeset/1 ensure the status: :paid, create one.
  • sent free conference to Ticket.changeset/1 ensure the status: :free, create many.

How we can apply these to our business logic:

  create table(:tickets) do
    add :conference_id, references(:conferences), null: false
    add :user_id, references(:users), null: false
    add :status, :string, null: false, default: "free"
   end

  create unique_index(:tickets, [:conference_id, :user_id, :status], where: "status = 'paid'")

How can we play?

iex> Ticket.changeset(%{conference: %{is_paid: false}, user: %{...}, status: :free}) |> Repo.insert()
[debug] QUERY OK
{:ok, 
  %Ticket%{id: 1, status: :free, conference_id: 1, user_id: 1}
}
iex> Ticket.changeset(%{conference: %{is_paid: false}, user: %{...}, status: :free}) |> Repo.insert()
[debug] QUERY OK
{:ok, 
  %Ticket%{id: 2, status: :free, conference_id: 1, user_id: 1}
}
iex> Ticket.changeset(%{conference: %{is_paid: true}, user: %{...}, status: :paid}) |> Repo.insert()
[debug] QUERY OK
{:ok, 
  %Ticket%{id: 3, status: :paid, conference_id: 2, user_id: 1}
}
iex> Ticket.changeset(%{conference: %{is_paid: true}, user: %{...}, status: :paid}) |> Repo.insert()
[debug] QUERY ERROR
** (Ecto.ConstraintError)

cool!

When to use the handle_params callback

The handle_params/3 callback is helpful for using the state in the URL to drive the presentation of your LiveView. This is nice because you can share the URL with anyone and see the same LiveView state. handle_params is invoked after mount or whenever there is a live navigation event. If your LiveView is changing state based on the URL, handle_params is the right place to assign values on your LiveView, as you will avoid processing both in mount/1 and handle_params/3. To trigger handle_params/3, push_patch/2 can be used server-side, while live_patch/2 will trigger handle_param/3 through a client-side interaction.

For example, imagine we want to use handle_params/3 to implement pagination, filtering, and sorting. Using these two examples, handle_params/3 can handle five different cases of URL state

  • only pagination /route?page=2&per_page=10
  • only filtering /route?filter=a
  • only sorting /route?sort_by=id&sort_order=asc
  • pagination, filtering, and sorting /route?page=2&per_page=10&filter=sneakers?sort_by=name&sort_order=asc
  • none specified (use defaults) /route
def handle_params(params, _url, socket) do
  paginate_options = %{page: params["page"], per_page: params["per_page"]}
  filter_options = %{filter: params["filter"]}
  sort_options = %{sort_by: params["sort_by"], sort_order: params["sort_order"]}

  shoes =
    Shoes.list_shoes(
      paginate: paginate_options,
      sort: sort_options,
      filter: filter_options
    )

  {:noreply,
    assign(socket,
      options: Map.merge(paginate_options, sort_options, filter_options),
      shoes: shoes
    )}
end

def handle_params(_params, _url, socket) do
  {:noreply, socket}
end

What you should know about the live_session macro

Imagine you have a few endpoints and would like to group their authorization rules. With live_session/3 , can achieve that!

live_session has three options:

  1. session - name of the session
  2. on_mount - callback function
  3. root_layout - apply a different layout to the group

It is important to understand the Security Considerations of live_session, especially for handling authentication and authorization in your LiveView.

In the following example, we use live_session to set a new root_layout only for admin users, and authorize admins only in the :adminUserHook

live_session :admins, 
  root_layout: {ExampleWeb.AdminLayoutView, :root},
  on_mount: {ExampleWeb.UserHook, :admin} do
  scope "/", ExampleWeb do
    pipe_through [:browser, :auth]

    live "/admin", HomeLive, :page
  end
end
defmodule ExampleWeb.AdminLayoutView do
  @moduledoc false
  
  use ExampleWeb, :view

  def render("root.html", assigns) do
    ~H"""
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <title>Admin Layout</title> 
      </head>
      <body>
        <h1>Admin</h1>
        <main>
          <%= @inner_content %>
        </main>
      </body>
    </html>
    """
  end
end

How to take leverage from on_mount to reduce code

Phoenix LiveView has implemented some cool features, and one of them is the on_mount/1 callback function.

This callback function will run before the mount/3 function in your LiveView.

There are two ways to set the on_mount callback function:

  1. In router using live_session/3.
  2. In your LiveView modules with on_mount macro.

If you need to do something before the mount/3 in all your LiveViews, live_session/3 is likely the best fit. However, if it isonly for a few them, the on_mount macro will be better for your needs.

on_mount is helpful for reducing repetitive code in your LiveViews. Let’s look at an example.

defmodule ExampleWeb.UserHook do
  import Phoenix.LiveView

  def on_mount(:default, _params, %{"current_user" => current_user} = _session, socket) do
    if authorized?(current_user) do
      {:cont, socket}
    else
      {:halt, socket}
    end
  end
  
  def on_mount(:admin, _params, %{"current_user" => current_user} = _session, socket) do
    if admin?(current_user) do
      {:cont, socket}
    else
      {:halt, socket}
    end
  end
end

The live_session/3 on Router:

live_session :default, on_mount: ExampleWeb.UserHook do
  scope "/", ExampleWeb do
    pipe_through [:browser, :auth]

    live "/", HomeLive, :page
  end
end

The on_mount macro:

defmodule ExampleWeb.HomeLive do
  use ExampleWeb, :live_view
  
  on_mount {ExampleWeb.UserHook, :admin}

  def mount(_params, _session, socket) do
    # ...
  end
end

How to import CSV file to the Database

Today I learned how to import CSV data file to the database, and populate the table.

Imagine you have this migration in your application with the following columns:

create table(:users) do
  add(:first_name, :string, null: false)
  add(:last_name, :string, null: false)
  add(:username, :string, null: false)
  add(:email, :string, null: false)
end

And this would be the CSV file:

First Name,Last Name,Username,Email
John,Doe,john_doe,john@doe.com
Jane,Doe,jane_doe,jane@doe.com

And how can I import my CSV file to the users’ table on the database?

~$ psql -U user -d database <<USERS
COPY users(first_name, last_name, username, email) FROM '/path/to/users.csv' DELIMITER ',' CSV HEADER;
USERS

After finishing, you will receive and output with COPY 2 the quantity of copies into your table.

Idempotence in Distributed Systems

Sooner or later, you will come across the term “idempotence” in the context of distributed systems. What is the relationship between these terms?

Let’s consider writing an REST API with a POST request. When you try to create a resource and call it multiple times, the system should only create this resource once, or update it, for a given unique entity.

A more specific example of this in a distributed system could be a payment system. A payment operation will be considered idempotent if we attempt to apply the same charge or payment multiple times, but it only gets processed once.

Creating idempotent operations in a distributed system can be challenging, especially if implemented in the application layer. If possible, you can push this responsibility to your database and ensure idempotence with features like unique indices.

This is what I’ve been learning and I’m excited to learn more!

Using the Keyword module for options

You should consider using Keyword.fetch!/2 and Keyword.get/3 for options to APIs.

Without options

defmodule MyApp do
  def config(name, author \\ "Herminio Torres", description \\ "Description") do
    %{
      name: name,
      author: author,
      description: description
    }
  end
end
iex> MyApp.config
config/1    config/2    config/3
iex> MyApp.config("my_app")
%{
  author: "Herminio Torres",
  description: "Description",
  name: "my_app"
}
iex> MyApp.config("my_app", "Change")
%{
  author: "Change",
  description: "Description",
  name: "my_app"
}
  • Creates a config function with many arities
  • You are forced to pass all paramaters when you intend to change just the last default argument.

With Options

defmodule MyApp do
  def config(opts) do
    name = Keyword.fetch!(opts, :name)
    author = Keyword.get(opts, :author, "Herminio Torres")
    description = Keyword.get(opts, :description, "Description")
    
    %{
      name: name,
      author: author,
      description: description
    }
  end
end
iex> MyApp.config([])
** (KeyError) key :name not found in: []
    (elixir 1.12.3) lib/keyword.ex:420: Keyword.fetch!/2
    iex:3: MyApp.config/1
iex> MyApp.config([name: "my_app"])
%{
  author: "Herminio Torres",
  description: "Description",
  name: "my_app"
}
iex> MyApp.config([name: "my_app", description: "Change"])
%{
  author: "Herminio Torres",
  description: "Change",
  name: "my_app"
}
  • The raised error leads you to which options are required
  • Keyword lists make the arguments named
  • Only one function arity is exposed

Awesome!

Taming data with Ecto.Enum and Ecto.Type

A coworker and I discussed about taking advantage of Ecto.Enum and Ecto.Type instead of having one more dependency.

The schema:

defmodule Blog.Category do
  use Blog.Schema

  schema "categories" do
    field(:name, Ecto.Enum, [:til, :elixir, :ecto])
  end
end

Divide & Conquer with Reflections and Ecto.Type.load/3:

iex> type = Blog.Category.__schema__(:type, :name)
{:parameterized, Ecto.Enum,
 %{
   mappings: [til: "til", elixir: "elixir", ecto: "ecto"],
   on_cast: %{"til" => :til, "elixir" => :elixir, "ecto" => :ecto},
   on_dump: %{til: "til", elixir: "elixir", ecto: "ecto"},
   on_load: %{"til" => :til, "elixir" => :elixir, "ecto" => :ecto},
   type: :string
 }}
iex> Ecto.Type.load(type, "unknown")
:error
iex> Ecto.Type.load(type, "ecto")
{:ok, :ecto}
iex> Ecto.Type.load(type, :ecto)
:error

In the meantime:

iex> Ecto.Enum.values(Blog.Category, :name)
[:til, :elixir, :ecto]
iex> Ecto.Enum.dump_values(Blog.Category, :name)
["til", "elixir", "ecto"]
iex> Ecto.Enum.mappings(Blog.Category, :name)
[til: "til", elixir: "elixir", ecto: "ecto"]

Also, now we have the same API ecto_enum:

iex> valid? = fn list, value -> Enum.any?(list, fn item -> item == value end) end
#Function<43.40011524/2 in :erl_eval.expr/5>
iex> categories = Ecto.Enum.dump_values(Blog.Category, :name)
["til", "elixir", "ecto"]
iex> category = "unknown"
"unknown"
iex> if valid?.(categories, category), do: {:ok, String.to_existing_atom(category)}, else: :error
:error
iex> category = "ecto"
"ecto"
iex> if valid?.(categories, category), do: {:ok, String.to_existing_atom(category)}, else: :error
{:ok, :ecto}

Cool!