What is the preferred way of injecting differentiated context into Query resolvers versus Mutation resolvers versus Subscription resolvers? My first thought was to have a resolver for the root objects (e.g. :Query) but I can’t decipher the syntax that would allow that (naive attempts prevent the schema from even being compiled). Other options would be to inspect the operations to some extent before feeding the initial context into lacinia (yuck) or wrapping all my resolver fns (yuck). It’s clear from the docs that the root objects have resolvers, but it’s not clear how one would overrride/extend them.
Gql spec does not allow that
It’s syntactically impossible unless you have multiple operations, in which case you must pick only one as said above
True, didn't know that. Thanks. The operation type is either query, mutation, or subscription and describes what type of operation you're intending to do. The operation type is required unless you're using the query shorthand syntax, in which case you can't supply a name or variable definitions for your operation.
For me, appreciating this limitation really eased the burden of building in good semantics. Thanks again, @favila.
What if you both have a mutation and a query on one request?
Root objects have resolvers?
objects have resolvers?
Root objects do.
do you mean a resolver that determines the Query/Mutation/Subscription object?
or one that resolves the field of that object?
because the former AFAIK does not exist; the latter does
I want to “resolve” all Queries with a single fn before the individual resolvers on the named queries are executed.
right, but that’s some kind of object resolver. AFAIK only field resolvers exist
What I really want is to modify the context for all Queries separately from the context for all Mutations.
When I do this stuff, I have to do it in interceptor-land, before executing the query
Well, there is a :Query key in the compiled schema, and it has a :resolver key on it -and that is an internal lacinia fn.
Right -in interceptor land could work, but what happens when a single request has both a mutation and a query?
make a lacinia context with everything you need (maybe wrapped in a delay), and the root fields pick one?
This seems relevant: https://lacinia.readthedocs.io/en/latest/roots.html#unions
That’s my current thinking, but it is not ideal. One of the goals is to make it hard for queries to use the wrong db value and for mutations to read outside the scope of a transaction (all this in Datomic, BTW).
For queries, I want to remove the connection from the context and only have a db value. For mutations I want to remove the db value and only provide a connection.
Since GraphQL is so hierarchical, I thought this would be easy. 😞
you can do this pretty easily with some helper using`l.resolve/with-context` and adjusting what’s available before calling a descendent resolver but you have to “wrap” every root field resolver.
there’s no field “edge” going into the root objects, so I’m not surprised if this isn’t possible
there’s no place to hang a resolver; there’d have to be something special
It seems like the place to hang the resolver is on the :Query object. In fact, there is already a resolver there (`wrap-resolver-to-ensure-resolver-result`)
But I see no way to wrap that resolver.
objects don’t have resolvers, so I don’t see how wrap-resolver-to-ensure-resolver-result is relevant
is there some magic field that resolves to the query/mutation/subscription objects?
My vocab may be inadequate, but it seems as though the root object have resolvers.
https://lacinia.readthedocs.io/en/latest/roots.html#root-object-names
And I see them in the compiled schema.
(get-in <compiled-schema> [:Query :resolver])
I suspect it is never called
do any other object types have resolvers?
I see them for :Query, :Mutation and :Subscription.
my schema has nil for that
Weird,,, I can see all the keys on the compiled schema. And they include those three objects.
Try this: (keys (schema/compile {}))
You misunderstand me, I have the :Query key, but not :resolver on that
:Query is a type
Ah.
not a field
(-> (l.schema/compile {})
:Query
keys)
=> (:category :type-name :description :fields :directives :compiled-directives :implements :tag :compiled-schema)but fields have resolvers
As a root object, isn’t it the parent of all my queries?
yes, it is
but resolvers live on edges, and there’s no field edge to them
again, maybe there is some way, but it won’t be a resolver
I always get nil as the “parent value” for any field on a root object
I’m digging through the code to see where that is initialized
it seems to be in executor/execute-query
I see a comment there
;; Outside of subscriptions, the ::root-value is nil.root-value (_::resolved-value_ context) (line 388 in my version) is where it’s initialized
so you could set ::executor/root-value on your context in your interceptor, but that still doesn’t let you any closer to what you want vs using public interfaces
I’m trying to find where the multiple operations in a single request are “iterated” ….
can gql execute multiple operations in one request?
do you mean multiple fields of a root object ?
e.g. multiple Mutation fields?
Would you consider a query and a mutation in the same request as multiple operations?
yes, they have to be
because they share no object in common
Right. But two queries, no?
(I’m still learning the GraphQL vocabulary)
An operation is a (possibly named and parameterized) selection of fields from one of the root objects
OK. So it’s not possible to have multiple operations in the same request -that simplifies the problem quite a bit.
you can still select multiple fields
Right, but in terms of differentiated context, that means only one setup of context per “request”
and you can also send multiple operations, but only execute one
whaaa?
> To execute a request, the executor must have a parsed https://spec.graphql.org/draft/#Document and a selected operation name to run if the document defines multiple operations, otherwise the document is expected to only contain a single operation. The result of the request is determined by the result of executing this operation according to the “Executing Operations” section below.
This is gql minutia, I don’t think you’ll encounter this in the wild outside of extensions
Got it. I can imagine some scenarios where the late binding could be useful.
I know the apollo ecosystem has some extension that allows operation batching, but it’s basically http pipelining
I assumed that because the syntax of a Document can include multiple operations that it was possible execute multiple operations in a single request. Wrong!
circling back, there’s probably some way in interceptor-land to inspect the root context right before it is sent to execute-query, determine if it’s a mutation/query/subscription, alter the app-context appropriately, and then send it down
But I’m still going to have to hack something together to differentiate the contexts. Although now it’s feasible to do it once in an interceptor.
or, you can just wrap those top-level field resolvers
Yeah, we’re thinking the same thing.
honestly wrapping isn’t a bad idea in general because of how error handling works with promises. a catch-all-exceptions and with-warning/error if caught is a lifesaver
resolvers are expected never to throw
I’ll take a look at the parsed (but not executed) query and see if the operation type is easily discerned. If not, I’m going to wrappers.
As usual, you have been a big help. Thank you.
It turns out to be trivial to differentiate in an interceptor based on (:operation-type <parsed-query>).