Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- Elixir Tips and Tricks That You Might Not Know Of
- # SaaS/Multitenant Migrations with Ecto
- ```elixir
- defmodule App.Team do
- use App, :schema
- ...
- def changeset(schema, params \\ %{}) do
- schema
- |> cast(params, @allowed)
- |> validate_required(@required)
- |> prepare_changes(fn changeset ->
- case changeset.action do
- :insert ->
- up(changeset.changes.handle)
- :delete ->
- down(changeset.changes.handle)
- end
- changeset
- end)
- end
- defp up(handle) do
- Repo.query!("CREATE SCHEMA \"#{handle}\"")
- Migrator.run(Repo, migration_dir(), :up, all: true, prefix: handle)
- end
- defp down(handle) do
- Migrator.run(Repo, migration_dir(), :down, all: true, prefix: handle)
- Repo.query!("DROP SCHEMA \"#{handle}\" CASCADE")
- end
- defp migration_dir do
- Application.app_dir(:domain, "priv/repo/organization_migrations")
- end
- end
- defmodule App.Plugs do
- def prefix_team(conn, opts) do
- uri = endpoint_module(conn).struct_url()
- team_uri = struct(URI, [
- host: conn.host,
- scheme: Atom.to_string(conn.scheme),
- port: conn.port
- ])
- team_uri.host
- |> String.trim_trailing(uri.host)
- |> String.trim_trailing(".")
- |> case do
- "" ->
- redirect(conn, to: Keyword.fetch!(opts, :redirect_to))
- handle ->
- team = Repo.get_by!(Team, handle: handle)
- conn
- |> put_private(:team, team)
- |> put_private(:prefix, Phoenix.Param.to_param(team))
- end
- end
- end
- defmodule App.Team do
- use App, :schema
- @derive {Phoenix.Param, key: :handle}
- end
- defmodule App.SomeController do
- import App.Plugs
- plug :prefix_team, redirect_to: page_url(App.Endpoint, :sign_in)
- def index(conn, _) do
- Repo.all(Something, prefix: conn.private.prefix)
- end
- end
- ```
- # Cross app eventbus with Registry
- ```elixir
- defmodule App.Notification do
- def start_link() do
- Registry.start_link(:unique, Notification, partitions: System.schedulers_online)
- end
- def subscribe(topic) do
- Registry.register(Notification, topic, nil)
- end
- def notify(topic, message) do
- Registry.dispatch(Notification, topic, fn entries ->
- for {pid, _} <- entries, do: send(pid, message)
- end)
- end
- end
- defmodule TeamController do
- ...
- def create(conn, params) do
- ...
- {:ok, team} -> Notification.notify(:team, {:created, team})
- end
- end
- defmodule Worker.TeamCreator do
- use GenServer
- ...
- def init(state) do
- Notification.subscribe(:team)
- super(state)
- end
- def handle_info({:created, team}, state) do
- # do something with the newly created team
- end
- ```
- # Repo composition
- ```elixir
- defmodule App.ComposableRepo do
- defmacro __using__(opts) do
- quote bind_quoted: binding() do
- @composable_repo Keyword.fetch!(opts, :composing)
- @composable_functions @composable_repo.__info__(:functions)
- @composable_functions
- |> Enum.map(fn
- {function, 0} ->
- {function, []}
- {function, arity} ->
- {function, Enum.map(1..arity, &(Macro.var(:"arg_#{&1}", __MODULE__)))}
- end)
- |> Enum.map(fn {function, arguments} ->
- defdelegate unquote(function)(unquote_splicing(arguments)), to: @composable_repo
- end)
- defoverridable @composable_functions
- end
- end
- end
- defmodule App.ComposedRepo do
- use App.ComposableRepo, composing: App.Repo
- ...
- def delete(schema, opts \\ []) do
- {force_delete?, opts} = Keyword.pop(opts, :force, false)
- if !force_delete? && soft_deletable?(schema) do
- schema
- |> change(deleted_at: DateTime.utc_now())
- |> update()
- else
- super(schema, opts)
- end
- end
- end
- ```
- # Custom Ecto Type
- ```elixir
- defmodule Domain.Tags do
- @moduledoc """
- Schema:
- defmodule Post do
- use Ecto.Schema
- schema "posts" do
- field :title, :string
- field :body, :string
- field :tags, Domain.TagType
- end
- end
- Migration:
- def change do
- create table(:posts) do
- add :title, :string
- add :body, :string
- add :tags, {:array, :string}
- end
- end
- """
- @behaviour Ecto.Type
- @spec type() :: {atom, atom}
- def type, do: {:array, :string}
- @spec cast(list(String.t) | String.t) :: {atom, list(String.t)}
- def cast(term) when is_list(term), do: {:ok, term}
- def cast(""), do: {:ok, []}
- def cast(term) when is_binary(term) do
- case get_delimiter(term) do
- :error -> :error
- delimeter -> {:ok, String.split(term, delimeter, trim: true)}
- end
- end
- def cast(_), do: :error
- @spec load(list(String.t)) :: {atom, list(String.t)}
- def load(term) when is_list(term), do: {:ok, term}
- @spec dump(list(String.t)) :: {atom, list(String.t)}
- def dump(term) when is_list(term), do: {:ok, term}
- def dump(_), do: :error
- @spec get_delimiter(String.t) :: String.t | atom
- defp get_delimiter(term) do
- cond do
- term =~ ~r/^([^,]+)(,\s)?([^,]+)*?$/ -> ", "
- term =~ ~r/^([^,]+),?([^,]+)*?$/ -> ","
- true -> :error
- end
- end
- end
- ```
- # Asynchronous or maybe even parallel assigns
- ```elixir
- defmodule App.DashboardController do
- use App, :controller
- plug :async_put_stats
- def index(conn, params) do
- render(conn)
- end
- def async_put_stats(conn, _) do
- conn
- |> async_assign(:weekly_sales, fn ->
- Sale
- |> Sale.to_stats(:weekly)
- |> Repo.all()
- end)
- |> async_assign(:trending_products, fn ->
- Product
- |> Product.trending()
- |> Product.to_stats(:weekly)
- |> Repo.all()
- end)
- |> await_assign(:weekly_sales)
- |> await_assign(:trending_products)
- end
- end
- ```
- # Why Datastructure > * (macro magic/overbloated function)
- ```elixir
- defmodule Media do
- defstruct [
- source: %Media.File{},
- sink: %Media.File{},
- transformations: [],
- validations: [],
- store_at: nil,
- store: Application.get_env(:media, :store, Media.LocalStore),
- errors: [],
- saved?: false,
- valid?: false
- ]
- def new(opts \\ []) do
- struct(Media, opts)
- end
- def save(media) do
- media
- |> apply_validations()
- |> case do
- %{valid?: true} = media -> {:ok, apply_transformations!(media)}
- %{valid?: false} = media -> {:error, media}
- end
- end
- end
- ```
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement