AshPhoenix.Form fails in submit without errors with embedded resources

I have a embedded resource like this:
defmodule Marketplace.Markets.Property.OffMarket do
@moduledoc false

use Ash.Resource,
data_layer: :embedded,
extensions: [AshGraphql.Resource]

code_interface do
define_for Marketplace.Markets

define :new
end

attributes do
alias Marketplace.Ash.Types.PhoneNumber

attribute :agent_name, :string
attribute :agent_phone_number, PhoneNumber

attribute :company_name, :string, allow_nil?: false
attribute :company_phone_number, PhoneNumber
end

graphql do
type :off_market
end

actions do
defaults [:update, :read, :destroy]

create :new, primary?: true
end
end
defmodule Marketplace.Markets.Property.OffMarket do
@moduledoc false

use Ash.Resource,
data_layer: :embedded,
extensions: [AshGraphql.Resource]

code_interface do
define_for Marketplace.Markets

define :new
end

attributes do
alias Marketplace.Ash.Types.PhoneNumber

attribute :agent_name, :string
attribute :agent_phone_number, PhoneNumber

attribute :company_name, :string, allow_nil?: false
attribute :company_phone_number, PhoneNumber
end

graphql do
type :off_market
end

actions do
defaults [:update, :read, :destroy]

create :new, primary?: true
end
end
This embedded resource is used as an attribute inside my Property resource like so: attribute :off_market, OffMarket, allow_nil?: true I'm having problems when trying to submit an update to it using AshPhoenix.Form.submit, when I run this, it will fail with a {:error, form} reply but there will be no error inside the form. This is how I initialize the form:
AshPhoenix.Form.for_update(property, :update,
api: Markets,
actor: user,
forms: [auto?: true],
prepare_source: fn changeset ->
changeset
|> Ash.Changeset.set_argument(:uploaded_images, [])
|> Ash.Changeset.set_argument(:removed_images, [])
end
)
AshPhoenix.Form.for_update(property, :update,
api: Markets,
actor: user,
forms: [auto?: true],
prepare_source: fn changeset ->
changeset
|> Ash.Changeset.set_argument(:uploaded_images, [])
|> Ash.Changeset.set_argument(:removed_images, [])
end
)
After this, I just run the submit function. An I doing something wrong here?
25 Replies
Blibs
BlibsOP2y ago
I'm not sure if only the information I gave is enough to see what the issue is, but at the same time my resources are pretty big, so what I did was to convert the form struct into a base64 so you can actually load it directly in elixir code if you think that will help
Blibs
BlibsOP2y ago
To load these files, you just need to paste the base64 content into a elixir string and then do string |> Base.decode64!() |> :erlang.binary_to_term()
ZachDaniel
ZachDaniel2y ago
The first thing to check is, on the errored form, what this returns:
AshPhoenix.Form.errors(form, for_path: :all)
AshPhoenix.Form.errors(form, for_path: :all)
The second thing is to look and see if you see any log messages about unhandled errors the third thing is to see what happens if you submit the form with force?: true
Blibs
BlibsOP2y ago
Thanks! I was not aware of the for_path: :all option! Running it I got:
AshPhoenix.Form.errors(form, for_path: :all)
%{
[:images, 0] => [uuid: "is required"],
[:images, 0, :large] => [
type: "is required",
url: "is required",
s3_path: "is required"
],
[:images, 0, :medium] => [
type: "is required",
url: "is required",
s3_path: "is required"
],
[:images, 0, :original] => [
type: "is required",
url: "is required",
s3_path: "is required"
],
[:images, 0, :small] => [
type: "is required",
url: "is required",
s3_path: "is required"
],
[:images, 0, :thumbnail] => [
type: "is required",
url: "is required",
s3_path: "is required"
]
}
AshPhoenix.Form.errors(form, for_path: :all)
%{
[:images, 0] => [uuid: "is required"],
[:images, 0, :large] => [
type: "is required",
url: "is required",
s3_path: "is required"
],
[:images, 0, :medium] => [
type: "is required",
url: "is required",
s3_path: "is required"
],
[:images, 0, :original] => [
type: "is required",
url: "is required",
s3_path: "is required"
],
[:images, 0, :small] => [
type: "is required",
url: "is required",
s3_path: "is required"
],
[:images, 0, :thumbnail] => [
type: "is required",
url: "is required",
s3_path: "is required"
]
}
But I still don't get that is the problem, here is the nested form of one of these errors:
Blibs
BlibsOP2y ago
Blibs
BlibsOP2y ago
it says in the submit_errors that it contains errors, but I don't get why is that, it says that the :type, :url and :s3_path attributes are required, but they are clearly filled in the form 🤔 In case this helps, here is the image and sub_image resources:
defmodule Marketplace.Markets.Property.Image do
@moduledoc false

alias Marketplace.Markets.Property.SubImage

use Ash.Resource,
data_layer: :embedded,
extensions: [AshGraphql.Resource]

code_interface do
define_for Marketplace.Markets

define :new
end

attributes do
attribute :uuid, :uuid, allow_nil?: false

attribute :category, :atom do
constraints one_of: [:bedroom, :bathroom]
end

attribute :description, :string

attribute :thumbnail, SubImage, allow_nil?: false
attribute :small, SubImage, allow_nil?: false
attribute :medium, SubImage, allow_nil?: false
attribute :large, SubImage, allow_nil?: false
attribute :original, SubImage, allow_nil?: false
end

graphql do
type :image
end

actions do
defaults [:update, :read, :destroy]

create :new, primary?: true
end
end
defmodule Marketplace.Markets.Property.Image do
@moduledoc false

alias Marketplace.Markets.Property.SubImage

use Ash.Resource,
data_layer: :embedded,
extensions: [AshGraphql.Resource]

code_interface do
define_for Marketplace.Markets

define :new
end

attributes do
attribute :uuid, :uuid, allow_nil?: false

attribute :category, :atom do
constraints one_of: [:bedroom, :bathroom]
end

attribute :description, :string

attribute :thumbnail, SubImage, allow_nil?: false
attribute :small, SubImage, allow_nil?: false
attribute :medium, SubImage, allow_nil?: false
attribute :large, SubImage, allow_nil?: false
attribute :original, SubImage, allow_nil?: false
end

graphql do
type :image
end

actions do
defaults [:update, :read, :destroy]

create :new, primary?: true
end
end
defmodule Marketplace.Markets.Property.SubImage do
@moduledoc false

use Ash.Resource,
data_layer: :embedded,
extensions: [AshGraphql.Resource]

code_interface do
define_for Marketplace.Markets

define :new
end

attributes do
attribute :url, :string, allow_nil?: false
attribute :type, :string, allow_nil?: false

attribute :s3_path, :string, allow_nil?: false, private?: true
end

graphql do
type :sub_image
end

actions do
defaults [:update, :read, :destroy]

create :new do
primary? true

argument :s3_path, :string, allow_nil?: false

change set_attribute(:s3_path, arg(:s3_path))
end
end
end
defmodule Marketplace.Markets.Property.SubImage do
@moduledoc false

use Ash.Resource,
data_layer: :embedded,
extensions: [AshGraphql.Resource]

code_interface do
define_for Marketplace.Markets

define :new
end

attributes do
attribute :url, :string, allow_nil?: false
attribute :type, :string, allow_nil?: false

attribute :s3_path, :string, allow_nil?: false, private?: true
end

graphql do
type :sub_image
end

actions do
defaults [:update, :read, :destroy]

create :new do
primary? true

argument :s3_path, :string, allow_nil?: false

change set_attribute(:s3_path, arg(:s3_path))
end
end
end
ZachDaniel
ZachDaniel2y ago
Are you validatitng the form on input? like phx-change="validate"?
Blibs
BlibsOP2y ago
Yes, but in this case I'm running it manually jsut to get the functions reply. Basically my in-code submit function looks more like this:
images = [
%MarketplaceWeb.Utils.FilesUpload.UploadedEntry{
uuid: "af64a8df-6d08-45ff-a268-1464a8d9fff8",
url: "http://localhost:4000/dev/static/s3/marketplace-properties-images-dev/db7c7c4a-2c99-40b8-9204-f9d6dad4dd91/af64a8df-6d08-45ff-a268-1464a8d9fff8/thumbnail.jpeg",
type: "image/jpeg"
}
]

removed_images = []

changeset_fn = fn changeset ->
changeset
|> Ash.Changeset.set_argument(:uploaded_images, images)
|> Ash.Changeset.set_argument(:removed_images, removed_images)
end

form = %{form | prepare_source: changeset_fn}

params = maybe_process_params(form.params)

AshPhoenix.Form.submit(form, params: params)
images = [
%MarketplaceWeb.Utils.FilesUpload.UploadedEntry{
uuid: "af64a8df-6d08-45ff-a268-1464a8d9fff8",
url: "http://localhost:4000/dev/static/s3/marketplace-properties-images-dev/db7c7c4a-2c99-40b8-9204-f9d6dad4dd91/af64a8df-6d08-45ff-a268-1464a8d9fff8/thumbnail.jpeg",
type: "image/jpeg"
}
]

removed_images = []

changeset_fn = fn changeset ->
changeset
|> Ash.Changeset.set_argument(:uploaded_images, images)
|> Ash.Changeset.set_argument(:removed_images, removed_images)
end

form = %{form | prepare_source: changeset_fn}

params = maybe_process_params(form.params)

AshPhoenix.Form.submit(form, params: params)
This will give the same errors as shown above
ZachDaniel
ZachDaniel2y ago
🤔 something seems strange there you're providing the images manually? what does params look like aftter your maybe_process_params/1 call?
Blibs
BlibsOP2y ago
maybe_process_params is just to "change" some fields, but it is not related to the images resource. Yes, The :uploaded_images and :removed_images are arguments that I use in a change to create a oban job to process the images before actually inserting them in the db (adding watermark, etc), so it is not actually related to the :images field of the property in this stage. The images from the form that are returning errors are actually images that are alraedy in the DB, what I don't get is why they are failing since we are just updating the resource without actually making any change to the images at all I will see if I can make the scope smaller just to try to make it easier to understand
ZachDaniel
ZachDaniel2y ago
The question stands about what the params look like that are being passed in
Blibs
BlibsOP2y ago
I pass the form.params to it, it would be an empty map in this case %{}
ZachDaniel
ZachDaniel2y ago
You've confirmed that for sure? Just want to make sure all our ducks are in a row 🙂 If you could potentially make a test reproduction in the ash_phoenix tests and/or push something up that I can pull down to reproduce then I could take a look
Blibs
BlibsOP2y ago
I will try to create a unit test, but here is a small examply that can reproduce the issue:
defmodule Marketplace.Markets.Property2.SubImage do
use Ash.Resource, data_layer: :embedded

attributes do
attribute :url, :string, allow_nil?: false
end

actions do
defaults [:create, :update, :read]
end
end


defmodule Marketplace.Markets.Property2.Image do
alias Marketplace.Markets.Property2.SubImage

use Ash.Resource, data_layer: :embedded

attributes do
attribute :uuid, :uuid, allow_nil?: false

attribute :thumbnail, SubImage, allow_nil?: false
end

actions do
defaults [:create, :update, :read]
end
end

defmodule Marketplace.Markets.Property2 do
@moduledoc false

alias Marketplace.Markets.Property2.Image

use Ash.Resource, data_layer: Ash.DataLayer.Ets

code_interface do
define_for Marketplace.Markets

define :create
end

attributes do
uuid_primary_key :id

attribute :images, {:array, Image}, default: []
end

actions do
defaults [:create, :update, :read]
end
end
defmodule Marketplace.Markets.Property2.SubImage do
use Ash.Resource, data_layer: :embedded

attributes do
attribute :url, :string, allow_nil?: false
end

actions do
defaults [:create, :update, :read]
end
end


defmodule Marketplace.Markets.Property2.Image do
alias Marketplace.Markets.Property2.SubImage

use Ash.Resource, data_layer: :embedded

attributes do
attribute :uuid, :uuid, allow_nil?: false

attribute :thumbnail, SubImage, allow_nil?: false
end

actions do
defaults [:create, :update, :read]
end
end

defmodule Marketplace.Markets.Property2 do
@moduledoc false

alias Marketplace.Markets.Property2.Image

use Ash.Resource, data_layer: Ash.DataLayer.Ets

code_interface do
define_for Marketplace.Markets

define :create
end

attributes do
uuid_primary_key :id

attribute :images, {:array, Image}, default: []
end

actions do
defaults [:create, :update, :read]
end
end
And then run:
alias Marketplace.{Markets, Markets.Property2}

images = [
%{uuid: Ecto.UUID.generate(), thumbnail: %{url: "some_url"}},
%{uuid: Ecto.UUID.generate(), thumbnail: %{url: "some_url"}}
]

{:ok, property} = Property2.create(%{images: images})

form = AshPhoenix.Form.for_update(property, :update, api: Markets, forms: [auto?: true])

# This will fail
AshPhoenix.Form.submit(form, params: %{})
alias Marketplace.{Markets, Markets.Property2}

images = [
%{uuid: Ecto.UUID.generate(), thumbnail: %{url: "some_url"}},
%{uuid: Ecto.UUID.generate(), thumbnail: %{url: "some_url"}}
]

{:ok, property} = Property2.create(%{images: images})

form = AshPhoenix.Form.for_update(property, :update, api: Markets, forms: [auto?: true])

# This will fail
AshPhoenix.Form.submit(form, params: %{})
So, @Zach Daniel I created a unit test for it in AshPhoenix, but weirdly enough I can't reproduce it running from it, only inside my project, even thought the code and libraries versions are exactly the same I will try to move it into a separated small project and see if I can reproduce it there
ZachDaniel
ZachDaniel2y ago
🤔 strange
Blibs
BlibsOP2y ago
The only thing that I can think that is different is that I have more ash libraries in my project, ash_authentication_phoenix, ash_graphql, etc. But I'm not using them in these resources, so I'm not sure if they somehow affect AshPhoenix.Form in some way
ZachDaniel
ZachDaniel2y ago
As far as I know, they shouldn't perhaps its fixed in the main branch of ash_phoenix or something?
Blibs
BlibsOP2y ago
You mean in my project? I looked into mix.lock and it is getting the latest version I will try to move it to another standalone project right now and see if I can make it trigger, hopefully I will be able to do that and give you a link to the project at least 😅 All right! I was able to create a small project that reproduces the issue https://github.com/sezaru/ash_phoenix_embedded_test @Zach Daniel when you have some free time can you try it out? Basically you just need to open iex and run:
# This will create the resource, create the form for update and run submit on it, it will return an error with the form:
{:error, form} = AshPhoenixEmbeddedTest.Run.run()

# Now if you run this:
AshPhoenix.Form.errors(form, for_path: :all)

# You should see this:
# %{
# [:images, 0] => [url: "is required"],
# [:images, 1] => [uuid: "is required"],
# [:images, 1, :thumbnail] => [url: "is required"]
# }
# This will create the resource, create the form for update and run submit on it, it will return an error with the form:
{:error, form} = AshPhoenixEmbeddedTest.Run.run()

# Now if you run this:
AshPhoenix.Form.errors(form, for_path: :all)

# You should see this:
# %{
# [:images, 0] => [url: "is required"],
# [:images, 1] => [uuid: "is required"],
# [:images, 1, :thumbnail] => [url: "is required"]
# }
ZachDaniel
ZachDaniel2y ago
Definitely something strange here... I feel like if updating embeds didn't work that I'd have heard about it before now 😆 but the reproduction is pretty clear
Blibs
BlibsOP2y ago
maybe it only doesn't work on arrays? not sure
ZachDaniel
ZachDaniel2y ago
found it 🙂
Blibs
BlibsOP2y ago
Damn that was fast!
ZachDaniel
ZachDaniel2y ago
when I have a reproduction, I can always fix things very fast ❤️ okay, pushed to main
Blibs
BlibsOP2y ago
Can confirm, that fixed it! Not sure why I couldn't trigger it when I run the same code but as a ash_phoenix unit test 🤔
ZachDaniel
ZachDaniel2y ago
¯\_(ツ)_/¯

Did you find this page helpful?