neo4j / graphql

A GraphQL to Cypher query execution layer for Neo4j and JavaScript GraphQL implementations.
https://neo4j.com/docs/graphql-manual/current/
Apache License 2.0
507 stars 149 forks source link

Only the last connection is updated when updating connections to an interface #2389

Closed Liam-Doodson closed 1 year ago

Liam-Doodson commented 2 years ago

Describe the bug Passing an array of nodes to connect in an update/create mutation, only the last node is actually connected. This only happens when the target of the connection is an interface type.

Type definitions

type Episode {
    runtime: Int!
    series: Series! @relationship(type: "HAS_EPISODE", direction: IN)
}

interface Production {
    title: String
    actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn")
}

type Movie implements Production {
    id: Int
    title: String
    actors: [Actor!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: IN)
}

type Series implements Production {
    title: String!
    episodes: [Episode!]! @relationship(type: "HAS_EPISODE", direction: OUT)
    actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn")
}

interface ActedIn @relationshipProperties {
    screenTime: Int
}

type Actor {
    name: String
    actedIn: [Production!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: OUT)
}

To Reproduce - Update Steps to reproduce the behaviour:

  1. Create 3 nodes by executing the following cypher:
    CREATE (m:Movie)
    CREATE (s:Series)
    CREATE (a:Actor)
    SET m.title = "MovieTitle"
    SET s.title = "SeriesTitle"
    SET a.name = "AnActor"
  2. Run a server with the above type definitions.
  3. Execute the following Mutation:
    mutation {
    updateActors(
    where: { name: "AnActor" }
    connect: {
      actedIn: [
        {
          edge: { screenTime: 1 }
          where: { node: { title: "MovieTitle" } }
        }
        {
          edge: { screenTime: 2 }
          where: { node: { title: "SeriesTitle" } }
        }
      ]
    }
    ) {
    actors {
      name
      actedInConnection {
        edges {
          screenTime
          node {
            title
          }
        }
      }
    }
    }
    }
  4. See that only the relationship to the series has been created:
    {
    "data": {
    "updateActors": {
      "actors": [
        {
          "name": "Daniel",
          "actedInConnection": {
            "edges": [
              {
                "screenTime": 2,
                "node": {
                  "title": "SeriesTitle"
                }
              }
            ]
          }
        }
      ]
    }
    }
    }

Expected behaviour A connection has been created to both the series and movie.

To Reproduce - Create Steps to reproduce the behaviour:

  1. Create 2 nodes by executing the following cypher (if not already created as above):
    CREATE (m:Movie)
    CREATE (s:Series)
    SET m.title = "MovieTitle"
    SET s.title = "SeriesTitle"
  2. Run a server with the above type definitions.
  3. Execute the following Mutation:
    mutation {
    createActors(input: [
    {
      name: "NewActor",
      actedIn: {
        connect: [
          {
            where: {
              node: {
                title: "MovieTitle"
              }
            }
          },
          {
            where: {
              node: {
                title: "SeriesTitle"
              }
            }
          }
        ]
      }
    }
    ]) {
    actors {
      name
      actedInConnection {
        edges {
          screenTime
          node {
            title
          }
        }
      }
    }
    }
    }
  4. See that only the relationship to the series has been created:
    {
    "data": {
    "createActors": {
      "actors": [
        {
          "name": "NewActor",
          "actedInConnection": {
            "edges": [
              {
                "screenTime": null,
                "node": {
                  "title": "SeriesTitle"
                }
              }
            ]
          }
        }
      ]
    }
    }
    }

Cypher:

MATCH (this:`Actor`)
WHERE this.name = $param0
WITH this
CALL {
    WITH this
    OPTIONAL MATCH (this_connect_actedIn0_node:Movie)
    WHERE this_connect_actedIn0_node.title = $this_connect_actedIn0_node_param0
    CALL {
        WITH *
        WITH collect(this_connect_actedIn0_node) as connectedNodes, collect(this) as parentNodes
        CALL {
            WITH connectedNodes, parentNodes
            UNWIND parentNodes as this
            UNWIND connectedNodes as this_connect_actedIn0_node
            MERGE (this)-[this_connect_actedIn0_relationship:ACTED_IN]->(this_connect_actedIn0_node)
            SET this_connect_actedIn0_relationship.screenTime = $this_connect_actedIn0_relationship_screenTime
            RETURN count(*) AS _
        }
        RETURN count(*) AS _
    }
        WITH this, this_connect_actedIn0_node
    RETURN count(*) AS connect_this_connect_actedIn_Movie
}
CALL {
        WITH this
    OPTIONAL MATCH (this_connect_actedIn1_node:Series)
    WHERE this_connect_actedIn1_node.title = $this_connect_actedIn1_node_param0
    CALL {
        WITH *
        WITH collect(this_connect_actedIn1_node) as connectedNodes, collect(this) as parentNodes
        CALL {
            WITH connectedNodes, parentNodes
            UNWIND parentNodes as this
            UNWIND connectedNodes as this_connect_actedIn1_node
            MERGE (this)-[this_connect_actedIn1_relationship:ACTED_IN]->(this_connect_actedIn1_node)
            SET this_connect_actedIn1_relationship.screenTime = $this_connect_actedIn1_relationship_screenTime
            RETURN count(*) AS _
        }
        RETURN count(*) AS _
    }
        WITH this, this_connect_actedIn1_node
    RETURN count(*) AS connect_this_connect_actedIn_Series
}
WITH this
CALL {
    WITH this
    OPTIONAL MATCH (this_connect_actedIn0_node:Movie)
    WHERE this_connect_actedIn0_node.title = $this_connect_actedIn0_node_param0
    CALL {
        WITH *
        WITH collect(this_connect_actedIn0_node) as connectedNodes, collect(this) as parentNodes
        CALL {
            WITH connectedNodes, parentNodes
            UNWIND parentNodes as this
            UNWIND connectedNodes as this_connect_actedIn0_node
            MERGE (this)-[this_connect_actedIn0_relationship:ACTED_IN]->(this_connect_actedIn0_node)
            SET this_connect_actedIn0_relationship.screenTime = $this_connect_actedIn0_relationship_screenTime
            RETURN count(*) AS _
        }
        RETURN count(*) AS _
    }
        WITH this, this_connect_actedIn0_node
    RETURN count(*) AS connect_this_connect_actedIn_Movie
}
CALL {
        WITH this
    OPTIONAL MATCH (this_connect_actedIn1_node:Series)
    WHERE this_connect_actedIn1_node.title = $this_connect_actedIn1_node_param0
    CALL {
        WITH *
        WITH collect(this_connect_actedIn1_node) as connectedNodes, collect(this) as parentNodes
        CALL {
            WITH connectedNodes, parentNodes
            UNWIND parentNodes as this
            UNWIND connectedNodes as this_connect_actedIn1_node
            MERGE (this)-[this_connect_actedIn1_relationship:ACTED_IN]->(this_connect_actedIn1_node)
            SET this_connect_actedIn1_relationship.screenTime = $this_connect_actedIn1_relationship_screenTime
            RETURN count(*) AS _
        }
        RETURN count(*) AS _
    }
        WITH this, this_connect_actedIn1_node
    RETURN count(*) AS connect_this_connect_actedIn_Series
}
WITH *
CALL {
    WITH this
    CALL {
        WITH this
        MATCH (this)-[this_connection_actedInConnectionthis0:ACTED_IN]->(this_Movie:`Movie`)
        WITH { screenTime: this_connection_actedInConnectionthis0.screenTime, node: { __resolveType: "Movie", title: this_Movie.title } } AS edge
        RETURN edge
        UNION
        WITH this
        MATCH (this)-[this_connection_actedInConnectionthis1:ACTED_IN]->(this_Series:`Series`)
        WITH { screenTime: this_connection_actedInConnectionthis1.screenTime, node: { __resolveType: "Series", title: this_Series.title } } AS edge
        RETURN edge
    }
    WITH collect(edge) AS edges
    WITH edges, size(edges) AS totalCount
    RETURN { edges: edges, totalCount: totalCount } AS this_actedInConnection
}
RETURN collect(DISTINCT this { .name, actedInConnection: this_actedInConnection }) AS data

Expected behavior A connection has been created to both the series and movie.

System (please complete the following information):

Additional Information The relations aren't just missing from the response, they are also missing in the actual database. Works as expected on disconnect.

neo4j-team-graphql commented 2 years ago

Many thanks for raising this bug report @Liam-Doodson. :bug: We will now attempt to reproduce the bug based on the steps you have provided.

Please ensure that you've provided the necessary information for a minimal reproduction, including but not limited to:

If you have a support agreement with Neo4j, please link this GitHub issue to a new or existing Zendesk ticket.

Thanks again! :pray:

neo4j-team-graphql commented 2 years ago

We've been able to confirm this bug using the steps to reproduce that you provided - many thanks @Liam-Doodson! :pray: We will now prioritise the bug and address it appropriately.

Liam-Doodson commented 1 year ago

Closing as this is caused by the same underlying issue as #2820