Search code examples
graphqlapollographql-yoga

is it safe to use context in graphql resolvers to pass variables to nested types


I have a deeply nested type in graphql which i resolve with content at different parts of the graph (without a direct parent->child)

Some of the child property in the resolver, needs a property from a parent (but sometimes not the parent directly above), so i am passing around this object into resolver context so that leaf nodes can access it.

example:

my db has a bunch of product items

{
  "products": [
    {
      "id": "1",
      "name": "product-1",
      "options": [
        {
          "id": "colour-gray",
          "items": [
            {
              "id": "not-selected",
              "name": "not selected",
              "price": 0
            },
            {
              "id": "selected",
              "name": "selected",
              "price": 10
            }
          ]
        },
        {
          "id": "colour-red",
          "items": [
            {
              "id": "not-selected",
              "name": "not selected",
              "price": 0
            },
            {
              "id": "selected",
              "name": "selected",
              "price": 10
            }
          ]
        }
      ]
    },
    {
      "id": "2",
      "name": "product-1",
      "options": [
        {
          "id": "colour-gray",
          "items": [
            {
              "id": "not-selected",
              "name": "not selected",
              "price": 0
            },
            {
              "id": "selected",
              "name": "selected",
              "price": 30
            }
          ]
        },
        {
          "id": "colour-red",
          "items": [
            {
              "id": "not-selected",
              "name": "not selected",
              "price": 0
            },
            {
              "id": "selected",
              "name": "selected",
              "price": 25
            }
          ]
        }
      ]
    }
  ]
}

I have a resolver for products, but i need to add content regarding options dynamically and present things like prices but these options are dependent on a product Id,

the final result is something like this:

{
  "id": "colour-gray",
  "items": [
    {
      "id": "not-selected",
      "name": "not selected",
      "price": 0,
      "content": "No cost added"
    },
    {
      "id": "selected",
      "name": "selected",
      "price": 10,
      "content": "only today it will cost 10, at discounted price for product-1"
    }
  ]
}

I have a service thats fills up products, and something seperately that serves content, Both are not connected, its all done through the graph.

To go around the fact the whatever populates content is not aware of the productId, we put this in the context object.

However i am aware that the context object is shared to all resolvers for the lifetime of the request.

Now my questions are:

  • While this seems to work, i am not entirely sure if this just coincidental or not.
  • if i start resolving these asynchronously would this fall apart. -if an upgrade in graphql happens would cause this to fall apart.
  • Is this bad practice, if so how can one resolve leaf objects that need context from above the graph (from a layer above its direct parent)

I cant find anywhere in the docs how gql resolves the graph. if it populates one at time from top of the graph all the way in, or it can attempt to do things concurrently.

Any help would be appreciated.


Solution

  • TLDR: yes.

    However often it is just simpler to add values to the child object so that they can be passed down to the grandchildren.

    Your model appears to look like:

    type Product {
      id: ID!
      name: String
      options: [Option]
    }
    
    type Option {
      id: ID!
      items: [Item]
    }
    
    type Item {
      id: ID!
      name: String
      price: Number
      content: String
    }
    

    The challenge is that to properly resolve an Item you need to know the id of the grandparent Product.

    What I recommend is that you include the product's id when you resolve options - this will cause it to pass down to the Item resolver.

    The options resolver for the Product type will naturally get the id of the parent product.

    options: ({ id:productId },args,context) => database('options').where({ productId })
    

    I'm using a knexjs style ORM syntax here but it will be based on whatever db and ORM you use.

    Here we're just returning all the columns from the options table that belong to the product in question regardless of whether or not they are included in the GraphQL type definition for Option. This means the productId column is going to get included.

    Now when GraphQL resolves the items field under the Option type, the parent field will include that productId field even though it will eventually get stripped out of the final result!

    items: ( { id:optionId, productId }, args, context ) => database('items').where({ optionId, productId })