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.
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:
parent
: this is the "current state of the object", provided by the resolvers that ran before it (this will make sense shortly)args
: if there are parameters defined in the schema, the values the request sends are passed in herecontext
: 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.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.
User.id
; call thatUser.birthDate
; call thatUser.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
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
?
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:
createUser
calls in the same operation, they have to happen one after the other, but name
, id
, and birthDate
could all run in parallel.