Managing many to many relationships

I have a quiz. and a question.
and relation is.

quiz many_to_many question
question many_to_many quiz
via quiz_question

Now, I want to have an api where I can write Quiz.update_quiz_with_question_ids(existing_quiz,[ qid_1,qid_2,qid_3]) which ends up relating question_ids and quiz_ids.

What I tried:


    update :update_quiz_with_question_ids do
      accept []

      argument :question_ids, {:array, :uuid} do
        allow_nil? false
      end

    change manage_relationship(:questions, :question_ids, type: :direct_control)
    end

which leaves with the following error

{:error,
 %Ash.Error.Invalid{
   errors: [
     %Ash.Error.Query.Required{
       field: :question_id,
       type: :argument,
       resource: PyqRatta.Databank.Question,
       changeset: nil,
       query: nil,
       error_context: [],
       vars: [],
       path: [],
       stacktrace: #Stacktrace<>,
       class: :invalid
     }
   ],
...

So, I tried a little differently.

update :update_quiz_with_question_ids do
      accept []

      argument :question_ids, {:array, :uuid} do
        allow_nil? false
      end

    change manage_relationship(:questions, :question_ids,            
               on_match: :ignore,
               on_lookup: :relate,
               on_no_match: :relate,
               # since we are adding more questions to the quiz.
               on_missing: :ignore)
    end

and same error persists. Finally, did one more tweak.

defmodule Databank.Changes.QuestionsFromQuestionIds do
  @moduledoc """
  Fetches questions from database to udpate the relationship from questions to quizzes
  """
  use Ash.Resource.Change

  alias  Databank.Quiz
  alias Databank.Question
  alias  Databank.QuizQuestion

  def change(changeset, opts, context) do
    quiz_id = Ash.Changeset.get_data(changeset, :id)
    qids = Ash.Changeset.get_argument(changeset, :question_ids)

    questions =
      Enum.reduce(qids, [], fn qid, acc ->
        {:ok, question} = Question.read(qid)
        acc ++ [question]
      end)

   Ash.Changeset.force_set_argument(changeset, :question_ids, questions)
  
   
  end
end

along with


    update :update_quiz_with_question_ids do
      accept []

      argument :question_ids, {:array, :uuid} do
        allow_nil? false
      end

      change Databank.Changes.QuestionsFromQuestionIds

      change manage_relationship(:question_ids, :questions, type: :direct_control)
    end

the changeset is :

#Ash.Changeset<
     api: Databank,
     action_type: :update,
     action: :update_quiz_with_question_id,
     attributes: %{},
     relationships: %{
       questions: [
         {[%{id: "39f41254-f959-4f1d-96d4-30500d96d8d1"}],
          [
            ignore?: false,
            on_missing: :destroy,
            on_match: :update,
            on_lookup: :ignore,
            on_no_match: :create,
            eager_validate_with: false,
            authorize?: true,
            meta: [inputs_was_list?: false, id: :question_id],
            type: :direct_control
          ]}
       ]
     },
     arguments: %{question_id: "39f41254-f959-4f1d-96d4-30500d96d8d1"},
     errors: [
       %Ash.Error.Invalid{
         errors: [
           %Ash.Error.Query.Required{
             field: :question_id,
             type: :argument,
             resource: Databank.Question,
             changeset: nil,
             query: nil,
             error_context: [],
             vars: [],
             path: [],
             stacktrace: #Stacktrace<>,
             class: :invalid
           }
         ],
         stacktraces?: true,
         changeset: nil,
         query: #Ash.Query<
           resource: Databank.Quiz,
           load: [questions: []],
           errors: [
             %Ash.Error.Invalid{
               errors: [
                 %Ash.Error.Query.Required{
                   field: :question_id,
                   type: :argument,
                   resource: Databank.Question,
                   changeset: nil,
                   query: nil,
                   error_context: [],
                   vars: [],
                   path: [],
                   stacktrace: #Stacktrace<>,
                   class: :invalid
                 }
               ],
               stacktraces?: true,
               changeset: nil,
               query: #Ash.Query<
                 resource: Databank.Question,
                 filter: #Ash.Filter<id == nil>,
                 errors: [
                   %Ash.Error.Query.Required{
                     field: :question_id,
                     type: :argument,
                     resource: Databank.Question,
                     changeset: nil,
                     query: nil,
                     error_context: [],
                     vars: [],
                     path: [],
                     stacktrace: #Stacktrace<>,
                     class: :invalid
                   }
                 ],
                 select: [:id, :question_text, :question_image, :type,
                  :correct_answer_text, :correct_answer_image,
                  :explanation_text, :explanation_image, :short_description,
                  :long_description, :year, :tags, :created_at, :updated_at]
               >,
               error_context: [],
               vars: [],
               path: [],
               stacktrace: #Stacktrace<>,
               class: :invalid
             },
             %Ash.Error.Query.Required{
               field: :question_id,
               type: :argument,
               resource: Databank.Question,
               changeset: nil,
               query: nil,
               error_context: [],
               vars: [],
               path: [],
               stacktrace: #Stacktrace<>,
               class: :invalid
             }
           ]
         >,
         error_context: [nil],
         vars: [],
         path: [],
         stacktrace: #Stacktrace<>,
         class: :invalid
       },
...

The reasoning behind this is : if manage_relationship can alter the fields via given relationship key. But that doesn’t appear to be the case for many to many

TL;DR

create an Api where
relation is.

quiz many_to_many question
question many_to_many quiz
via quiz_question

Quiz.update_quiz_with_question_ids(existing_quiz,[ qid_1,qid_2,qid_3]) which ends up relating question_ids and quiz_ids.

type: :append_and_remove. :direct_control will attempt to create, destroy and update questions to make the relationship look exactly like the provided values. So any given new value that is missing has to be enough input to call the corresponding create action. type: :append_and_remove will attempt to relate and unrelate existing questions.

 update :update_quiz_with_question_id do
      accept []

      argument :question_id, :uuid do
        allow_nil? false
      end

      change manage_relationship(:question_id, :questions, type: :append_and_remove)
    end

this doesn’t work either. same error. I can put a small demo repo, if you’d like

Ah, so the required argument bit is because arguments are required to be provided when the changeset is built. If you have an argument with allow_nil? false, you can’t set it in a change to avoid the error.

You should be able to do

argument :question_ids, {:array, :uuid} do
  allow_nil? false
end

change manage_relationship(:question_ids, :questions, type: :append_and_remove)

and then Ash.Changeset.for_....(%{question_ids: [....]})

If you just want them to add, and not remove ones they didn’t supply, then use type: :append

try running this test via

mix test test/pyq_ratta/quiz_test.exs:30 on branch feat/upload_questions

That makes sense.

However, even after trying that, tests fail. I suspect it is something else. :\

branch name is feat/upload_questions

I may have time to look tomorrow. Are you getting an error with that setup? Or how are your tests failing?

errors are:

{
              :error,
              %Ash.Error.Invalid{
                __exception__: true,
                changeset: #Ash.Changeset<api: PyqRatta.Databank, action_type: :update, action: :update_quiz_with_question_ids, attributes: %{}, relationships: %{questions: [{[%{id: "1085ee61-68b6-4bb8-ad7c-375b7129443f"}, %{id: "d86052e1-8feb-420b-92ce-17f5d35d9c07"}], [ignore?: false, on_missing: :ignore, on_match: :ignore, on_lookup: :relate, on_no_match: :error, eager_validate_with: false, authorize?: true, meta: [inputs_was_list?: true, id: :question_ids], type: :append]}]}, arguments: %{question_ids: ["1085ee61-68b6-4bb8-ad7c-375b7129443f", "d86052e1-8feb-420b-92ce-17f5d35d9c07"]}, errors: [%Ash.Error.Invalid{errors: [%Ash.Error.Query.Required{field: :question_id, type: :argument, resource: PyqRatta.Databank.Question, changeset: nil, query: nil, error_context: [], vars: [], path: [], stacktrace: #Stacktrace<>, class: :invalid}], stacktraces?: true, changeset: nil, query: #Ash.Query<resource: PyqRatta.Databank.Quiz, load: [questions: []], errors: [%Ash.Error.Invalid{errors: [%Ash.Error.Query.Required{field: :question_id, type: :argument, resource: PyqRatta.Databank.Question, changeset: nil, query: nil, error_context: [], vars: [], path: [], stacktrace: #Stacktrace<>, class: :invalid}], stacktraces?: true, changeset: nil, query: #Ash.Query<resource: PyqRatta.Databank.Question, filter: #Ash.Filter<id == nil>, errors: [%Ash.Error.Query.Required{field: :question_id, type: :argument, resource: PyqRatta.Databank.Question, changeset: nil, query: nil, error_context: [], vars: [], path: [], stacktrace: #Stacktrace<>, class: :invalid}], select: [:id, :question_text, :question_image, :type, :correct_answer_text, :correct_answer_image, :explanation_text, :explanation_image, :short_description, :long_description, :year, :tags, :created_at, :updated_at]>, error_context: [], vars: [], path: [], stacktrace: #Stacktrace<>, class: :invalid}, %Ash.Error.Query.Required{field: :question_id, type: :argument, resource: PyqRatta.Databank.Question, changeset: nil, query: nil, error_context: [], vars: [], path: [], stacktrace: #Stacktrace<>, class: :invalid}]>, error_context: [nil], vars: [], path: [], stacktrace: #Stacktrace<>, class: :invalid}, %Ash.Error.Invalid{errors: [%Ash.Error.Query.Required{field: :question_id, type: :argument, resource: PyqRatta.Databank.Question, changeset: nil, query: nil, error_context: [], vars: [], path: [], stacktrace: #Stacktrace<>, class: :invalid}], stacktraces?: true, changeset: nil, query: #Ash.Query<resource: PyqRatta.Databank.Question, filter: #Ash.Filter<id == nil>, errors: [%Ash.Error.Query.Required{field: :question_id, type: :argument, resource: PyqRatta.Databank.Question, changeset: nil, query: nil, error_context: [], vars: [], path: [], stacktrace: #Stacktrace<>, class: :invalid}], select: [:id, :question_text, :question_image, :type, :correct_answer_text, :correct_answer_image, :explanation_text, :explanation_image, :short_description, :long_description, :year, :tags, :created_at, :updated_at]>, error_context: [], vars: [], path: [], stacktrace: #Stacktrace<>, class: :invalid}, %Ash.Error.Query.Required{field: :question_id, type: :argument, resource: PyqRatta.Databank.Question, changeset: nil, query: nil, error_context: [], vars: [], path: [], stacktrace: #Stacktrace<>, class: :invalid}], data: #PyqRatta.Databank.Quiz<questions: #Ash.NotLoaded<:relationship>, questions_join_assoc: #Ash.NotLoaded<:relationship>, __meta__: #Ecto.Schema.Metadata<:loaded, "quiz">, id: "457b3c38-0874-41e8-b725-96aa3631fb8b", short_description: nil, long_description: nil, year: nil, tags: [], created_at: ~U[2024-01-11 03:35:07.410725Z], updated_at: ~U[2024-01-11 03:35:07.410725Z], aggregates: %{}, calculations: %{}, ...>, context: %{actor: nil, authorize?: false}, valid?: true>,
                class: :invalid,
                error_context: [nil, nil, nil],
                errors: [%Ash.Error.Query.Required{field: :question_id, type: :argument, resource: PyqRatta.Databank.Question, changeset: nil, query: nil, error_context: [], vars: [], path: [], stacktrace: #Stacktrace<>, class: :invalid}],
                path: [],
                query: #Ash.Query<resource: PyqRatta.Databank.Quiz, load: [questions: []], errors: [%Ash.Error.Invalid{errors: [%Ash.Error.Query.Required{field: :question_id, type: :argument, resource: PyqRatta.Databank.Question, changeset: nil, query: nil, error_context: [], vars: [], path: [], stacktrace: #Stacktrace<>, class: :invalid}], stacktraces?: true, changeset: nil, query: #Ash.Query<resource: PyqRatta.Databank.Question, filter: #Ash.Filter<id == nil>, errors: [%Ash.Error.Query.Required{field: :question_id, type: :argument, resource: PyqRatta.Databank.Question, changeset: nil, query: nil, error_context: [], vars: [], path: [], stacktrace: #Stacktrace<>, class: :invalid}], select: [:id, :question_text, :question_image, :type, :correct_answer_text, :correct_answer_image, :explanation_text, :explanation_image, :short_description, :long_description, :year, :tags, :created_at, :updated_at]>, error_context: [], vars: [], path: [], stacktrace: #Stacktrace<>, class: :invalid}, %Ash.Error.Query.Required{field: :question_id, type: :argument, resource: PyqRatta.Databank.Question, changeset: nil, query: nil, error_context: [], vars: [], path: [], stacktrace: #Stacktrace<>, class: :invalid}]>,
                stacktrace: #Stacktrace<>,
                stacktraces?: true,
                vars: []
              }
            }
     stacktrace:
       test/pyq_ratta/quiz_test.exs:41: (test)

error as Github Gist


just realised I had not pushed migrations. pushed now.

Weird I don’t see how you could be getting a required argument called :question_id when you don’t even have an argument anywhere in the question resource called :question_id.

Ah, never mind, you do :slight_smile:

This is the issue:

    read :read do
      argument :question_id, :uuid do
        allow_nil? false
      end

      # to indicate that only one record will be returned
      get? true
      primary? true

      filter expr(id == ^arg(:question_id))
    end

Your primary read action should not have required arguments. If it does, anything that tries to use it (like manage_relationship won’t know what to supply as the argument value.

I’d suggest leaving your primary read action as defaults [:read, ....] in most cases, and adding a separate action for your get.

Would never have guessed this error :stuck_out_tongue:

Thank you so much ! :slight_smile: That works.

1 Like