Skip to content

Commit

Permalink
Fix 4450 (#4482)
Browse files Browse the repository at this point in the history
* test: add regression tests for 4450

* bug fix 4450

---------

Co-authored-by: Michael Webb <[email protected]>
  • Loading branch information
MacondoExpress and mjfwebb authored Jan 5, 2024
1 parent b2b750d commit 760d598
Show file tree
Hide file tree
Showing 3 changed files with 218 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -107,23 +107,25 @@ export class ConnectionFilter extends Filter {

public getPredicate(queryASTContext: QueryASTContext): Cypher.Predicate | undefined {
if (!hasTarget(queryASTContext)) throw new Error("No parent node found!");
if (this.subqueryPredicate) return this.subqueryPredicate;
else {
const target = this.getTargetNode(queryASTContext);
const relationship = new Cypher.Relationship({
type: this.relationship.type,
});
if (this.subqueryPredicate) {
return this.subqueryPredicate;
}

const pattern = new Cypher.Pattern(queryASTContext.target)
.withoutLabels()
.related(relationship)
.withDirection(this.relationship.getCypherDirection())
.to(target);
const target = this.getTargetNode(queryASTContext);
const relationship = new Cypher.Relationship({
type: this.relationship.type,
});

const nestedContext = queryASTContext.push({ target, relationship });
const pattern = new Cypher.Pattern(queryASTContext.target)
.withoutLabels()
.related(relationship)
.withDirection(this.relationship.getCypherDirection())
.to(target);

const nestedContext = queryASTContext.push({ target, relationship });

const predicate = this.createRelationshipOperation(pattern, nestedContext);
if (!predicate) return undefined;
const predicate = this.createRelationshipOperation(pattern, nestedContext);
if (predicate) {
return this.wrapInNotIfNeeded(predicate);
}
}
Expand Down Expand Up @@ -202,14 +204,15 @@ export class ConnectionFilter extends Filter {
const nestedSubqueries = f
.getSubqueries(queryASTContext)
.map((sq) => new Cypher.Call(sq).innerWith(queryASTContext.target));

const selection = f.getSelection(queryASTContext);
const predicate = f.getPredicate(queryASTContext);
const clauses = [...selection, ...nestedSubqueries];
if (predicate) {
innerFiltersPredicates.push(predicate);
return nestedSubqueries;
return clauses;
}

return nestedSubqueries;
return clauses;
});

if (subqueries.length === 0) return []; // Hack logic to change predicates logic
Expand Down
115 changes: 115 additions & 0 deletions packages/graphql/tests/integration/issues/4450.int.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* 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 { graphql } from "graphql";
import type { Driver } from "neo4j-driver";
import { Neo4jGraphQL } from "../../../src/classes";
import { cleanNodes } from "../../utils/clean-nodes";
import { UniqueType } from "../../utils/graphql-types";
import Neo4j from "../neo4j";

describe("https://github.com/neo4j/graphql/issues/4450", () => {
let driver: Driver;
let neo4j: Neo4j;
let neo4jGraphql: Neo4jGraphQL;

const Actor = new UniqueType("Actor");
const Scene = new UniqueType("Scene");
const Location = new UniqueType("Location");

beforeAll(async () => {
neo4j = new Neo4j();
driver = await neo4j.getDriver();
const typeDefs = /* GraphQL */ `
type ${Actor} {
name: String
scene: [${Scene}!]! @relationship(type: "IN_SCENE", properties: "ActorScene", direction: OUT)
}
type ${Scene} {
number: Int
actors: [${Actor}!]! @relationship(type: "IN_SCENE", properties: "ActorScene", direction: IN)
location: ${Location}! @relationship(type: "AT_LOCATION", direction: OUT)
}
type ${Location} {
city: String
scenes: [${Scene}!]! @relationship(type: "AT_LOCATION", direction: IN)
}
interface ActorScene @relationshipProperties {
cut: Boolean
}
`;

neo4jGraphql = new Neo4jGraphQL({
typeDefs,
driver,
});

const session = await neo4j.getSession();
try {
await session.run(
`
CREATE (:${Actor} {name: "actor-1"})-[:IN_SCENE {cut: true}]->(:${Scene} {number: 1})-[:AT_LOCATION]->(:${Location} {city: "test"})
`,
{}
);
} finally {
await session.close();
}
});

afterAll(async () => {
const session = await neo4j.getSession();
try {
await cleanNodes(session, [Actor, Scene, Location]);
} finally {
await session.close();
}
await driver.close();
});

test("filtering through a connection to a many-to-1 relationship should work", async () => {
const schema = await neo4jGraphql.getSchema();

const query = /* GraphQL */ `
query {
${Actor.plural}(where: { sceneConnection_SOME: { edge: { cut: true }, node: { location: { city: "test" } } } }) {
name
}
}
`;

const response = await graphql({
schema,
source: query,
contextValue: neo4j.getContextValues(),
});

expect(response.errors).toBeFalsy();
expect(response.data).toEqual({
[Actor.plural]: [
{
name: "actor-1",
},
],
});
});
});
83 changes: 83 additions & 0 deletions packages/graphql/tests/tck/issues/4450.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* 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 gql from "graphql-tag";
import { Neo4jGraphQL } from "../../../src";
import { formatCypher, formatParams, translateQuery } from "../utils/tck-test-utils";

describe("https://github.com/neo4j/graphql/issues/4450", () => {
test("filtering through a connection to a many-to-1 relationship should work", async () => {
const typeDefs = /* GraphQL */ `
type Actor {
name: String
scene: [Scene!]! @relationship(type: "IN_SCENE", properties: "ActorScene", direction: OUT)
}
type Scene {
number: Int
actors: [Actor!]! @relationship(type: "IN_SCENE", properties: "ActorScene", direction: IN)
location: Location! @relationship(type: "AT_LOCATION", direction: OUT)
}
type Location {
city: String
scenes: [Scene!]! @relationship(type: "AT_LOCATION", direction: IN)
}
interface ActorScene @relationshipProperties {
cut: Boolean
}
`;

const neoSchema = new Neo4jGraphQL({ typeDefs });

const query = gql`
query {
actors(where: { sceneConnection_SOME: { edge: { cut: true }, node: { location: { city: "test" } } } }) {
name
}
}
`;

const result = await translateQuery(neoSchema, query);

expect(formatCypher(result.cypher)).toMatchInlineSnapshot(`
"MATCH (this:Actor)
CALL {
WITH this
MATCH (this)-[this0:IN_SCENE]->(this1:Scene)
OPTIONAL MATCH (this1)-[:AT_LOCATION]->(this2:Location)
WITH *, count(this2) AS locationCount
WITH *
WHERE ((locationCount <> 0 AND this2.city = $param0) AND this0.cut = $param1)
RETURN count(this1) > 0 AS var3
}
WITH *
WHERE var3 = true
RETURN this { .name } AS this"
`);

expect(formatParams(result.params)).toMatchInlineSnapshot(`
"{
\\"param0\\": \\"test\\",
\\"param1\\": true
}"
`);
});
});

0 comments on commit 760d598

Please sign in to comment.