changed
CHANGELOG.md
|
@@ -2,6 +2,15 @@
|
2
2
|
|
3
3
|
## Unreleased
|
4
4
|
|
5
|
+ ## [0.1.3] - 2021-08-30
|
6
|
+
|
7
|
+ ### Changed
|
8
|
+
|
9
|
+ - Raise `EctoNestedChangeset.NotLoadedError` in case the relation field of a
|
10
|
+ loaded resource is not preloaded.
|
11
|
+ - Handle list operations on root level relation fields if the field is not
|
12
|
+ preloaded and the data is not persisted.
|
13
|
+
|
5
14
|
## [0.1.2] - 2021-08-29
|
6
15
|
|
7
16
|
### Fixed
|
changed
README.md
|
@@ -2,7 +2,8 @@
|
2
2
|
|
3
3
|
# EctoNestedChangeset
|
4
4
|
|
5
|
- This is an experimental package for manipulating nested Ecto changesets.
|
5
|
+ This is an experimental package for manipulating nested
|
6
|
+ [Ecto](https://github.com/elixir-ecto/ecto) changesets.
|
6
7
|
|
7
8
|
## Installation
|
8
9
|
|
|
@@ -11,15 +12,18 @@ Add `ecto_nested_changeset` to your list of dependencies in `mix.exs`:
|
11
12
|
```elixir
|
12
13
|
def deps do
|
13
14
|
[
|
14
|
- {:ecto_nested_changeset, "~> 0.1.2"}
|
15
|
+ {:ecto_nested_changeset, "~> 0.1.3"}
|
15
16
|
]
|
16
17
|
end
|
17
18
|
```
|
18
19
|
|
19
|
- See the module documentation of `EctoNestedChangeset` for usage examples.
|
20
|
-
|
21
20
|
## Usage
|
22
21
|
|
22
|
+ The primary use case of this library is the manipulation of
|
23
|
+ [Ecto](https://github.com/elixir-ecto/ecto) changesets
|
24
|
+ used as a source for dynamic, nested forms in
|
25
|
+ [Phoenix LiveView](https://github.com/phoenixframework/phoenix_live_view).
|
26
|
+
|
23
27
|
```elixir
|
24
28
|
category = %Category{
|
25
29
|
posts: [
|
|
@@ -72,9 +76,20 @@ There is an example Phoenix application with a dynamic nested LiveView form in
|
72
76
|
the `/example` folder of the repository.
|
73
77
|
|
74
78
|
```bash
|
75
|
- cd example
|
79
|
+ git clone https://github.com/woylie/ecto_nested_changeset.git
|
80
|
+ cd ecto_nested_changeset/example
|
76
81
|
mix setup
|
77
82
|
mix phx.server
|
78
83
|
```
|
79
84
|
|
80
|
- You can access the list of pet owners at http://localhost:4000.
|
85
|
+ Note that Postgres needs to be running to use the application.
|
86
|
+
|
87
|
+ You can access the application at http://localhost:4000.
|
88
|
+
|
89
|
+ ## Status
|
90
|
+
|
91
|
+ This library has a very narrow purpose, which means that even though it is
|
92
|
+ young, it is unlikely that new functionality is going to be added or that the
|
93
|
+ API is going to change. Should you miss something, though, don't hesitate to
|
94
|
+ open an issue. Any issues and problems that may arise will be dealt with
|
95
|
+ swiftly.
|
changed
hex_metadata.config
|
@@ -3,8 +3,9 @@
|
3
3
|
{<<"description">>,<<"Helpers for manipulating nested Ecto changesets">>}.
|
4
4
|
{<<"elixir">>,<<"~> 1.8">>}.
|
5
5
|
{<<"files">>,
|
6
|
- [<<"lib">>,<<"lib/ecto_nested_changeset.ex">>,<<".formatter.exs">>,
|
7
|
- <<"mix.exs">>,<<"README.md">>,<<"LICENSE">>,<<"CHANGELOG.md">>]}.
|
6
|
+ [<<"lib">>,<<"lib/ecto_nested_changeset.ex">>,<<"lib/exceptions.ex">>,
|
7
|
+ <<".formatter.exs">>,<<"mix.exs">>,<<"README.md">>,<<"LICENSE">>,
|
8
|
+ <<"CHANGELOG.md">>]}.
|
8
9
|
{<<"licenses">>,[<<"MIT">>]}.
|
9
10
|
{<<"links">>,
|
10
11
|
[{<<"Changelog">>,
|
|
@@ -17,4 +18,4 @@
|
17
18
|
{<<"optional">>,false},
|
18
19
|
{<<"repository">>,<<"hexpm">>},
|
19
20
|
{<<"requirement">>,<<"~> 3.7">>}]]}.
|
20
|
- {<<"version">>,<<"0.1.2">>}.
|
21
|
+ {<<"version">>,<<"0.1.3">>}.
|
changed
lib/ecto_nested_changeset.ex
|
@@ -208,8 +208,10 @@ defmodule EctoNestedChangeset do
|
208
208
|
"""
|
209
209
|
@spec delete_at(Changeset.t(), [atom | non_neg_integer] | atom, keyword) ::
|
210
210
|
Changeset.t()
|
211
|
- def delete_at(%Changeset{} = changeset, path, opts \\ []),
|
212
|
- do: nested_update(:delete, changeset, path, opts)
|
211
|
+ def delete_at(%Changeset{} = changeset, path, opts \\ []) do
|
212
|
+ mode = opts[:mode] || {:action, :replace}
|
213
|
+ nested_update(:delete, changeset, path, mode)
|
214
|
+ end
|
213
215
|
|
214
216
|
defp nested_update(operation, changeset, field, value) when is_atom(field),
|
215
217
|
do: nested_update(operation, changeset, [field], value)
|
|
@@ -217,9 +219,14 @@ defmodule EctoNestedChangeset do
|
217
219
|
defp nested_update(:append, %Changeset{} = changeset, [field], value)
|
218
220
|
when is_atom(field) do
|
219
221
|
new_value =
|
220
|
- case {get_change_or_field(changeset, field), changeset.action} do
|
221
|
- {%NotLoaded{}, :insert} -> [value]
|
222
|
- {previous_value, _} -> previous_value ++ [value]
|
222
|
+ case get_change_or_field(changeset, field) do
|
223
|
+ %NotLoaded{} ->
|
224
|
+ if Ecto.get_meta(changeset.data, :state) == :built,
|
225
|
+ do: [value],
|
226
|
+ else: raise(EctoNestedChangeset.NotLoadedError, field: field)
|
227
|
+
|
228
|
+ previous_value ->
|
229
|
+ previous_value ++ [value]
|
223
230
|
end
|
224
231
|
|
225
232
|
Changeset.put_change(changeset, field, new_value)
|
|
@@ -234,9 +241,14 @@ defmodule EctoNestedChangeset do
|
234
241
|
defp nested_update(:prepend, %Changeset{} = changeset, [field], value)
|
235
242
|
when is_atom(field) do
|
236
243
|
new_value =
|
237
|
- case {get_change_or_field(changeset, field), changeset.action} do
|
238
|
- {%NotLoaded{}, :insert} -> [value]
|
239
|
- {previous_value, _} -> [value | previous_value]
|
244
|
+ case get_change_or_field(changeset, field) do
|
245
|
+ %NotLoaded{} ->
|
246
|
+ if Ecto.get_meta(changeset.data, :state) == :built,
|
247
|
+ do: [value],
|
248
|
+ else: raise(EctoNestedChangeset.NotLoadedError, field: field)
|
249
|
+
|
250
|
+ previous_value ->
|
251
|
+ [value | previous_value]
|
240
252
|
end
|
241
253
|
|
242
254
|
Changeset.put_change(changeset, field, new_value)
|
|
@@ -257,9 +269,14 @@ defmodule EctoNestedChangeset do
|
257
269
|
defp nested_update(:insert, %Changeset{} = changeset, [field, index], value)
|
258
270
|
when is_atom(field) and is_integer(index) do
|
259
271
|
new_value =
|
260
|
- case {get_change_or_field(changeset, field), changeset.action} do
|
261
|
- {%NotLoaded{}, :insert} -> [value]
|
262
|
- {previous_value, _} -> List.insert_at(previous_value, index, value)
|
272
|
+ case get_change_or_field(changeset, field) do
|
273
|
+ %NotLoaded{} ->
|
274
|
+ if Ecto.get_meta(changeset.data, :state) == :built,
|
275
|
+ do: [value],
|
276
|
+ else: raise(EctoNestedChangeset.NotLoadedError, field: field)
|
277
|
+
|
278
|
+ previous_value ->
|
279
|
+ List.insert_at(previous_value, index, value)
|
263
280
|
end
|
264
281
|
|
265
282
|
Changeset.put_change(changeset, field, new_value)
|
|
@@ -283,31 +300,28 @@ defmodule EctoNestedChangeset do
|
283
300
|
List.update_at(items, index, &func.(&1))
|
284
301
|
end
|
285
302
|
|
286
|
- defp nested_update(:delete, items, [index], opts)
|
303
|
+ defp nested_update(:delete, items, [index], mode)
|
287
304
|
when is_list(items) and is_integer(index) do
|
288
|
- case Enum.at(items, index) do
|
289
|
- %Changeset{action: :insert} ->
|
305
|
+ case {Enum.at(items, index), mode} do
|
306
|
+ {%Changeset{action: :insert}, _} ->
|
290
307
|
List.delete_at(items, index)
|
291
308
|
|
292
|
- %{} = item ->
|
293
|
- case opts[:mode] || {:action, :replace} do
|
294
|
- {:action, :delete} ->
|
295
|
- List.replace_at(
|
296
|
- items,
|
297
|
- index,
|
298
|
- item |> change() |> Map.put(:action, :delete)
|
299
|
- )
|
309
|
+ {%{} = item, {:action, :delete}} ->
|
310
|
+ List.replace_at(
|
311
|
+ items,
|
312
|
+ index,
|
313
|
+ item |> change() |> Map.put(:action, :delete)
|
314
|
+ )
|
300
315
|
|
301
|
- {:action, :replace} ->
|
302
|
- List.delete_at(items, index)
|
316
|
+ {%{}, {:action, :replace}} ->
|
317
|
+ List.delete_at(items, index)
|
303
318
|
|
304
|
- {:flag, field} ->
|
305
|
- List.replace_at(
|
306
|
- items,
|
307
|
- index,
|
308
|
- item |> change() |> put_change(field, true)
|
309
|
- )
|
310
|
- end
|
319
|
+ {%{} = item, {:flag, field}} when is_atom(field) ->
|
320
|
+ List.replace_at(
|
321
|
+ items,
|
322
|
+ index,
|
323
|
+ item |> change() |> put_change(field, true)
|
324
|
+ )
|
311
325
|
|
312
326
|
_item ->
|
313
327
|
List.delete_at(items, index)
|
added
lib/exceptions.ex
|
@@ -0,0 +1,12 @@
|
1
|
+ defmodule EctoNestedChangeset.NotLoadedError do
|
2
|
+ @moduledoc """
|
3
|
+ Raised when a relation field that is updated is not preloaded.
|
4
|
+ """
|
5
|
+ defexception [:field, :message]
|
6
|
+
|
7
|
+ def exception(opts) do
|
8
|
+ field = Keyword.fetch!(opts, :field)
|
9
|
+ message = "field `#{inspect(field)}` is not loaded"
|
10
|
+ %__MODULE__{field: field, message: message}
|
11
|
+ end
|
12
|
+ end
|
changed
mix.exs
|
@@ -1,7 +1,7 @@
|
1
1
|
defmodule EctoNestedChangeset.MixProject do
|
2
2
|
use Mix.Project
|
3
3
|
|
4
|
- @version "0.1.2"
|
4
|
+ @version "0.1.3"
|
5
5
|
@source_url "https://github.com/woylie/ecto_nested_changeset"
|
6
6
|
|
7
7
|
def project do
|