graphql

cch1 2023-10-12T18:39:42.117269Z

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.

favila 2023-10-16T13:23:41.388059Z

Gql spec does not allow that

favila 2023-10-16T13:24:43.458889Z

It’s syntactically impossible unless you have multiple operations, in which case you must pick only one as said above

gklijs 2023-10-16T13:29:07.345889Z

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.

cch1 2023-10-16T13:31:10.775429Z

For me, appreciating this limitation really eased the burden of building in good semantics. Thanks again, @favila.

gklijs 2023-10-16T05:39:02.635449Z

What if you both have a mutation and a query on one request?

favila 2023-10-12T18:54:01.515689Z

Root objects have resolvers?

favila 2023-10-12T18:54:12.138029Z

objects have resolvers?

cch1 2023-10-12T18:54:29.212159Z

Root objects do.

favila 2023-10-12T18:54:54.999239Z

do you mean a resolver that determines the Query/Mutation/Subscription object?

favila 2023-10-12T18:55:05.262669Z

or one that resolves the field of that object?

favila 2023-10-12T18:55:20.164869Z

because the former AFAIK does not exist; the latter does

cch1 2023-10-12T18:56:06.060899Z

I want to “resolve” all Queries with a single fn before the individual resolvers on the named queries are executed.

favila 2023-10-12T18:56:38.151409Z

right, but that’s some kind of object resolver. AFAIK only field resolvers exist

cch1 2023-10-12T18:56:38.184739Z

What I really want is to modify the context for all Queries separately from the context for all Mutations.

favila 2023-10-12T18:57:07.603709Z

When I do this stuff, I have to do it in interceptor-land, before executing the query

cch1 2023-10-12T18:57:14.233739Z

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.

cch1 2023-10-12T18:57:44.932599Z

Right -in interceptor land could work, but what happens when a single request has both a mutation and a query?

favila 2023-10-12T19:00:12.495819Z

make a lacinia context with everything you need (maybe wrapped in a delay), and the root fields pick one?

cch1 2023-10-12T19:00:12.865569Z

This seems relevant: https://lacinia.readthedocs.io/en/latest/roots.html#unions

cch1 2023-10-12T19:02:24.878889Z

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).

cch1 2023-10-12T19:03:05.809179Z

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.

cch1 2023-10-12T19:03:40.564019Z

Since GraphQL is so hierarchical, I thought this would be easy. 😞

favila 2023-10-12T19:05:48.196349Z

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.

favila 2023-10-12T19:06:30.965509Z

there’s no field “edge” going into the root objects, so I’m not surprised if this isn’t possible

favila 2023-10-12T19:06:51.427579Z

there’s no place to hang a resolver; there’d have to be something special

cch1 2023-10-12T19:07:39.298379Z

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`)

cch1 2023-10-12T19:07:55.015659Z

But I see no way to wrap that resolver.

favila 2023-10-12T19:08:38.586349Z

objects don’t have resolvers, so I don’t see how wrap-resolver-to-ensure-resolver-result is relevant

favila 2023-10-12T19:09:04.502329Z

is there some magic field that resolves to the query/mutation/subscription objects?

cch1 2023-10-12T19:09:14.924709Z

My vocab may be inadequate, but it seems as though the root object have resolvers.

cch1 2023-10-12T19:09:32.017919Z

And I see them in the compiled schema.

cch1 2023-10-12T19:09:49.161329Z

(get-in <compiled-schema> [:Query :resolver])

favila 2023-10-12T19:10:38.162229Z

I suspect it is never called

favila 2023-10-12T19:11:00.498219Z

do any other object types have resolvers?

cch1 2023-10-12T19:11:26.391859Z

I see them for :Query, :Mutation and :Subscription.

favila 2023-10-12T19:11:41.730419Z

my schema has nil for that

cch1 2023-10-12T19:13:33.284229Z

Weird,,, I can see all the keys on the compiled schema. And they include those three objects.

cch1 2023-10-12T19:14:07.572119Z

Try this: (keys (schema/compile {}))

favila 2023-10-12T19:14:41.968269Z

You misunderstand me, I have the :Query key, but not :resolver on that

favila 2023-10-12T19:14:46.363419Z

:Query is a type

cch1 2023-10-12T19:14:46.734149Z

Ah.

favila 2023-10-12T19:14:50.013859Z

not a field

favila 2023-10-12T19:15:09.640799Z

(-> (l.schema/compile {})
    :Query
    keys)
=> (:category :type-name :description :fields :directives :compiled-directives :implements :tag :compiled-schema)

favila 2023-10-12T19:15:33.105739Z

but fields have resolvers

cch1 2023-10-12T19:15:52.229929Z

As a root object, isn’t it the parent of all my queries?

favila 2023-10-12T19:16:16.577829Z

yes, it is

favila 2023-10-12T19:16:29.669349Z

but resolvers live on edges, and there’s no field edge to them

favila 2023-10-12T19:17:04.870389Z

again, maybe there is some way, but it won’t be a resolver

favila 2023-10-12T19:17:56.802599Z

I always get nil as the “parent value” for any field on a root object

favila 2023-10-12T19:18:11.723449Z

I’m digging through the code to see where that is initialized

favila 2023-10-12T19:19:57.144239Z

it seems to be in executor/execute-query

👀 1
favila 2023-10-12T19:20:07.148619Z

I see a comment there

;; Outside of subscriptions, the ::root-value is nil.

favila 2023-10-12T19:20:59.856119Z

root-value (_::resolved-value_ context) (line 388 in my version) is where it’s initialized

favila 2023-10-12T19:22:07.883799Z

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

cch1 2023-10-12T19:24:23.529519Z

I’m trying to find where the multiple operations in a single request are “iterated” ….

favila 2023-10-12T19:26:01.294319Z

can gql execute multiple operations in one request?

favila 2023-10-12T19:27:25.136829Z

do you mean multiple fields of a root object ?

favila 2023-10-12T19:27:34.605519Z

e.g. multiple Mutation fields?

cch1 2023-10-12T19:28:06.690719Z

Would you consider a query and a mutation in the same request as multiple operations?

favila 2023-10-12T19:28:18.330989Z

yes, they have to be

favila 2023-10-12T19:28:24.237429Z

because they share no object in common

cch1 2023-10-12T19:28:30.915789Z

Right. But two queries, no?

cch1 2023-10-12T19:28:47.274889Z

(I’m still learning the GraphQL vocabulary)

favila 2023-10-12T19:29:45.476699Z

An operation is a (possibly named and parameterized) selection of fields from one of the root objects

cch1 2023-10-12T19:30:15.090609Z

OK. So it’s not possible to have multiple operations in the same request -that simplifies the problem quite a bit.

favila 2023-10-12T19:30:30.377059Z

you can still select multiple fields

cch1 2023-10-12T19:30:56.476569Z

Right, but in terms of differentiated context, that means only one setup of context per “request”

favila 2023-10-12T19:30:57.635319Z

and you can also send multiple operations, but only execute one

cch1 2023-10-12T19:31:08.649919Z

whaaa?

favila 2023-10-12T19:32:04.241839Z

> 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.

favila 2023-10-12T19:32:18.834169Z

This is gql minutia, I don’t think you’ll encounter this in the wild outside of extensions

cch1 2023-10-12T19:33:02.647249Z

Got it. I can imagine some scenarios where the late binding could be useful.

favila 2023-10-12T19:33:04.575349Z

I know the apollo ecosystem has some extension that allows operation batching, but it’s basically http pipelining

cch1 2023-10-12T19:34:30.461009Z

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!

favila 2023-10-12T19:35:20.870769Z

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

cch1 2023-10-12T19:35:32.302139Z

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.

favila 2023-10-12T19:35:36.249609Z

or, you can just wrap those top-level field resolvers

cch1 2023-10-12T19:35:43.223959Z

Yeah, we’re thinking the same thing.

favila 2023-10-12T19:36:41.648219Z

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

favila 2023-10-12T19:36:50.931599Z

resolvers are expected never to throw

cch1 2023-10-12T19:38:23.265939Z

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.

cch1 2023-10-12T19:38:45.499939Z

As usual, you have been a big help. Thank you.

cch1 2023-10-12T19:53:50.398689Z

It turns out to be trivial to differentiate in an interceptor based on (:operation-type <parsed-query>).