Skip to content
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

Schema-level root resolve function #353

Closed
helfer opened this issue Apr 10, 2016 · 10 comments
Closed

Schema-level root resolve function #353

helfer opened this issue Apr 10, 2016 · 10 comments

Comments

@helfer
Copy link
Contributor

helfer commented Apr 10, 2016

Having worked with GraphQL for a while now, I find that having resolve functions for fields is the really nice abstraction at the core of GraphQL that makes it so powerful. However, there seems to be one point in the query execution where the abstraction breaks down, and I'm not sure why so I thought I'd ask here to be enlightened:

Why is there no resolve function at the root?

Having no resolve function at the root essentially means that there are as many entry points into the graph as there are fields of the query, mutation and subscription types. If you need to do any logic common to all queries (eg. authenticating a user) you either have to create an extra type and make all your queries a field of that, or you have to do it outside of GraphQL and pass the result in as the root value or context. Why is that? Wouldn't it be much nicer, if mutation and subscriptions were considered just like fields of the schema type? By doing that, the resolve tree would be rooted and no longer a forest (it would still be three trees, actually, but if that's an issue we could consider adding a root for that).

With schema-level resolve functions, there would in many cases be no more need for writing custom code that runs outside the resolve functions on the server. Instead, the server (eg. express-graphql) could just pass in the request object (and whatever else it needs) to the root resolve function, which would be the logical place for containing any logic that has to run before any other resolvers run.

I know there will most likely still be a need to run some checks etc. before a request is passed to validation and execution, but I think having a root resolve function would be really nice to have nevertheless because it makes the resolver abstraction more consistent.

@JeffRMoore
Copy link
Contributor

Wow, Interesting idea.

Now that context and root are separate, I wonder if a schema level resolve function shouldn't be the initial source of root instead of passing it to execute?

I also ran into some complexity in #304 because the select operation was both the root operation and an interior operation. Wonder if this would improve that?

Worth some thought. Thanks.

@leebyron
Copy link
Contributor

leebyron commented May 7, 2016

This is pretty interesting. I suppose we just haven't run into a use case where we would need such a thing. Also resolve functions are currently properties of fields, and this would be a divergence from that.

I suppose the question here is what the semantic purpose of such a resolve function should be? For fields, the purpose is to produce a sub-value given a parent value, where fields at the top level of a query accept the "root" value as input. If there were to be a resolver function above the top level of a query responsible for producing the root value, what would its input be?

@Globegitter
Copy link

Globegitter commented May 13, 2016

@leebyron Isn't that exactly what in Relay the viewer field is for? At least to my understanding it seems to have very similar purposes. Not sure about all the advantages and disadvantages but I can see some benefits from having one entry point, i.e. removing the need for the viewer (which itself does introduce some additional complexity for people not familiar with it).

@Globegitter
Copy link

Globegitter commented Jun 8, 2016

Having come across that again, I think it would be very useful to have schema-level root resolve function. Given our use-cases the purpose of such root-level resolve (or maybe it should be called differently to not be confusing. Something like prepareRoot could be fitting), would be to prepare things that are needed at the root level/for all queries, e.g. authentication, or even for the planning phase (#304) this could be useful to allow for optimization at the root level (in fact it seems to me quite natural to have a place where you can see: 'oh this is the whole query I am now going to process and you can do something with that information before I am doing it if you want or not.').

Also this would seem to me a good place to create the 'root' values for the top level as @JeffRMoore is saying, and even seems to me more natural then passing it in through the execute call.

@OlegIlyenko
Copy link
Contributor

OlegIlyenko commented Jun 8, 2016

@Globegitter What you described in the last comment reminds me of something that was recently discussed in different places. I actually had very similar kind of use-cases coming from myself and other users of sangria. I introduced the concept of query reducers in sangria in order to address these use-cases.

The main idea is to introduce a query analysis phase in between a query validation and execution phases. It is able to see and analyze the whole query and make some interesting things with this information. For example it is able to see whether query has some "protected" fields and if it does, then it will go to an external service and fetch additional user details. It is an expensive operation, so you don't really want to do this if query only asks for a publically visible fields (this is a real use-case that I heard from at least several sangria users):

Query Analysis

(also look at the next slide where I listed some of the use-cases)

In recent discussion about the decorators we also discussed a possibility of query analysis phase. This may be interesting for you, in case you haven't seen it yet:

https://github.com/apollostack/graphql-decorators/issues/1#issuecomment-217146736

@leebyron
Copy link
Contributor

Sorry for the delayed response here.

@Globegitter the viewer field in Relay is really more of a relic of how GraphQL used to work at Facebook before it was improved and open sourced. GraphQL actually used to not have a concept of root types and instead had root fields. Every query started with exactly one root field and a resolver for the root field began the query (there was also no root value then). We quickly realized that we wanted a root field that would give us access to "everything" and viewer became the sanctioned dumping ground. When redesigning GraphQL we realized that having both concepts of root fields and types with fields was not only unnecessary but also left us with a schema that required arbitrary root fields to access more than one thing at a time. Replacing root fields with root types not only reduced two concepts into one it also made it possible to no longer rely on a wrapping viewer field and instead put things which deserve to be top level directly on the Query root type. Then we had to deal with losing the concept of a root resolver. Since each root field resolver became a regular field resolver on the root type, this was basically solved immediately.

Relay encourages use of adding a top level viewer field because it still only supports one field at the top of your query, a relic from the older version of GraphQL Relay originally supported. This field on the root type lets you query multiple things at once. I understand that the Relay team is working on a whole suite of improvements, one of which is the ability to eliminate the restriction of a single field at the top of the query, which makes the viewer work-around no longer necessary.


I still have some questions as to why a root resolver would be necessary. In my mind you could very easily do this today without any changes to the library:

// Today:
var rootObject = { ... }
var results = await graphql(myQuery, myArgs, myContext, rootObject);

// Tomorrow:
var rootObject = { ... }
var moreSpecificRootObject = resolveRoot(myQuery, myArgs, myContext, rootObject);
var results = await graphql(myQuery, myArgs, myContext, moreSpecificRootObject);

In my mind anything that you can do before the validation and execution sequence begins should be able to be provided easily outside of the library in this manner. But perhaps I'm missing something, let me know!

@Globegitter
Copy link

@leebyron thank you for that detailed answer and for giving some clarification on graphql/relay design decisions.

And you are right, we have found a way to make this possible using a 'lazy object' (only when a key gets accessed the first time its value gets evaluated and then frozen for subsequent accesses) and passing it in as the rootObject. While that worked well enough it also had some of its own issues and seems a bit counter intuitive: 'let's just lazily put in all possible shared information so we have it available for any possible query'.

The other way of course would be to parse the query before creating that rootObject and then it wouldn't need to be lazy as we are just pre-computing shared information that we know we need. Parsing the query is something that graphql already does, so it would be nice to somehow have access to this parsed query (and not do the same computation twice) so we can e.g. pre-compute specific information (very similar to what the planning phase will allow, just at the very top level).

So you are right, it is not necessary to have a root-resolve and more or less everything it already possible today it would imo be very nice to be able to access the parsed query before graphql resolves anything.

What you are writing under tomorrow seems quite promising. What exactly is resolveRoot? Is that something that graphql could provide?

But that yeah that is making me think of a good approach (which might be what you had in mind already). This resolveRoot function you showed in your example could be provided by graphql as a stand-alone function. It would provide the parsed query, allow to create a specific root Object, do whatever and then afterwards graphql could simply re-use that cached query. This imo would also go hand-in-hand with the proposed planning phase.

@leebyron
Copy link
Contributor

Great thoughts, thanks for this perspective, @Globegitter.

My intuition was that the graphql() function is a good default, but any more sophisticated usage would import the individual steps directly and run them with the sophistication introduced as necessary between parse/validate/execute.

I can see why if you wanted to do some "query planning" that you would want to do that between validate and execute and using the parsed query.

Does importing the parse/validate/execute functions directly unblock this use case? What should we do to enable this sort of sophistication while keeping the base graphql function very simple?

@He-Pin
Copy link

He-Pin commented Jul 28, 2016

In our implement we could pass the filed or resolved field of the current context as a resolved parameter to the field which accept parameters,at the sametime ,we have globally function/field too,I don't want to argue whether that is great or needed,we both have different use cases,yes,we did that way.

@yaacovCR
Copy link
Contributor

I think this issue can be closed based on the above comments by @leebyron as well as potential new aot functionality tracked in #3314

feel free to reopen as necessary

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

No branches or pull requests

8 participants