Skip to content
This repository has been archived by the owner on Sep 3, 2021. It is now read-only.

Suggestion: User-driven mutations with neo4j-graphql selection resolution #215

Closed
a-type opened this issue Mar 6, 2019 · 7 comments
Closed

Comments

@a-type
Copy link
Contributor

a-type commented Mar 6, 2019

Context: I've been developing an app with an Apollo server for the past year using plain old neo4j-driver and I was getting tired of the verbosity and worried about all the extra database queries, so I thought I'd come back to this project and see how it's doing. After integrating it, I'm super impressed with the query experience! But the mutations miss my use case completely.

My use case

Building out my GraphQL app from scratch without relying on generated code has left me with a less CRUD-style schema, which I think has some great benefits.

For instance, let's consider the authorization around allowing a user to create a new post. If I take a naive / generated CRUD approach, I've got to figure out how to properly authorize both the CreatePost and AddUserPost mutations to create my post and attach it to the user, respectively. Do I really want to allow the client app to create arbitrary relationships between Users and Posts? Obviously they should only be allowed to create posts attached to their own User. Now I have to add some kind of directive or middleware logic to enforce that.

However, if I'm less constrained by CRUD semantics, I can just create a mutation like this:

const typeDefs = `
type Mutation {
  createPost(input: PostCreateInput!): Post!
}
`

const resolvers = {
  Mutation: {
    async createPost(_parent, { input }, ctx) {
      await ctx.graph.createPostForUser(input, ctx.user.id);
    }
  }
}

I don't even have to write authorization logic in this scenario. The user can only create posts attached to their own identity, which is securely verified by their authentication token. After all I'm writing a client API, not an ORM; I don't need things to be 100% CRUD, I am trying to enable behaviors for users. On top of that, I just have one mutation for the client, not two; my backend technology decisions are not leaking into my API surface area and ruining my semantics.

Where this library comes in

Now, obviously I could just write those kinds of mutations and use neo4j-graphql-js for my read operations--and in fact that's what I do right now. But GraphQL doesn't make distinctions between read and write exclusively. When I run a mutation, my client will often request a selection set which includes deeply nested data which was not involved in the mutation itself. That's where neo4j-graphql-js really shines; the ability to convert those complex selection sets into Cypher queries automatically.

What I'd like to see is a way to enable a user to write their own mutation, Cypher and all, to do exactly what they want-- and then delegate to the Cypher conversion layer of neo4j-graphql-js to fulfill the selection set requested by the client as either another query clause, or a second query entirely.

I already tried to hack this together using the library as it is today:

const resolver = async (_parent, { input }, ctx, info) => {
  // first do our custom mutation logic using neo4j-driver
  const post = await ctx.graph.updatePost(input);
  // then pass the Post id to neo4jgraphql to do the heavy lifting
  // of generating a Cypher query for the whole `info` selection set
  return neo4jgraphql(_parent, { id: post.id }, ctx, info);
};

... but the library complained:

Error: Do not know how to handle this type of mutation. Mutation does not follow naming convention.

at which point I briefly considered manually modifying my info parameter to change the selection operation type to query and try to trick neo4jgraphql, but then decided it might be more productive to tell y'all about my use case and start some discussion on the topic.

Closing thoughts

I'll say it again, I really love the usage of this library for query operations. There's so much value just in the conversion layer between GraphQL and Cypher. Enabling more use cases may be as simple as decoupling that layer from the rest of the library so it can be reused in more situations. But, as it stands, the mutations fall short for me. It seems like my use case (serving a GraphQL client API to an app) is not really the prime target here, for sure. Maybe I'm missing the point of the library completely - do let me know if so. But it's just so close to working for me, I thought I would at least present my case here.

@a-type a-type changed the title Suggestion: Be less opinionated about mutations Suggestion: User-driven mutations with neo4j-graphql selection resolution Mar 6, 2019
@a-type
Copy link
Contributor Author

a-type commented Mar 12, 2019

Since this didn't generate much talk, I started reading the source to determine how hard it would be to fork this repo and add the features I wanted.

And, hey, it turns out there's an undocumented feature of adding a @cypher directive to a mutation to create custom mutations! See an example in the tests. I'm going to give this a shot and see if it will work for my use case. If so, I'll pivot this issue to be focused on adding documentation around this feature, since it seems to be extremely useful but isn't mentioned anywhere but the source and tests.

Update: it worked beautifully! I just added a @cypher directive to my mutation and referenced my input variables by name using query argument syntax (like $input.foo). Then in my resolver I did a little business logic and then delegated to neo4jgraphl by invoking it with the original resolver params. The proper query and selections were generated and run. This is a great feature, it need some documentation! I didn't see any contribution guide and I'm not quite sure where the docs live, though.

@a-type
Copy link
Contributor Author

a-type commented Mar 23, 2019

I read over the docs again and realized there's a whole API spec I didn't even see before which covers most of these topics. I'll go ahead and close this.

@a-type a-type closed this as completed Mar 23, 2019
@aonamrata
Copy link

lucky this post is pretty recent. Thanks to @a-type . I was trying to do a simple create and got this same error. Can you please post a sample of your schema and resolver for reference. I think I am missing something.

@a-type
Copy link
Contributor Author

a-type commented Apr 24, 2019

@aonamrata here's how I would modify my original example:

const typeDefs = `
type Mutation {
  createPost(input: PostCreateInput!): Post! 
    @cypher(
      """
      MATCH (user:User {id:$cypherParams.userId})
      CREATE (post:Post)<-[:AUTHOR_OF]-(user)
      SET post += $input
      RETURN post
      """
    )
}
`

const resolvers = {
  Mutation: {
    createPost: neo4jgraphql
  }
};

// to create my context for my GraphQL server:

const createContext = async (req) => {
  // this logic you will need to implement yourself - you just need to know the
  // authenticated user for this request, which may be stored in a token or header
  const user = await getUser(req); 
  return {
    driver, // neo4j driver
    
    // all fields in this object will be included in @cypher directives
    cypherParams: {
      userId: user && user.id
    }
  };
};

Note the use of $cypherParams within the custom Cypher query for the mutation. I populate those params when I create the context for my GraphQL server. From there it's as simple as writing a write query and setting my resolver to neo4jgraphql.

@a-type
Copy link
Contributor Author

a-type commented Apr 24, 2019

For generating unique IDs for created resources, I also created a directive @generateId which simply adds another argument (called id) to the field intrinsically. Then I can reference it by $id within my Cypher query and assign it to my new node. That's only needed if you manage your own custom unique IDs on your nodes like I do. I don't have the source for @generateId on hand, but it's one of the simplest directives you could write (see docs).

@manonthemat
Copy link

I'm getting the same error in a slightly different scenario.

I want to reuse a mutation that I've created using the cypher directive from another mutation:
https://stackoverflow.com/questions/63733026/error-do-not-know-how-to-handle-this-type-of-mutation-mutation-does-not-follow

Using neo4j-graphql-js version 2.16.1. 🤔

@MarianAlexandruAlecu
Copy link

@a-type a slightly better approach is to use $auth.jwt.sub instead of $cypherParams.userId - in this way you are sure that the logged user is attached. $auth is injected automatically on setup

Also, it would be great to have a few examples of creating custom directives that change the cypher query accordingly to our needs (for example: link the logged user).

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants