Skip to content

[Feature] Add resolvers to arguments #3789

@Eleveres

Description

@Eleveres

Details:

  • Instead of receiving the classic parent, args, ctx parameters, argument resolvers would receive value which formally corresponds to the value of the argument and ctx the 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions