AshPhoenix nested form: How to make the id of parent form resourse available to the child form

I have a nested form involving resources Task which has_many TaskMembers. Since I’m passing [%TaskMember{}] as data, the first TaskMember form type shows action_type :update (and passes validation when submitted alone), whereas from the 2nd TaskMember form and thereafter, the type becomes :create, and somehow this one causes a submit_error like the following. I believe the error is referring to the task_id required per the relationship definition in TaskMember as shown below.
I thought manage_relationship in Task’s create action will handle the passing of task id to TaskMember, but it doesn’t seem to be true in this case. What could I be missing? Thanks in advance.

    errors: [
            %Ash.Error.Changes.Required{
              field: :task_id,
              type: :attribute,
              resource: MyApp.Accounts.TaskMember,
              changeset: nil,
              query: nil,
              error_context: [],
              vars: [],
              path: [],
              stacktrace: #Stacktrace<>,
              class: :invalid
            }
          ],
 relationships do
    belongs_to :task, MyApp.Tasks.Task do
      api MyApp.Tasks
      allow_nil? false
      attribute_writable? true
    end

Live.ex

 form =
      AshPhoenix.Form.for_create(Task, :create,
        api: MyApp.Tasks,
        actor: socket.assigns.current_user,
        forms: [
          auto?: true,
          task_members: [
            resource: TaskMember,
            data: [%TaskMember{}],
            create_action: :create,
            update_action: :update,
            type: :list,
            actor: socket.assigns.current_user
          ]

  def handle_event("add_task_member", _params, socket) do
    form = AshPhoenix.Form.add_form(socket.assigns.form, :task_members)

    task_member_list = form.forms.task_members

    new_form = %{
      socket.assigns.form
      | forms: %{socket.assigns.form.forms | task_members: task_member_list}
    }

    {:noreply,
     socket
     |> assign(form: new_form)
  end

task.ex

 create :create do
  ...
     argument :task_members, {:array, :map}
      change manage_relationship(:task_members, type: :create)
    end

This is something that ash_phoenix ought to be handling for you. Let me take a look.

EDIT: in the meantime, please update ash/ash_phoenix if you aren’t on the latest, just to rule it out as something that has been resolved.

This is interesting, as far as I can tell the code is in place to not surface this error. Let me know if updating dependencies doesn’t help. The next step would be getting a reproducible test case. If your project is public and/or you can push up a failing test that I can run in some way, that would be great. The basic gist of why this should work is that, when managing related forms, AshPhoenix sets some context called accessing_from, which Ash.Changeset uses to know that it should not consider the belongs_to attribute “missing” f

Thank you! Updating AshPhoenix from 1.2.23 to 1.2.24 didn’t make a difference. Let me try to put together a test case that replicates the problem. In the meantime, do you think the fact that Task and TaskMember are under different APIs (ie. Tasks vs. Accounts.) can make a difference?

Task has_many Tasks.Comments, and I noticed that one does not have the same validation issue.

While putting together a simple project to replicate the issue, I hit a roadblock. Repo.on_transaction_begin/1 is undefined or private
I see it fails because the argument doesn’t match the type signature of transaction_reason due to the extra actor key in the map or the absence of record , but am not sure what causes it.

%{type: :create, metadata: %{resource: AshAddFormTest.Tasks.Task, action: :create, actor: nil}

The test project is in this public repo.


** (Ash.Error.Unknown) Unknown Error

Context: resolving data on perform AshAddFormTest.Tasks.Task.create
* Context: resolving data on perform AshAddFormTest.Tasks.Task.create

** (UndefinedFunctionError) function AshAddFormTest.Repo.on_transaction_begin/1 is undefined or private
  (ash_add_form_test 0.1.0) AshAddFormTest.Repo.on_transaction_begin(%{type: :create, metadata: %{resource: AshAddFormTest.Tasks.Task, action: :create, actor: nil}, data_layer_context: %{}})
  (ash_postgres 1.3.64) lib/data_layer.ex:2646: anonymous fn/3 in AshPostgres.DataLayer.transaction/4
  (ecto_sql 3.11.1) lib/ecto/adapters/sql.ex:1358: anonymous fn/3 in Ecto.Adapters.SQL.checkout_or_transaction/4
  (db_connection 2.6.0) lib/db_connection.ex:1710: DBConnection.run_transaction/4
  (ash 2.17.12) lib/ash/changeset/changeset.ex:1807: anonymous fn/3 in Ash.Changeset.with_hooks/3
  (ash 2.17.12) lib/ash/changeset/changeset.ex:1939: anonymous fn/2 in Ash.Changeset.transaction_hooks/2
  (ash 2.17.12) lib/ash/changeset/changeset.ex:1788: Ash.Changeset.with_hooks/3
  (ash 2.17.12) lib/ash/actions/create/create.ex:356: anonymous fn/11 in Ash.Actions.Create.as_requests/5
  (ash 2.17.12) lib/ash/engine/request.ex:1135: Ash.Engine.Request.do_try_resolve_local/4
  (ash 2.17.12) lib/ash/engine/request.ex:284: Ash.Engine.Request.do_next/1
  (ash 2.17.12) lib/ash/engine/request.ex:213: Ash.Engine.Request.next/1
  (ash 2.17.12) lib/ash/engine/engine.ex:712: Ash.Engine.advance_request/2
  (ash 2.17.12) lib/ash/engine/engine.ex:637: Ash.Engine.fully_advance_request/2
  (ash 2.17.12) lib/ash/engine/engine.ex:578: Ash.Engine.do_run_iteration/2
  (elixir 1.15.5) lib/enum.ex:2510: Enum."-reduce/3-lists^foldl/2-0-"/3
  (ash 2.17.12) lib/ash/engine/engine.ex:307: Ash.Engine.run_to_completion/1
  (ash 2.17.12) lib/ash/engine/engine.ex:252: Ash.Engine.do_run/2
  (ash 2.17.12) lib/ash/engine/engine.ex:148: Ash.Engine.run/2
  (ash 2.17.12) lib/ash/actions/create/create.ex:123: Ash.Actions.Create.do_run/4
  (ash 2.17.12) lib/ash/actions/create/create.ex:46: Ash.Actions.Create.run/4
    (ash_add_form_test 0.1.0) AshAddFormTest.Repo.on_transaction_begin(%{type: :create, metadata: %{resource: AshAddFormTest.Tasks.Task, action: :create, actor: nil}, data_layer_context: %{}})
    (ash_postgres 1.3.64) lib/data_layer.ex:2646: anonymous fn/3 in AshPostgres.DataLayer.transaction/4
    (ecto_sql 3.11.1) lib/ecto/adapters/sql.ex:1358: anonymous fn/3 in Ecto.Adapters.SQL.checkout_or_transaction/4
    (db_connection 2.6.0) lib/db_connection.ex:1710: DBConnection.run_transaction/4
    (ash 2.17.12) lib/ash/changeset/changeset.ex:1807: anonymous fn/3 in Ash.Changeset.with_hooks/3
    (ash 2.17.12) lib/ash/changeset/changeset.ex:1939: anonymous fn/2 in Ash.Changeset.transaction_hooks/2
    (ash 2.17.12) lib/ash/changeset/changeset.ex:1788: Ash.Changeset.with_hooks/3
    (ash 2.17.12) lib/ash/actions/create/create.ex:356: anonymous fn/11 in Ash.Actions.Create.as_requests/5
    (ash 2.17.12) lib/ash/engine/request.ex:1135: Ash.Engine.Request.do_try_resolve_local/4
    (ash 2.17.12) lib/ash/engine/request.ex:284: Ash.Engine.Request.do_next/1
    (ash 2.17.12) lib/ash/engine/request.ex:213: Ash.Engine.Request.next/1
    (ash 2.17.12) lib/ash/engine/engine.ex:712: Ash.Engine.advance_request/2
    (ash 2.17.12) lib/ash/engine/engine.ex:637: Ash.Engine.fully_advance_request/2
    (ash 2.17.12) lib/ash/engine/engine.ex:578: Ash.Engine.do_run_iteration/2
    (elixir 1.15.5) lib/enum.ex:2510: Enum."-reduce/3-lists^foldl/2-0-"/3
    (ash 2.17.12) lib/ash/engine/engine.ex:307: Ash.Engine.run_to_completion/1
    (ash 2.17.12) lib/ash/engine/engine.ex:252: Ash.Engine.do_run/2
    (ash 2.17.12) lib/ash/engine/engine.ex:148: Ash.Engine.run/2
    (ash 2.17.12) lib/ash/actions/create/create.ex:123: Ash.Actions.Create.do_run/4
    iex:2: (file)

In your repo, replace

  use Ecto.Repo,
    otp_app: :ash_add_form_test,
    adapter: Ecto.Adapters.Postgres

with

  use AshPostgres.Repo,
    otp_app: :ash_add_form_test
1 Like

Okay. I was able to reproduce the problem using the above repo.
submit_errors: [task_id: {"is required", []}],

Here is the full output I received upon form submission.


save output =#################: {:error,
 #AshPhoenix.Form<
   resource: AshAddFormTest.Tasks.Task,
   action: :create,
   type: :create,
   params: %{
     "task_members" => %{
       "0" => %{"email" => "larry@email.com", "member_type" => "1"},
       "1" => %{"email" => "Team@email.com", "member_type" => "1"}
     },
     "task_name" => "Test"
   },
   source: #Ash.Changeset<
     action_type: :create,
     action: :create,
     attributes: %{task_name: "Test"},
     relationships: %{
       task_members: [
         {[
            %{"email" => "larry@email.com", "member_type" => "1"},
            %{"email" => "Team@email.com", "member_type" => "1"}
          ],
          [
            ignore?: false,
            on_missing: :ignore,
            on_match: :ignore,
            on_lookup: :ignore,
            on_no_match: :create,
            eager_validate_with: false,
            authorize?: true,
            meta: [inputs_was_list?: true, id: :task_members],
            type: :create
          ]}
       ]
     },
     arguments: %{
       task_members: [
         %{"email" => "larry@email.com", "member_type" => "1"},
         %{"email" => "Team@email.com", "member_type" => "1"}
       ]
     },
     errors: [],
     data: #AshAddFormTest.Tasks.Task<
       comments: #Ash.NotLoaded<:relationship>,
       task_members: #Ash.NotLoaded<:relationship>,
       __meta__: #Ecto.Schema.Metadata<:built, "tasks">,
       id: nil,
       task_name: nil,
       inserted_at: nil,
       updated_at: nil,
       aggregates: %{},
       calculations: %{},
       ...
     >,
     valid?: true
   >,
   name: "form",
   data: nil,
   form_keys: [
     task_members: [
       resource: AshAddFormTest.Accounts.TaskMember,
       data: [
         #AshAddFormTest.Accounts.TaskMember<
           task: #Ash.NotLoaded<:relationship>,
           __meta__: #Ecto.Schema.Metadata<:built, "task_members">,
           id: nil,
           member_type: nil,
           row_order: nil,
           email: nil,
           inserted_at: nil,
           updated_at: nil,
           task_id: nil,
           aggregates: %{},
           calculations: %{},
           ...
         >
       ],
       create_action: :create,
       update_action: :update,
       type: :list
     ]
   ],
   forms: %{
     task_members: [
       #AshPhoenix.Form<
         resource: AshAddFormTest.Accounts.TaskMember,
         action: :update,
         type: :update,
         params: %{"email" => "larry@email.com", "member_type" => "1"},
         source: #Ash.Changeset<
           action_type: :update,
           action: :update,
           attributes: %{member_type: 1, email: "larry@email.com"},
           relationships: %{},
           errors: [],
           data: #AshAddFormTest.Accounts.TaskMember<
             task: #Ash.NotLoaded<:relationship>,
             __meta__: #Ecto.Schema.Metadata<:built, "task_members">,
             id: nil,
             member_type: nil,
             row_order: nil,
             email: nil,
             inserted_at: nil,
             updated_at: nil,
             task_id: nil,
             aggregates: %{},
             calculations: %{},
             ...
           >,
           valid?: true
         >,
         name: "form[task_members][0]",
         data: #AshAddFormTest.Accounts.TaskMember<
           task: #Ash.NotLoaded<:relationship>,
           __meta__: #Ecto.Schema.Metadata<:built, "task_members">,
           id: nil,
           member_type: nil,
           row_order: nil,
           email: nil,
           inserted_at: nil,
           updated_at: nil,
           task_id: nil,
           aggregates: %{},
           calculations: %{},
           ...
         >,
         form_keys: [],
         forms: %{},
         api: nil,
         method: "put",
         submit_errors: [],
         id: "form_task_members_0",
         transform_errors: nil,
         original_data: #AshAddFormTest.Accounts.TaskMember<
           task: #Ash.NotLoaded<:relationship>,
           __meta__: #Ecto.Schema.Metadata<:built, "task_members">,
           id: nil,
           member_type: nil,
           row_order: nil,
           email: nil,
           inserted_at: nil,
           updated_at: nil,
           task_id: nil,
           aggregates: %{},
           calculations: %{},
           ...
         >,
         transform_params: nil,
         prepare_params: nil,
         prepare_source: nil,
         warn_on_unhandled_errors?: true,
         any_removed?: false,
         added?: false,
         changed?: true,
         touched_forms: MapSet.new([:row_order, "_form_type", "_touched",
          "email", "id", "member_type"]),
         valid?: true,
         errors: true,
         submitted_once?: true,
         just_submitted?: true,
         ...
       >,
       #AshPhoenix.Form<
         resource: AshAddFormTest.Accounts.TaskMember,
         action: :create,
         type: :create,
         params: %{"email" => "Team@email.com", "member_type" => "1"},
         source: #Ash.Changeset<
           action_type: :create,
           action: :create,
           attributes: %{member_type: 1, email: "Team@email.com", row_order: 0},
           relationships: %{},
           errors: [
             %Ash.Error.Changes.Required{
               field: :task_id,
               type: :attribute,
               resource: AshAddFormTest.Accounts.TaskMember,
               changeset: nil,
               query: nil,
               error_context: [],
               vars: [],
               path: [],
               stacktrace: #Stacktrace<>,
               class: :invalid
             }
           ],
           data: #AshAddFormTest.Accounts.TaskMember<
             task: #Ash.NotLoaded<:relationship>,
             __meta__: #Ecto.Schema.Metadata<:built, "task_members">,
             id: nil,
             member_type: nil,
             row_order: nil,
             email: nil,
             inserted_at: nil,
             updated_at: nil,
             task_id: nil,
             aggregates: %{},
             calculations: %{},
             ...
           >,
           valid?: false
         >,
         name: "form[task_members][1]",
         data: nil,
         form_keys: [],
         forms: %{},
         api: nil,
         method: "post",
         submit_errors: [task_id: {"is required", []}],
         id: "form_task_members_1",
         transform_errors: nil,
         original_data: nil,
         transform_params: nil,
         prepare_params: nil,
         prepare_source: nil,
         warn_on_unhandled_errors?: true,
         any_removed?: false,
         added?: true,
         changed?: true,
         touched_forms: MapSet.new([:row_order, "_form_type", "email",
          "member_type"]),
         valid?: false,
         errors: true,
         submitted_once?: true,
         just_submitted?: true,
         ...
       >
     ]
   },
   api: AshAddFormTest.Tasks,
   method: "post",
   submit_errors: [],
   id: "form",
   transform_errors: nil,
   original_data: nil,
   transform_params: nil,
   prepare_params: nil,
   prepare_source: nil,
   warn_on_unhandled_errors?: true,
   any_removed?: false,
   added?: false,
   changed?: true,
   touched_forms: MapSet.new(["task_members", "task_name"]),
   valid?: false,
   errors: true,
   submitted_once?: true,
   just_submitted?: true,
   ...
 >}

So the issue was caused by defining task_members on top of auto: true as in

 forms: [
          auto?: true,
          task_members: [
            resource: TaskMember,
            data: [%TaskMember{}],
            create_action: :create,
            update_action: :update,
            type: :list,
            actor: socket.assigns.current_user
          ]

Removing task_members: [..] fixed the issue.