Article summary
According to documentation for Apollo’s GraphQL-tools:
You don’t need to specify resolvers for every type in your schema. If you don’t specify a resolver, GraphQL.js falls back to a default one…
The documentation goes on to state that the default resolver will look for a property on the parent object with the field name that’s being resolved. If that property is not a function, the value of the property is returned. But if the property does contain a function, then the default resolver calls it, and “passes the query arguments into that function.”
It wasn’t clear to me exactly what this meant, so I did some experimenting.
Normal Resolver Signature
The documentation states that every resolver function accepts four positional arguments:
fieldName(obj, args, context, info) { result }
You can read the Apollo documentation for an explanation of each argument.
When a resolver is explicitly specified for a type/field, that resolver will always be used. And even if a parent resolver provides a value for a field, it will be overridden by the result of the explicitly specified resolver.
const resolverMap = {
Query: {
getAuthor(obj, args, context, info) {
return { name: "Frank" };
},
},
Author: {
name(obj, args, context, info) {
console.log("Provided name", obj.name);
return "Hank";
}
},
};
The above resolvers would resolve to “Hank” for the author’s name (but the console.log
would print out “Frank”).
Default Resolver
However, if a resolver is not specified for a field, then the default resolver is used. As described above, if the parent object has a property with the relevant field name, that value will be returned. Modifying the previous example:
const resolverMap = {
Query: {
getAuthor(obj, args, context, info) {
return { name: "Frank" };
},
},
};
This code results in the name of the author always resolving to “Frank.”
If the property with the relevant field name contains a function, the default resolver will call that function. This function has the same signature as a normal resolver. Continuing with the same example:
function nameFunc(obj, args, context, info) {
return "Sal";
}
const resolverMap = {
Query: {
getAuthor(obj, args, context, info) {
return { name: nameFunc };
},
},
};
In this configuration, the author’s name will always be “Sal.”
Note that the args
argument are any arguments specified for that particular field. So for this query:
query {
getAuthor(id: 5){
name(foo: "bar")
}
}
The nameFunc
above would have an args
value of { foo: "bar" }
passed into it.
Conclusion
I’ve definitely gotten confused by the different ways a resolver can be specified and what can be passed into a resolver function. Hopefully, this post can help clarify things a bit.
Thanks for writing this article! I had the same confusion, and this really helps clarify things.
I tried this with latest graphql.js and apollo-server and I always get a GraphQL type error like this:
If in the query resolver I have this:
Query {
findStuff() {
return { count: async () => {
// Resolve count only
return 123; }
}
}
Float cannot represent non numeric value: [function count]
Where “Float” is whatever the field’s type is supposed to be.
If I rely on default resolver for the value and not a function, then it works.
// This works, and I don’t have to manually implement the resolver for the return type.
Query {
findStuff() {
return { count: 123 }
}
}
Also I should mention that the count function doesn’t work regardless of whether it’s async or not.
Interesting. I was using an older version of Apollo Server when I tested this out. The documentation still indicates this should work, but I wonder if something has changed? I’ll have to upgrade to the latest Apollo Server and try it out.
I found out the reason and it seems like an issue with mergeSchemas from graphql-tools.
If the resolvers are passed to mergeSchemas, they don’t do the function calling for default resolvers.