Skip to content

How can I get the parent fields from ctx? #881

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
gottfrois opened this issue Aug 10, 2017 · 20 comments
Closed

How can I get the parent fields from ctx? #881

gottfrois opened this issue Aug 10, 2017 · 20 comments

Comments

@gottfrois
Copy link

Hey guys,

I was looking for a way to get the parent objects from the ctx variable but could not find anything. Here is an example:

query {
  user(id: 1) {
    abilities(first: 10) {
      edges {
        node {
          name
          pricing {
            cost
          }
        }
      }
    }
  }
}

The `pricing resolver look like that:

field :pricing, Types::Ability::PricingType do
  resolve -> ability, args, ctx {
    user = ???
    ability.pricing_for(user)
  }
end

How can I get the user from the user(id: 1) root query?
Thank you!

@rmosolgo
Copy link
Owner

There's not a way to get parent objects from the resolve function :S

Options I can think of are:

  • Pass the userId as an argument to pricing :S
  • Create user-aware proxy objects for abilities so that you can access the user later on

I can't think of any more :S

@nemoDreamer
Copy link

nemoDreamer commented Aug 21, 2017

@gottfrois I found that you can get an Array of single Argument's by doing ctx.irep_node.parent.ast_node.arguments, but I agree that there has to be a simpler way.
(I find the lack of getters here pretty discouraging...)

There's no way to get a true Arguments object, like the args one, w/ a to_h method, etc...

@nemoDreamer
Copy link

If ctx is my "context", why aren't there easy ways to access its actual data? 😭

@nemoDreamer
Copy link

  def self.get_parent_argument_from_ctx(ctx, name)
    arg = ctx.irep_node.parent.ast_node.arguments.find { |a| a.name == name }
    arg ? arg.value : nil
  end

@rmosolgo
Copy link
Owner

I don't recommend going through the AST -- for example, if a client uses a variable, you'll get the name of the variable instead of a value.

You can backtrack irep_nodes using parent:

def self.get_parent_argument_from_ctx(ctx, name) 
  parent_node = ctx.irep_node.parent
  parent_args = parent_node.arguments
  # ^ Arguments 
  parent_args[name]
end 

@gottfrois
Copy link
Author

The above also does not solve the original issue since I'm looking for the resolved value for the user(id: 1) and not a way to get the arguments.

Is there a way to get the resolved value from the ast?

@rmosolgo
Copy link
Owner

get the resolved value

I can't think of any way to backtrack and get that value :(

@nemoDreamer
Copy link

Thanks, @rmosolgo!
Sorry @gottfrois, I misread your question... Makes sense now!

@rmosolgo
Copy link
Owner

#923 will add support for backtracking via

context.parent.value 
# #<User ...>

@jorroll
Copy link
Contributor

jorroll commented Sep 2, 2017

@gottfrois until #923 fixes this issue, what I've done is create a simple wrapper object that allows me to easily pass parents down like so:

class GraphqlWrapper 
  attr_reader :gwobject, :gwdata

  def initialize(gwobject:, gwdata:)
    @gwobject = gwobject
    @gwdata = gwdata
  end

  def method_missing(name, *args)
    if args.any?
      @gwobject.public_send(name, args)
    else
      @gwobject.public_send(name)
    end
  end
end

In your case, instead of grabbing an abilities object with something like user.abilities.first, you would have:

ability = GraphqlWrapper.new(
  gwobject: user.abilities.first,
  gwdata: user
)

The resulting ability object would behave like a normal ability object, except you can call ability.gwdata on it to get the user.

Obviously the additional objects add some overhead, but depending on your use case, this could be an easy solution.

@gottfrois
Copy link
Author

Thanks @thefliik for the tip!

@rmosolgo
Copy link
Owner

This will be supported in 1.7.0, where you can access ctx.parent, for example:

ctx # => GraphQL::Query::Context::FieldResolutionContext 
ctx.parent # => another GraphQL::Query::Context::FieldResolutionContext 
ctx.parent.irep_node # => the query node parent object
ctx.parent.object # => the object used for resolving the parent field 
ctx.parent.value # => the in-progress result for the parent field

@SeanRoberts
Copy link

Is it still possible to access FieldResolutionContext in the new class-based API? The instance method context returns a GraphQL::Query::Context instead of a GraphQL::Query::Context::FieldResolutionContext

@rmosolgo
Copy link
Owner

No, those objects aren't passed in anymore. Since we're working on an object-by-object basis, there's no good way to inject those into each method without adding a lot of boilerplate.

In the meantime, attributes of ctx may be injected using extras:: http://graphql-ruby.org/type_definitions/objects.html#extra-field-metadata

I'm not sure about the long-term future here, so if you want to share something about your use case, please do, and I'll keep it in mind!

@SeanRoberts
Copy link

SeanRoberts commented Apr 20, 2018

I think that will probably solve the issue, I'm also just trying to climb back up the chain to access my root object from a deeply nested one. The nested object has a computed value that's created from a combination of itself and the parent object, something kind of like this:

  user(id: 1) { 
    favoriteBooks {
      title
      isbn
      isAuthor
    }
  }

Where isAuthor would be computed by checking if book.author === user. If there's a better way to pass the user into the BookType or to do those kinds of calculations I'm definitely open to alternatives. I guess the correct thing would be to pull something like authoredBooks and favoriteBooks on the user and then let the client determine authorship, but that feels inconvenient and would be a downgrade from the existing REST API.

Thanks for responding so quickly and thanks for this great library!

@SeanRoberts
Copy link

SeanRoberts commented Apr 20, 2018

Also, based on this comment it sounds like maybe what I want to do is actually discouraged in GraphQL, but that I could also probably just use the current_user from context because it's okay to treat that as implicit in the query.

@rmosolgo
Copy link
Owner

Yeah, I agree here:

on the wrong object

Another design would allow a query like this:

  user(id: 1) { 
    favoriteBooks {
      isAuthor 
      book {
        title
        isbn
      }
    }
  }

where favoriteBooks returns some intermediary object, which has both user and book (something like a Favorite object?), then the Favorite.isAuthor field can naturally compare @user == @book.author, since it has both of those objects already.

But of course, sometimes we really need parent, so it continues! 😆

@SeanRoberts
Copy link

That makes sense. I was hoping to do a drop-in replacement for our existing REST API, but in a lot of places it's really tailored to the client in ways that GraphQL probably can't or shouldn't be. The whole point of this exercise is to create something that works across multiple clients and multiple use-contexts though so it probably makes sense to revisit how some of the data is presented.

Thanks again for the help 😄

@jorroll
Copy link
Contributor

jorroll commented Sep 6, 2018

Hey @rmosolgo, regarding

if you want to share something about your use case, please do, and I'll keep it in mind!

I'm experimenting with schema stitching in graphql-ruby using graphql-remote_loader (which you helpfully linked to in #1812 👍) and prisma. The second graphql API I'm sending queries to is a largely identical to my graphql-ruby schema, so for the fields I want to stitch together, I simply "foward" the query on to the second API. This is accomplished by building up the second query piece by piece, merging all the pieces together, and sending it off.

i.e.
query { person { id } } + query { person { firstName } } = query { person { id\n firstName } }

Or, the more complex scenerio

query { person(where: {id: "1"}) { firstName } }
+
query { person(where: {id: "1"}) { friends(limit: 2) { id } } }
+
query { person(where: {id: "1"}) { friends(limit: 2) { firstName } } }
=
query { person(where: {id: "1"}) { firstName\n friends(limit: 2) { id\n firstName } } }

And this works! But....

And maybe you can see where I'm going with this, but in order to accomplish this from a deeply nested field, I need to crawl up the query tree and build the query string. (i.e. for the friends firstName field, I need to prepend its query string with query { person(where: {id: "1"}) { friends(limit: 2)

At the moment, the context object gives me access to the top level query, so I'm crawling from the top down and, since the entire query representation is in the context, I need to figure out what's applicable to whatever deeply nested field I'm currently looking at.

If the context let me enter the query tree at the current, deeply nested location however, then I could crawl up it nice and easy. Hopefully this makes sense.

Any suggestions? Definitely understand if this is just outside the scope of graphql-ruby at this point.

Update

At the moment, I think the best way to handle this situation is to

  1. Add a custom metadata property to your schema's field method which tags the field as one that should be stiched
  2. Create a custom query analyzer which uses this custom metadata to gather together all the nodes in an incoming query which should be stitched.
  3. Create a custom printer which processes these language nodes and converts them to a graphql string which is then fed into graphql-remote_loader.

Having tested this, it works.

@rmosolgo
Copy link
Owner

It's been a while, but #2634 adds "scoped" context, a hash of values which are only available to child selections of the field where they were added. For example, if you could set ctx.set_scoped(:author, author), and then for child fields (and grandchild fields), ctx[:author] would return that value.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants