Skip to content

Commit

Permalink
Merge pull request #141 from cschmatzler/main
Browse files Browse the repository at this point in the history
add insert_or_update/2 and insert_or_update!/2
  • Loading branch information
izelnakri committed May 30, 2021
2 parents 80c7a87 + f5fe1ab commit aa9fea2
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 2 deletions.
36 changes: 34 additions & 2 deletions lib/paper_trail.ex
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,35 @@ defmodule PaperTrail do
|> model_or_error(:insert)
end

@doc """
Upserts a record to the database with a related version insertion in one transaction.
"""
@spec insert_or_update(changeset :: Ecto.Changeset.t(model), options :: Keyword.t()) ::
{:ok, %{model: model, version: Version.t()}} | {:error, Ecto.Changeset.t(model) | term}
when model: struct
def insert_or_update(
changeset,
options \\ [origin: nil, meta: nil, originator: nil, prefix: nil]
) do
PaperTrail.Multi.new()
|> PaperTrail.Multi.insert_or_update(changeset, options)
|> PaperTrail.Multi.commit()
end

@doc """
Same as insert_or_update/2 but returns only the model struct or raises if the changeset is invalid.
"""
@spec insert_or_update!(changeset :: Ecto.Changeset.t(model), options :: Keyword.t()) :: model
when model: struct
def insert_or_update!(
changeset,
options \\ [origin: nil, meta: nil, originator: nil, prefix: nil]
) do
changeset
|> insert_or_update(options)
|> model_or_error(:insert_or_update)
end

@doc """
Updates a record from the database with a related version insertion in one transaction
"""
Expand Down Expand Up @@ -116,15 +145,18 @@ defmodule PaperTrail do

@spec model_or_error(
result :: {:ok, %{required(:model) => model, optional(any()) => any()}},
action :: :insert | :update | :delete
action :: :insert | :insert_or_update | :update | :delete
) ::
model
when model: struct()
defp model_or_error({:ok, %{model: model}}, _action) do
model
end

@spec model_or_error(result :: {:error, reason :: term}, action :: :insert | :update | :delete) ::
@spec model_or_error(
result :: {:error, reason :: term},
action :: :insert | :insert_or_update | :update | :delete
) ::
no_return
defp model_or_error({:error, %Ecto.Changeset{} = changeset}, action) do
raise Ecto.InvalidChangesetError, action: action, changeset: changeset
Expand Down
36 changes: 36 additions & 0 deletions lib/paper_trail/multi.ex
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,33 @@ defmodule PaperTrail.Multi do
end
end

def insert_or_update(
%Ecto.Multi{} = multi,
changeset,
options \\ [
origin: nil,
meta: nil,
originator: nil,
prefix: nil,
model_key: :model,
version_key: :version,
ecto_options: []
]
) do
case get_state(changeset) do
:built ->
insert(multi, changeset, options)

:loaded ->
update(multi, changeset, options)

state ->
raise ArgumentError,
"the changeset has an invalid state " <>
"for PaperTrail.insert_or_update/2 or PaperTrail.insert_or_update!/2: #{state}"
end
end

def delete(
%Ecto.Multi{} = multi,
struct,
Expand Down Expand Up @@ -199,4 +226,13 @@ defmodule PaperTrail.Multi do
end
end
end

defp get_state(%Ecto.Changeset{data: %{__meta__: %{state: state}}}), do: state

defp get_state(%{__struct__: _}) do
raise ArgumentError,
"giving a struct to PaperTrail.insert_or_update/2 or " <>
"PaperTrail.insert_or_update!/2 is not supported. " <>
"Please use an Ecto.Changeset"
end
end
90 changes: 90 additions & 0 deletions test/paper_trail/base_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,96 @@ defmodule PaperTrailTest do
}
end

test "PaperTrail.insert_or_update/2 creates a new record when it does not already exist" do
user = create_user()

{:ok, result} =
Company.changeset(%Company{}, @create_company_params)
|> PaperTrail.insert_or_update(originator: user)

company_count = Company.count()
version_count = Version.count()

company = result[:model] |> serialize
version = result[:version] |> serialize

assert Map.keys(result) == [:model, :version]
assert company_count == 1
assert version_count == 1

assert Map.drop(company, [:id, :inserted_at, :updated_at]) == %{
name: "Acme LLC",
is_active: true,
city: "Greenwich",
website: nil,
address: nil,
facebook: nil,
twitter: nil,
founded_in: nil,
location: %{country: "Brazil"}
}

assert Map.drop(version, [:id, :inserted_at]) == %{
event: "insert",
item_type: "SimpleCompany",
item_id: company.id,
item_changes: company,
originator_id: user.id,
origin: nil,
meta: nil
}

assert company == first(Company, :id) |> @repo.one |> serialize
end

test "PaperTrail.insert_or_update/2 updates a record when already exists" do
user = create_user()
{:ok, insert_result} = create_company_with_version()

{:ok, result} =
Company.changeset(insert_result[:model], @update_company_params)
|> PaperTrail.insert_or_update(originator: user)

company_count = Company.count()
version_count = Version.count()

company = result[:model] |> serialize
version = result[:version] |> serialize

assert Map.keys(result) == [:model, :version]
assert company_count == 1
assert version_count == 2

assert Map.drop(company, [:id, :inserted_at, :updated_at]) == %{
name: "Acme LLC",
is_active: true,
city: "Hong Kong",
website: "http://www.acme.com",
address: nil,
facebook: "acme.llc",
twitter: nil,
founded_in: nil,
location: %{country: "Chile"}
}

assert Map.drop(version, [:id, :inserted_at]) == %{
event: "update",
item_type: "SimpleCompany",
item_id: company.id,
item_changes: %{
city: "Hong Kong",
website: "http://www.acme.com",
facebook: "acme.llc",
location: %{country: "Chile"}
},
originator_id: user.id,
origin: nil,
meta: nil
}

assert company == first(Company, :id) |> @repo.one |> serialize
end

test "updating a company with originator creates a correct company version" do
user = create_user()
{:ok, insert_result} = create_company_with_version()
Expand Down

0 comments on commit aa9fea2

Please sign in to comment.