I have problems using the context in resolve functions. Specifically, I want to add / change values in the context to reuse them in different resolve functions across a single query. Consider this example:
let schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'RootQueryType',
fields: {
user: {
type: new GraphQLObjectType({
name: 'user',
fields: {
name: {type: GraphQLString},
address: {
type: new GraphQLObjectType({
name: 'address',
fields: {
city: {type: GraphQLString},
street: {type: GraphQLString}
}
}),
resolve (root, args, ctx) {
return {
city: 'New York',
street: ctx.street // won't work, because ctx is once again undefined!
}
}
}
}
}),
resolve (root, args, ctx) { // ctx is initially undefined
ctx = {
street: '10th Street'
}
return {
name: 'Erik'
}
}
}
}
})
})
let query = '{user: {address: {street}}}'
graphql(schema, query)
In the resolve function for user, ctx is initially undefined. My attempt to set ctx to {street: '10th Street'} in the resolve function for user does not succeed: once the resolve function for address is called, ctx is once again undefined, causing an error.
The above code _does_ work if I provide a context explicitly during querying (i.e., if I change the last line in the code), for example:
graphql(schema, query, null, {}) // passing empty object as context
While some libraries pass a context per default, (for example, express-graphql sets the context to the Express.js request object) should graphql-js not per default provide an empty context object? Or at least hold the value set to ctx across resolve functions related to a single query?
This issue may be related to https://github.com/graphql/graphql-js/issues/354, the discussion of which drifted towards parallel execution of resolve functions, though.
Hey @ErikWittern
context SHOULD be given to graphql call, and then passed to all resolvers.
resolver may call functions inside the context object, but that's a bad practice to just rewrite fields in the object.
Also, Note that this is query. Query should not modify context all. that's what mutation are for.
so in this specific case you are abusing context.
what you probably want to do instead is to pass it as value as this is resolving of specific type, so it will look something like that:
let schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'RootQueryType',
fields: {
user: {
type: new GraphQLObjectType({
name: 'user',
fields: {
name: {type: GraphQLString},
address: {
type: new GraphQLObjectType({
name: 'address',
fields: {
city: {type: GraphQLString},
street: {type: GraphQLString}
}
}),
resolve (root, args, ctx) {
return {
city: 'New York',
street: root.street,
}
}
}
}
}),
resolve (root, args, ctx) {
return {
name: 'Erik',
street: '10th Street',
}
}
}
}
})
})
let query = '{user: {address: {street}}}'
graphql(schema, query)
@DxCx Thank you for your comments!
I have two follow-up questions, maybe you can help me with them.
First, I wonder whether having to explicitly provide a context is a good design-choice? I personally don't like the syntax graphql(schema, query, null, {}) much, especially as a rootValue (e.g., null) also needs to be passed. Having to pass context explicitly is also a trap that newbies (like myself) can easily fall into. But maybe I am just lacking understanding of the rationale behind this decision.
Second, I struggle to understand why the context should not be modified in queries. I assumed queries should be safe and idempotent with regard to the underlying data, but why with regard to the context? To give the concrete use-case I face: I have a resolve function for user, which performs an HTTP request to ../users/{id}. Another resolver fetches data on a car from ../users/{id}/car. I would like to compose these resolve functions to allow queries like this:
{
user (id: "123") {
car { // needs to be resolved by fetching data via HTTP from ../users/{id}/car
model
}
}
}
To perform the request ../users/{id}/car, I would like to reuse the id that was originally passed to the resolve function of user - how can I pass this value on to the resolve function for car? Is that not exactly what the context should be used for?
Nope, context should be used for isolating implementation from schema itself.
what you are looking for, is just using root value..
for example, context object can provide 2 functions:
{
getUser(id),
getUserCar(id),
}
this way the resolver itself of user will call ctx.getUser(args.id) while resolver of car will call ctx.getUserCar(root.id);
the function provided in context may send http request to reterive data, and later on can be updated to connect to database instead, schema remains the same.
@DxCx Again, thank you for your comments! I am getting a much better picture now about the intent / semantics of context.
In my specific scenario, the resolve function of car cannot just call ctx.getUserCar(root.id), unfortunately, because the data returned by the resolve function of user does not include the id field (while this may seem odd, it is a reality of the APIs I am writing the GraphQL interface for). So it seems I have to pass on id myself in the data returned by the resolve function of user, for example by sticking it (and other data I want to pass to other resolve functions) into a custom field _dataFromRoot (or similar). While this works, it can potentially lead to collision with fields in the response data from the HTTP request. I thus wonder whether another kind of "context" may be a good idea, which is basically used to pass data between resolve functions?
In any case, I will close this issue as it was rather a misunderstanding on my part than an actual problem with graphql-js.
@ErikWittern why not multiplexing the original id given to getUser? hence:
function getUser(id) {
return getUserDataFromEndpoint(id)
.then((res) => Object.assign({ id }, res));
}
@DxCx Yes, that makes sense in this example, I agree! Thank you!
Most helpful comment
Nope, context should be used for isolating implementation from schema itself.
what you are looking for, is just using root value..
for example, context object can provide 2 functions:
this way the resolver itself of user will call
ctx.getUser(args.id)while resolver of car will callctx.getUserCar(root.id);the function provided in context may send http request to reterive data, and later on can be updated to connect to database instead, schema remains the same.