Packages
ecto
1.1.6
3.14.1
3.14.0
3.13.6
3.13.5
3.13.4
3.13.3
3.13.2
3.13.1
3.13.0
3.12.6
3.12.5
3.12.4
3.12.3
3.12.2
3.12.1
3.12.0
3.11.2
3.11.1
3.11.0
3.10.3
3.10.2
3.10.1
3.10.0
3.9.6
3.9.5
3.9.4
3.9.3
3.9.2
3.9.1
3.9.0
3.8.4
3.8.3
3.8.2
3.8.1
3.8.0
3.7.2
3.7.1
3.7.0
3.6.2
3.6.1
3.6.0
3.5.8
3.5.7
3.5.6
3.5.5
3.5.4
3.5.3
3.5.2
3.5.1
3.5.0
3.5.0-rc.1
3.5.0-rc.0
3.4.6
3.4.5
3.4.4
3.4.3
3.4.2
3.4.1
3.4.0
3.3.4
3.3.3
3.3.2
3.3.1
3.3.0
3.2.5
3.2.4
3.2.3
3.2.2
3.2.1
3.2.0
3.1.7
3.1.6
3.1.5
3.1.4
3.1.3
3.1.2
3.1.1
3.1.0
3.0.9
3.0.8
3.0.7
3.0.6
3.0.5
3.0.4
3.0.3
3.0.2
3.0.1
3.0.0
3.0.0-rc.1
3.0.0-rc.0
2.2.12
2.2.11
2.2.10
2.2.9
2.2.8
2.2.7
2.2.6
2.2.5
2.2.4
2.2.3
2.2.2
2.2.1
2.2.0
2.2.0-rc.1
2.2.0-rc.0
2.1.6
2.1.5
2.1.4
2.1.3
2.1.2
2.1.1
2.1.0
2.1.0-rc.5
2.1.0-rc.4
2.1.0-rc.3
2.1.0-rc.2
2.1.0-rc.1
2.1.0-rc.0
2.0.6
2.0.5
2.0.4
2.0.3
2.0.2
2.0.1
2.0.0
2.0.0-rc.6
2.0.0-rc.5
2.0.0-rc.4
2.0.0-rc.3
2.0.0-rc.2
2.0.0-rc.1
2.0.0-rc.0
2.0.0-beta.2
2.0.0-beta.1
2.0.0-beta.0
1.1.9
1.1.8
1.1.7
1.1.6
1.1.5
1.1.4
1.1.3
1.1.2
1.1.1
1.1.0
1.0.7
1.0.6
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
0.16.0
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.13.1
0.13.0
0.12.1
0.12.0
0.12.0-rc
0.11.3
0.11.2
0.11.1
0.11.0
0.10.3
0.10.2
0.10.1
0.10.0
0.9.0
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.0
0.5.1
0.5.0
0.4.0
0.3.0
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.0
A toolkit for data mapping and language integrated query for Elixir
Current section
Files
Jump to
Current section
Files
lib/ecto/embedded.ex
defmodule Ecto.Embedded do
@moduledoc false
alias __MODULE__
alias Ecto.Changeset
@type t :: %Embedded{cardinality: :one | :many,
strategy: :replace | atom,
on_replace: Changeset.Relation.on_replace,
field: atom, owner: atom, related: atom,
on_cast: Changeset.Relation.on_cast}
@behaviour Ecto.Changeset.Relation
@on_replace_opts [:raise, :mark_as_invalid, :delete]
defstruct [:cardinality, :field, :owner, :related, :on_replace,
strategy: :replace, on_cast: :changeset]
@doc """
Builds the embedded struct.
## Options
* `:cardinality` - tells if there is one embedded model or many
* `:strategy` - which strategy to use when storing items
* `:related` - name of the embedded model
* `:on_cast` - the changeset function to call during casting
* `:on_replace` - the action taken on embedded models when the model is
replaced
"""
def struct(module, name, opts) do
opts =
opts
|> Keyword.put_new(:on_cast, :changeset)
|> Keyword.put_new(:on_replace, :raise)
unless opts[:on_replace] in @on_replace_opts do
raise ArgumentError, "invalid `:on_replace` option for #{inspect name}. " <>
"The only valid options are: " <>
Enum.map_join(@on_replace_opts, ", ", &"`#{inspect &1}`")
end
struct(%Embedded{field: name, owner: module}, opts)
end
@doc """
Callback invoked by repository to prepare embeds.
"""
def prepare(changeset, [], _adapter, _repo_action), do: changeset
def prepare(changeset, embeds, adapter, repo_action) do
types = changeset.types
changeset = merge_delete_changes(changeset, embeds, types, repo_action)
update_in changeset.changes, fn changes ->
Enum.reduce(embeds, changes, fn name, changes ->
case Map.fetch(changes, name) do
{:ok, changeset} ->
{:embed, embed} = Map.get(types, name)
Map.put(changes, name, prepare_each(embed, changeset, adapter, repo_action))
:error ->
changes
end
end)
end
end
defp merge_delete_changes(changeset, embeds, types, :delete) do
Enum.reduce(embeds, changeset, fn field, acc ->
{:embed, embed} = Map.get(types, field)
Changeset.put_embed(acc, field, Ecto.Changeset.Relation.empty(embed))
end)
end
defp merge_delete_changes(changeset, _, _, _), do: changeset
defp prepare_each(%{cardinality: :one}, nil, _adapter, _action) do
nil
end
defp prepare_each(%{cardinality: :one} = embed, changeset, adapter, action) do
check_action!(changeset.action, action, embed)
prepare_each(changeset, embed, adapter)
end
defp prepare_each(%{cardinality: :many} = embed, changesets, adapter, action) do
for changeset <- changesets do
check_action!(changeset.action, action, embed)
prepare_each(changeset, embed, adapter)
end
end
defp prepare_each(%Changeset{valid?: false}, %{related: model}, _adapter) do
raise ArgumentError, "changeset for embedded #{model} is invalid, " <>
"but the parent changeset was not marked as invalid"
end
defp prepare_each(%Changeset{action: :update, changes: changes} = changeset,
_embed, _adapter) when changes == %{} do
changeset
end
defp prepare_each(%Changeset{model: %{__struct__: model}, action: action} = changeset,
%{related: model} = embed, adapter) do
callback = callback_for(:before, action)
Ecto.Model.Callbacks.__apply__(model, callback, changeset)
|> autogenerate_id(action, model, embed, adapter)
|> autogenerate(action, model, adapter)
|> prepare(model.__schema__(:embeds), adapter, action)
end
defp prepare_each(%Changeset{model: model}, %{related: expected}, _adapter) do
raise ArgumentError, "expected changeset for embedded model `#{inspect expected}`, " <>
"got: #{inspect model}"
end
defp check_action!(:update, :insert, %{related: model}),
do: raise(ArgumentError, "got action :update in changeset for embedded #{model} while inserting")
defp check_action!(:delete, :insert, %{related: model}),
do: raise(ArgumentError, "got action :delete in changeset for embedded #{model} while inserting")
defp check_action!(_, _, _), do: :ok
defp autogenerate_id(changeset, :insert, model, embed, adapter) do
case model.__schema__(:autogenerate_id) do
{key, :binary_id} ->
if Map.get(changeset.changes, key) || Map.get(changeset.model, key) do
changeset
else
update_in changeset.changes, &Map.put(&1, key, adapter.embed_id(embed))
end
other ->
raise ArgumentError, "embedded model `#{inspect model}` must have " <>
"`:binary_id` primary key with `autogenerate: true`, got: #{inspect other}"
end
end
defp autogenerate_id(changeset, action, _model, _embed, _adapter) when action in [:update, :delete] do
for {_, nil} <- Ecto.primary_key(changeset.model) do
raise Ecto.NoPrimaryKeyValueError, struct: changeset.model
end
changeset
end
defp autogenerate(changeset, :delete, _model, _adapter) do
changeset
end
defp autogenerate(%{types: types} = changeset, action, model, adapter) do
update_in changeset.changes, fn changes ->
Enum.reduce model.__schema__(:autogenerate, action), changes,
fn {k, mod, args}, acc ->
if Map.get(acc, k) do
acc
else
Map.put(acc, k, load!(types, k, apply(mod, :autogenerate, args), adapter))
end
end
end
end
defp load!(types, k, v, adapter) do
type = Map.fetch!(types, k)
case adapter.load(type, v) do
{:ok, v} -> v
:error -> raise ArgumentError, "cannot load `#{inspect v}` as type #{inspect type}"
end
end
@doc false
def build(%Embedded{related: related}) do
related.__struct__
end
@doc false
def on_replace(%Embedded{on_replace: :delete}, changeset) do
{:delete, changeset}
end
@doc false
def on_repo_action(%{field: field, related: model} = embed,
changeset, parent, adapter, repo, _repo_action, opts) do
changeset = on_repo_action(changeset, model, adapter, repo, opts)
maybe_replace_one!(embed, changeset.action, Map.get(parent, field))
{:ok, apply_changes(changeset)}
end
defp on_repo_action(%Changeset{action: :update, changes: changes} = changeset,
_model, _adapter, _repo, _opts) when changes == %{} do
changeset
end
defp on_repo_action(%Changeset{action: action, changes: changes} = changeset,
model, adapter, repo, opts) do
callback = callback_for(:after, action)
related = Map.take(changes, model.__schema__(:embeds))
{:ok, changeset} =
Ecto.Changeset.Relation.on_repo_action(changeset, related, adapter, repo, opts)
Ecto.Model.Callbacks.__apply__(model, callback, changeset)
end
defp maybe_replace_one!(%{cardinality: :one, related: model}, :insert, current) when current != nil do
changeset = Changeset.change(current)
changeset = Ecto.Model.Callbacks.__apply__(model, :before_delete, changeset)
Ecto.Model.Callbacks.__apply__(model, :after_delete, changeset)
:ok
end
defp maybe_replace_one!(_embed, _action, _current), do: :ok
defp apply_changes(%{action: :delete}), do: nil
defp apply_changes(changeset) do
model = Changeset.apply_changes(changeset)
put_in(model.__meta__.state, :loaded)
end
types = [:before, :after]
actions = [:insert, :update, :delete]
for type <- types, action <- actions do
defp callback_for(unquote(type), unquote(action)), do: unquote(:"#{type}_#{action}")
end
defp callback_for(_type, nil) do
raise ArgumentError, "embedded changeset action not set"
end
end