Phoenix absinthe interface with resolve type that has middleware in it throws **(KeyError) key :middleware not found in: %Absinthe.Type.Object{...}

I have a courses table, if user request for the course detail, I first check if the user is the course owner, admin or normal user (student).

If student, the student only be able to view info such as title, description, instructor profile, course sections/chapters of the course and only if the course is published.

If course owner or admin, he/she be able to view everything about the course, including which admins that approved the course.

Here are my graphql schema :course definitions:

object :course do
  field :id, non_null(:id)
  field :title, non_null(:string)
  field :description, non_null(:string)
  field :is_publish, non_null(:boolean)

  field :approvals, list_of(:course_approval) do
    middleware Middleware.Authorize, ["instructor", "admin"]
    resolve &Resolvers.Studies.course_approvals/3
  end
  field :course_sections, list_of(:course_section) do
    resolve &Resolvers.Studies.course_section/3
  end
  field :instructor, non_null(:user) do
    resolve &Resolvers.Accounts.course_instructor/3
  end
end

object :studies_queries do
  field :course, :course do
    arg :title, non_null(:string)

    resolve &Resolvers.Studies.course/3

    middleware &is_allow_course_view/2
  end
end

defp is_allow_course_view(%{value: %{is_publish: true}} = res, _), do: res
defp is_allow_course_view(%{
  value: %{user_id: user_id},
  context: %{current_user: %{id: id}}
} = res, _) when user_id == id, do: res

defp is_allow_course_view(%{context: %{current_user: current_user}} = res, _) do
  preloaded = current_user |> Repo.preload(:roles)
  roles = preloaded.roles

  cond do
    Enum.any?(roles, & &1.title == "admin) -> res
    true -> res |> Absinthe.Resolution.put_result({:error, "Access Denied"})
  end
end

defp is_allow_course_view(res, _) do
  res |> Absinthe.Resolution.put_result({:error, "Access Denied"})
end

While the above method works fine, but it is better to refactor it and use interface like this below:

interface :course_info do
  field :id, non_null(:string)
  field :title, non_null(:string)
  field :description, non_null(:string)
  field :inserted_at, non_null(:date_scalar)
  field :updated_at, non_null(:date_scalar)

  resolve_type &resolve_course_info_type/2
end

defp resolve_course_info_type(_, %{context: %{current_user: nil}), do: :student_course_view
defp resolve_course_info_type(%{user_id: user_id}, %{context: %{current_user: current_user}}) do
  if user_id == current_user.id do
    :owner_course_view
  else
    preloaded = current_user |> Repo.preload(:roles)
    roles = preloaded.roles

    if Enum.any?(roles, & &1.title == "admin" do
      :owner_course_view
    else
      :student_course_view
    end
  end
end

object :student_course_view do
  # .. all fields from interface

  field :instructor, non_null(:user) do
    resolve &Resolvers.Accounts.course_instructor/3
  end
  field :course_sections, list_of(:course_section) do
    resolve &Resolvers.Studies.course_section/3
  end

  interface :course_info

  middleware fn
    ${value: %{is_publish: true}} = res, _ -> res
    res, _ -> res |> Absinthe.Resolution.put_result({:error, "Access Denied"})
  end
end

 object :owner_course_view do
  # .. all fields from interface
  field is_publish, non_null(:boolean)

  field :approvals, list_of(:course_approval) do
    resolve &Resolvers.Studies.course_approvals/3
  end
  field :instructor, non_null(:user) do
    resolve &Resolvers.Accounts.course_instructor/3
  end
  field :course_sections, list_of(:course_section) do
    resolve &Resolvers.Studies.course_section/3
  end

  interface :course_info
end

object :studies_queries do
  field :course, :course_info do
    arg :title, non_null(:string)

    resolve &Resolvers.Studies.course/3
  end
end

The refactored one does not work unless I remove the middleware fn ... from student_course_view

Here is the error:

== Compilation error in file lib/koompi_learn_server_web/schema/studies_types.ex
== ** (KeyError) key :middleware not found in: %Absinthe.Type.Object{__private__: [], __reference__: nil, description: nil, field_imports: [], fields: {:%{}, [line: 1], [course_sections: {:%, [line: 1], [Absinthe.Type.Field, {:%{}, [line: 1], [__private__: [], complexity: nil, config: nil, default_value: nil, description: nil, triggers: [], deprecation: nil, identifier: :course_sections, name: "course_sections", middleware: [{:{}, [line: 1], [{Absinthe.Resolution, :call}, {:&, [line: 66], [{:/, [], [{{:., [], [KoompiLearnServerWeb.Resolvers.Studies, :course_sections]}, [], []}, 3]}]}]}], __reference__: {:%{}, [line: 1], [identifier: :course_sections, location:...
absinthe) lib/absinthe/type/object.ex:97: anonymous fn/2 in Absinthe.Type.Object.__struct__/1
(elixir) lib/enum.ex:1899: Enum."-reduce/3-lists^foldl/2-0-"/3
(stdlib) erl_eval.erl:677: :erl_eval.do_apply/6
(stdlib) erl_eval.erl:232: :erl_eval.expr/5
(stdlib) erl_eval.erl:233: :erl_eval.expr/5
(stdlib) erl_eval.erl:232: :erl_eval.expr/5
(stdlib) erl_eval.erl:233: :erl_eval.expr/5
(absinthe) /home/shadowlegend/projects/koompi_learn_server/lib/koompi_learn_server_web/schema/studies_types.ex:1: Absinthe.Schema.Notation.Writer.__before_compile__/1
(elixir) lib/kernel/parallel_compiler.ex:198: anonymous fn/4 in Kernel.ParallelCompiler.spawn_workers/6

One workaround is which instead of using middleware, I only return the ttile name the user request for and the rest of the field null by changing the interface to this below:

interface :course_info do
  # field :id, non_null(:string)
  field :title, non_null(:string)
  # field :description, non_null(:string)
  # field :inserted_at, non_null(:date_scalar)
  # field :updated_at, non_null(:date_scalar)

  resolve_type &resolve_course_info_type/2
end

Hey @hengly, we could definitely use a better error message, but your issue is just that you’ve got the middleware macro on the object definition and not a field definition. It doesn’t make any sense to talk about middleware on objects.

object :student_course_view do
  # .. all fields from interface

  field :instructor, non_null(:user) do
    resolve &Resolvers.Accounts.course_instructor/3
  end
  field :course_sections, list_of(:course_section) do
    resolve &Resolvers.Studies.course_section/3
  end

  interface :course_info

  middleware fn
    ${value: %{is_publish: true}} = res, _ -> res
    res, _ -> res |> Absinthe.Resolution.put_result({:error, "Access Denied"})
  end
end

Now that I look at this more closely though see at least two syntax errors so maybe that isn’t what you’re doing?

Sorry for the late reply. In the situation, the reason I used middleware on object was to control if object should present to whoever request for it or not, in this case is the student. But because of the error, I have refactored my code to as below:

interface :course_info do
  field :title, non_null(:string)

  resolve_type &resolve_course_info_type/2
end

defp resolve_coures_info_type(%{is_publish: is_publish, user_id: user_d}, %{context: %{current_user: current_user}}) do
  preloaded = current_user |> Repo.preload(:roles)
  roles = preloaded.roles

  if user_id == current_user.id or Enum.any?(roles, & &1.title == "admin") do
    :owner_course_view
  else
    if is_publish, do: :student_course_view, else: nil
  end
end

defp resolve_course_info_type(%{is_publish: true}, _), do: :student_course_view
defp resolve_course_info_type(_, _), do: nil

object :owner_course_view do
  interface :course_info

  # ... those file from course_info
  
  field :approvals, list_of(:course_approval), resolve: assoc(:course_approval)
  field :instructor, non_null(:user), resolve: assoc(:user)
  field :course_sections, list_of(:course_section), resolve: assoc(:course_section)
end

object :student_course_view do
  # ... simliar to owner course view but no approvals field shown
end

object :studies_queries do
  field :course, :course_info do
    arg :title, non_null(:string)
    resolve &Resolvers.Studies.course/3
  end
end

The code above work just fine and I could get the result I was expected