Deeply nested embeds

I have a panel resource which embeds an array of dins

defmodule NoSpark.Builder.Panel do
  use Ash.Resource,
    data_layer: AshSqlite.DataLayer,
    domain: NoSpark.Builder

  sqlite do
    table "panels"
    repo NoSpark.Repo
  end

  attributes do
    uuid_primary_key :id

    attribute :name, :string do
      allow_nil? false
      public? true
    end

    attribute :description, :string, public?: true

    attribute :slots, :integer do
      allow_nil? false
      public? true
      constraints min: 0
    end

    attribute :dins, {:array, NoSpark.Builder.Din}, public?: true, default: []
  end

  code_interface do
    define :create, action: :create
    define :read_all, action: :read
    define :update, action: :update
    define :destroy, action: :destroy
    define :update_dins, args: [:dins]
    define :get_by_id, args: [:id], action: :by_id
  end

  actions do
    defaults [:read, :destroy]

    create :create do
      accept [:name, :description, :slots]
    end

    update :update do
      accept [:name, :description]
    end

    update :update_dins do
      accept [:dins]
    end

    read :by_id do
      argument :id, :uuid, allow_nil?: false
      get? true
      filter expr(id == ^arg(:id))
    end
  end
end

Then the dins also have din which has another embed array of DinModules

defmodule NoSpark.Builder.Din do
  use Ash.Resource, data_layer: :embedded, embed_nil_values?: false

  attributes do
    uuid_primary_key :id

    attribute :label, :string, public?: true
    attribute :description, :string, public?: true
    attribute :modules, {:array, NoSpark.Builder.DinModule}, public?: true, default: []
  end
end

At the last step i have the DinModule resource which is an embed with a relationship

defmodule NoSpark.Builder.DinModule do
  use Ash.Resource,
    data_layer: :embedded,
    embed_nil_values?: false

  attributes do
    uuid_primary_key :id

    attribute :label, :string, public?: true
    attribute :description, :string, public?: true

    attribute :slots, :integer do
      allow_nil? false
      public? true
      constraints min: 0
    end
  end

  relationships do
    belongs_to :module, NoSpark.Builder.Module do
      allow_nil? false
      public? true
    end
  end
end

I’ve gotten creating and deleting dins on a panel working

But when i try to update a din and add a DinModule it fails, here’s the code that i’m trying to do this with:

  def handle_event(
        "add-module-to-din",
        %{"id" => id},
        %{assigns: %{add_module: module, panel: panel, dins: dins}} = socket
      ) do
    din = Enum.find(dins, &(&1.id == id))

    din =
      Map.put(
        din,
        :modules,
        din.modules ++
          [
            %{
              label: module.title,
              slots: module.slots,
              module_id: module.id
            }
          ]
      )

    panel = Panel.update_dins!(panel, dins ++ [din])

    socket =
      socket
      |> assign(:add_module, nil)
      |> assign_panel(panel.id)

    {:noreply, socket}
  end

This throws an error which i don’t understand:

[debug] HANDLE EVENT "add-module-to-din" in NoSparkWeb.PanelLive.Show
  Parameters: %{"id" => "aad06dc3-8ea7-4647-9a6a-78a2da98b0fd", "value" => ""}
[error] GenServer #PID<0.854.0> terminating
** (Protocol.UndefinedError) protocol String.Chars not implemented for {:type, :primary_key} of type Tuple. This protocol is implemented for the following type(s): Ash.CiString, Atom, BitString, Date, DateTime, Decimal, Exqlite.Query, Float, Integer, List, NaiveDateTime, Phoenix.LiveComponent.CID, Time, URI, Version, Version.Requirement
    (elixir 1.17.2) lib/string/chars.ex:3: String.Chars.impl_for!/1
    (elixir 1.17.2) lib/string/chars.ex:22: String.Chars.to_string/1
    (stdlib 5.2.3) maps.erl:416: :maps.fold_1/4
    (elixir 1.17.2) lib/enum.ex:2543: Enum.join/2
    (no_spark 0.1.0) deps/ash/lib/ash/embeddable_type.ex:655: NoSpark.Builder.Din.apply_constraints_array/2
    (ash 3.2.6) lib/ash/type/type.ex:883: Ash.Type.apply_constraints/3
    (ash 3.2.6) lib/ash/changeset/changeset.ex:4779: Ash.Changeset.do_change_attribute/4
    (stdlib 5.2.3) maps.erl:416: :maps.fold_1/4
    (ash 3.2.6) lib/ash/changeset/changeset.ex:1751: Ash.Changeset.handle_params/4
    (ash 3.2.6) lib/ash/changeset/changeset.ex:1640: Ash.Changeset.do_for_action/4
    (no_spark 0.1.0) deps/ash/lib/ash/code_interface.ex:781: NoSpark.Builder.Panel.update_dins!/4

I’m not sure how this should work, or is there a better way of doing this, before i’ve done it with tables. But wanted to try with embeds.

here’s my ash deps:

{:ash, "~> 3.2"},
{:ash_sqlite, "~> 0.1.3"},
{:ash_phoenix, "~> 2.0"},

Any pointers are greatly appreciated

This is a bug while producing an error message that would tell you that the resources you’re adding are not unique on some given set of keys. I will push a fix up to main. I’ll be releasing a new version of ash in the next day or two, or you can use main.

EDIT: fixed in main

1 Like

Thanks Zack!

I had another issue which was copying an existing din into the dins, but the new error made this clear!