carlgleisner

carlgleisner

Custom types and AshAdmin

I’ve been banging my head with using custom types in a resource with AshAdmin. In short, the fields for the custom types keep resetting when touching other fields.

I’m pretty sure that I’m not holding it wrong and made a reproduction repo with an extensive write-up in the README. The reproduction has resource with a custom type attribute with the implementation lifted from docs as well as an attribute of type :money from AshMoney.

Is there anything in particular one should consider when using custom types in AshAdmin or could there be a potential issue there?


The README write-up:

Reproduction for custom Ash.Type question

This is a reproduction of an issue I’ve had with custom types.

In short, using the example implementation for float from the documentation resets the attribute upon validation when changing other attributes in the changeset (AshAdmin).

This app has a domain Entities with one resource Entity having the attributes:

  • :name of the built-in type :string,
  • :size of the custom type :custom_float defined in this codebase and
  • :balance of the custom type :money from AshMoney (as installed by Igniter).

The body of the function that implements the custom type :custom_float is lifted verbatim from the example in the documentation:

defmodule DemoRepo.Types.CustomFloat do
  use Ash.Type

  @impl Ash.Type
  def storage_type(_), do: :float

  @impl Ash.Type
  def cast_input(nil, _), do: {:ok, nil}

  def cast_input(value, _) do
    Ecto.Type.cast(:float, value)
  end

  @impl Ash.Type
  def cast_stored(nil, _), do: {:ok, nil}

  def cast_stored(value, _) do
    Ecto.Type.load(:float, value)
  end

  @impl Ash.Type
  def dump_to_native(nil, _), do: {:ok, nil}

  def dump_to_native(value, _) do
    Ecto.Type.dump(:float, value)
  end
end

The type :custom_float is, as far as I understand it, duly configured in config/config.exs like so:

config :ash,
  # yada yada yada
  custom_types: [
    money: AshMoney.Types.Money,
    custom_float: DemoRepo.Types.CustomFloat
  ]

When creating an Entity in AshAdmin using a default :create action that accepts :name, :size and :balance:

defaults [
  :read,
  :destroy,
  create: [:name, :size, :balance], # <- This bad boy right here
  update: [:name, :size, :balance]
]

it is possible to enter a value for :size in the form and have it passed to the changeset for persistence. However, touching any other field - thus triggering a validation - while :size has a value will set :size to nil. The same goes for the :balance attribute.

Creating an Entity using a create action :instrumented_create:

create :instrumented_create do
  accept [:name, :size]

  change fn changeset, _context -> changeset |> dbg() end
end

yields the same result.

Looking at the output after having put first name: "A" and then size: 1 in the form:

changeset #=> #Ash.Changeset<
  domain: DemoRepo.Entities,
  action_type: :create,
  action: :instrumented_create,
  attributes: %{name: "A", size: 1.0},
  relationships: %{},
  errors: [],
  data: %DemoRepo.Entities.Entity{
    id: nil,
    name: nil,
    size: nil,
    balance: nil,
    __meta__: #Ecto.Schema.Metadata<:built, "entities">
  },
  valid?: true
>

and then looking after again editing the :name field in the form one gets:

changeset #=> #Ash.Changeset<
  domain: DemoRepo.Entities,
  action_type: :create,
  action: :instrumented_create,
  attributes: %{name: "Ab", size: 1.0},
  relationships: %{},
  errors: [],
  data: %DemoRepo.Entities.Entity{
    id: nil,
    name: nil,
    size: nil,
    balance: nil,
    __meta__: #Ecto.Schema.Metadata<:built, "entities">
  },
  valid?: true
>

where size: 1.0 actually shows up in the in the changeset, but has disappeard in the form.

Touching :name again, quite reasonably, results in the following since :size was not left in the form and therefore didn’t make it to the params:

changeset #=> #Ash.Changeset<
  domain: DemoRepo.Entities,
  action_type: :create,
  action: :instrumented_create,
  attributes: %{name: "Abc", size: nil},
  relationships: %{},
  errors: [],
  data: %DemoRepo.Entities.Entity{
    id: nil,
    name: nil,
    size: nil,
    balance: nil,
    __meta__: #Ecto.Schema.Metadata<:built, "entities">
  },
  valid?: true
>

As for doing the same but with :balance (that is a “known quantity”):

changeset #=> #Ash.Changeset<
  domain: DemoRepo.Entities,
  action_type: :create,
  action: :instrumented_create,
  attributes: %{name: "A", balance: Money.new(:SEK, "1")},
  relationships: %{},
  errors: [],
  data: %DemoRepo.Entities.Entity{
    id: nil,
    name: nil,
    size: nil,
    balance: nil,
    __meta__: #Ecto.Schema.Metadata<:built, "entities">
  },
  valid?: true
>

touching :name again whereby the :balance field in the form is reset:

changeset #=> #Ash.Changeset<
  domain: DemoRepo.Entities,
  action_type: :create,
  action: :instrumented_create,
  attributes: %{name: "Ab", balance: Money.new(:SEK, "1")},
  relationships: %{},
  errors: [],
  data: %DemoRepo.Entities.Entity{
    id: nil,
    name: nil,
    size: nil,
    balance: nil,
    __meta__: #Ecto.Schema.Metadata<:built, "entities">
  },
  valid?: true
>

and then one more time touching :name:

changeset #=> #Ash.Changeset<
  domain: DemoRepo.Entities,
  action_type: :create,
  action: :instrumented_create,
  attributes: %{name: "Abc", balance: nil},
  relationships: %{},
  errors: [],
  data: %DemoRepo.Entities.Entity{
    id: nil,
    name: nil,
    size: nil,
    balance: nil,
    __meta__: #Ecto.Schema.Metadata<:built, "entities">
  },
  valid?: true
>

Finally, opening a record for updating with :instrumented_update puts out:

changeset #=> #Ash.Changeset<
  domain: DemoRepo.Entities,
  action_type: :update,
  action: :instrumented_update,
  attributes: %{},
  relationships: %{},
  errors: [],
  data: %DemoRepo.Entities.Entity{
    id: "fef55f7d-7eaf-418f-80e9-6425be8c7b66",
    name: "Joe",
    size: nil,
    balance: Money.new(:SEK, "3"),
    __meta__: #Ecto.Schema.Metadata<:loaded, "entities">
  },
  valid?: true
>

leaving the field :balance with the value nil in the form. Since one cannot save both :balance and :size, :size was already nil when opening the :instrumented_update form. The same thing happens with the default :update form.

Environment

  • macOS 15.6
  • erlang 27.3.4
  • elixir 1.18.4-otp-27
  • ash 3.5.33
  • ash_admin 0.13.13 ← Only tested in AshAdmin
  • ash_money 0.2.3
  • ash_phoenix 2.3.12
  • ash_postgres 2.6.14
  • ash_sql 0.2.89
  • phoenix_live_view 1.1.3 ← ?:warning:

BTW, LiveView complains about asset versions:

LiveView asset version mismatch. JavaScript version 1.1.0-rc.3 vs. server 1.1.3. To avoid issues, please ensure that your assets use the same version as the server.

Generation

The application was generated with the following commands:

mix archive.install hex igniter_new --force
mix archive.install hex phx_new 1.8.0-rc.4 --force

mix igniter.new demo_repo --with phx.new --install ash,ash_phoenix \
  --install ash_postgres,ash_admin --install ash_money --yes

cd demo_repo && mix ash.setup

Written by a human being :otter:

Where Next?

Popular in Questions Top

qwerescape
Is there a way to get the call stack or stack trace at any point in the code? Not from exceptions, but an expression that returns how the...
New
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
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
shahryarjb
Hello, I get Persian date from my client and convert it to normal calendar like this: def jalali_string_to_miladi_english_number(persi...
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers’ Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
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
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
chensan
I have a User schema with a :from_id field set to type :string: defmodule TweetBot.Repo.Migrations.CreateUsers do use Ecto.Migration ...
New

Other popular topics 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
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID&lt;0.412.0&gt; terminating ** (Postgrex.Error) FATAL...
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New
AstonJ
Please see the new poll here: Which code editor or IDE do you use? (Poll) (2022 Edition) It’s been a while since we first asked this, I...
208 31307 143
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New
Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
hariharasudhan94
Lets say I have map like this fetching from my database %{"_id" =&gt; #BSON.ObjectId&lt;58eb1a7a9ad169198c3dXXXX&gt;, "email" =&gt; ...
New

We're in Beta

About us Mission Statement