Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Add resolvers to arguments #3789

Closed
Eleveres opened this issue Nov 29, 2022 · 3 comments
Closed

[Feature] Add resolvers to arguments #3789

Eleveres opened this issue Nov 29, 2022 · 3 comments

Comments

@Eleveres
Copy link

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.

@dburles
Copy link

dburles commented Jan 30, 2023

I've also pondered the idea for validating arguments through an argument resolver, I'd suggest some amendments:

  1. Argument resolvers should receive all arguments (instead of just their own values), to allow for comparisons between other arguments.
  2. The values returned by argument resolvers probably should just be available on the args object on the main resolver, so you aren't mutating the context object.
  3. Errors thrown inside args resolvers 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: #361

@AhmedAyachi
Copy link

AhmedAyachi commented Nov 13, 2023

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.

@yaacovCR
Copy link
Contributor

yaacovCR commented Oct 2, 2024

@AhmedAyachi thank you so much for commenting. It’s great that this has been solved in user land!

I am going to close this issue for now, if there’s additional functionality that we need to integrate, we can revisit.

@yaacovCR yaacovCR closed this as not planned Won't fix, can't repro, duplicate, stale Oct 2, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants