graphql icon indicating copy to clipboard operation
graphql copied to clipboard

feature request: custom logic on top of generated queries and mutations in resolvers

Open maiieul opened this issue 2 years ago • 8 comments

Is your feature request related to a problem? Please describe.

I'd like to keep using generated queries and mutations and add custom logic on top of them (e.g. request to another api/database) in a resolver with an identical name, but now it overwrites those generated queries and mutations.

Given the following type definitions from the docs:

type Actor {
    actorId: ID!
    name: String
}

type Mutation {
    createActor(name: String!): Actor
        @cypher(
            statement: """
            CREATE (a:Actor {name: $name})
            RETURN a
            """
        )
}

Let's say I'd like to be able to add custom logic on top of the createActor and deleteActors generated mutations;

But right now, the following code

const resolvers = {
  Mutation: {
    createActor: async (_source, args, context, info) => {
      console.log("I can't create an Actor anymore!")
    }
    deleteActors: async (_source, args, context, info) => {
      console.log("I can't delete Actors anymore!")
    }
  },
};

completely overwrites the generated createActor and deleteActors mutations. No cypher query gets executed.

As of now, if I want to add custom logic to queries and mutations, I have to do it in a separate resolver, leading to 2 network requests from client to server instead of 1. And I have to rewrite the cypher code manually, which not too hard when I can use the OGM but not easy when using the @cypher directive.

Describe the solution you'd like

This was actually possible in neo4j-graphql-js, simply by returning the neo4jgraphql function in each resolvers:

const resolvers = {
  Mutation: {
    createActor: async (_source, args, context, info) => {
      console.log("I can add custom logic on top of the generated mutation thanks to the neo4jgraphql function below!")
      return neo4jgraphql(_source, args, context, info);
    }
    deleteActors: async (_source, args, context, info) => {
      console.log("I can add custom logic on top of the generated mutation thanks to the neo4jgraphql function below!")
      return neo4jgraphql(_source, args, context, info);
    }
  },
};

Additional context

I think you can find inspiration on how to implement a similar function to neo4jgraphql here to prevent overwriting resolvers and here for the actual neo4jgraphql function.

maiieul avatar Jan 04 '22 17:01 maiieul

Posted this workaround in the discord, but pasting it here just in case:

// ------------------------------------------------------------------- Is there a reason you can't use something like graphql-middleware to wrap the resolver? Basically it lets you hook into the resolver tree so you can add logic or dispatch side effects.

It's not the same as getting the generated cypher, but you'd be able to modify the arguments sent to the resolver and then handle the returned results.

So, e.g., if you wanted to add filtering logic, you could hook into the resolver after it's been executed, and only return the filtered objects

// graphql-middleware resolver map

const middleware = {
  Query: {
    someQuery: async (resolve, parent, args, context, info) => {
      // do something before the query
      const newArgs = { dateCreated: new Date().toISOString(), ...args }

      // run the default resolver action
      const result = await resolve(parent, newArgs, context, info)

      // do something with the results before returning them
      return {
        ...result,
        data: result.data.filter(d => d.id !== 4)
      }
   }
}

This would also mean you could do it all in one round trip to the database, since the pre and post processing happens at the node graphql / server layer

litewarp avatar Jan 20 '22 03:01 litewarp

Thank you @litewarp for this workaround. I didn't know about graphql-middleware. Seems to be pretty much what I'm looking for indeed, and it works like a charm.

Now that I know this, I agree that the neo4jgraphql function wouldn't be that useful. Yet, I think that mentioning graphql-middleware in the docs would be useful for many users. I've seen a few people on discord who probably didn't know about it either since they seemed to be missing the neo4jgraphql function as well.

maiieul avatar Jan 20 '22 12:01 maiieul

Now that I know this, I agree that the neo4jgraphql function wouldn't be that useful. Yet, I think that mentioning graphql-middleware in the docs would be useful for many users. I've seen a few people on discord who probably didn't know about it either since they seemed to be missing the neo4jgraphql function as well.

I think documentation is a good idea. I'll try and create some example applications over the weekend that show off some relay / schema wrapping stuff, but feel free to hit me up on the discord if you get stuck with anything in particular.

litewarp avatar Jan 20 '22 18:01 litewarp

Now that I know this, I agree that the neo4jgraphql function wouldn't be that useful. Yet, I think that mentioning graphql-middleware in the docs would be useful for many users. I've seen a few people on discord who probably didn't know about it either since they seemed to be missing the neo4jgraphql function as well.

I think documentation is a good idea. I'll try and create some example applications over the weekend that show off some relay / schema wrapping stuff, but feel free to hit me up on the discord if you get stuck with anything in particular.

For sure, this should definitely be documented.

Give us a shout when you're finished with those example applications, they could be a great resource which we could drop into our README and/or documentation! 🙂

darrellwarde avatar Jan 20 '22 22:01 darrellwarde

One thing I noted in a Discord discussion of this workaround is that the autogenerated resolvers get their args by parsing the query AST, rather than using the args supplied in the resolver function. This is generally great for queries, but in mutations it prevents folks from using graphql-middleware to inject business logic that changes the shape of things like args.input before they get sent to the database.

(Posting this here so I can link it from my draft PR 😅 )

nelsonpecora avatar Feb 03 '22 15:02 nelsonpecora

Using something like graphql-middleware on top-level mutations doesn't really account for nested create, update or delete operations though, does it?

Sure, I could add logic if I wanted to start by creating an actor first, but if I create a movie and then further down the tree decide to create an actor, that logic won't be called, will it?

Tenrys avatar May 20 '22 15:05 Tenrys

@Tenrys my understanding is that the middleware is called on the Type. But on mutations, I think you're correctly it will only fire before/after the top-level mutation.

If you wanted to handle nested mutations, you could wrap the entire mutation with the middleware and then use something like graphql-parse-resolve-info to parse the selection set in the info object to see if the nested key is there.

The other options are to write a custom resolver for those nested writes, or try and use the callback directive if possible.

litewarp avatar May 21 '22 01:05 litewarp

I've thought about using the callback directive but the information passed to custom callbacks is really limited, so there's not much you can do with it from my experience.

By "the entire mutation", do you mean passing a function for Mutation so it handles all of them? I hadn't heard about that package so maybe I'll take a look at it.

Tenrys avatar May 21 '22 01:05 Tenrys

For the time being, we are going to be closing any issues requesting JavaScript lifecycle hooks, due to the reasons outlined in this comment. Pasted below for ease of reading:

This is going to be closed for the time being. I believe there to be a variety of options for hooking into the request lifecycle using JavaScript, however, we are still considering options for doing this using Cypher.

As can be seen in Apollo's documentation of the request lifecycle, there are a variety of events in which a plugin can interject. There are products such as Envelop and graphql-middleware which can be used for request injection.

darrellwarde avatar Sep 21 '22 10:09 darrellwarde