Skip to content

Commit

Permalink
Fix Cypher when filtering by aggregations over different relationship…
Browse files Browse the repository at this point in the history
… properties types (#5943)

* Fix Cypher when filtering by aggregations over different relationship properites types

* Update friendly-pigs-wait.md

* Fix issues

* Add small explanation

* Fix edge case
  • Loading branch information
darrellwarde authored Jan 22, 2025
1 parent fd3f015 commit 6153d68
Show file tree
Hide file tree
Showing 5 changed files with 472 additions and 88 deletions.
5 changes: 5 additions & 0 deletions .changeset/friendly-pigs-wait.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@neo4j/graphql": patch
---

Fix Cypher when filtering by aggregations over different relationship properties types
14 changes: 13 additions & 1 deletion packages/graphql/src/translate/queryAST/factory/FilterFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -683,7 +683,19 @@ export class FilterFactory {
);
}

if (fieldName === "edge") {
if (fieldName === "edge" && relationship.propertiesTypeName) {
// This conditional handles when the relationship is an interface which is also being accessed through an interface
if (
isInterfaceEntity(relationship.target) &&
Object.keys(value).some((v) => relationship.siblings?.includes(v))
) {
return Object.entries(value).flatMap(([k, v]) => {
if (k === relationship.propertiesTypeName) {
return this.createAggregationNodeFilters(v as Record<string, any>, relationship);
}
return [];
});
}
return this.createAggregationNodeFilters(value as Record<string, any>, relationship);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import type { UniqueType } from "../../../../utils/graphql-types";
import { TestHelper } from "../../../../utils/tests-helper";

describe("aggregations-where-edge-string interface relationships of interface types", () => {
let testHelper: TestHelper;

let Movie: UniqueType;
let Series: UniqueType;
let Production: UniqueType;
let Actor: UniqueType;
let Cameo: UniqueType;
let Person: UniqueType;

beforeEach(async () => {
testHelper = new TestHelper();

Movie = testHelper.createUniqueType("Movie");
Series = testHelper.createUniqueType("Series");
Production = testHelper.createUniqueType("Production");
Actor = testHelper.createUniqueType("Actor");
Cameo = testHelper.createUniqueType("Cameo");
Person = testHelper.createUniqueType("Person");

const typeDefs = /* GraphQL */ `
interface ${Production} {
title: String
}
type ${Movie} implements ${Production} @node {
title: String
}
type ${Series} implements ${Production} @node {
title: String
}
interface ${Person} {
name: String
productions: [${Production}!]! @declareRelationship
}
type ${Actor} implements ${Person} @node {
name: String
productions: [${Production}!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn")
}
type ${Cameo} implements ${Person} @node {
name: String
productions: [${Production}!]! @relationship(type: "APPEARED_IN", direction: OUT, properties: "AppearedIn")
}
type ActedIn @relationshipProperties {
role: String
}
type AppearedIn @relationshipProperties {
role: String
}
`;

await testHelper.initNeo4jGraphQL({ typeDefs });
});

afterEach(async () => {
await testHelper.close();
});

test("should return nodes aggregated across different relationship properties types", async () => {
await testHelper.executeCypher(
`
CREATE (a:${Actor} { name: "A" })-[:ACTED_IN { role: "definitely too long" }]->(g:${Movie} { title: "G" })
CREATE (a)-[:ACTED_IN { role: "extremely long" }]->(g)
CREATE (b:${Actor} { name: "B" })-[:ACTED_IN { role: "a" }]->(h:${Series} { title: "H" })
CREATE (b)-[:ACTED_IN { role: "b" }]->(h)
CREATE (c:${Actor} { name: "C" })
CREATE (d:${Cameo} { name: "D" })-[:APPEARED_IN { role: "too long" }]->(i:${Movie} { title: "I" })
CREATE (d)-[:APPEARED_IN { role: "also too long" }]->(i)
CREATE (e:${Cameo} { name: "E" })-[:APPEARED_IN { role: "s" }]->(j:${Series} { title: "J" })
CREATE (e)-[:APPEARED_IN { role: "very long" }]->(j)
CREATE (f:${Cameo} { name: "F" })
`
);

const query = /* GraphQL */ `
query People {
${Person.plural}(
where: {
productionsAggregate: {
edge: {
AppearedIn: { role_SHORTEST_LENGTH_LT: 3 }
ActedIn: { role_AVERAGE_LENGTH_LT: 5 }
}
}
}
) {
name
}
}
`;

const gqlResult = await testHelper.executeGraphQL(query);

if (gqlResult.errors) {
console.log(JSON.stringify(gqlResult.errors, null, 2));
}

expect(gqlResult.errors).toBeUndefined();

expect((gqlResult.data as any)[Person.plural]).toIncludeSameMembers([{ name: "E" }, { name: "B" }]);
});

test("should return nodes aggregated across relationship properties and count", async () => {
await testHelper.executeCypher(
`
CREATE (a:${Actor} { name: "A" })-[:ACTED_IN { role: "definitely too long" }]->(g:${Movie} { title: "G" })
CREATE (a)-[:ACTED_IN { role: "extremely long" }]->(g)
CREATE (b:${Actor} { name: "B" })-[:ACTED_IN { role: "a" }]->(h:${Series} { title: "H" })
CREATE (b)-[:ACTED_IN { role: "b" }]->(h)
CREATE (b2:${Actor} { name: "B2" })-[:ACTED_IN { role: "a" }]->(h2:${Series} { title: "H2" })
CREATE (b2)-[:ACTED_IN { role: "b" }]->(h2)
CREATE (b2)-[:ACTED_IN { role: "b" }]->(h2)
CREATE (c:${Actor} { name: "C" })
CREATE (d:${Cameo} { name: "D" })-[:APPEARED_IN { role: "too long" }]->(i:${Movie} { title: "I" })
CREATE (d)-[:APPEARED_IN { role: "also too long" }]->(i)
CREATE (e:${Cameo} { name: "E" })-[:APPEARED_IN { role: "s" }]->(j:${Series} { title: "J" })
CREATE (e)-[:APPEARED_IN { role: "very long" }]->(j)
CREATE (e)-[:APPEARED_IN { role: "another very long" }]->(j)
CREATE (f:${Cameo} { name: "F" })
`
);

const query = /* GraphQL */ `
query People {
${Person.plural}(
where: { productionsAggregate: { edge: { ActedIn: { role_AVERAGE_LENGTH_LT: 5 } }, count_LT: 3 } }
) {
name
}
}
`;

const gqlResult = await testHelper.executeGraphQL(query);

if (gqlResult.errors) {
console.log(JSON.stringify(gqlResult.errors, null, 2));
}

expect(gqlResult.errors).toBeUndefined();

expect((gqlResult.data as any)[Person.plural]).toIncludeSameMembers([
{ name: "D" },
{ name: "B" },
{ name: "F" },
]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -575,42 +575,39 @@ describe("aggregations-where-edge-string interface relationships of concrete typ
]);
});


test(
"should return posts where the LONGEST edge like String is EQUAL to",
async () => {
const testString = generate({
charset: "alphabetic",
readable: true,
});

const shortestTestString = generate({
charset: "alphabetic",
readable: true,
length: 10,
});

const testString2 = generate({
charset: "alphabetic",
readable: true,
length: 11,
});

const longestTestString = generate({
charset: "alphabetic",
readable: true,
length: 12,
});

await testHelper.executeCypher(
`
test("should return posts where the LONGEST edge like String is EQUAL to", async () => {
const testString = generate({
charset: "alphabetic",
readable: true,
});

const shortestTestString = generate({
charset: "alphabetic",
readable: true,
length: 10,
});

const testString2 = generate({
charset: "alphabetic",
readable: true,
length: 11,
});

const longestTestString = generate({
charset: "alphabetic",
readable: true,
length: 12,
});

await testHelper.executeCypher(
`
CREATE (:${Post} {testString: "${testString}"})<-[:LIKES { testString: "${shortestTestString}" }]-(:${User} {testString: "${shortestTestString}"})
CREATE (:${Post} {testString: "${testString}"})<-[:LIKES { testString: "${testString2}" }]-(:${User} {testString: "${testString2}"})
CREATE (:${Post} {testString: "${testString}"})<-[:LIKES { testString: "${longestTestString}" }]-(:${User} {testString: "${longestTestString}"})
`
);
);

const query = `
const query = `
{
${Post.plural}(where: { testString_EQ: "${testString}", likesAggregate: { edge: { testString_LONGEST_LENGTH_EQUAL: ${longestTestString.length} } } }) {
testString
Expand All @@ -621,64 +618,60 @@ describe("aggregations-where-edge-string interface relationships of concrete typ
}
`;

const gqlResult = await testHelper.executeGraphQL(query);

if (gqlResult.errors) {
console.log(JSON.stringify(gqlResult.errors, null, 2));
}
const gqlResult = await testHelper.executeGraphQL(query);

expect(gqlResult.errors).toBeUndefined();
if (gqlResult.errors) {
console.log(JSON.stringify(gqlResult.errors, null, 2));
}

expect((gqlResult.data as any)[Post.plural]).toEqual([
{
testString,
likes: [{ testString: longestTestString }],
},
]);
}
);
expect(gqlResult.errors).toBeUndefined();

expect((gqlResult.data as any)[Post.plural]).toEqual([
{
testString,
likes: [{ testString: longestTestString }],
},
]);
});

describe("AVERAGE", () => {
test(
"should return posts where the %s of edge like Strings is EQUAL to",
async () => {
const testString = generate({
charset: "alphabetic",
readable: true,
});

const testString1 = generate({
charset: "alphabetic",
readable: true,
length: 10,
});

const testString2 = generate({
charset: "alphabetic",
readable: true,
length: 11,
});

const testString3 = generate({
charset: "alphabetic",
readable: true,
length: 12,
});

const avg = (10 + 11 + 12) / 3;

await testHelper.executeCypher(
`
test("should return posts where the %s of edge like Strings is EQUAL to", async () => {
const testString = generate({
charset: "alphabetic",
readable: true,
});

const testString1 = generate({
charset: "alphabetic",
readable: true,
length: 10,
});

const testString2 = generate({
charset: "alphabetic",
readable: true,
length: 11,
});

const testString3 = generate({
charset: "alphabetic",
readable: true,
length: 12,
});

const avg = (10 + 11 + 12) / 3;

await testHelper.executeCypher(
`
CREATE (p:${Post} {testString: "${testString}"})
CREATE(p)<-[:LIKES { testString: "${testString1}" }]-(:${User} {testString: "${testString}"})
CREATE(p)<-[:LIKES { testString: "${testString2}" }]-(:${User} {testString: "${testString}"})
CREATE(p)<-[:LIKES { testString: "${testString3}" }]-(:${User} {testString: "${testString}"})
CREATE (:${Post} {testString: "${testString}"})
`
);
);

const query = `
const query = `
{
${Post.plural}(where: { testString_EQ: "${testString}", likesAggregate: { edge: { testString_AVERAGE_LENGTH_EQUAL: ${avg} } } }) {
testString
Expand All @@ -689,19 +682,18 @@ describe("aggregations-where-edge-string interface relationships of concrete typ
}
`;

const gqlResult = await testHelper.executeGraphQL(query);
const gqlResult = await testHelper.executeGraphQL(query);

if (gqlResult.errors) {
console.log(JSON.stringify(gqlResult.errors, null, 2));
}
if (gqlResult.errors) {
console.log(JSON.stringify(gqlResult.errors, null, 2));
}

expect(gqlResult.errors).toBeUndefined();
expect(gqlResult.errors).toBeUndefined();

const [post] = (gqlResult.data as any)[Post.plural] as any[];
expect(post.testString).toEqual(testString);
expect(post.likes).toHaveLength(3);
}
);
const [post] = (gqlResult.data as any)[Post.plural] as any[];
expect(post.testString).toEqual(testString);
expect(post.likes).toHaveLength(3);
});

test("should return posts where the average of edge like Strings is GT than", async () => {
const testString = generate({
Expand Down
Loading

0 comments on commit 6153d68

Please sign in to comment.