Skip to content

Commit

Permalink
Merge pull request #5616 from neo4j/fix-validation-on-nested-update
Browse files Browse the repository at this point in the history
Fix 1-1 validation on nested update
  • Loading branch information
angrykoala authored Oct 7, 2024
2 parents 9c38513 + b7552f8 commit ebe9c29
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 24 deletions.
5 changes: 5 additions & 0 deletions .changeset/lucky-icons-marry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@neo4j/graphql": patch
---

Fix cardinality validation on nested unions
55 changes: 55 additions & 0 deletions packages/graphql/src/translate/create-update-and-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
12 changes: 6 additions & 6 deletions packages/graphql/tests/integration/issues/1430.int.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
47 changes: 29 additions & 18 deletions packages/graphql/tests/tck/issues/1430.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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\\": {}
}"
`);
Expand Down
2 changes: 2 additions & 0 deletions packages/graphql/tests/tck/rfcs/rfc-003.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit ebe9c29

Please sign in to comment.