graphql
graphql copied to clipboard
feature request: custom logic on top of generated queries and mutations in resolvers
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.
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
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.
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 theneo4jgraphql
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.
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 theneo4jgraphql
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! 🙂
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 😅 )
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 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.
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.
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.