Create new node, relationship and instantiate properties

Hi I'm quite new to GRANDstack but so far it's been quite enjoyable. As of right now I am trying to create a new node in my database with a relation and fill out its properties.

Right now I am attempting to construct a Definition from my front end. In order for a definition to be complete we require the following:

  1. A title
  2. A Section that it is related by DEFINITION_OF
  3. A list of other definitions that are used in the current definition. (USED_BY)

Here is the section of the schema that will put this into context

type Section {
    title: String!
    definitions: [Definition] @relation(name: "DEFINITION_OF", direction: IN)
    theorems: [Theorem] @relation(name: "THEOREM_OF", direction: IN)
}

type Definition {
    title: String!
    content: String
    definitionsUsed: [Definition] @relation(name: "USED_BY", direction: IN)
}

I am using the auto generated resolvers that neo4j-graphql.js has made for me. So in order to complete the three steps, I need to use the following.

Now in the front end I am trying to figure out how I can do these three actions when then "create definition" button is pressed.

So far I have the following queries defined:

const CREATE_DEFINITION = gql`
  mutation CreateDefinition($title: String!, $content: String!) {
    CreateDefinition(title: $type, content: $content) {
      title
      content
    }
  }
`;

const CONNECT_DEF_TO_SEC  = gql`
  mutation DefToSec($di: _DefinitionInput, $si: _SectionInput) {
    AddSectionDefinitions(from: $di, to: $si) {
      from {
        title
      }
      to {
        title
      }
    }
  }
`;

const ADD_DEF_USED  = gql`
  mutation DefUsed($df: _DefinitionInput, $dt: _DefinitionInput) {
    AddDefinitionDefinitionsUsed(from: $df, to: $dt) {
      from {
        title
      }
      to {
        title
      }
    }
  }
`;

And then I was going to an example from: Mutations in Apollo Client - Apollo GraphQL Docs, just doing three different mutations in sequence. Although I was a little confused on how where I would do my three ... useMutation(...) calls, as in their example they something like this (sections omitted for brevity):

const GET_TODOS = gql`
  query GetTodos {
    todos {
      id
    }
  }
`;

function AddTodo() {
  let input;
  const [addTodo] = useMutation(ADD_TODO, {
   ...
  return (
    <div>
      <form
        onSubmit={e => {
          e.preventDefault();
          addTodo({ variables: { type: input.value } });
          input.value = "";
        }}
       ...
}

And my code is a react class like this:

class NewDefinitionForm extends React.Component {


  constructor(props) {
    ....
  }

  handleChange(event) {
    this.setState({inputValue: event.target.value});
  }

  handleSubmit(event) {
    alert('A name was submitted: ' + this.state.value);
    // defs_used = this.duValue.split(',');
    event.preventDefault();
  }

  render() {
    return (
      <div>
        <h4>Create a new definition:</h4>
        <form onSubmit={this.handleSubmit}>
          <label>
            Title:
            <input type="text" value={this.state.inputValue} onChange={this.handleChange} />
            Content:
            <textarea value={this.state.taValue} onChange={this.handleChange} />
            Definitions Used:
            <textarea value={this.state.duValue} onChange={this.handleChange} />
          </label>
          <input type="submit" value="Create" />
        </form>
      </div>
    );
  }
}

And so everything is a method inside of it, so I'm not to sure "when" to make those ...useMutation(...) calls. (this could be the total wrong approach anyways)

At this point, I haven't seen people doing multiple queries at once, and was thinking it might be a better idea to move this logic to one custom mutation in my schema.

I just wanted to have some support/feedback on what the best decision is here. Thanks so much!

In situations like that, where you have a few operations to perform, it's quite a bit easier to go ahead and write your own custom Cypher mutation that takes care of this action in one operation vs. use 3 different auto mutations to perform the same action. However, if you're going to go that route and use three separate mutations, you can chain them together like:

const [mutation1, {...stuff}]  = useMutation(YOUR_MUTATION);
const [mutation2, {...stuff}]  = useMutation(YOUR_MUTATION);
const [mutation3, {...stuff}]  = useMutation(YOUR_MUTATION);

const yourFunction = async () => {
  try {
    const yourVariable = await mutation1({variables: {...your variables})
  } catch (error) {
      console.warn(error)
    }
  ..... the other mutations in order that you want to run
}

As you've already pointed out, that's a lot of code to run but if you don't want to write your own custom Cypher mutation, it's how you'll have to do it.

1 Like

Some quick follow up questions.

I would never just make a Definition without doing those three steps I showed, is there a way to just disable that auto-generated resolver?

One of the auto-generated resolvers called AddDefinitionDefinitionsUsed (my third screenshot) took in from: _DefinitionInput!, to: _DefinitionInput! and the _DefinitionInput only required the title of the Definition. I found this confusing because I know that the titles don't have to be unique in my database (I thought we might have had to provide the id of the definition instead).

Do you know what would happen in the case where I have multiple definitions with the same name? And do you think I should change it to having to provide the id?

Edit: I was working on the custom cypher query and I ran into this:

I was planning on using the cypher generated ID's but not it seems like a bad idea. How should I be generating my ID's then?

Edit2: I've been trying to come up with the cypher query that would do this, so far I have the following:

MATCH (s) WHERE id(s) = 6 
CREATE (s) <- [:DEFINITION_OF] - (x :Definition {title: 'newest', content: 'content'})  
WITH x, [10, 7] as ids 
UNWIND ids as i  
MATCH (d) WHERE id(d) = i
CREATE (x) <- [:USED_BY] - (d) 
RETURN x

which results in the correct behavior:

Though when I try to make a similar call from the grapql playground nothing occurs in the database.

Here is the mutation in the schema:


type Mutation {
    createDefinition(sec_id: ID!, title: String!, content: String, definitionsUsed: [ID!]) : Definition @cypher(statement: """
    MATCH (s) WHERE id(s) = $sec_id 
    CREATE (s) <- [:DEFINITION_OF] - (x :Definition {title: $title, content: $content})  
    WITH x, $definitionsUsed as ids 
    UNWIND ids as i  
    MATCH (d) WHERE id(d) = i
    CREATE (x) <- [:USED_BY] - (d) 
    RETURN x
    """)
}

and the output in the console as it was run.

09:02:00 api | [nodemon] starting `babel-node src/index.js`
09:02:02 api | GraphQL server ready at http://0.0.0.0:4001/graphql
09:05:07 api | 2020-10-30T13:05:07.686Z neo4j-graphql-js CALL apoc.cypher.doIt("MATCH (s) WHERE id(s) = $sec_id 
09:05:07 api | CREATE (s) <- [:DEFINITION_OF] - (x :Definition {title: $title, content: $content})  
09:05:07 api | WITH x, $definitionsUsed as ids 
09:05:07 api | UNWIND ids as i  
09:05:07 api | MATCH (d) WHERE id(d) = i
09:05:07 api | CREATE (x) <- [:USED_BY] - (d) 
09:05:07 api | RETURN x", {sec_id:$sec_id, title:$title, content:$content, definitionsUsed:$definitionsUsed, first:$first, offset:$offset}) YIELD value
09:05:07 api |     WITH apoc.map.values(value, [keys(value)[0]])[0] AS `definition`
09:05:07 api |     RETURN `definition` { .title } AS `definition`
09:05:07 api | 2020-10-30T13:05:07.687Z neo4j-graphql-js {
09:05:07 api |   "sec_id": "6",
09:05:07 api |   "title": "new_lad",
09:05:07 api |   "content": "nuffing",
09:05:07 api |   "definitionsUsed": [
09:05:07 api |     "10"
09:05:07 api |   ],
09:05:07 api |   "first": -1,
09:05:07 api |   "offset": 0
09:05:07 api | }

Any ideas?

You're able to exclude types in your schema generation step, neo4j-graphql-js/graphql-schema-generation-augmentation.md at master · neo4j-graphql/neo4j-graphql-js · GitHub. If that's what you want to do. You can create a random ID that will not be repeated by assigning the ID yourself, you can do that on the front end or in a custom mutation with id: randomUUID(). I would certainly add an ID and then use that to call the nodes vs. a name.

1 Like