Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add insert_or_update/2 and insert_or_update!/2 #141

Merged
merged 1 commit into from May 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
36 changes: 34 additions & 2 deletions lib/paper_trail.ex
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]
Copy link
Owner

@izelnakri izelnakri May 28, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you also add the following to the default options list:

          model_key: :model,
          version_key: :version,
          ecto_options: [],
          initial_version_key: :initial_version,

) 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
Expand Up @@ -142,6 +142,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 @@ -192,4 +219,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
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