defmodule EctoMerge do @moduledoc """ This module holds helper funcions for recursively merging ecto records ands it's associations. """ import __MODULE__.Validator, only: [validate_mergeable!: 1] import __MODULE__.AssociationMerger, only: [merge_assocs: 4] alias __MODULE__.Utils alias Ecto.{Changeset, Multi} @doc "Merges a list of records and it's associations recursively" def merge!(repo, records) do validate_mergeable!(records) {changeset, records_to_delete} = merge_into_changeset(repo, records) {:ok, %{result: result}} = Multi.new |> Multi.insert(:result, changeset) |> delete(records_to_delete ++ records) |> repo.transaction result end defp merge_into_changeset(repo, records) do schema = get_schema(records) assocs = schema.__schema__(:associations) assocs_info = Enum.map(assocs, &schema.__schema__(:association, &1)) base_changeset = schema |> struct() |> schema.changeset(%{}) Enum.reduce( records, {base_changeset, []}, fn record, {acc_changeset, acc_records_to_delete} -> {changeset, records_to_delete} = acc_changeset |> merge_plain_attrs(record) |> merge_assocs(record, assocs_info, repo) {changeset, acc_records_to_delete ++ records_to_delete} end ) end defp merge_plain_attrs(changeset, record) do plain_attrs = record |> Map.from_struct |> Map.delete(:id) |> Utils.cleanup_changes Changeset.change(changeset, plain_attrs) end defp get_schema([head | _tail]), do: get_schema(head) defp get_schema(%{__struct__: schema}), do: schema defp delete(%Ecto.Multi{} = multi, records_to_delete) do records_to_delete |> Enum.with_index |> Enum.reduce(multi, fn({record, i}, acc_multi) -> Multi.delete(acc_multi, :"delete_extra_record_#{i}", record) end) end end