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