halostatue

halostatue

Clearing an `embeds_many` entry (`cast_embed/3`)

If I am using cast_embed/3, how do I clear an embeds_many entry?

I have a table flows that has a options JSONB column

CREATE TABLE flows (
  id bigint generated always as identity primary key,
  options JSONB
);

I have defined the schema as follows:

defmodule Test.Flow do
  use Ecto.Schema
  import Ecto.Changeset

  defmodule Option do
    use Ecto.Schema
    import Ecto.Changeset

    @primary_key false
    embedded_schema do
      field :value, :string
    end

    def changeset(opts \\ %__MODULE__{}, attrs) do
      cast(opts, attrs, [:value])
    end
  end

  schema "flows" do
    embeds_many :options, Option, on_replace: :delete
  end

  def changeset(flows \\ %__MODULE__{}, attrs) do
    flows
    |> cast(attrs, [:id])
    |> cast_embed(:options)
    |> validate_length(:options, min: 1)
  end
end

In Typescript type terms, the type would look something like this:

interface Flow {
  id: number
  options?: [Option, ...Option[]] | null
}

interface Option {
  value: string
}

That is, options should be null or an array of at least one Option. But I can’t make that happen:

iex(1)> {:ok, f} = Repo.insert(Test.Flow.changeset(%{})
{:ok,
 %Test.Flow{
   __meta__: #Ecto.Schema.Metadata<:loaded, "flows">,
   id: 1,
   options: []
 }}
iex(2)> Test.Flow.changeset(f, %{options: nil})
#Ecto.Changeset<
  action: nil,
  changes: %{},
  errors: [options: {"is invalid", [validation: :embed, type: {:array, :map}]}],
  data: #Test.Flow<>,
  valid?: false
>
iex(3)> Repo.update!(f, %{options: [%{}]})
%Test.Flow{
  __meta__: #Ecto.Schema.Metadata<:loaded, "flows">,
  id: 1,
  options: [%Test.Flow.Option{value: nil}]
}
iex(4)> f = Repo.get(TestFlow, 1)
%Test.Flow{
  __meta__: #Ecto.Schema.Metadata<:loaded, "flows">,
  id: 1,
  options: [%Test.Flow.Option{value: nil}]
}
iex(5)> Test.Flow.changeset(f, %{options: []})
#Ecto.Changeset<
  action: nil,
  changes: %{
    options: [
      #Ecto.Changeset<action: :replace, changes: %{}, errors: [],
       data: #Test.Flow.Option<>, valid?: true>
    ]
  },
  errors: [
    options: {"should have at least %{count} item(s)",
     [count: 1, validation: :length, kind: :min, type: :list]}
  ],
  data: #Test.Flow<>,
  valid?: false
>

This feels like it should be possible, if not easy, but I don’t really see a way to do it, especially since there doesn’t appear to be a distinction between attrs of %{} (options is missing) and %{options: nil} (options is explicitly nulled). That is, when I do Test.Flow.changeset(f, %{options: nil}).changes, I get %{}.

I suppose that I could use a sigil value (:none), but that feels…awkward.

Marked As Solved

halostatue

halostatue

One workaround is to define an Ecto.Type. My example here isn’t well organized (the type here should be a different module, OptionList), but that’s fine for an example:

defmodule Test.Flow do
  use Ecto.Schema
  import Ecto.Changeset

  defmodule Option do
    use Ecto.Schema
    use Ecto.Type

    import Ecto.Changeset

    @primary_key false
    embedded_schema do
      field :value, :string
    end

    def changeset(opts \\ %__MODULE__{}, attrs) do
      cast(opts, attrs, [:value])
    end

    @impl Ecto.Type
    def type, do: {:array, :map}

    @impl Ecto.Type
    def cast([_ | _] = list) do
      cond do
        Enum.all?(list, &is_struct(&1, __MODULE__)) -> {:ok, list}
        Enum.all?(list, &is_map/1) -> load(list)
        true -> :error
      end
    end

    def cast(nil), do: {:ok, nil}
    def cast(_), do: :error

    @impl Ecto.Type
    def load(nil), do: {:ok, nil}

    def load(data) when is_list(data) do
      {
        :ok,
        Enum.map(data, fn entry ->
          struct!(
            __MODULE__,
            for {k, v} <- entry do
              {String.to_existing_atom(k), v}
            end
          )
        end)
      }
    end

    @impl Ecto.Type
    def dump([_ | _] = list) do
      if Enum.all?(list, &match?(%__MODULE__{}, &1)) do
        {:ok, Enum.map(list, &Map.from_struct/1)}
      else
        :error
      end
    end

    def dump(nil), do: {:ok, nil}
    def dump(_), do: :error
  end

  schema "flows" do
    field :options, Option
  end

  def changeset(flows \\ %__MODULE__{}, attrs) do
    cast(flows, attrs, [:id, :options])
  end
end

It’s a lot of work, and I suspect that some of it could be wrapped in a macro.

I fear that the lesson here is to avoid embeds_many if your column is intentionally nullable, because Ecto works against your design in this case.

Where Next?

Popular in Questions Top

aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
New
tduccuong
Hi, is there any work on GUI with Elixir, that is similar to Electron/Javascript? My idea is to bundle Phoenix and BEAM into a single se...
New
LegitStack
I’m trying to make a websocket server in Phoenix or raw Elixir. I heard about gun, I think I could use cowboy, but since I’m not that sma...
New
JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
PeterCarter
There are pre-rolled solutions for other frameworks that do work. However, Phoenix does not seem to have these. Have people had good expe...
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

Other popular topics Top

danschultzer
None of the current solutions worked well for me, so I went ahead and built a user management system from scratch. This project took far...
548 29377 241
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
msaraiva
Surface is an experimental library built on top of Phoenix LiveView and its new LiveComponent API that aims to provide a more declarative...
564 43622 214
New
dokuzbir
I want to highlight html closing tags when i click a html tag. That works in .html files but doesnt work for html.eex templates. How can...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
klo
Got a question about when to concat vs. prepending items to list then reversing to achieve appending. So i know lists boil down to [1 | ...
New
komlanvi
Hi everyone, I was playing with phoenix liveView but I run into an issue. I have a form and want to validate each input text when the te...
New
marick
I had some trouble figuring out how to make many-to-many associations work. Once I got it working, I wrote a blog post. Because I’m a nov...
New
sergio
Kind of like when jquery came out, it was super necessary. Existing drag and drop libraries have a bunch of baggage to support old browse...
New

We're in Beta

About us Mission Statement