Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cherry pick typename filter fix #6042

Merged
merged 2 commits into from
Mar 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/wicked-shrimps-draw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@neo4j/graphql": patch
---

Fixed bug that causes connection fields for interfaces to not be able to be filtered using the typename filters.
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ export class OperationsFactory {
operation: T;
whereArgs: Record<string, any>;
resolveTreeEdgeFields: Record<string, ResolveTree>;
partialOf?: InterfaceEntityAdapter | UnionEntityAdapter;
}): T {
return this.connectionFactory.hydrateConnectionOperationAST(arg);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export class ConnectionFactory {
operation: connectionPartial,
whereArgs: resolveTreeWhere,
resolveTreeEdgeFields,
partialOf: target,
});
});

Expand Down Expand Up @@ -327,6 +328,7 @@ export class ConnectionFactory {
operation,
whereArgs,
resolveTreeEdgeFields,
partialOf,
}: {
relationship?: RelationshipAdapter;
target: ConcreteEntityAdapter;
Expand All @@ -335,6 +337,7 @@ export class ConnectionFactory {
operation: T;
whereArgs: Record<string, any>;
resolveTreeEdgeFields: Record<string, ResolveTree>;
partialOf?: InterfaceEntityAdapter | UnionEntityAdapter,
}): T {
const entityOrRel = relationship ?? target;

Expand Down Expand Up @@ -381,6 +384,7 @@ export class ConnectionFactory {
rel: relationship,
entity: target,
where: whereArgs,
partialOf,
});

operation.setNodeFields(nodeFields);
Expand Down
148 changes: 148 additions & 0 deletions packages/graphql/tests/integration/issues/6031.int.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*
* 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 type { UniqueType } from "../../utils/graphql-types";
import { TestHelper } from "../../utils/tests-helper";

describe("https://github.com/neo4j/graphql/issues/6031", () => {
const testHelper = new TestHelper();
let typeDefs: string;

let Movie: UniqueType;
let Actor: UniqueType;
let Series: UniqueType;

beforeAll(async () => {
Movie = testHelper.createUniqueType("Movie");
Actor = testHelper.createUniqueType("Actor");
Series = testHelper.createUniqueType("Series");

typeDefs = /* GraphQL */ `
interface Production {
title: String!
}

type ${Series} implements Production @node {
title: String!
}

type ${Movie} implements Production @node {
title: String!
actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn")
}
type ${Actor} @node {
name: String!
ratings: [Int!]!
lastRating: Int
productions: [Production!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn")
}

type ActedIn @relationshipProperties {
year: Int
}
`;

await testHelper.executeCypher(`
CREATE(a:${Actor.name} { name: "Keanu" })
CREATE(a)-[:ACTED_IN]->(m:${Movie.name} { title: "The Matrix" })
CREATE(a)-[:ACTED_IN]->(s:${Series.name} { title: "The Matrix the series" })
`);

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

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

test("typename should be supported on top-level connection where", async () => {
const query = /* GraphQL */ `
query TopLevelCount {
productionsConnection(where: { typename: [${Movie}] }) {
edges {
node {
title
}
}
}
}
`;

const queryResult = await testHelper.executeGraphQL(query);
expect(queryResult.errors).toBeUndefined();
expect(queryResult.data).toEqual({
productionsConnection: {
edges: expect.toIncludeSameMembers([
{
node: {
title: "The Matrix",
},
},
]),
},
});
});

test("typename should be supported on nested-level connection where", async () => {
const query = /* GraphQL */ `
query TopLevelCount {
${Actor.operations.connection} {
edges {
node {
name
productionsConnection(where: { node: { typename: [${Movie}] } }) {
edges {
node {
title
}
}
}
}
}
}

}
`;

const queryResult = await testHelper.executeGraphQL(query);
expect(queryResult.errors).toBeUndefined();
expect(queryResult.data).toEqual({
[Actor.operations.connection]: {
edges: [
{
node: {
name: "Keanu",
productionsConnection: {
edges: expect.toIncludeSameMembers([
{
node: {
title: "The Matrix",
},
},
]),
},
},
},
],
},
});
});
});
150 changes: 150 additions & 0 deletions packages/graphql/tests/tck/issues/6031.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*
* 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("https://github.com/neo4j/graphql/issues/6031", () => {
let typeDefs: string;
let neoSchema: Neo4jGraphQL;

beforeAll(() => {
typeDefs = /* GraphQL */ `
interface Production {
title: String!
}

type Series implements Production @node {
title: String!
}

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

type Actor @node {
name: String!
ratings: [Int!]!
lastRating: Int
productions: [Production!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn")
}

type ActedIn @relationshipProperties {
year: Int
}
`;

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

test("Top-level connection typename filtering", async () => {
const query = /* GraphQL */ `
query {
productionsConnection(where: { typename: [Movie] }) {
edges {
node {
title
}
}
}
}
`;

const result = await translateQuery(neoSchema, query);

expect(formatCypher(result.cypher)).toMatchInlineSnapshot(`
"CALL {
MATCH (this0:Series)
WHERE this0:Movie
WITH { node: { __resolveType: \\"Series\\", __id: id(this0), title: this0.title } } AS edge
RETURN edge
UNION
MATCH (this1:Movie)
WHERE this1:Movie
WITH { node: { __resolveType: \\"Movie\\", __id: id(this1), title: this1.title } } AS edge
RETURN edge
}
WITH collect(edge) AS edges
WITH edges, size(edges) AS totalCount
RETURN { edges: edges, totalCount: totalCount } AS this"
`);

expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`);
});

test("Nested-level connection typename filtering", async () => {
const query = /* GraphQL */ `
query {
actorsConnection {
edges {
node {
name
productionsConnection(where: { node: { typename: [Movie] } }) {
edges {
node {
title
}
}
}
}
}
}
}
`;

const result = await translateQuery(neoSchema, query);

expect(formatCypher(result.cypher)).toMatchInlineSnapshot(`
"MATCH (this0:Actor)
WITH collect({ node: this0 }) AS edges
WITH edges, size(edges) AS totalCount
CALL {
WITH edges
UNWIND edges AS edge
WITH edge.node AS this0
CALL {
WITH this0
CALL {
WITH this0
MATCH (this0)-[this1:ACTED_IN]->(this2:Series)
WHERE this2:Movie
WITH { node: { __resolveType: \\"Series\\", __id: id(this2), title: this2.title } } AS edge
RETURN edge
UNION
WITH this0
MATCH (this0)-[this3:ACTED_IN]->(this4:Movie)
WHERE this4:Movie
WITH { node: { __resolveType: \\"Movie\\", __id: id(this4), title: this4.title } } AS edge
RETURN edge
}
WITH collect(edge) AS edges
WITH edges, size(edges) AS totalCount
RETURN { edges: edges, totalCount: totalCount } AS var5
}
RETURN collect({ node: { name: this0.name, productionsConnection: var5, __resolveType: \\"Actor\\" } }) AS var6
}
RETURN { edges: var6, totalCount: totalCount } AS this"
`);

expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`);
});
});
Loading