Skip to content

Context does not maintain value across resolve functions #953

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
ErikWittern opened this issue Jul 18, 2017 · 6 comments
Closed

Context does not maintain value across resolve functions #953

ErikWittern opened this issue Jul 18, 2017 · 6 comments

Comments

@ErikWittern
Copy link

ErikWittern commented Jul 18, 2017

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 #354, the discussion of which drifted towards parallel execution of resolve functions, though.

@DxCx
Copy link

DxCx commented Jul 18, 2017

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)

@ErikWittern
Copy link
Author

@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?

@DxCx
Copy link

DxCx commented Jul 19, 2017

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.

@ErikWittern
Copy link
Author

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

@DxCx
Copy link

DxCx commented Jul 19, 2017

@ErikWittern why not multiplexing the original id given to getUser? hence:

function getUser(id) {
  return getUserDataFromEndpoint(id)
    .then((res) => Object.assign({ id }, res));
}

@ErikWittern
Copy link
Author

@DxCx Yes, that makes sense in this example, I agree! Thank you!

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

2 participants