diff --git a/.changeset/lucky-icons-marry.md b/.changeset/lucky-icons-marry.md new file mode 100644 index 0000000000..fda31a306b --- /dev/null +++ b/.changeset/lucky-icons-marry.md @@ -0,0 +1,5 @@ +--- +"@neo4j/graphql": patch +--- + +Fix cardinality validation on nested unions diff --git a/packages/graphql/src/translate/create-update-and-params.ts b/packages/graphql/src/translate/create-update-and-params.ts index 141335b5b3..898deb89b2 100644 --- a/packages/graphql/src/translate/create-update-and-params.ts +++ b/packages/graphql/src/translate/create-update-and-params.ts @@ -343,6 +343,30 @@ export default function createUpdateAndParams({ } if (update.connect) { + if (relationField.interface) { + if (!relationField.typeMeta.array) { + const inStr = relationField.direction === "IN" ? "<-" : "-"; + const outStr = relationField.direction === "OUT" ? "->" : "-"; + + const validatePredicates: string[] = []; + refNodes.forEach((refNode) => { + const validateRelationshipExistence = `EXISTS((${varName})${inStr}[:${relationField.type}]${outStr}(:${refNode.name}))`; + validatePredicates.push(validateRelationshipExistence); + }); + + if (validatePredicates.length) { + subquery.push("WITH *"); + subquery.push( + `WHERE apoc.util.validatePredicate(${validatePredicates.join( + " OR " + )},'Relationship field "%s.%s" cannot have more than one node linked',["${ + relationField.connectionPrefix + }","${relationField.fieldName}"])` + ); + } + } + } + const connectAndParams = createConnectAndParams({ context, callbackBucket, @@ -402,6 +426,37 @@ export default function createUpdateAndParams({ }; } + if (!relationField.typeMeta.array) { + subquery.push("WITH *"); + + const validatePredicateTemplate = (condition: string) => + `WHERE apoc.util.validatePredicate(${condition},'Relationship field "%s.%s" cannot have more than one node linked',["${relationField.connectionPrefix}","${relationField.fieldName}"])`; + + const singleCardinalityValidationTemplate = (nodeName) => + `EXISTS((${varName})${inStr}[:${relationField.type}]${outStr}(:${nodeName}))`; + + if (relationField.union && relationField.union.nodes) { + const validateRelationshipExistence = relationField.union.nodes.map( + singleCardinalityValidationTemplate + ); + subquery.push( + validatePredicateTemplate(validateRelationshipExistence.join(" OR ")) + ); + } else if (relationField.interface && relationField.interface.implementations) { + const validateRelationshipExistence = relationField.interface.implementations.map( + singleCardinalityValidationTemplate + ); + subquery.push( + validatePredicateTemplate(validateRelationshipExistence.join(" OR ")) + ); + } else { + const validateRelationshipExistence = singleCardinalityValidationTemplate( + refNode.name + ); + subquery.push(validatePredicateTemplate(validateRelationshipExistence)); + } + } + const { create: nestedCreate, params, diff --git a/packages/graphql/tests/integration/issues/1430.int.test.ts b/packages/graphql/tests/integration/issues/1430.int.test.ts index fe376b4d72..fd265ba8bb 100644 --- a/packages/graphql/tests/integration/issues/1430.int.test.ts +++ b/packages/graphql/tests/integration/issues/1430.int.test.ts @@ -158,7 +158,7 @@ describe("https://github.com/neo4j/graphql/issues/1430", () => { const updateMutation = ` mutation ddfs{ ${testAbce.operations.update}(where: { id: "${abcesId}" } - create: { interface: { node: { ${testChildOne.name}: { name: "childone name2" } } } } + update: { interface: { create: { node: { ${testChildOne.name}: { name: "childone name2" } } } } } ){ ${testAbce.plural} { id @@ -232,10 +232,10 @@ describe("https://github.com/neo4j/graphql/issues/1430", () => { const abcesId = (createMutationResults.data as any)[testAbce.operations.create][testAbce.plural][0].id; const updateMutation = ` - mutation { - ${testAbce.operations.update}( - where: { id: "${abcesId}" } - connect: { interface: { where: { node: { name: "childone name connect" } } } } + mutation { + ${testAbce.operations.update}( + where: { id: "${abcesId}" } + update: { interface: { connect: { where: { node: { name: "childone name connect" } } } } } ) { ${testAbce.plural} { id @@ -312,7 +312,7 @@ describe("https://github.com/neo4j/graphql/issues/1430", () => { mutation { ${testAbce.operations.update}( where: { id: "${abcesId}" } - create: { interface: { node: { ${testChildOne.name}: { name: "childone anme nested create" } } } } + update: { interface: { create: { node: { ${testChildOne.name}: { name: "childone anme nested create" } } } } } ) { ${testAbce.plural} { id diff --git a/packages/graphql/tests/tck/issues/1430.test.ts b/packages/graphql/tests/tck/issues/1430.test.ts index 9dbb51fcb3..ac3c39c85f 100644 --- a/packages/graphql/tests/tck/issues/1430.test.ts +++ b/packages/graphql/tests/tck/issues/1430.test.ts @@ -119,7 +119,7 @@ describe("https://github.com/neo4j/graphql/issues/1430", () => { mutation { updateAbces( where: { id: "TestId" } - connect: { interface: { where: { node: { name: "childone name connect" } } } } + update: { interface: { connect: { where: { node: { name: "childone name connect" } } } } } ) { abces { id @@ -138,42 +138,54 @@ describe("https://github.com/neo4j/graphql/issues/1430", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "MATCH (this:ABCE) WHERE this.id = $param0 + WITH this + CALL { + WITH this WITH * WHERE apoc.util.validatePredicate(EXISTS((this)-[:HAS_INTERFACE]->(:ChildOne)) OR EXISTS((this)-[:HAS_INTERFACE]->(:ChildTwo)),'Relationship field \\"%s.%s\\" cannot have more than one node linked',[\\"ABCE\\",\\"interface\\"]) WITH * CALL { WITH this - OPTIONAL MATCH (this_connect_interface0_node:ChildOne) - WHERE this_connect_interface0_node.name = $this_connect_interface0_node_param0 + OPTIONAL MATCH (this_interface0_connect0_node:ChildOne) + WHERE this_interface0_connect0_node.name = $this_interface0_connect0_node_param0 CALL { WITH * - WITH collect(this_connect_interface0_node) as connectedNodes, collect(this) as parentNodes + WITH collect(this_interface0_connect0_node) as connectedNodes, collect(this) as parentNodes CALL { WITH connectedNodes, parentNodes UNWIND parentNodes as this - UNWIND connectedNodes as this_connect_interface0_node - MERGE (this)-[:HAS_INTERFACE]->(this_connect_interface0_node) + UNWIND connectedNodes as this_interface0_connect0_node + MERGE (this)-[:HAS_INTERFACE]->(this_interface0_connect0_node) } } - WITH this, this_connect_interface0_node - RETURN count(*) AS connect_this_connect_interface_ChildOne0 + WITH this, this_interface0_connect0_node + RETURN count(*) AS connect_this_interface0_connect_ChildOne0 + } + RETURN count(*) AS update_this_ChildOne } CALL { - WITH this - OPTIONAL MATCH (this_connect_interface1_node:ChildTwo) - WHERE this_connect_interface1_node.name = $this_connect_interface1_node_param0 + WITH this + WITH * + WHERE apoc.util.validatePredicate(EXISTS((this)-[:HAS_INTERFACE]->(:ChildOne)) OR EXISTS((this)-[:HAS_INTERFACE]->(:ChildTwo)),'Relationship field \\"%s.%s\\" cannot have more than one node linked',[\\"ABCE\\",\\"interface\\"]) + WITH * + CALL { + WITH this + OPTIONAL MATCH (this_interface0_connect0_node:ChildTwo) + WHERE this_interface0_connect0_node.name = $this_interface0_connect0_node_param0 CALL { WITH * - WITH collect(this_connect_interface1_node) as connectedNodes, collect(this) as parentNodes + WITH collect(this_interface0_connect0_node) as connectedNodes, collect(this) as parentNodes CALL { WITH connectedNodes, parentNodes UNWIND parentNodes as this - UNWIND connectedNodes as this_connect_interface1_node - MERGE (this)-[:HAS_INTERFACE]->(this_connect_interface1_node) + UNWIND connectedNodes as this_interface0_connect0_node + MERGE (this)-[:HAS_INTERFACE]->(this_interface0_connect0_node) } } - WITH this, this_connect_interface1_node - RETURN count(*) AS connect_this_connect_interface_ChildTwo1 + WITH this, this_interface0_connect0_node + RETURN count(*) AS connect_this_interface0_connect_ChildTwo0 + } + RETURN count(*) AS update_this_ChildTwo } WITH * CALL { @@ -198,8 +210,7 @@ describe("https://github.com/neo4j/graphql/issues/1430", () => { expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"TestId\\", - \\"this_connect_interface0_node_param0\\": \\"childone name connect\\", - \\"this_connect_interface1_node_param0\\": \\"childone name connect\\", + \\"this_interface0_connect0_node_param0\\": \\"childone name connect\\", \\"resolvedCallbacks\\": {} }" `); diff --git a/packages/graphql/tests/tck/rfcs/rfc-003.test.ts b/packages/graphql/tests/tck/rfcs/rfc-003.test.ts index a5b3bfc595..584a3e70e1 100644 --- a/packages/graphql/tests/tck/rfcs/rfc-003.test.ts +++ b/packages/graphql/tests/tck/rfcs/rfc-003.test.ts @@ -620,6 +620,8 @@ describe("tck/rfs/003", () => { "MATCH (this:Movie) WHERE this.id = $param0 WITH this + WITH * + WHERE apoc.util.validatePredicate(EXISTS((this)<-[:DIRECTED]-(:Director)),'Relationship field \\"%s.%s\\" cannot have more than one node linked',[\\"Movie\\",\\"director\\"]) CREATE (this_director0_create0_node:Director) SET this_director0_create0_node.id = $this_director0_create0_node_id MERGE (this)<-[:DIRECTED]-(this_director0_create0_node)