Search code examples
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

    • id
    • name
    • birthDate
    • createdDate

    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.

    • id: there IS a function User.id; call that
    • birthDate: there IS a function User.birthDate; call that
    • name: there is NOT a function User.name, so use the default resolver. Since you didn't pass a function for User.name, it'll just return parent.name. Here is the general idea of the "default resolver":
    const defaultResolver = (parent, args, context, info) => {
      const { fieldName } = info;
      return parent[fieldName];
    }
    

    and you asked for

    • createdDate: there is NOT a function User.createdDate, so it will uses the default resolver. Since the parent didn't have a createdDate (you didn't return that from your createUser), it returns undefined. The GraphQL engine now has to check if null (what undefined is treated as) is allowed for the createdDate. Your schema didn't have an exclamation point, so now your return data looks like this:
    {
      "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?

    • day
    • month
    • year

    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:

    • "Query" and "Mutation" (and the advanced "Subscription") are special because they're top-level, so they're entrypoints into the graph.
    • "Mutation" is additionally special in that the properties MUST run in serial from the top to the bottom of the request. This is true only for the properties of the "Mutation" type, and not nested properties. In our example if you had two createUser calls in the same operation, they have to happen one after the other, but name, id, and birthDate could all run in parallel.