graphqlgraphql-js

Understanding GraphQL Mutation and how data is passed to output field


I'm a beginner learning GraphQL and was going through the following example on

https://graphql.com/learn/mutations/

  **Mutation definition:**

         type Mutation {
              updateProducePrice(producePriceInput: UpdateProducePriceInput!): UpdateProducePriceResponse
            }
            
            input UpdateProducePriceInput {
              produceItemId: ID!
              newPrice: Int!
            }
            
            type UpdateProducePriceResponse {
              code: Int!
              success: Boolean!
              message: String!
              produceItem: Produce
            }
    
    
    **Mutation request:**
    
    mutation UpdateProducePrice($producePriceInputVariable: UpdateProducePriceInput!) {
      updateProducePrice(producePriceInput: $producePriceInputVariable) {
        success
        message
        produceItem {
          name
          price
        }
      }
    }
    
    **Variable input:**
    
        {
          "producePriceInputVariable": {
            "produceItemId": "F4",
            "newPrice": 113 
          }
        }

**Response:**

{
  "data": {
    "updateProducePrice": {
      "success": true,
      "message": "Price successfully updated for orange, from 41 to 113",
      "produceItem": {
        "name": "orange",
        "price": 113
      }
    }
  }
}

Since produceItem:Produce is defined in type UpdateProducePriceResponse and the mutation returns the name and price (which is updated from 41 to 113) for Produce type given the Id and newPrice as inputs.

How does the value of newPrice passed on to the price field as defined in Produce type? I was reading about Resolvers but couldn't figure how this would be working under the hood.


Solution

  • ELI5 answer that doesn't go into the nitty-gritty details:

    Every single field on every single object type has a "resolver". A resolver is a function with four parameters:

    1. parent: this is the "current state of the object", provided by the resolvers that ran before it (this will make sense shortly)
    2. args: if there are parameters defined in the schema, the values the request sends are passed in here
    3. context: when a request starts, you define this object, and it tells the function the meta information about the request (this is where you should look at the current user). The same value is used for every resolver.
    4. info: this is information about the state of the operation. It will include the name of the field the resolver is for, the path from this field all the way up to the root, etc.

    Let's use a trimmed-down schema to show how this works (note that this schema is good as an example, but doesn't follow best practices):

    type Mutation {
      createUser(userData: UserInput!): User
    }
    
    type Date {
      day: Int!
      month: Int!
      year: Int!
    }
    
    type User {
      id: ID!
      name: String!
      birthDate: Date!
      createdDate: Date
    }
    
    input UserInput {
      name: String!
      birthDate: String! # Let's assume yyyy/mm/dd
    }
    

    Now we want to make this request:

    mutation {
      createUser(userData: { name: "David", birthDate: "2020/12/20" }) {
        id
        birthDate {
          day
          month
          year
        }
        name
        createdDate {
          day
          month
          year
        }
      }
    }
    

    First, the GraphQL engine looks at your entrypoint, which is Mutation.createUser. It checks to see if there is a resolver object with "Mutation" in it, and it checks if there is a "createUser" function on that object. I'm going to use all synchronous code so that await/async/promises don't confuse things.

    const resolvers = {
      ... <other code>
      Mutation: {
        createUser(parent, args) {
          const { userData: { name, birthDate } } = args;
    
          const { id } = someDb.createUser({ name, birthDate });
          
          return {
            id,
            name,
          };
        }
      },
      ... <other code>
    }
    

    There IS a Mutation resolver object, and it has a property called createUser, which is a function. It calls this function and takes the response. It also knows that the response type of that function must be "User", so it adds a property called __typename. Here is the current data:

    {
      "createUser": {
        "__typename": "User",
        "id": "some-uuid-went-here",
        "name": "David"
      }
    }
    

    Great. Now it checks what fields you asked for on the object returned by "createUser". You asked for

    The __typename is User, so now the engine checks to see if you have a resolver for each of these:

    const resolvers = {
      ... <other code>
      User: {
        id(parent) {
          // let's base64 encode these for external use
          return Buffer.from(parent.id).toString('base64');
        },
        birthDate(parent) {
          // `id` here is the id returned from the parent's resolver, not the base64 value
          const { birthDate } = someDb.findUserById(parent.id);
          const dateParts = birthDate.split('/');
          return {
            day: Number(dateParts[2]),
            month: Number(dateParts[1]),
            year: Number(dateParts[0]),
          };
        },
      },
      ... <other code>
    }
    

    There is a User resolver object.

    const defaultResolver = (parent, args, context, info) => {
      const { fieldName } = info;
      return parent[fieldName];
    }
    

    and you asked for

    {
      "createUser": {
        "__typename": "User",
        "id": "c29tZS11dWlkLXdlbnQtaGVyZQ==",
        "name": "David",
        "birthDate": {
          "day": 20,
          "month": 12,
          "year": 2020
        },
        "createdDate": undefined
      }
    }
    

    It looks done, but it isn't. Now it goes to the next level. what fields did you ask for on birthDate?

    And the __typename was... Date.

    Do the resolvers have Date with functions of those properties? Let's say no it doesn't. So the default resolver will be used.

    Now your final json after all resolvers:

    {
      "createUser": {
        "__typename": "User",
        "id": "c29tZS11dWlkLXdlbnQtaGVyZQ==",
        "name": "David",
        "birthDate": {
          "day": 20,
          "month": 12,
          "year": 2020
        },
        "createdDate": undefined
      }
    }
    

    ** Side note: every type and every property works roughly the same: