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