Graphql-js: Resolve data from parent fields

Created on 1 Mar 2016  路  2Comments  路  Source: graphql/graphql-js

The resolve callback on a GraphQL field gets invoked with "root" as the first argument, which can be used to calculate the data for the field. Is there anyway to use data from a parent object to compute a field?

Take this example:

This returned from the underlying api

[
  {
    "id": 123,
    "buyer": {
      "id": 234,
      "preferredCurrency": "EUR"
    },
    "item": {
      "title": "item title",
      "price": {
        "convertedAmounts": {
          "USD": 100,
          "EUR": 90,
          "GBP": 80
        }
      }
    },
    ... 
  }
]

I'd like to write a query like this:

{
  item {
    price {
      buyerPreferredAmount {
        amount
        currency
      }
    }
  }
}

Which returns:

[
  {
    "item": {
        "price": {
          "buyerPreferredAmount": {
            "amount": 90,
            "currency": "EUR"
          }
        }
    }
  }
]

Where the schema is defined like this:


const CurrencyAmount = new graphql.GraphQLObjectType({
  name: 'CurrencyAmount',
  fields: {
    amount: {type: graphql.GraphQLFloat},
    currency: {type: graphql.GraphQLString},
  }
});

const FinancialAmount = new graphql.GraphQLObjectType({
  name: "FinancialAmount",
  fields: {
    buyerPreferredAmount: {
      type: CurrencyAmount,
      resolve(root, args, info) {
         // How can I do something like this?
         // const buyerCurrency = parent.parent.parent.buyer.preferredCurrency 
         return { currency: buyerCurrency, amount: root.convertedAmounts[buyerCurrency] };
      }
    }
  }
});

const Item = new graphql.GraphQLObjectType({
  name: "Item",
  fields: {
    price: {type: FinancialAmount}
  }
});

const Order = new graphql.GraphQLObjectType({
  name: "Order",
  fields: {
    item: {type: Item}
  }
});

How can I implement the resolve method of the buyerCurrency field on FinancialAmount?

question

Most helpful comment

So there are a few ways to do this.

If you just care about a single user (probably the user making the query), you could treat that as being implicit in the query. At FB, we do this all the time; we could do username(name: "zuck"){isViewerFriend} to ask if the logged in viewer is friend's with Zuck, for example, which implicitly assumes the viewer.

If you care about different user's depending on the data (for example, an order has a buyer and an item, which looks like the case here), it gets trickier. In particular, I think the key question here is whether an item has a buyer, or whether an item is independent of the buyer, and it is only associated with the buyer in the order (I've reduced the depth of the stack here for response clarity, but the logic can be applied to whether a financial amount has a buyer as well).

  • If an item's data model has a buyer (i.e. there is a different item for every order), then your problem is solved; whatever the underlying object that is being used to represent the item is, it should have a reference to the buyer.
  • If an item _does not_ have a buyer (i.e. different orders share the same item), then it would feel weird to me to have anything associated with the buyer on the item (almost definitionally); what if you had a query with two orders for the same item? Now they are returning different data for buyerPreferredAmount depending on which part of the query we are in, which could mess with caching. So instead of having buyerPreferredAmount on item, I would suggest hoisting that field to the first level that has both the buyer and the item; in this case, that is probably the order.

So the answer to the metaquestion about getting access to the parent is that GraphQL does not allow that in general; we want the objects returned in the query to be identical no matter where they were queried from so that we can cache them clearly; if an item in a "store" and an item in an "order" and an item in a "sale list" are all the same item, then we shouldn't be able to change the data on the item by looking at the parent. On the other hand, a lot of times an object in a query _as part of its data model_ contains a parent pointer; within Facebook, you can't get the parent post from a comment through GraphQL by traversing parent pointers... but a comment as part of its data model has a getParentPost method that GraphQL can expose like any other field. Finally, when a parent pointer doesn't exist on the data model (by design) but we need access to the parent data for a field, that's usually a sign that the field is on the wrong object, and it should actually live at a higher point in the tree, where the underlying object has access to all of the data it needs.

Hope this helps!

All 2 comments

So there are a few ways to do this.

If you just care about a single user (probably the user making the query), you could treat that as being implicit in the query. At FB, we do this all the time; we could do username(name: "zuck"){isViewerFriend} to ask if the logged in viewer is friend's with Zuck, for example, which implicitly assumes the viewer.

If you care about different user's depending on the data (for example, an order has a buyer and an item, which looks like the case here), it gets trickier. In particular, I think the key question here is whether an item has a buyer, or whether an item is independent of the buyer, and it is only associated with the buyer in the order (I've reduced the depth of the stack here for response clarity, but the logic can be applied to whether a financial amount has a buyer as well).

  • If an item's data model has a buyer (i.e. there is a different item for every order), then your problem is solved; whatever the underlying object that is being used to represent the item is, it should have a reference to the buyer.
  • If an item _does not_ have a buyer (i.e. different orders share the same item), then it would feel weird to me to have anything associated with the buyer on the item (almost definitionally); what if you had a query with two orders for the same item? Now they are returning different data for buyerPreferredAmount depending on which part of the query we are in, which could mess with caching. So instead of having buyerPreferredAmount on item, I would suggest hoisting that field to the first level that has both the buyer and the item; in this case, that is probably the order.

So the answer to the metaquestion about getting access to the parent is that GraphQL does not allow that in general; we want the objects returned in the query to be identical no matter where they were queried from so that we can cache them clearly; if an item in a "store" and an item in an "order" and an item in a "sale list" are all the same item, then we shouldn't be able to change the data on the item by looking at the parent. On the other hand, a lot of times an object in a query _as part of its data model_ contains a parent pointer; within Facebook, you can't get the parent post from a comment through GraphQL by traversing parent pointers... but a comment as part of its data model has a getParentPost method that GraphQL can expose like any other field. Finally, when a parent pointer doesn't exist on the data model (by design) but we need access to the parent data for a field, that's usually a sign that the field is on the wrong object, and it should actually live at a higher point in the tree, where the underlying object has access to all of the data it needs.

Hope this helps!

Thanks for the very helpful response. I hadn't considered the caching implications. I think hoisting the fields to the higher level is the best approach.

Was this page helpful?
0 / 5 - 0 ratings