Nested form error - using create when I (think I) want update?

axdc
2023-04-23

axdc:

I have a nested form that has been working wonderfully to edit a Site and its related resources, and now I’m attempting to manage the Users who will be permissioned to use the Site as well.

When I click to do my add_form for Users, I get a crash with:

[error] GenServer #PID<0.25850.0> terminating
** (AshPhoenix.Form.NoActionConfigured) Attempted to add a form at path: [:edit_users], but no `create_action` was configured.

The relationship is many_to_many:

many_to_many :users, MyApp.Accounts.User do
      api MyApp.Accounts
      through MyApp.Sites.SitesUsers
      source_attribute :id
      source_attribute_on_join_resource :site_id
      destination_attribute :id
      destination_attribute_on_join_resource :user_id
    end

In my action I have:

change manage_relationship(:edit_users, :users,
               on_lookup: :relate,
               on_no_match: :ignore,
               on_missing: :unrelate,
               on_match: :ignore
             )

Because you should only be able to select from the existing users in a select widget and attach them to the Site, not create new users from this form (they require passwords and all that, they have their own separate form for creating). Similar to the tags example I found here, except without creating new ones.

But it looks (based on my current understanding of the error) like the form helper is expecting there to be a create action involved. Am I misunderstanding some component of the system? The form is made like this:

form =
      AshPhoenix.Form.for_update(site, :update_existing_site,
        api: MyApp.Sites,
        forms: [auto?: true],
        actor: socket.assigns.current_user
      )
      |> to_form

Perhaps forms: auto? is getting confused somehow? Have I incompletely specified my resources so as to guide it?

ZachDaniel:

Can I see how you are adding the form?

ZachDaniel:

Like your call to AshPhoenix.Form.add_form

axdc:

def handle_event("add_form", %{"path" => path}, socket) do
  form = AshPhoenix.Form.add_form(socket.assigns.form, path)
  {:noreply, assign(socket, form: form)}
end

axdc:

The path used is "form[edit_users]"

ZachDaniel:

Try this: form = AshPhoenix.Form.add_form(socket.assigns.form, path, data: the_user_you_want_to_edit, type: :update)

axdc:

okay so the users are actually getting added from a livecomponent message so that looks like this

  @impl true
  def handle_info({MyAppWeb.TypeaheadComponent, {:typeahead_selection, email}}, socket) do
    form =
      AshPhoenix.Form.add_form(socket.assigns.form, "form[edit_users]",
        params: %{"email" => email}
      )

    socket = socket |> assign(form: form)

    {:noreply, socket}
  end

axdc:

adding type: :update to that after params changes the error to:

[error] GenServer #PID<0.1878.0> terminating
** (AshPhoenix.Form.NoActionConfigured) The `data` key was configured for [:edit_users], but no `update_action` was configured. Please configure one.

axdc:

Should this all be pointing at the join resource instead of at the user?

ZachDaniel:

oh, so are the users just getting added like “connected” to the form?

ZachDaniel:

You can try type: :read

axdc:

The users should be related if they already exist, and I suppose there should be an error of some sort if someone somehow manages to attach a user to the form that doesn’t already exist (they’re all in a select widget of preexisting users). This form updates Site resources like Domains and Configuration, but the Users section is purely for relating already existing ones.

ZachDaniel:

Then yeah type: :read should be what you want 🙂

axdc:

Ok, yes, it appears that type: :read allows the form to be successfully added to the page, unlike before, nice. I’m not fully understanding why a read type action is used here, is there a section in the guide that might explain that? I know that reads sometimes have to be used in interesting ways until bulk actions are supported, right? Something similar?

Now it’s saying

[warning] Unhandled error in form submission for MyApp.Accounts.User.read

This error was unhandled because it did not implement the `AshPhoenix.FormData.Error` protocol.

** (Ash.Error.Invalid.NoSuchResource) No such resource MyApp.Sites.SitesUsers

MyApp.Sites.SitesUsers exists, is in the registry, etc, so troubleshooting that now..

ZachDaniel:

🤔

ZachDaniel:

The reason for using read is because the form itself is not to modify a resource, but to look one up

ZachDaniel:

That error is strange if the resource is definitely in the registry. Could you have passed in the wrong value for api when creating the form?

axdc:

Sites are in one api, Users are in another. Could that be causing trouble?

ZachDaniel:

Potentially, yeah

ZachDaniel:

In your relationship, if you cross api boundaries, you need to set the api option

ZachDaniel:

i.e

belongs_to :user, MyApp.Accounts.User do
  api MyApp.Accounts
end

ZachDaniel:

In your registry, do you have the resource validations extension?

use Ash.Registry, extensions: [Ash.Registry.ResourceValidations]

axdc:

relationships do
    # https://discord.com/channels/711271361523351632/1074712810505908254

    many_to_many :users, MyApp.Accounts.User do
      api MyApp.Accounts #<- like this?
      through MyApp.Sites.SitesUsers
      source_attribute :id
      source_attribute_on_join_resource :site_id
      destination_attribute :id
      destination_attribute_on_join_resource :user_id
    end
...
end

axdc:

Yes, both registries have that extension at the top

ZachDaniel:

oh, this is interesting

ZachDaniel:

its likely because of the through relationship

axdc:

have I munted the relationship :<

ZachDaniel:

Nah, I don’t think so

ZachDaniel:

I think this is actually an oversight

ZachDaniel:

try this:

  has_many :sites_users, MyApp.Accounts.SiteUsers do
    ...
  end

  many_to_many :users, MyApp.Accounts.User do
    api MyApp.Accounts #<- like this?
    through MyApp.Sites.SitesUsers
    join_relationship :sites_users
    source_attribute :id
    source_attribute_on_join_resource :site_id
    destination_attribute :id
    destination_attribute_on_join_resource :user_id
  end

ZachDaniel:

I’m not sure that will work. I think the basic issue is that we’re assuming that both the join relationship and the destination are in the same api

ZachDaniel:

yeah that probably won’t work. Somewhere where we are managing relationships we are making a bad assumption internally. Need to figure out how to handle this properly. You might be the first person who made a many_to_many across api boundaries

ZachDaniel:

😆

ZachDaniel:

okay, so it might work in the interim if you do the example above but explicitly setting the api to the parent api

ZachDaniel:

  has_many :sites_users, MyApp.Accounts.SiteUsers do
    api MyApp.Sites
  end

  many_to_many :users, MyApp.Accounts.User do
    api MyApp.Accounts
    through MyApp.Sites.SitesUsers
    join_relationship :sites_users
    source_attribute :id
    source_attribute_on_join_resource :site_id
    destination_attribute :id
    destination_attribute_on_join_resource :user_id
  end

ZachDaniel:

I’m making a potential fix to ash core that will have the join relationship check either the destination api or the source api for the resource in question

ZachDaniel:

if its not explicitly configured

ZachDaniel:

okay, I’m actually on vacation at the moment so I don’t have much time to dedicate to it, but I believe I’ve just fixed the issue in mind, if you wouldn’t mind trying the main branch of ash

axdc:

{:ash, github: "ash-project/ash", branch: "main", override: true},

delete build folder, mix clean --all , mix deps.clean --all , mix deps.get , mix compile

Same error on form submit

[warning] Unhandled error in form submission for Panacea.Accounts.User.read

This error was unhandled because it did not implement the `AshPhoenix.FormData.Error` protocol.

** (Ash.Error.Invalid.NoSuchResource) No such resource Panacea.Sites.SitesUsers

I don’t get that error if I do

has_many :sites_users, MyApp.Sites.SitesUsers do
  api MyApp.Sites
end

But the form does not redirect and no data is written. I don’t see any errors when IO.inspecting the form at this point

ZachDaniel:

When you inspect errors how are you doing it?

axdc:

def handle_event("save", %{"form" => form}, socket) do
    form = AshPhoenix.Form.validate(socket.assigns.form, form)

    case AshPhoenix.Form.submit(form) do
      {:ok, site} ->
        {:noreply,
         socket
         |> put_flash(:info, "Site updated successfully")
         |> push_navigate(to: ~p"/commander/sites/#{site}")}

      {:error, form} ->
        IO.inspect(form) # <- here
        {:noreply, assign(socket, form: form)}
    end
  end

ZachDaniel:

try IO.inspect(AshPhoenix.Form.errors(form, for_path: :all)

axdc:

%{}

axdc:

I /believe/ I’ve got this right according to the ash-hq docs, but neither specifying :all nor specifying the specific form path reveals anything: https://gitlab.com/avoh-labs/panacea/-/blob/main/lib/panacea_web/live/commander/sites_live/edit.ex#L87

That means there are no validation errors in the form itself, right? Just the existing No Such Resource warning when it attempts to save, which seems to have something to do with acting across api boundaries

ZachDaniel:

Oh so you’re still getting that issue?

ZachDaniel:

[warning] Unhandled error in form submission for Panacea.Accounts.User.read

That one?

axdc:

Yes, despite being on ash main

ZachDaniel:

Okay, does the warning include a stack trace?

ZachDaniel:

Does everything work if you manually specify the api, like you did here? https://discord.com/channels/711271361523351632/1099572167638794290/1100330595042734170

axdc:

All I get are a ton of teal debug messages for the ash/ecto transactions, and then on form submit the yellow [warning]

[debug] QUERY OK db=0.2ms
rollback []
↳ anonymous fn/3 in Ash.Changeset.with_hooks/3, at: lib/ash/changeset/changeset.ex:1615
[warning] Unhandled error in form submission for Panacea.Accounts.User.read

This error was unhandled because it did not implement the `AshPhoenix.FormData.Error` protocol.

** (Ash.Error.Invalid.NoSuchResource) No such resource Panacea.Sites.SitesUsers

%{}
[debug] Replied in 46ms

The page does not redirect or crash

axdc:

I’m currently manually specifying the api like that in the Site resource: https://gitlab.com/avoh-labs/panacea/-/blob/main/lib/panacea/sites/resources/site.ex#L136

ZachDaniel:

Okay. I’ll be back at my laptop in a bit and will build a proper reproduction and finish this once and for all 🙂

axdc:

🙇 please let me know if there’s anything I can provide to track down what’s going on, thank you!!

ZachDaniel:

just to confirm SitesUsers is part of the Sites registry

ZachDaniel:

you didn’t move it or anything when debugging?

axdc:

I have not moved it since its creation, it has always been under the Sites api

axdc:

the Accounts api is pretty much wholesale the one from the auth documentation, then I tried to tie it into Sites with a cross-api many-to-many following the tags examples i found in this discord, and the join relationship is specified under Sites

ZachDaniel:

yeah, okay now that I’m at my laptop, this is all much clearer

ZachDaniel:

I have a plan that will simplify all of this 🙂

ZachDaniel:

okay, try ash main 🙂

ZachDaniel:

What it should do is give you compile time error messages if its misconfigured, and in your case should tell you to define the join relationship (although I think you already have, so this might just fix the issue)

axdc:

compiling 🙂

axdc:

I’m still seeing the same message on submit if I add a user:

[debug] QUERY OK db=0.7ms
rollback []
↳ anonymous fn/3 in Ash.Changeset.with_hooks/3, at: lib/ash/changeset/changeset.ex:1615
[warning] Unhandled error in form submission for Panacea.Accounts.User.read

This error was unhandled because it did not implement the `AshPhoenix.FormData.Error` protocol.

** (Ash.Error.Invalid.NoSuchResource) No such resource Panacea.Sites.SitesUsers

%{}
[debug] Replied in 143ms

I deleted the build directory, deleted dependencies, recompiled, it says it pulled ash

 mix deps.get        
* Getting ash (https://github.com/ash-project/ash.git - origin/main)
remote: Enumerating objects: 24290, done.        
remote: Counting objects: 100% (2038/2038), done.        
remote: Compressing objects: 100% (990/990), done.        
remote: Total 24290 (delta 1077), reused 1967 (delta 1035), pack-reused 22252        
Resolving Hex dependencies...
Resolution completed in 0.774s

axdc:

Your commit looks like documentation changed as well so I’m reading back through that trying to see what I have set up incorrectly

ZachDaniel:

Did you do mix deps.update ash to update the lock to the latest commit?

ZachDaniel:

That’s the only way to make it use the latest version of a git dependency

axdc:

COMPILE ERROR OKAY

axdc:

i didn’t realize there was any version tracking going on under the hood beyond specifying origin/main 😂 i’ll remember that.

axdc:

beautiful error! working through this now

Compiling 20 files (.ex)
** (EXIT from #PID<0.95.0>) an exception was raised:
    ** (RuntimeError) Resource `Panacea.Sites.SitesUsers` is not accepted by api `Panacea.Accounts` for autogenerated join relationship: `:users_join_assoc`

Relationship was generated by the `many_to_many` relationship `:users_join_assoc`

If the `through` resource `Panacea.Sites.SitesUsers` is not accepted by the same
api as the destination resource `Panacea.Sites.SitesUsers`,
then you must define that relationship manually. To define it manually, add the following to your
relationships:

    has_many :users_join_assoc, Panacea.Sites.SitesUsers do
      # configure the relationship attributes
      ...
    end

You can use a name other than `:users_join_assoc`, but if you do, make sure to
add that to `:users_join_assoc`, i.e

    many_to_many :users_join_assoc, Panacea.Sites.SitesUsers do
      ...
      join_relationship_name :your_new_name
    end

ZachDaniel:

ah, I messed up the error message

ZachDaniel:

That should say many_to_many relationship :the_many_to_many_name

axdc:

I’m not sure what it’s saying, in the error the the through resource is the same as the destination resource?

axdc:

or because the join resource is not accepted by the accounts api, do I have to define something under the accounts api to make it accept it?

ZachDaniel:

How are you defining the join relationship currently?

ZachDaniel:

Do you have a has_many relationship set up for the many_to_many ?

ZachDaniel:

or are you letting it do it automatically?

axdc:

Something wasn’t working with the relationship initially, so I added that join resource based on some examples I found here to do with posts and tags, i think I have the link as a comment in there still. My understanding was that it’s supposed to kind of automatically intuit what’s going on but in my case it needed an explicit join resource

ZachDaniel:

got it

ZachDaniel:

So you haven’t actually connected the relationships

ZachDaniel:

    has_many :sites_users, Panacea.Sites.SitesUsers do
      api Panacea.Sites
    end

    # https://discord.com/channels/711271361523351632/1074712810505908254

    many_to_many :users, Panacea.Accounts.User do
      api(Panacea.Accounts)
      through(Panacea.Sites.SitesUsers)
      source_attribute(:id)
      source_attribute_on_join_resource(:site_id)
      destination_attribute(:id)
      destination_attribute_on_join_resource(:user_id)
    end

ZachDaniel:

you want to add join_relationship :sites_users

axdc:

got it 🤦 With that addition I can successfully persist users to the database and they show up from queries in iex. just gotta figure out what I need to do to show the existing ones in the form

ZachDaniel:

You probably need to load the relationship on the data

ZachDaniel:

before creating your form

axdc:

It’s being loaded along with the other working resources in handle_params on the edit action. Am I right to suspect the form might need some manual work?

edit.ex

 @impl true
  def handle_params(%{"id" => id}, _, socket) do
    site =
      Site.get_by_id!(id, actor: socket.assigns.current_user)
      |> Panacea.Sites.load!([:domains, :configuration, :profiles, :users])

    form =
      AshPhoenix.Form.for_update(site, :update_existing_site,
        api: Panacea.Sites,
        forms: [auto?: true],
        actor: socket.assigns.current_user
      )
      |> to_form

    all_users_options =
      for user <- Panacea.Accounts.User.read_all!(actor: socket.assigns.current_user) do
        %{
          name: Ash.CiString.to_comparable_string(user.email),
          value: Ash.CiString.to_comparable_string(user.email)
        }
      end

    socket =
      socket
      |> apply_title(socket.assigns.live_action)
      |> assign(
        :site,
        site
      )
      |> assign(form: form)
      |> assign(all_users_options: all_users_options)

    {:noreply, socket}
  end

ZachDaniel:

I don’t think so

ZachDaniel:

You’re using “inputs for” to loop over each nested form right?

axdc:

Yes, inputs_for here: https://gitlab.com/avoh-labs/panacea/-/blob/main/lib/panacea_web/live/commander/sites_live/edit.html.heex#L23 Loading here: https://gitlab.com/avoh-labs/panacea/-/blob/main/lib/panacea_web/live/commander/sites_live/edit.html.heex#L23

Loading with the same code works in iex but not in show.ex or edit.ex, IO.inspect ing there shows a blank users list. Could it be a policies thing?

ZachDaniel:

yes, it definitely could be 🙂

ZachDaniel:

Try passing authorize?: false when loading

ZachDaniel:

if you get the full list then its a policies thing

axdc:

Yes that was it! confirmed policies issue