[Feature] Add resolvers to arguments
Details:
-
Instead of receiving the classic
parent, args, ctxparameters, argument resolvers would receivevaluewhich formally corresponds to the value of the argument andctxthe context. -
Unlike normal resolvers, argument resolvers can't return a value. To communicate information with the main resolver, the context will be used to attach desired information.
-
Argument resolvers will be executed sequentially in the order of the argument's definition. This allows clear communication between all argument resolvers and with the main resolver.
This feature would allow the pre-processing of arguments before reaching the main resolver. It would make parts of the code more readable and reusable.
As demonstrated in the example below, an arg resolver could fetch an item in the db using an id passed as an argument and then attach the resolved item to the context so that it can be used by the main resolver later on. It could also handle errors, such as ItemNotFound if the id doesn't match an item in the db.
The sequential execution allows arg resolvers to be reused but to behave in different ways depending on the context. In the following example, we want to ensure that Entity has a unique name before adding it to the db. But we also want to ensure the uniqueness of the name while editing Entity's name. Therefore, we can reuse our arg resolver for both mutations. However, we don't want editEntity to throw an error if the name hasn't changed. This can be solved using the context: if ctx.entity exists, we can check to see if we are changing Entity's name to a new name. If ctx.entity doesn't exist, this means we are adding a new entity to the db and that we should ensure the Entity's name uniqueness.
Exemple
import {
GraphQLObjectType,
GraphQLString,
GraphQLBoolean,
GraphQLList,
GraphQLID
} from 'graphql'
export const GraphQLEntity = new GraphQLObjectType({
name: 'Entity',
fields: () => ({
id: { type: GraphQLID },
name: { type: GraphQLString },
}),
})
export const query = {
getEntity: {
type: GraphQLEntity,
args: {
id: {
type: GraphQLID,
resolve: getEntity,
},
},
resolve: async (parent, args, ctx) => {
return ctx.entity
},
}
}
export const mutation = {
addEntity: {
type: GraphQLEntity,
args: {
name: {
type: GraphQLString,
resolve: ensureUniqueName,
},
},
resolve: addEntity,
},
editEntity: {
type: GraphQLEntity,
args: {
id: {
type: GraphQLID,
resolve: getEntity,
},
name: {
type: GraphQLString,
resolve: ensureUniqueName,
},
},
resolve: editEntity,
},
deleteEntity: {
type: GraphQLEntity,
args: {
id: {
type: GraphQLID,
resolve: getEntity,
},
},
resolve: deleteEntity,
},
}
async function getEntity(id, ctx) {
const entity = await findInDb({id: id})
if (entity === null) {
throw new Error('EntityNotFound')
}
ctx.entity = entity
}
async function ensureUniqueName(name, ctx) {
if (ctx.entity?.name !== name) {
if ((await findInDb({ name: name })) !== null) {
throw new Error('NameAlreadyInUse')
}
}
}
async function addEntity(parent, args, ctx) {
const newEntity = {
id: generateId(),
name: args.name,
}
await addToDb(newEntity)
return newEntity
}
async function editEntity(parent, args, ctx) {
ctx.entity.name = args.name
await updateInDb(ctx.entity)
return ctx.entity
}
async function deleteEntity(parent, args, ctx) {
await deleteFromDb(ctx.entity)
return ctx.entity
}
Implementation
While I believe that this feature can become very handy, it will be optional and won't change anything to the execution of the code when not used.
To make it work, a similar function to executeFields should be created to loop on fieldDef.args and run the resolvers if defined.
I've also pondered the idea for validating arguments through an argument resolver, I'd suggest some amendments:
- Argument resolvers should receive all arguments (instead of just their own values), to allow for comparisons between other arguments.
- The values returned by argument resolvers probably should just be available on the
argsobject on the main resolver, so you aren't mutating thecontextobject. - Errors thrown inside
argsresolvers would need to generate an appropriate GraphQL validation error response.
For point 3 eg.
args: {
limit: {
type: new GraphQLNonNull(GraphQLInt),
resolve(args, context) {
if (args.limit > 1000) {
throw Error("Must be < 1000.")
}
return limit;
}
}
}
There's probably a lot more to consider, the idea sort of seems an obvious one, and maybe there's a good reason we're missing as to why it hasn't been implemented.
Related: https://github.com/graphql/graphql-js/issues/361
Hello, I've worked with GraphQL lately and i encountered such scenarios where such feature is required to keep clean organized code. As I've used graphql v15.8.0 and such feature is not implemented i created it myself, check repo . If such features are now available in higher versions, please use em instead.