Skip to content

Commit aa9fea2

Browse files
authored
Merge pull request #141 from cschmatzler/main
add insert_or_update/2 and insert_or_update!/2
2 parents 80c7a87 + f5fe1ab commit aa9fea2

File tree

3 files changed

+160
-2
lines changed

3 files changed

+160
-2
lines changed

lib/paper_trail.ex

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,35 @@ defmodule PaperTrail do
6161
|> model_or_error(:insert)
6262
end
6363

64+
@doc """
65+
Upserts a record to the database with a related version insertion in one transaction.
66+
"""
67+
@spec insert_or_update(changeset :: Ecto.Changeset.t(model), options :: Keyword.t()) ::
68+
{:ok, %{model: model, version: Version.t()}} | {:error, Ecto.Changeset.t(model) | term}
69+
when model: struct
70+
def insert_or_update(
71+
changeset,
72+
options \\ [origin: nil, meta: nil, originator: nil, prefix: nil]
73+
) do
74+
PaperTrail.Multi.new()
75+
|> PaperTrail.Multi.insert_or_update(changeset, options)
76+
|> PaperTrail.Multi.commit()
77+
end
78+
79+
@doc """
80+
Same as insert_or_update/2 but returns only the model struct or raises if the changeset is invalid.
81+
"""
82+
@spec insert_or_update!(changeset :: Ecto.Changeset.t(model), options :: Keyword.t()) :: model
83+
when model: struct
84+
def insert_or_update!(
85+
changeset,
86+
options \\ [origin: nil, meta: nil, originator: nil, prefix: nil]
87+
) do
88+
changeset
89+
|> insert_or_update(options)
90+
|> model_or_error(:insert_or_update)
91+
end
92+
6493
@doc """
6594
Updates a record from the database with a related version insertion in one transaction
6695
"""
@@ -116,15 +145,18 @@ defmodule PaperTrail do
116145

117146
@spec model_or_error(
118147
result :: {:ok, %{required(:model) => model, optional(any()) => any()}},
119-
action :: :insert | :update | :delete
148+
action :: :insert | :insert_or_update | :update | :delete
120149
) ::
121150
model
122151
when model: struct()
123152
defp model_or_error({:ok, %{model: model}}, _action) do
124153
model
125154
end
126155

127-
@spec model_or_error(result :: {:error, reason :: term}, action :: :insert | :update | :delete) ::
156+
@spec model_or_error(
157+
result :: {:error, reason :: term},
158+
action :: :insert | :insert_or_update | :update | :delete
159+
) ::
128160
no_return
129161
defp model_or_error({:error, %Ecto.Changeset{} = changeset}, action) do
130162
raise Ecto.InvalidChangesetError, action: action, changeset: changeset

lib/paper_trail/multi.ex

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,33 @@ defmodule PaperTrail.Multi do
149149
end
150150
end
151151

152+
def insert_or_update(
153+
%Ecto.Multi{} = multi,
154+
changeset,
155+
options \\ [
156+
origin: nil,
157+
meta: nil,
158+
originator: nil,
159+
prefix: nil,
160+
model_key: :model,
161+
version_key: :version,
162+
ecto_options: []
163+
]
164+
) do
165+
case get_state(changeset) do
166+
:built ->
167+
insert(multi, changeset, options)
168+
169+
:loaded ->
170+
update(multi, changeset, options)
171+
172+
state ->
173+
raise ArgumentError,
174+
"the changeset has an invalid state " <>
175+
"for PaperTrail.insert_or_update/2 or PaperTrail.insert_or_update!/2: #{state}"
176+
end
177+
end
178+
152179
def delete(
153180
%Ecto.Multi{} = multi,
154181
struct,
@@ -199,4 +226,13 @@ defmodule PaperTrail.Multi do
199226
end
200227
end
201228
end
229+
230+
defp get_state(%Ecto.Changeset{data: %{__meta__: %{state: state}}}), do: state
231+
232+
defp get_state(%{__struct__: _}) do
233+
raise ArgumentError,
234+
"giving a struct to PaperTrail.insert_or_update/2 or " <>
235+
"PaperTrail.insert_or_update!/2 is not supported. " <>
236+
"Please use an Ecto.Changeset"
237+
end
202238
end

test/paper_trail/base_test.exs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,96 @@ defmodule PaperTrailTest do
120120
}
121121
end
122122

123+
test "PaperTrail.insert_or_update/2 creates a new record when it does not already exist" do
124+
user = create_user()
125+
126+
{:ok, result} =
127+
Company.changeset(%Company{}, @create_company_params)
128+
|> PaperTrail.insert_or_update(originator: user)
129+
130+
company_count = Company.count()
131+
version_count = Version.count()
132+
133+
company = result[:model] |> serialize
134+
version = result[:version] |> serialize
135+
136+
assert Map.keys(result) == [:model, :version]
137+
assert company_count == 1
138+
assert version_count == 1
139+
140+
assert Map.drop(company, [:id, :inserted_at, :updated_at]) == %{
141+
name: "Acme LLC",
142+
is_active: true,
143+
city: "Greenwich",
144+
website: nil,
145+
address: nil,
146+
facebook: nil,
147+
twitter: nil,
148+
founded_in: nil,
149+
location: %{country: "Brazil"}
150+
}
151+
152+
assert Map.drop(version, [:id, :inserted_at]) == %{
153+
event: "insert",
154+
item_type: "SimpleCompany",
155+
item_id: company.id,
156+
item_changes: company,
157+
originator_id: user.id,
158+
origin: nil,
159+
meta: nil
160+
}
161+
162+
assert company == first(Company, :id) |> @repo.one |> serialize
163+
end
164+
165+
test "PaperTrail.insert_or_update/2 updates a record when already exists" do
166+
user = create_user()
167+
{:ok, insert_result} = create_company_with_version()
168+
169+
{:ok, result} =
170+
Company.changeset(insert_result[:model], @update_company_params)
171+
|> PaperTrail.insert_or_update(originator: user)
172+
173+
company_count = Company.count()
174+
version_count = Version.count()
175+
176+
company = result[:model] |> serialize
177+
version = result[:version] |> serialize
178+
179+
assert Map.keys(result) == [:model, :version]
180+
assert company_count == 1
181+
assert version_count == 2
182+
183+
assert Map.drop(company, [:id, :inserted_at, :updated_at]) == %{
184+
name: "Acme LLC",
185+
is_active: true,
186+
city: "Hong Kong",
187+
website: "http://www.acme.com",
188+
address: nil,
189+
facebook: "acme.llc",
190+
twitter: nil,
191+
founded_in: nil,
192+
location: %{country: "Chile"}
193+
}
194+
195+
assert Map.drop(version, [:id, :inserted_at]) == %{
196+
event: "update",
197+
item_type: "SimpleCompany",
198+
item_id: company.id,
199+
item_changes: %{
200+
city: "Hong Kong",
201+
website: "http://www.acme.com",
202+
facebook: "acme.llc",
203+
location: %{country: "Chile"}
204+
},
205+
originator_id: user.id,
206+
origin: nil,
207+
meta: nil
208+
}
209+
210+
assert company == first(Company, :id) |> @repo.one |> serialize
211+
end
212+
123213
test "updating a company with originator creates a correct company version" do
124214
user = create_user()
125215
{:ok, insert_result} = create_company_with_version()

0 commit comments

Comments
 (0)