absinthe-graphql/dataloader

Multiple has_many through associations with queryable chasing

Closed this issue · 2 comments

Versions

Ecto: 2.2.11
Dataloader 1.0.4

Overview

In our system, we have the following contexts:

   User:
      has_many :institution_users, InstitutionUser
      has_many :institutions,       through: [:institution_users, :institution]
      has_many :institution_spaces, through: [:institutions, :spaces]

   Institution:
       has_many :spaces, Space
       has_many :institution_users, InstitutionUser
       has_many :users, through: [:institution_users, :user]

Our goal is to serialize the institution_spaces as part of our user object in the schema:

  object :user do
    field :name,  :string

    field :institution_spaces, list_of(:institution_space) do
      resolve dataloader Dataloaders.User, fn _, _, res ->
        { :institution_spaces, %{user_id: Util.Resolution.user_id(res)} }
      end
    end

The associations work correctly via raw Ecto when preloading.

Error

Serializing it via the schema results in:
** (CaseClauseError) no case clause matching: %Ecto.Association.HasThrough{cardinality: :many, field: :institutions, on_cast: nil, owner: Api.Users.User, owner_key: :id, relationship: :child, through: [:institution_users, :institution], unique: true} stemming from the stack trace of:

     stacktrace:
       (dataloader) lib/dataloader/ecto.ex:327: Dataloader.Source.Dataloader.Ecto.chase_down_queryable/2
       (dataloader) lib/dataloader/ecto.ex:337: Dataloader.Source.Dataloader.Ecto.get_keys/2
       (dataloader) lib/dataloader/ecto.ex:247: Dataloader.Source.Dataloader.Ecto.fetch/3
       (dataloader) lib/dataloader/ecto.ex:287: Dataloader.Source.Dataloader.Ecto.load/3
       (elixir) lib/enum.ex:1899: Enum."-reduce/3-lists^foldl/2-0-"/3
       (dataloader) lib/dataloader.ex:123: Dataloader.load_many/4
       (absinthe) lib/absinthe/resolution/helpers.ex:255: Absinthe.Resolution.Helpers.do_dataloader/6
       (absinthe) lib/absinthe/resolution.ex:209: Absinthe.Resolution.call/2
       (absinthe) lib/absinthe/phase/document/execution/resolution.ex:209: Absinthe.Phase.Document.Execution.Resolution.reduce_resolution/1
       (absinthe) lib/absinthe/phase/document/execution/resolution.ex:168: Absinthe.Phase.Document.Execution.Resolution.do_resolve_field/4
       (absinthe) lib/absinthe/phase/document/execution/resolution.ex:153: Absinthe.Phase.Document.Execution.Resolution.do_resolve_fields/6
       (absinthe) lib/absinthe/phase/document/execution/resolution.ex:72: Absinthe.Phase.Document.Execution.Resolution.walk_result/5
       (absinthe) lib/absinthe/phase/document/execution/resolution.ex:257: Absinthe.Phase.Document.Execution.Resolution.build_result/4
       (absinthe) lib/absinthe/phase/document/execution/resolution.ex:153: Absinthe.Phase.Document.Execution.Resolution.do_resolve_fields/6
       (absinthe) lib/absinthe/phase/document/execution/resolution.ex:72: Absinthe.Phase.Document.Execution.Resolution.walk_result/5
       (absinthe) lib/absinthe/phase/document/execution/resolution.ex:53: Absinthe.Phase.Document.Execution.Resolution.perform_resolution/3
       (absinthe) lib/absinthe/phase/document/execution/resolution.ex:24: Absinthe.Phase.Document.Execution.Resolution.resolve_current/3
       (absinthe) lib/absinthe/pipeline.ex:269: Absinthe.Pipeline.run_phase/3
       (absinthe_plug) lib/absinthe/plug.ex:414: Absinthe.Plug.run_query/4
       (absinthe_plug) lib/absinthe/plug.ex:240: Absinthe.Plug.call/2

Notes

It looks like it goes through https://github.com/absinthe-graphql/dataloader/blob/master/lib/dataloader/ecto.ex#L310 correctly and matches the HasThrough in the case. Then, it runs again at https://github.com/absinthe-graphql/dataloader/blob/master/lib/dataloader/ecto.ex#L326. Based on how our associations are setup, this returns an additional HasThrough struct which then results in the error.

gbxl commented

Experiencing the same behavior with a plain resolver.

  object :shelf do
    field :id, :id
    field :name, :string
    field :products, list_of(:product), resolve: dataloader(Product) // Also tried with "Project" in place of "Product", same result
  end

Where the Shelf object is associated to products like so:

defmodule Shelf do
    belongs_to(:position_taxon, Taxon)
    has_many(:classifications, through: [:position_taxon, :classifications])
    has_many(:products, through: [:classifications, :product])

I get

[error] #PID<0.1860.0> running MasterProxy.Plug (cowboy_protocol) terminated
Server: localhost:4000 (http)
Request: POST /api/platform/merchandising/
** (exit) an exception was raised:
    ** (CaseClauseError) no case clause matching: %Ecto.Association.HasThrough{cardinality: :many, field: :classifications, on_cast: nil, owner: Shelf, owner_key: :position_taxon_id, relationship: :child, through: [:position_taxon, :classifications], unique: true}
        (dataloader) lib/dataloader/ecto.ex:327: Dataloader.Source.Dataloader.Ecto.chase_down_queryable/2
        (dataloader) lib/dataloader/ecto.ex:337: Dataloader.Source.Dataloader.Ecto.get_keys/2
        (dataloader) lib/dataloader/ecto.ex:247: Dataloader.Source.Dataloader.Ecto.fetch/3
        (dataloader) lib/dataloader/ecto.ex:287: Dataloader.Source.Dataloader.Ecto.load/3
        (elixir) lib/enum.ex:1925: Enum."-reduce/3-lists^foldl/2-0-"/3
        (dataloader) lib/dataloader.ex:123: Dataloader.load_many/4
        (absinthe) lib/absinthe/resolution/helpers.ex:255: Absinthe.Resolution.Helpers.do_dataloader/6
        (absinthe) lib/absinthe/resolution.ex:209: Absinthe.Resolution.call/2
        (absinthe) lib/absinthe/phase/document/execution/resolution.ex:209: Absinthe.Phase.Document.Execution.Resolution.reduce_resolution/1
        (absinthe) lib/absinthe/phase/document/execution/resolution.ex:168: Absinthe.Phase.Document.Execution.Resolution.do_resolve_field/4
        (absinthe) lib/absinthe/phase/document/execution/resolution.ex:153: Absinthe.Phase.Document.Execution.Resolution.do_resolve_fields/6
        (absinthe) lib/absinthe/phase/document/execution/resolution.ex:72: Absinthe.Phase.Document.Execution.Resolution.walk_result/5
        (absinthe) lib/absinthe/phase/document/execution/resolution.ex:257: Absinthe.Phase.Document.Execution.Resolution.build_result/4
        (absinthe) lib/absinthe/phase/document/execution/resolution.ex:153: Absinthe.Phase.Document.Execution.Resolution.do_resolve_fields/6
        (absinthe) lib/absinthe/phase/document/execution/resolution.ex:72: Absinthe.Phase.Document.Execution.Resolution.walk_result/5
        (absinthe) lib/absinthe/phase/document/execution/resolution.ex:98: Absinthe.Phase.Document.Execution.Resolution.walk_results/6
        (absinthe) lib/absinthe/phase/document/execution/resolution.ex:87: Absinthe.Phase.Document.Execution.Resolution.walk_result/5
        (absinthe) lib/absinthe/phase/document/execution/resolution.ex:257: Absinthe.Phase.Document.Execution.Resolution.build_result/4
        (absinthe) lib/absinthe/phase/document/execution/resolution.ex:98: Absinthe.Phase.Document.Execution.Resolution.walk_results/6
        (absinthe) lib/absinthe/phase/document/execution/resolution.ex:77: Absinthe.Phase.Document.Execution.Resolution.walk_result/5

Fixed by #65