defmodule ExTenant.Changeset do
@moduledoc """
The function `cast_tenanted(changset, attrs, allowed)`
------------------------------------------------------
- injects the `tenant_id` key/atom into the allowed fields
- retrieves the `tenant_id` value from the process dictionary
- injects the %{"tenant_id" => tenant_id} into the attrs map
- and then calls the standard Ecto.Changeset.cast function
***An exception will be raised in the following two situations***
- if the `Repo` was not set in the Config.exs file
- if the tenant_id in the process dictionary is nil
Here is an example
------------------
defmodule Post do
use ExTenant.Schema
use ExTenant.Changeset
...
defp changeset(attrs) do
%__MODULE__{}
|> cast_tenanted(params, [:name, :body])
end
end
"""
@doc false
defmacro __using__(_opts) do
quote do
import ExTenant.Changeset
import Ecto.Changeset
end
end
@doc """
Basically call this like the standard `cast` function and the module
macros will handle all the `tenant_id` injecting
Example
-------
def changeset(attrs) do
%__MODULE__{}
|> cast_tenanted(attrs, [:name, :body])
end
"""
defmacro cast_tenanted(changeset, params, allowed) do
quote bind_quoted: [cs: changeset, params: params, allowed: allowed] do
tenanted_allowed =
[:tenant_id, allowed]
|> List.flatten()
tenant_id = retrieve_tenant_id()
tenanted_params =
params
|> convert_params_to_binary()
|> inject_tenant_into_params(tenant_id)
Ecto.Changeset.cast(cs, tenanted_params, tenanted_allowed)
end
end
@doc false
def convert_params_to_binary(params) do
Enum.reduce(params, nil, fn
{key, _value}, nil when is_binary(key) ->
nil
{key, _value}, _ when is_binary(key) ->
raise ArgumentError,
"expected params to be a map with atoms or string keys, " <>
"got a map with mixed keys: #{inspect(params)}"
{key, value}, acc when is_atom(key) ->
Map.put(acc || %{}, Atom.to_string(key), value)
end) || params
end
@doc false
def inject_tenant_into_params(params, tenant_id) do
tenanted_field = ExTenant.Utils.tenanted_field()
tenant_field = "#{tenanted_field}"
params
|> Map.put(tenant_field, tenant_id)
end
@doc false
def retrieve_tenant_id do
case Application.get_env(:ex_tenant, :tenant_repo) do
nil ->
raise_no_tenant_id_error(nil)
repo ->
repo.get_tenant_id()
|> raise_no_tenant_id_error()
end
end
defp raise_no_tenant_id_error(nil), do: raise(ExTenant.TenantNotSetError)
defp raise_no_tenant_id_error(id), do: id
end