diff --git a/packages/graphql/src/translate/create-where-and-params.ts b/packages/graphql/src/translate/create-where-and-params.ts index 8779994080..6037b2f9e1 100644 --- a/packages/graphql/src/translate/create-where-and-params.ts +++ b/packages/graphql/src/translate/create-where-and-params.ts @@ -514,12 +514,15 @@ function createWhereAndParams({ context, recursing: true, }); - - innerClauses.push(`${recurse[0]}`); - res.params = { ...res.params, ...recurse[1] }; + if (recurse[0]) { + innerClauses.push(`${recurse[0]}`); + res.params = { ...res.params, ...recurse[1] }; + } }); - res.clauses.push(`(${innerClauses.join(` ${key} `)})`); + if (innerClauses.length) { + res.clauses.push(`(${innerClauses.join(` ${key} `)})`); + } return res; } diff --git a/packages/graphql/tests/integration/issues/360.int.test.ts b/packages/graphql/tests/integration/issues/360.int.test.ts new file mode 100644 index 0000000000..ccab1445f0 --- /dev/null +++ b/packages/graphql/tests/integration/issues/360.int.test.ts @@ -0,0 +1,205 @@ +/* + * 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 { Driver } from "neo4j-driver"; +import { graphql } from "graphql"; +import { generate } from "randomstring"; +import camelcase from "camelcase"; +import pluralize from "pluralize"; +import { Neo4jGraphQL } from "../../../src/classes"; +import neo4j from "../neo4j"; + +describe("360", () => { + let driver: Driver; + + beforeAll(async () => { + driver = await neo4j(); + }); + + afterAll(async () => { + await driver.close(); + }); + + test("should return all nodes when AND is used and members are optional", async () => { + const session = driver.session(); + + const type = `${generate({ + charset: "alphabetic", + readable: true, + })}Event`; + + const pluralType = pluralize(camelcase(type)); + + const typeDefs = ` + type ${type} { + id: ID! + name: String + start: DateTime + end: DateTime + activity: String + } + `; + + const neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + + const query = ` + query ($rangeStart: DateTime, $rangeEnd: DateTime, $activity: String) { + ${pluralType}(where: { AND: [{ start_GTE: $rangeStart }, { start_LTE: $rangeEnd }, { activity: $activity }] }) { + id + } + } + `; + + try { + await session.run( + ` + CREATE (:${type} {id: randomUUID(), name: randomUUID(), start: datetime(), end: datetime()}) + CREATE (:${type} {id: randomUUID(), name: randomUUID(), start: datetime(), end: datetime()}) + CREATE (:${type} {id: randomUUID(), name: randomUUID(), start: datetime(), end: datetime()}) + ` + ); + + const gqlResult = await graphql({ + schema: neoSchema.schema, + source: query, + contextValue: { driver }, + }); + + expect(gqlResult.errors).toBe(undefined); + expect((gqlResult.data as any)[pluralType]).toHaveLength(3); + } finally { + await session.close(); + } + }); + + test("should return all nodes when OR is used and members are optional", async () => { + const session = driver.session(); + + const type = `${generate({ + charset: "alphabetic", + readable: true, + })}Event`; + + const pluralType = pluralize(camelcase(type)); + + const typeDefs = ` + type ${type} { + id: ID! + name: String + start: DateTime + end: DateTime + activity: String + } + `; + + const neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + + const query = ` + query ($rangeStart: DateTime, $rangeEnd: DateTime, $activity: String) { + ${pluralType}(where: { OR: [{ start_GTE: $rangeStart }, { start_LTE: $rangeEnd }, { activity: $activity }] }) { + id + } + } + `; + + try { + await session.run( + ` + CREATE (:${type} {id: randomUUID(), name: randomUUID(), start: datetime(), end: datetime()}) + CREATE (:${type} {id: randomUUID(), name: randomUUID(), start: datetime(), end: datetime()}) + CREATE (:${type} {id: randomUUID(), name: randomUUID(), start: datetime(), end: datetime()}) + ` + ); + + const gqlResult = await graphql({ + schema: neoSchema.schema, + source: query, + contextValue: { driver }, + }); + + expect(gqlResult.errors).toBe(undefined); + expect((gqlResult.data as any)[pluralType]).toHaveLength(3); + } finally { + await session.close(); + } + }); + + test("should recreate given test in issue and return correct results", async () => { + const session = driver.session(); + + const type = `${generate({ + charset: "alphabetic", + readable: true, + })}Event`; + + const pluralType = pluralize(camelcase(type)); + + const typeDefs = ` + type ${type} { + id: ID! + name: String + start: DateTime + end: DateTime + activity: String + } + `; + + const neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + + const rangeStart = new Date().toISOString(); + const rangeEnd = new Date().toISOString(); + + const query = ` + query ($rangeStart: DateTime, $rangeEnd: DateTime, $activity: String) { + ${pluralType}(where: { OR: [{ start_GTE: $rangeStart }, { start_LTE: $rangeEnd }, { activity: $activity }] }) { + id + } + } + `; + + try { + await session.run( + ` + CREATE (:${type} {id: randomUUID(), name: randomUUID(), start: datetime($rangeStart), end: datetime($rangeEnd)}) + CREATE (:${type} {id: randomUUID(), name: randomUUID(), start: datetime($rangeStart), end: datetime($rangeEnd)}) + CREATE (:${type} {id: randomUUID(), name: randomUUID(), start: datetime(), end: datetime()}) + `, + { rangeStart, rangeEnd } + ); + + const gqlResult = await graphql({ + schema: neoSchema.schema, + source: query, + contextValue: { driver }, + variableValues: { rangeStart, rangeEnd }, + }); + + expect(gqlResult.errors).toBe(undefined); + expect((gqlResult.data as any)[pluralType]).toHaveLength(3); + } finally { + await session.close(); + } + }); +}); diff --git a/packages/graphql/tests/tck/tck-test-files/cypher/issues/360.md b/packages/graphql/tests/tck/tck-test-files/cypher/issues/360.md new file mode 100644 index 0000000000..77c2101eef --- /dev/null +++ b/packages/graphql/tests/tck/tck-test-files/cypher/issues/360.md @@ -0,0 +1,157 @@ +## #360 + + + +Type definitions: + +```schema +type Event { + id: ID! + name: String + start: DateTime + end: DateTime + activity: String +} +``` + +--- + +### Should exclude undefined members in AND + +**GraphQL input** + +```graphql +query($rangeStart: DateTime, $rangeEnd: DateTime, $activity: String) { + events( + where: { + AND: [ + { start_GTE: $rangeStart } + { start_LTE: $rangeEnd } + { activity: $activity } + ] + } + ) { + start + activity + } +} +``` + +```graphql-params +{ + "rangeStart": "2021-07-18T00:00:00+0100", + "rangeEnd": "2021-07-18T23:59:59+0100" +} +``` + +**Expected Cypher output** + +```cypher +MATCH (this:Event) +WHERE (this.start >= $this_AND_start_GTE AND this.start <= $this_AND1_start_LTE) +RETURN this { + start: apoc.date.convertFormat(toString(this.start), "iso_zoned_date_time", "iso_offset_date_time"), + .activity +} as this +``` + +**Expected Cypher params** + +```cypher-params +{ + "this_AND1_start_LTE": { + "day": 18, + "hour": 22, + "minute": 59, + "month": 7, + "nanosecond": 0, + "second": 59, + "timeZoneId": null, + "timeZoneOffsetSeconds": 0, + "year": 2021 + }, + "this_AND_start_GTE": { + "day": 17, + "hour": 23, + "minute": 0, + "month": 7, + "nanosecond": 0, + "second": 0, + "timeZoneId": null, + "timeZoneOffsetSeconds": 0, + "year": 2021 + } +} +``` + +--- + +### Should exclude undefined members in OR + +**GraphQL input** + +```graphql +query($rangeStart: DateTime, $rangeEnd: DateTime, $activity: String) { + events( + where: { + OR: [ + { start_GTE: $rangeStart } + { start_LTE: $rangeEnd } + { activity: $activity } + ] + } + ) { + start + activity + } +} +``` + +```graphql-params +{ + "rangeStart": "2021-07-18T00:00:00+0100", + "rangeEnd": "2021-07-18T23:59:59+0100" +} +``` + +**Expected Cypher output** + +```cypher +MATCH (this:Event) +WHERE (this.start >= $this_OR_start_GTE OR this.start <= $this_OR1_start_LTE) +RETURN this { + start: apoc.date.convertFormat(toString(this.start), "iso_zoned_date_time", "iso_offset_date_time"), + .activity +} as this +``` + +**Expected Cypher params** + +```cypher-params +{ + "this_OR1_start_LTE": { + "day": 18, + "hour": 22, + "minute": 59, + "month": 7, + "nanosecond": 0, + "second": 59, + "timeZoneId": null, + "timeZoneOffsetSeconds": 0, + "year": 2021 + }, + "this_OR_start_GTE": { + "day": 17, + "hour": 23, + "minute": 0, + "month": 7, + "nanosecond": 0, + "second": 0, + "timeZoneId": null, + "timeZoneOffsetSeconds": 0, + "year": 2021 + } +} +``` + +---