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
… properites types
  • Loading branch information
darrellwarde committed Jan 20, 2025
1 parent fd3f015 commit 373c796
Show file tree
Hide file tree
Showing 4 changed files with 286 additions and 0 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 properites types
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,14 @@ export class FilterFactory {
}

if (fieldName === "edge") {
if (isInterfaceEntity(relationship.target)) {
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
Expand Up @@ -946,3 +946,112 @@ describe("aggregations-where-edge-string interface relationships of concrete typ
});
});
});

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: { shortestLength: { lt: 3 } } }
ActedIn: { role: { averageLength: { 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" }]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/*
* Copyright (c) "Neo4j"
* Neo4j Sweden AB [http://neo4j.com]
*
* This file is part of Neo4j.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { Neo4jGraphQL } from "../../../../../src";
import { formatCypher, formatParams, translateQuery } from "../../../utils/tck-test-utils";

describe("Cypher Aggregations where edge with String", () => {
let typeDefs: string;
let neoSchema: Neo4jGraphQL;

beforeAll(() => {
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
}
`;

neoSchema = new Neo4jGraphQL({
typeDefs,
});
});

test("should count number of interface relationships", async () => {
const query = /* GraphQL */ `
query ActorsAggregate {
actors(where: { productionsAggregate: { count: { lt: 3 } } }) {
name
}
}
`;

const result = await translateQuery(neoSchema, query);

expect(formatCypher(result.cypher)).toMatchInlineSnapshot(`
"MATCH (this:Actor)
CALL {
WITH this
MATCH (this)-[this0:ACTED_IN]->(this1)
WHERE (this1:Movie OR this1:Series)
RETURN count(this1) < $param0 AS var2
}
WITH *
WHERE var2 = true
RETURN this { .name } AS this"
`);

expect(formatParams(result.params)).toMatchInlineSnapshot(`
"{
\\"param0\\": {
\\"low\\": 3,
\\"high\\": 0
}
}"
`);
});

test("should generate Cypher to aggregate over multiple relationship properties types", async () => {
const query = /* GraphQL */ `
query People {
people(
where: {
productionsAggregate: {
edge: {
AppearedIn: { role: { shortestLength: { lt: 3 } } }
ActedIn: { role: { averageLength: { lt: 5 } } }
}
}
}
) {
name
}
}
`;

const result = await translateQuery(neoSchema, query);

expect(formatCypher(result.cypher)).toMatchInlineSnapshot(`
"CALL {
MATCH (this0:Actor)
CALL {
WITH this0
MATCH (this0)-[this1:ACTED_IN]->(this2)
WHERE (this2:Movie OR this2:Series)
RETURN avg(size(this1.role)) < $param0 AS var3
}
WITH *
WHERE var3 = true
WITH this0 { .name, __resolveType: \\"Actor\\", __id: id(this0) } AS this0
RETURN this0 AS this
UNION
MATCH (this4:Cameo)
CALL {
WITH this4
MATCH (this4)-[this5:APPEARED_IN]->(this6)
WHERE (this6:Movie OR this6:Series)
RETURN min(size(this5.role)) < $param1 AS var7
}
WITH *
WHERE var7 = true
WITH this4 { .name, __resolveType: \\"Cameo\\", __id: id(this4) } AS this4
RETURN this4 AS this
}
WITH this
RETURN this AS this"
`);

expect(formatParams(result.params)).toMatchInlineSnapshot(`
"{
\\"param0\\": 5,
\\"param1\\": {
\\"low\\": 3,
\\"high\\": 0
}
}"
`);
});
});

0 comments on commit 373c796

Please sign in to comment.