Two Nodes with multiple connections of same type

Hi everybody, even though I am using graphql and neo4j for some time I stumbled over this problem.
We usually use neo4j/graphql for creating queries, but all our mutations are written by hand. So I wanted to research a little bit on how mutations work with neo4j/graphql

If you want to connect two nodes multiple times with neo4j/graphql library, where the connection have different relationship-properties, how would one express this in schema.graphql?

Example:
You have nodes of type City.
Two Cities may have multiple connections, for instance Train and Car. The distances may vary from transportationType to transportationType.

scalar Neo4jDate

type City implements  @mutation (operations: [CREATE, UPDATE, DELETE]) {
  id: ID! @id
  name: String!
  connectedTo: [City!]! @relationship(type: "CITY_CONNECTION", properties: "CityConnectionProperties", direction: OUT)
}

interface CityConnectionProperties {
  transportationType: TransportationType! 
  distance: Int! 
}

enum TransportationType {
  Car
  Train
}

With this schema definition, i can only create one relation between two cities. If I try to add multiple of them i get the following error:

Neo4jGraphQLRelationshipValidationError: City.connectedTo required exactly once for a specific City

However, on neo4j level it is absolutely fine to do something like this:

This project is am absolute minimal setup just with apollo and neo4j/graphql library.

Can someone point me out where I am wrong in my head?

Thanks in advance.

City nodes will have both incoming and outgoing relationships of the same type. Not an expert on GraphQL but maybe it is as simple as specifying queryDirection: DEFAULT_DIRECTED?

Thanks for the answer.
Unfortunately the direction is not the issue here.
With this schema, I can't create a second connection between two nodes .
For instance:
Hamburg an Bremen are connected.
They should have two connections of type "CITY_CONNECTION". One of type Train, one of type Car with different distances.

Mutation:

mutation UpdateCities($connect: CityConnectInput, $where: CityWhere) {
  updateCities(connect: $connect, where: $where) {
    cities {
      id
      name
    }
    info {
      nodesCreated
      nodesDeleted
      relationshipsCreated
      relationshipsDeleted
    }
  }
}

Input

{
  "connect": {
    "connectedTo": [
      {
        "edge": {
          "distance": 125,
          "transportationType": "Train"
        },
        "overwrite": false,
        "where": {
          "node": {
            "id": "1d17637f-c762-493c-a2d9-79436a6c851b"
          }
        }
      }
    ]
  },
  "where": {
    "id": "28c198af-ccf9-478a-b7cd-48c7fac4efd2"
  }
}

Results in:

{
  "data": {
    "cities": [
      {
        "id": "28c198af-ccf9-478a-b7cd-48c7fac4efd2",
        "name": "Hamburg",
        "connectedToConnection": {
          "edges": [
            {
              "properties": {
                "transportationType": "Train",
                "distance": 125
              },
              "node": {
                "id": "1d17637f-c762-493c-a2d9-79436a6c851b",
                "name": "Bremen"
              }
            }
          ]
        }
      },
      {
        "id": "1d17637f-c762-493c-a2d9-79436a6c851b",
        "name": "Bremen",
        "connectedToConnection": {
          "edges": [
            {
              "properties": {
                "transportationType": "Train",
                "distance": 125
              },
              "node": {
                "id": "28c198af-ccf9-478a-b7cd-48c7fac4efd2",
                "name": "Hamburg"
              }
            }
          ]
        }
      }
    ]
  }
}

But if i want to add another connection for those two cities with type car, i get this behaviour:

Input:

{
  "connect": {
    "connectedTo": [
      {
        "edge": {
          "distance": 150,
          "transportationType": "Car"
        },
        "overwrite": false,
        "where": {
          "node": {
            "id": "1d17637f-c762-493c-a2d9-79436a6c851b"
          }
        }
      }
    ]
  },
  "where": {
    "id": "28c198af-ccf9-478a-b7cd-48c7fac4efd2"
  }
}

Response:

{
  "errors": [
    {
      "message": "City.connectedTo required exactly once for a specific City",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "updateCities"
      ],
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "stacktrace": [
          "Neo4jGraphQLRelationshipValidationError: City.connectedTo required exactly once for a specific City",
          "    at new Neo4jGraphQLError (/Users/jgeissler/Documents/privat/graphql-server-example/node_modules/@neo4j/graphql/dist/classes/Error.js:25:9)",
          "    at new Neo4jGraphQLRelationshipValidationError (/Users/jgeissler/Documents/privat/graphql-server-example/node_modules/@neo4j/graphql/dist/classes/Error.js:56:9)",
          "    at Executor.formatError (/Users/jgeissler/Documents/privat/graphql-server-example/node_modules/@neo4j/graphql/dist/classes/Executor.js:86:24)",
          "    at Executor.execute (/Users/jgeissler/Documents/privat/graphql-server-example/node_modules/@neo4j/graphql/dist/classes/Executor.js:73:24)",
          "    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)",
          "    at async execute (/Users/jgeissler/Documents/privat/graphql-server-example/node_modules/@neo4j/graphql/dist/utils/execute.js:28:20)",
          "    at async Object.resolve [as updateCities] (/Users/jgeissler/Documents/privat/graphql-server-example/node_modules/@neo4j/graphql/dist/schema/resolvers/mutation/update.js:35:31)"
        ]
      }
    }
  ],
  "data": null
}

In my mind this should work, as the properties from the relation are different. I know this can easily be done via Cypher, but how to model it with the neo4j/graphql lib?

:face_with_monocle:

Thanks in advance!

Hey @GevatterTod, this looks like it could be a bug because that error should only ever happen if the relationship field is defined as City! (without the list) - it comes from checking the cardinality of the relationship.

I do notice you're using an older version of the library (relationship properties are being defined as an interface) - any chance you could get fully up to date and see if you're still having an issue here?

Hi @darrell.warde,

thank you for answering.

I updated the libs as you suggested, deleted node_modules (to be safe), did npm install and updated the code to not use the interface but a type instead.

extract from package.json

"dependencies": {
    "@apollo/server": "^4.11.0",
    "@neo4j/graphql": "^5.9.0",
    "copyfiles": "^2.4.1",
    "dotenv": "^16.4.5",
    "graphql": "^16.9.0",
    "neo4j-driver": "^5.26.0"
  },
  "devDependencies": {
    "@types/node": "^22.5.1",
    "typescript": "^5.5.4"
  }

schema.graphql

type City @mutation (operations: [CREATE, UPDATE, DELETE]) {
  id: ID! @id
  name: String!
  connectedTo: [City!]! @relationship(type: "CITY_CONNECTION", properties: "CityConnectionProperties", direction: OUT, queryDirection: DEFAULT_UNDIRECTED)
}

type CityConnectionProperties @relationshipProperties {
  transportationType: TransportationType! 
  distance: Int! 
}

enum TransportationType {
  Car
  Train
}

query for cities:

query Cities {
  cities {
    id
    name
    connectedToConnection {
      edges {
        properties {
          transportationType
          distance
        }
        node {
          id
          name
        }
      }
    }
  }
}

response:

{
  "data": {
    "cities": [
      {
        "id": "28c198af-ccf9-478a-b7cd-48c7fac4efd2",
        "name": "Hamburg",
        "connectedToConnection": {
          "edges": [
            {
              "properties": {
                "transportationType": "Car",
                "distance": 180
              },
              "node": {
                "id": "1d17637f-c762-493c-a2d9-79436a6c851b",
                "name": "Bremen"
              }
            }
          ]
        }
      },
      {
        "id": "1d17637f-c762-493c-a2d9-79436a6c851b",
        "name": "Bremen",
        "connectedToConnection": {
          "edges": [
            {
              "properties": {
                "transportationType": "Car",
                "distance": 180
              },
              "node": {
                "id": "28c198af-ccf9-478a-b7cd-48c7fac4efd2",
                "name": "Hamburg"
              }
            }
          ]
        }
      }
    ]
  }
}

update mutation:

mutation UpdateCities($where: CityWhere, $update: CityUpdateInput) {
  updateCities(where: $where, update: $update) {
    cities {
      id
      name
    }
    info {
      nodesCreated
      nodesDeleted
      relationshipsCreated
      relationshipsDeleted
    }
  }
}

update mutation input:

{
  "update": {
    "connectedTo": [
      {
        "connect": [
          {
            "where": {
              "node": {
                "id": "1d17637f-c762-493c-a2d9-79436a6c851b"
              }
            },
            "edge": {
              "distance": 180,
              "transportationType": "Train"
            },
            "overwrite": false
          }
        ]
      }
    ]
  },
  "where": {
    "id": "28c198af-ccf9-478a-b7cd-48c7fac4efd2"
  }
}

response update mutation:

{
  "errors": [
    {
      "message": "City.connectedTo required exactly once for a specific City",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "updateCities"
      ],
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "stacktrace": [
          "Neo4jGraphQLRelationshipValidationError: City.connectedTo required exactly once for a specific City",
          "    at new Neo4jGraphQLError (/Users/jgeissler/Documents/privat/graphql-server-example/node_modules/@neo4j/graphql/dist/classes/Error.js:25:9)",
          "    at new Neo4jGraphQLRelationshipValidationError (/Users/jgeissler/Documents/privat/graphql-server-example/node_modules/@neo4j/graphql/dist/classes/Error.js:56:9)",
          "    at Executor.formatError (/Users/jgeissler/Documents/privat/graphql-server-example/node_modules/@neo4j/graphql/dist/classes/Executor.js:86:24)",
          "    at Executor.execute (/Users/jgeissler/Documents/privat/graphql-server-example/node_modules/@neo4j/graphql/dist/classes/Executor.js:73:24)",
          "    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)",
          "    at async execute (/Users/jgeissler/Documents/privat/graphql-server-example/node_modules/@neo4j/graphql/dist/utils/execute.js:28:20)",
          "    at async Object.resolve [as updateCities] (/Users/jgeissler/Documents/privat/graphql-server-example/node_modules/@neo4j/graphql/dist/schema/resolvers/mutation/update.js:37:31)"
        ]
      }
    }
  ],
  "data": null
}

I could provide you a sample project if that helps.

Thanks in advance!

@GevatterTod thank you for getting that fully up-to-date!

Either a sample project or reproduction steps in a GitHub issue (Issues · neo4j/graphql · GitHub) would be incredibly helpful here - I think this could be a bug which we need to fix.

I hope you don't mind raising an issue for us? Thanks!

1 Like

I created a new issue in GitHub

Cheers and thanks for the help so far!
Really appreciated.

One more question. I stumbled upon this:

In version 6.0.0 of the Neo4j GraphQL Library we're going to be making some drastic changes, including removing the ability to model 1:1 relationships as in this bug report - all relationships will have to be modelled as lists. Fundamentally, we've been battling the database for the past 3 years, and it's time to have the library only support features which the database itself natively supports. Until the database supports full schema including relationship cardinality, this will not be supported in the library, to avoid these kinds of issues.

It feels like there could be a connection between the issue and this comment above.

Indeed, it's related in the sense that it's just impossible for us to insert this relationship cardinality validation in all cases, so in future we will just say that all relationships must be lists (much like in Neo4j). This will actually be in version 7.0.0 according to our plan, but it's coming!

But the problem in this thread feels more like a bug because the Cypher that is causing this error just shouldn't be being generated with the relationship being generated as a list.

Thanks for the issue - we'll take a look!