-
Couldn't load subscription status.
- Fork 2.1k
Description
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.