diff --git a/CHANGELOG.md b/CHANGELOG.md index f18aff0..2cc3058 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## v0.0.13 (2025-01-15) +- Add support for array fields + ## v0.0.12 (2023-02-02) - Update dependencies diff --git a/lib/waffle_ecto/schema.ex b/lib/waffle_ecto/schema.ex index b4a774e..9ca93b8 100644 --- a/lib/waffle_ecto/schema.ex +++ b/lib/waffle_ecto/schema.ex @@ -90,31 +90,52 @@ defmodule Waffle.Ecto.Schema do def do_apply_changes(%{__meta__: _} = data), do: data def check_and_apply_scope(params, scope, options) do - Enum.reduce(params, [], fn - # Don't wrap nil casts in the scope object - {field, nil}, fields -> - [{field, nil} | fields] - - # Allow casting Plug.Uploads - {field, upload = %{__struct__: Plug.Upload}}, fields -> - [{field, {upload, scope}} | fields] - - # Allow casting binary data structs - {field, upload = %{filename: filename, binary: binary}}, fields - when is_binary(filename) and is_binary(binary) -> - [{field, {upload, scope}} | fields] - - {field, upload = %{filename: filename, path: path}}, fields - when is_binary(filename) and is_binary(path) -> - path = String.trim(path) - upload = %{upload | path: path} - if path_allowed?(path, options), do: [{field, {upload, scope}} | fields], else: fields - - # If casting a binary (path), ensure we've explicitly allowed paths - {field, path}, fields when is_binary(path) -> - path = String.trim(path) - if path_allowed?(path, options), do: [{field, {path, scope}} | fields], else: fields + params + |> Enum.reduce([], fn {field, value}, fields -> + [{field, apply_scope(value, scope, options)} | fields] end) + |> Enum.reject(fn + {_field, :invalid} -> true + {_field, values} when is_list(values) -> Enum.any?(values, &(&1 == :invalid)) + _else -> false + end) + end + + # Don't wrap nil casts in the scope object + def apply_scope(nil, _scope, _options) do + nil + end + + def apply_scope(values, scope, options) when is_list(values) do + Enum.map(values, &apply_scope(&1, scope, options)) + end + + # Allow casting Plug.Uploads + def apply_scope(%{__struct__: Plug.Upload} = upload, scope, _options) do + {upload, scope} + end + + # Allow casting binary data structs + def apply_scope(%{filename: filename, binary: binary} = upload, scope, _options) + when is_binary(filename) and is_binary(binary) do + {upload, scope} + end + + # If casting a binary (path), ensure we've explicitly allowed paths + def apply_scope(%{filename: filename, path: path} = upload, scope, options) + when is_binary(filename) and is_binary(path) do + path = String.trim(path) + + if path_allowed?(path, options) do + {%{upload | path: path}, scope} + else + :invalid + end + end + + def apply_scope(path, scope, options) when is_binary(path) do + path = String.trim(path) + if path_allowed?(path, options), do: {path, scope}, else: :invalid end defp path_allowed?(path, options) do diff --git a/mix.exs b/mix.exs index 3c75342..7111d09 100644 --- a/mix.exs +++ b/mix.exs @@ -1,7 +1,7 @@ defmodule Waffle.Ecto.Mixfile do use Mix.Project - @version "0.0.12" + @version "0.0.13" def project do [app: :waffle_ecto, diff --git a/test/schema_test.exs b/test/schema_test.exs index 7417dab..7551109 100644 --- a/test/schema_test.exs +++ b/test/schema_test.exs @@ -11,6 +11,7 @@ defmodule WaffleTest.Ecto.Schema do schema "users" do field(:first_name, :string) field(:avatar, DummyDefinition.Type) + field(:images, {:array, DummyDefinition.Type}) end def changeset(user, params \\ :invalid) do @@ -39,6 +40,13 @@ defmodule WaffleTest.Ecto.Schema do |> cast(params, ~w(first_name)a) |> cast_attachments(params, ~w(avatar)a) end + + def images_changeset(user, params \\ :invalid) do + user + |> cast(params, ~w(first_name)a) + |> cast_attachments(params, ~w(images)a) + |> validate_required(:images) + end end def build_upload(path) do @@ -63,6 +71,24 @@ defmodule WaffleTest.Ecto.Schema do %{file_name: "file.png", updated_at: _} = cs.changes.avatar end + test_with_mock "cascades storage success with an array", DummyDefinition, + store: fn {%{__struct__: Plug.Upload, path: "/path/to/my/file.png", filename: "file.png"}, + %TestUser{}} -> + {:ok, "file.png"} + end do + upload1 = build_upload("/path/to/my/file.png") + upload2 = build_upload("/path/to/my/file.png") + + cs = TestUser.images_changeset(%TestUser{}, %{"images" => [upload1, upload2]}) + + assert cs.valid? + + [ + %{file_name: "file.png", updated_at: _}, + %{file_name: "file.png", updated_at: _} + ] = cs.changes.images + end + test_with_mock "cascades storage error into an error", DummyDefinition, store: fn {%{__struct__: Plug.Upload, path: "/path/to/my/file.png", filename: "file.png"}, %TestUser{}} ->