Absinthe: Resolvers do not receive enough AST Context to resolve Fragments

Created on 25 Jun 2016  路  8Comments  路  Source: absinthe-graphql/absinthe

Note: I have discussed this issue with @benwilson512 on IRC

First-off, absinthe is fantastic, one of the coolest pieces of tech I've had the pleasure of working with in a long time, so thanks for all the hard work.

I have wired up my schema that for now consists of a simple model: User has_many Packs and a Pack belongs_to User which represents the different Backpack configurations a user might take on various hiking/camping trips.

I implemented a dynamic resolution system that walks the incoming AST and recursively figures out the preloads needed to resolve the incoming GraphQL query to an Ecto query.

This worked great until I wired it up to Relay which quickly introduced Query Fragments. Unfortunately the AST object passed to resolvers is scoped to the node being resolved, so while the it will include a FragmentSpread letting me know that I need to look at "F1" to continue processing the resolution, there's no way to actually get access to the AST node that represents "F1" and see what fields are in it.

Some potential solutions I see:

  1. Inline Fragments into the Node AST. Pro: Overall structure doesn't change. Con: Lots of duplicated data.
  2. Pass a third argument to resolvers which is a Map of Fragments keyed on the Fragment Name. Pro: The map could be shared globally by all involved resolvers, easy to process a FragmentSpread in a node AST and quickly look up it's name in the Map. Con: Changes the signature of resolvers.
  3. Provide a helper function that would return a Fragment AST given it's name. Pro: No backwards-comparability issues. Con: Might not even be possible? That context would need to be available somewhere.
Feature

Most helpful comment

(Adding this to the v1.2 milestone as a reminder. The resolution via flattening is going well, and we need to make sure the subselection fields are provided to resolvers in our new Info struct -- preferably filtered for the appropriate type condition or supporting projection.)

All 8 comments

@dpehrson Thanks for the kind words about Absinthe!

I feel your pain on this issue. @benwilson512 and I have been discussing this recently. Some brief comments, off the cuff:

  • The AST introspection is a hack -- but it's all we have at the moment to support optimized DB queries, so we need to support it -- and better than we are now. Relay can create _very_ complicated documents, as you point out. So we need to fix this, if even for the short term.
  • At the moment expansion of fragments occurs _during_ execution, and the logic is non-trivial, to say the least.

My feeling on this is that your first recommendation is the closest to what we want to achieve. The duplicated data isn't much of a concern, given some other benefits. I'd argue instead of "inlining" into an AST, we create a new tree type that models the final structure to run through execution. This has the benefit of also significantly simplifying execution. Incidently, this approach is fairly close to Sangria's concept of projection, which has been on our radar for awhile.

I'll start looking at this approach this weekend.

@bruce Thanks for the quick reply and openness to digging into a solution. I am really excited about the possibilities with Elixir/Phoenix/React/GraphQL and I'm pretty sure if I can get Fragments resolved, my path will be cleared to start building the app I'm going for. I originally sat down to try and resolve this on my own but quickly realized I was in over my head which is why I reached back out to Ben on IRC.

Please let me know if there is any other information/help I can provide such as more detail about my use cases, etc.

The similarities between the high-level design of GraphQL queries/mutations vs Ecto queries/changesets seems like such a great fit, I'm curious to see how transparent we can make the resolutions.

This could be an immensely powerful pairing of technologies for the future of web APIs/apps.

As a followup, after spending the rest of the day digging further in, I came across something we might be able to leverage.

In dealing with how to respond to root-level node queries that relay requires, I ran across this query syntax:

query {
  node(id: "UGFjazo0OQ==") {
    id,
    ... on Pack {
      id,
      name,
      user {
        id,
        ... on User {
          username
        }
      }
    }
  }
}

When executed my abstract resolver hits upon Absinthe.Language.InlineFragment containing the fragment spread. which can be handled by simply peeling off it's selection_set and recursing further.

If you're already thinking about going route number 1 and flattening named fragments, a completely transparent way to do so would be to simply replace all Absinthe.Language.FragmentSpread objects with Absinthe.Language.InlineFragment objects taken from the root query.

Not sure how feasible this is for all use cases, but it seems like it would work for mine.

@dpehrson Just a note to let you know we haven't forgotten about this. We're working on an "intermediate representation" approach in #124 that involves flattening fragments and will help us break validation and execution into more flexible, discrete pieces, while giving us an avenue to do some long-overdue cleanup on some of the gnarlier pieces of logic, especially in execution.

Not only will this provide a more consistent data structure for this "hack" approach to use, but it should give us the bits we need to build much better approaches to replace it.

@bruce Thanks so much for the followup, I'll be watching both of these issues for progress. Again, let me know if there's any way I can help. Starting next week I will be working full time on my startup @trailpost which is what I am hoping to use absinthe to power.

(Adding this to the v1.2 milestone as a reminder. The resolution via flattening is going well, and we need to make sure the subselection fields are provided to resolvers in our new Info struct -- preferably filtered for the appropriate type condition or supporting projection.)

Changes in v1.2 have changed the landscape significantly with this issue:

  • With batching, a lot of the performance problems around querying (eg, N+1 issues) are handled: see the updated http://absinthe-graphql.org/guides/ecto-best-practices/
  • The last argument to resolvers, an Absinthe.Resolution struct, has a flattened list of :fields that can be accessed; the AST node is no longer available. Checking those fields (and paying attention to their type_conditions, if you're dealing with an abstract type), selection of specific DB columns is possible. We haven't put a bow on a simple "projection API," but it's a straightforward process manually.

:heart: you guys, I just started working on my system again yesterday, and now I get this notification, what rad timing.

Was this page helpful?
0 / 5 - 0 ratings