From be497fc5a5c0b4beff0de82d7b2011fb1e786b5c Mon Sep 17 00:00:00 2001 From: Daniel Starns Date: Tue, 27 Jul 2021 15:06:06 +0100 Subject: [PATCH 1/6] fix: issue #360 only append where if its truthy --- .../graphql/src/translate/create-where-and-params.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) 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; } From 5abd1c682355c9fc2835fd1baf765b5ffca20141 Mon Sep 17 00:00:00 2001 From: Daniel Starns Date: Tue, 27 Jul 2021 15:07:55 +0100 Subject: [PATCH 2/6] test: coverage for issue #360 --- .../tests/integration/issues/360.int.test.ts | 204 ++++++++++++++++++ .../tck/tck-test-files/cypher/issues/360.md | 157 ++++++++++++++ 2 files changed, 361 insertions(+) create mode 100644 packages/graphql/tests/integration/issues/360.int.test.ts create mode 100644 packages/graphql/tests/tck/tck-test-files/cypher/issues/360.md 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..9a6c4d1de9 --- /dev/null +++ b/packages/graphql/tests/integration/issues/360.int.test.ts @@ -0,0 +1,204 @@ +/* + * 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 }, + }); + + 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 + } +} +``` + +--- From aec4fe82b0be690f8aea6d229a261032f51c4fed Mon Sep 17 00:00:00 2001 From: Daniel Starns Date: Wed, 28 Jul 2021 08:38:25 +0100 Subject: [PATCH 3/6] Update packages/graphql/tests/integration/issues/360.int.test.ts Co-authored-by: Darrell Warde <8117355+darrellwarde@users.noreply.github.com> --- packages/graphql/tests/integration/issues/360.int.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/graphql/tests/integration/issues/360.int.test.ts b/packages/graphql/tests/integration/issues/360.int.test.ts index 9a6c4d1de9..ccab1445f0 100644 --- a/packages/graphql/tests/integration/issues/360.int.test.ts +++ b/packages/graphql/tests/integration/issues/360.int.test.ts @@ -193,6 +193,7 @@ describe("360", () => { schema: neoSchema.schema, source: query, contextValue: { driver }, + variableValues: { rangeStart, rangeEnd }, }); expect(gqlResult.errors).toBe(undefined); From 10f45b17729d94e769ae99d23632ae710a25ff4b Mon Sep 17 00:00:00 2001 From: Arnaud Gissinger <37625778+mathix420@users.noreply.github.com> Date: Wed, 28 Jul 2021 09:44:53 +0200 Subject: [PATCH 4/6] feat(graphql): add allowUnauthenticated parameter to auth rules (#355) * feat(graphql): add allowUnauthenticated parameter to auth rules * add(graphql): documentation for allowUnauthenticated * add(graphql): docs warning section for allowUnauthenticated * fix(graphql): createAuthPredicate handling null and undefined * add(graphql): integration tests for allowUnauthenticated * update: tests to make them work with EXISTS removals * fix: formatting issues * add: more complex test cases for allowUnauthenticated * update: allowUnauthenticated documentation * Update docs/asciidoc/auth/authentication.adoc Co-authored-by: Darrell Warde <8117355+darrellwarde@users.noreply.github.com> --- docs/asciidoc/auth/authentication.adoc | 23 + packages/graphql/src/schema/get-auth.ts | 2 +- .../translate/create-auth-and-params.test.ts | 154 ++++++- .../src/translate/create-auth-and-params.ts | 34 +- packages/graphql/src/types.ts | 1 + .../auth/allow-unauthenticated.int.test.ts | 398 ++++++++++++++++++ .../cypher/directives/auth/arguments/allow.md | 46 +- .../cypher/directives/auth/arguments/bind.md | 18 +- .../cypher/directives/auth/arguments/where.md | 82 ++-- .../cypher/directives/auth/projection.md | 8 +- .../tests/tck/tck-test-files/cypher/union.md | 2 +- 11 files changed, 672 insertions(+), 96 deletions(-) create mode 100644 packages/graphql/tests/integration/auth/allow-unauthenticated.int.test.ts diff --git a/docs/asciidoc/auth/authentication.adoc b/docs/asciidoc/auth/authentication.adoc index 8a68847f0a..9bc32dadf6 100644 --- a/docs/asciidoc/auth/authentication.adoc +++ b/docs/asciidoc/auth/authentication.adoc @@ -92,3 +92,26 @@ type Todo { extend type Todo @auth(rules: [{ isAuthenticated: true }]) ---- + +== `allowUnauthenticated` +In some cases, you may want to allow unauthenticated requests while also having auth-based rules. You can use the `allowUnauthenticated` parameter to avoid throwing an exception if no auth is present in the context. + +In the example below, only the publisher can see his blog posts if it is not published yet. Once the blog post is published, anyone can see it. + +[source, graphql] +---- +type BlogPost + @auth( + rules: [ + { + operations: [READ] + where: { OR: [{ publisher: "$jwt.sub" }, { published: true }] } + allowUnauthenticated: true + } + ] + ) { + id: ID! + publisher: String! + published: Boolean! +} +---- diff --git a/packages/graphql/src/schema/get-auth.ts b/packages/graphql/src/schema/get-auth.ts index 91e0b3d3bc..f29171dd41 100644 --- a/packages/graphql/src/schema/get-auth.ts +++ b/packages/graphql/src/schema/get-auth.ts @@ -21,7 +21,7 @@ import { DirectiveNode, valueFromASTUntyped } from "graphql"; import { Auth, AuthRule, AuthOperations } from "../types"; const validOperations: AuthOperations[] = ["CREATE", "READ", "UPDATE", "DELETE", "CONNECT", "DISCONNECT"]; -const validFields = ["operations", "AND", "OR", "allow", "where", "bind", "isAuthenticated", "roles"]; +const validFields = ["operations", "AND", "OR", "allow", "where", "bind", "isAuthenticated", "allowUnauthenticated", "roles"]; function getAuth(directive: DirectiveNode): Auth { const auth: Auth = { rules: [], type: "JWT" }; diff --git a/packages/graphql/src/translate/create-auth-and-params.test.ts b/packages/graphql/src/translate/create-auth-and-params.test.ts index 430239fb29..8cfd5419a1 100644 --- a/packages/graphql/src/translate/create-auth-and-params.test.ts +++ b/packages/graphql/src/translate/create-auth-and-params.test.ts @@ -95,7 +95,7 @@ describe("createAuthAndParams", () => { expect(trimmer(result[0])).toEqual( trimmer(` - EXISTS(this.id) AND this.id = $this_auth_allow0_id OR ANY(r IN ["admin"] WHERE ANY(rr IN $auth.roles WHERE r = rr)) + this.id IS NOT NULL AND this.id = $this_auth_allow0_id OR ANY(r IN ["admin"] WHERE ANY(rr IN $auth.roles WHERE r = rr)) `) ); @@ -175,7 +175,7 @@ describe("createAuthAndParams", () => { expect(trimmer(result[0])).toEqual( trimmer(` - EXISTS(this.id) AND this.id = $this_auth_allow0_id OR ANY(r IN ["admin"] WHERE ANY(rr IN $auth.roles WHERE r = rr)) + this.id IS NOT NULL AND this.id = $this_auth_allow0_id OR ANY(r IN ["admin"] WHERE ANY(rr IN $auth.roles WHERE r = rr)) `) ); @@ -253,7 +253,7 @@ describe("createAuthAndParams", () => { expect(trimmer(result[0])).toEqual( trimmer(` - ANY(r IN ["admin"] WHERE ANY(rr IN $auth.roles WHERE r = rr)) AND EXISTS(this.id) AND this.id = $this_auth_allow0_id + ANY(r IN ["admin"] WHERE ANY(rr IN $auth.roles WHERE r = rr)) AND this.id IS NOT NULL AND this.id = $this_auth_allow0_id `) ); @@ -334,7 +334,7 @@ describe("createAuthAndParams", () => { expect(trimmer(result[0])).toEqual( trimmer(` - EXISTS(this.id) AND this.id = $this${key}0_auth_allow0_id ${key} ANY(r IN ["admin"] WHERE ANY(rr IN $auth.roles WHERE r = rr)) + this.id IS NOT NULL AND this.id = $this${key}0_auth_allow0_id ${key} ANY(r IN ["admin"] WHERE ANY(rr IN $auth.roles WHERE r = rr)) `) ); @@ -422,11 +422,11 @@ describe("createAuthAndParams", () => { trimmer(` ANY(r IN ["admin"] WHERE ANY(rr IN $auth.roles WHERE r = rr)) AND - EXISTS(this.id) AND this.id = $this_auth_allow0_id + this.id IS NOT NULL AND this.id = $this_auth_allow0_id AND - EXISTS(this.id) AND this.id = $thisAND0_auth_allow0_id AND ANY(r IN ["admin"] WHERE ANY(rr IN $auth.roles WHERE r = rr)) + this.id IS NOT NULL AND this.id = $thisAND0_auth_allow0_id AND ANY(r IN ["admin"] WHERE ANY(rr IN $auth.roles WHERE r = rr)) AND - EXISTS(this.id) AND this.id = $thisOR0_auth_allow0_id OR ANY(r IN ["admin"] WHERE ANY(rr IN $auth.roles WHERE r = rr)) + this.id IS NOT NULL AND this.id = $thisOR0_auth_allow0_id OR ANY(r IN ["admin"] WHERE ANY(rr IN $auth.roles WHERE r = rr)) `) ); @@ -536,7 +536,7 @@ describe("createAuthAndParams", () => { expect(trimmer(result[0])).toEqual( trimmer(` - (EXISTS(this.id) AND this.id = $this_auth_allow0_${key}0_id ${key} EXISTS(this.id) AND this.id = $this_auth_allow0_${key}1_id ${key} EXISTS(this.id) AND this.id = $this_auth_allow0_${key}2_id) + (this.id IS NOT NULL AND this.id = $this_auth_allow0_${key}0_id ${key} this.id IS NOT NULL AND this.id = $this_auth_allow0_${key}1_id ${key} this.id IS NOT NULL AND this.id = $this_auth_allow0_${key}2_id) `) ); @@ -685,5 +685,143 @@ describe("createAuthAndParams", () => { }); }).toThrow("Unauthenticated"); }); + + test("should showcase the allowUnauthenticated behavior with undefined $jwt", () => { + const idField = { + fieldName: "id", + typeMeta: { + name: "ID", + array: false, + required: false, + pretty: "String", + input: { + where: { + type: "String", + pretty: "String", + }, + create: { + type: "String", + pretty: "String", + }, + update: { + type: "String", + pretty: "String", + }, + }, + }, + otherDirectives: [], + arguments: [], + }; + + // @ts-ignore + const node: Node = { + name: "Movie", + relationFields: [], + cypherFields: [], + enumFields: [], + scalarFields: [], + primitiveFields: [idField], + dateTimeFields: [], + interfaceFields: [], + objectFields: [], + pointFields: [], + authableFields: [idField], + auth: { + rules: [ + { allow: { id: "$jwt.sub" }, allowUnauthenticated: true }, + { operations: ["CREATE"], roles: ["admin"] }, + { roles: ["admin"] }, + ], + type: "JWT", + }, + }; + + // @ts-ignore + const neoSchema: Neo4jGraphQL = { + nodes: [node], + }; + + // @ts-ignore + const context: Context = { neoSchema, jwt: {} }; + + const result = createAuthAndParams({ + context, + entity: node, + operation: "READ", + allow: { parentNode: node, varName: "this" }, + }); + + expect(trimmer(result[0])).toEqual(trimmer('false OR ANY(r IN ["admin"] WHERE ANY(rr IN $auth.roles WHERE r = rr))')); + expect(result[1]).toEqual({}); + }); + + test("should showcase the allowUnauthenticated behavior with undefined $context", () => { + const idField = { + fieldName: "id", + typeMeta: { + name: "ID", + array: false, + required: false, + pretty: "String", + input: { + where: { + type: "String", + pretty: "String", + }, + create: { + type: "String", + pretty: "String", + }, + update: { + type: "String", + pretty: "String", + }, + }, + }, + otherDirectives: [], + arguments: [], + }; + + // @ts-ignore + const node: Node = { + name: "Movie", + relationFields: [], + cypherFields: [], + enumFields: [], + scalarFields: [], + primitiveFields: [idField], + dateTimeFields: [], + interfaceFields: [], + objectFields: [], + pointFields: [], + authableFields: [idField], + auth: { + rules: [ + { allow: { id: "$context.nop" }, allowUnauthenticated: true }, + { operations: ["CREATE"], roles: ["admin"] }, + { roles: ["admin"] }, + ], + type: "JWT", + }, + }; + + // @ts-ignore + const neoSchema: Neo4jGraphQL = { + nodes: [node], + }; + + // @ts-ignore + const context: Context = { neoSchema, jwt: {} }; + + const result = createAuthAndParams({ + context, + entity: node, + operation: "READ", + allow: { parentNode: node, varName: "this" }, + }); + + expect(trimmer(result[0])).toEqual(trimmer('false OR ANY(r IN ["admin"] WHERE ANY(rr IN $auth.roles WHERE r = rr))')); + expect(result[1]).toEqual({}); + }); }); }); diff --git a/packages/graphql/src/translate/create-auth-and-params.ts b/packages/graphql/src/translate/create-auth-and-params.ts index 419cf31020..d7d25b2854 100644 --- a/packages/graphql/src/translate/create-auth-and-params.ts +++ b/packages/graphql/src/translate/create-auth-and-params.ts @@ -67,6 +67,7 @@ function createAuthPredicate({ } const { jwt } = context; + const { allowUnauthenticated } = rule; const result = Object.entries(rule[kind] as any).reduce( (res: Res, [key, value]) => { @@ -75,7 +76,10 @@ function createAuthPredicate({ (value as any[]).forEach((v, i) => { const authPredicate = createAuthPredicate({ - rule: { [kind]: v } as AuthRule, + rule: { + [kind]: v, + allowUnauthenticated + } as AuthRule, varName, node, chainStr: `${chainStr}_${key}${i}`, @@ -92,8 +96,8 @@ function createAuthPredicate({ const authableField = node.authableFields.find((field) => field.fieldName === key); if (authableField) { - const [, jwtPath] = (value as string).split("$jwt."); - const [, ctxPath] = (value as string).split("$context."); + const [, jwtPath] = (value as string)?.split?.("$jwt.") || []; + const [, ctxPath] = (value as string)?.split?.("$context.") || []; let paramValue: string | null = value as string; if (jwtPath) { @@ -102,13 +106,19 @@ function createAuthPredicate({ paramValue = dotProp.get({ value: context }, `value.${ctxPath}`) as string; } - if (paramValue === undefined) { + if (paramValue === undefined && allowUnauthenticated !== true) { throw new Neo4jGraphQLAuthenticationError("Unauthenticated"); } - const param = `${chainStr}_${key}`; - res.params[param] = paramValue; - res.strs.push(`EXISTS(${varName}.${key}) AND ${varName}.${key} = $${param}`); + if (paramValue === undefined) { + res.strs.push("false"); + } else if (paramValue === null) { + res.strs.push(`${varName}.${key} IS NULL`); + } else { + const param = `${chainStr}_${key}`; + res.params[param] = paramValue; + res.strs.push(`${varName}.${key} IS NOT NULL AND ${varName}.${key} = $${param}`); + } } const relationField = node.relationFields.find((x) => key === x.fieldName); @@ -134,7 +144,10 @@ function createAuthPredicate({ context, chainStr: `${chainStr}_${key}`, varName: relationVarName, - rule: { [kind]: { [k]: v } } as AuthRule, + rule: { + [kind]: { [k]: v }, + allowUnauthenticated + } as AuthRule, kind, }); resultStr += authPredicate[0]; @@ -192,7 +205,10 @@ function createAuthAndParams({ } const authWhere = createAuthPredicate({ - rule: { where: authRule.where }, + rule: { + where: authRule.where, + allowUnauthenticated: authRule.allowUnauthenticated + }, context, node: where.node, varName: where.varName, diff --git a/packages/graphql/src/types.ts b/packages/graphql/src/types.ts index 975bd6604a..e536d163b9 100644 --- a/packages/graphql/src/types.ts +++ b/packages/graphql/src/types.ts @@ -47,6 +47,7 @@ export interface Context { export interface BaseAuthRule { isAuthenticated?: boolean; + allowUnauthenticated?: boolean; allow?: { [k: string]: any } | "*"; bind?: { [k: string]: any } | "*"; where?: { [k: string]: any } | "*"; diff --git a/packages/graphql/tests/integration/auth/allow-unauthenticated.int.test.ts b/packages/graphql/tests/integration/auth/allow-unauthenticated.int.test.ts new file mode 100644 index 0000000000..63ae1c08f3 --- /dev/null +++ b/packages/graphql/tests/integration/auth/allow-unauthenticated.int.test.ts @@ -0,0 +1,398 @@ +/* + * 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 { Socket } from "net"; +import { graphql } from "graphql"; +import { Driver } from "neo4j-driver"; +import { IncomingMessage } from "http"; +import { generate } from "randomstring"; + +import neo4j from "../neo4j"; +import { Neo4jGraphQL } from "../../../src/classes"; + +// Reference: https://github.com/neo4j/graphql/pull/355 +// Reference: https://github.com/neo4j/graphql/issues/345 +// Reference: https://github.com/neo4j/graphql/pull/342#issuecomment-884061188 +describe("auth/allow-unauthenticated", () => { + let driver: Driver; + + beforeAll(async () => { + driver = await neo4j(); + }); + + afterAll(async () => { + await driver.close(); + }); + + describe("allowUnauthenticated with allow", () => { + test("should return a Post without errors", async () => { + const typeDefs = ` + type Post { + id: ID! + publisher: String! + published: Boolean! + } + + extend type Post @auth(rules: [ + { allow: { OR: [ + { publisher: "$jwt.sub" }, + { published: true }, + ] }, allowUnauthenticated: true } + ]) + `; + + const postId = generate({ charset: "alphabetic" }); + + const query = ` + { + posts(where: { id: ${postId} }) { + id + } + } + `; + + const secret = "secret"; + const session = driver.session({ defaultAccessMode: "WRITE" }); + const neoSchema = new Neo4jGraphQL({ typeDefs, config: { jwt: { secret } } }); + + await session.run(` + CREATE (:Post {id: "${postId}", publisher: "nop", published: true}) + `); + + const socket = new Socket({ readable: true }); + const req = new IncomingMessage(socket); + + const gqlResult = await graphql({ + contextValue: { driver, req }, + schema: neoSchema.schema, + source: query, + }); + + // Check that no errors have been throwed + expect(gqlResult.errors).toBeUndefined(); + + // Check if returned data is what we really want + expect(gqlResult.data?.posts?.[0]?.id).toBe(postId); + }); + + test("should throw a Forbidden error", async () => { + const typeDefs = ` + type Post { + id: ID! + publisher: String! + published: Boolean! + } + + extend type Post @auth(rules: [ + { allow: { OR: [ + { publisher: "$jwt.sub" }, + { published: true }, + ] }, allowUnauthenticated: true } + ]) + `; + + const postId = generate({ charset: "alphabetic" }); + + const query = ` + { + posts(where: { id: ${postId} }) { + id + } + } + `; + + const secret = "secret"; + const session = driver.session({ defaultAccessMode: "WRITE" }); + const neoSchema = new Neo4jGraphQL({ typeDefs, config: { jwt: { secret } } }); + + await session.run(` + CREATE (:Post {id: "${postId}", publisher: "nop", published: false}) + `); + + const socket = new Socket({ readable: true }); + const req = new IncomingMessage(socket); + + const gqlResult = await graphql({ + contextValue: { driver, req }, + schema: neoSchema.schema, + source: query, + }); + + // Check that a Forbidden error have been throwed + expect((gqlResult.errors as any[])[0].message).toEqual("Forbidden"); + expect((gqlResult.errors as any[])).toHaveLength(1); + + // Check if returned data is what we really want + expect(gqlResult.data?.posts).toBeUndefined(); + }); + + test("should throw a Forbidden error if at least one result isn't allowed", async () => { + const typeDefs = ` + type Post { + id: ID! + publisher: String! + published: Boolean! + } + + extend type Post @auth(rules: [ + { allow: { OR: [ + { publisher: "$jwt.sub" }, + { published: true }, + ] }, allowUnauthenticated: true } + ]) + `; + + const postId = generate({ charset: "alphabetic" }); + const postId2 = generate({ charset: "alphabetic" }); + + const query = ` + { + posts(where: { OR: [{id: ${postId}}, {id: ${postId2}}] }) { + id + } + } + `; + + const secret = "secret"; + const session = driver.session({ defaultAccessMode: "WRITE" }); + const neoSchema = new Neo4jGraphQL({ typeDefs, config: { jwt: { secret } } }); + + await session.run(` + CREATE (:Post {id: "${postId}", publisher: "nop", published: false}) + CREATE (:Post {id: "${postId2}", publisher: "nop", published: true}) + `); + + const socket = new Socket({ readable: true }); + const req = new IncomingMessage(socket); + + const gqlResult = await graphql({ + contextValue: { driver, req }, + schema: neoSchema.schema, + source: query, + }); + + // Check that a Forbidden error have been throwed + expect((gqlResult.errors as any[])[0].message).toEqual("Forbidden"); + expect((gqlResult.errors as any[])).toHaveLength(1); + + // Check if returned data is what we really want + expect(gqlResult.data?.posts).toBeUndefined(); + }); + }); + + describe("allowUnauthenticated with where", () => { + test("should return a Post without errors", async () => { + const typeDefs = ` + type Post { + id: ID! + publisher: String! + published: Boolean! + } + + extend type Post @auth(rules: [ + { where: { OR: [ + { publisher: "$jwt.sub" }, + { published: true }, + ] }, allowUnauthenticated: true } + ]) + `; + + const postId = generate({ charset: "alphabetic" }); + + const query = ` + { + posts(where: { id: ${postId} }) { + id + } + } + `; + + const secret = "secret"; + const session = driver.session({ defaultAccessMode: "WRITE" }); + const neoSchema = new Neo4jGraphQL({ typeDefs, config: { jwt: { secret } } }); + + await session.run(` + CREATE (:Post {id: "${postId}", publisher: "nop", published: true}) + `); + + const socket = new Socket({ readable: true }); + const req = new IncomingMessage(socket); + + const gqlResult = await graphql({ + contextValue: { driver, req }, + schema: neoSchema.schema, + source: query, + }); + + // Check that no errors have been throwed + expect(gqlResult.errors).toBeUndefined(); + + // Check if returned data is what we really want + expect(gqlResult.data?.posts?.[0]?.id).toBe(postId); + }); + + test("should return an empty array without errors", async () => { + const typeDefs = ` + type Post { + id: ID! + publisher: String! + published: Boolean! + } + + extend type Post @auth(rules: [ + { where: { OR: [ + { publisher: "$jwt.sub" }, + { published: true }, + ] }, allowUnauthenticated: true } + ]) + `; + + const postId = generate({ charset: "alphabetic" }); + + const query = ` + { + posts(where: { id: ${postId} }) { + id + } + } + `; + + const secret = "secret"; + const session = driver.session({ defaultAccessMode: "WRITE" }); + const neoSchema = new Neo4jGraphQL({ typeDefs, config: { jwt: { secret } } }); + + await session.run(` + CREATE (:Post {id: "${postId}", publisher: "nop", published: false}) + `); + + const socket = new Socket({ readable: true }); + const req = new IncomingMessage(socket); + + const gqlResult = await graphql({ + contextValue: { driver, req }, + schema: neoSchema.schema, + source: query, + }); + + // Check that no errors have been throwed + expect(gqlResult.errors).toBeUndefined(); + + // Check if returned data is what we really want + expect(gqlResult.data?.posts).toStrictEqual([]); + }); + + test("should only return published Posts without errors", async () => { + const typeDefs = ` + type Post { + id: ID! + publisher: String! + published: Boolean! + } + + extend type Post @auth(rules: [ + { where: { OR: [ + { publisher: "$jwt.sub" }, + { published: true }, + ] }, allowUnauthenticated: true } + ]) + `; + + const postId = generate({ charset: "alphabetic" }); + const postId2 = generate({ charset: "alphabetic" }); + + const query = ` + { + posts(where: { OR: [{id: ${postId}}, {id: ${postId2}}] }) { + id + } + } + `; + + const secret = "secret"; + const session = driver.session({ defaultAccessMode: "WRITE" }); + const neoSchema = new Neo4jGraphQL({ typeDefs, config: { jwt: { secret } } }); + + await session.run(` + CREATE (:Post {id: "${postId}", publisher: "nop", published: false}) + CREATE (:Post {id: "${postId2}", publisher: "nop", published: true}) + `); + + const socket = new Socket({ readable: true }); + const req = new IncomingMessage(socket); + + const gqlResult = await graphql({ + contextValue: { driver, req }, + schema: neoSchema.schema, + source: query, + }); + + // Check that no errors have been throwed + expect(gqlResult.errors).toBeUndefined(); + + // Check if returned data is what we really want + expect(gqlResult.data?.posts).toContainEqual({ id: postId2 }); + expect(gqlResult.data?.posts).toHaveLength(1); + }); + }); + + describe("allowUnauthenticated with bind", () => { + test("should throw Forbiden error only", async () => { + const typeDefs = ` + type User { + id: ID + } + + extend type User @auth(rules: [{ + operations: [CREATE], + bind: { id: "$jwt.sub" }, + allowUnauthenticated: true + }]) + `; + + const query = ` + mutation { + createUsers(input: [{id: "not bound"}]) { + users { + id + } + } + } + `; + + const secret = "secret"; + const neoSchema = new Neo4jGraphQL({ typeDefs, config: { jwt: { secret } } }); + + const socket = new Socket({ readable: true }); + const req = new IncomingMessage(socket); + + const gqlResult = await graphql({ + contextValue: { driver, req }, + schema: neoSchema.schema, + source: query, + }); + + // Check that a Forbidden error have been throwed + expect((gqlResult.errors as any[])[0].message).toEqual("Forbidden"); + expect((gqlResult.errors as any[])).toHaveLength(1); + + // Check if returned data is what we really want + expect(gqlResult.data?.posts).toBeUndefined(); + }); + }); +}); diff --git a/packages/graphql/tests/tck/tck-test-files/cypher/directives/auth/arguments/allow.md b/packages/graphql/tests/tck/tck-test-files/cypher/directives/auth/arguments/allow.md index 4f1730a469..463399d5c6 100644 --- a/packages/graphql/tests/tck/tck-test-files/cypher/directives/auth/arguments/allow.md +++ b/packages/graphql/tests/tck/tck-test-files/cypher/directives/auth/arguments/allow.md @@ -86,7 +86,7 @@ extend type Comment ```cypher MATCH (this:User) -CALL apoc.util.validate(NOT(EXISTS(this.id) AND this.id = $this_auth_allow0_id), "@neo4j/graphql/FORBIDDEN", [0]) +CALL apoc.util.validate(NOT(this.id IS NOT NULL AND this.id = $this_auth_allow0_id), "@neo4j/graphql/FORBIDDEN", [0]) RETURN this { .id } as this ``` @@ -125,9 +125,9 @@ RETURN this { .id } as this ```cypher MATCH (this:User) -CALL apoc.util.validate(NOT(EXISTS(this.id) AND this.id = $this_auth_allow0_id), "@neo4j/graphql/FORBIDDEN", [0]) +CALL apoc.util.validate(NOT(this.id IS NOT NULL AND this.id = $this_auth_allow0_id), "@neo4j/graphql/FORBIDDEN", [0]) WITH this -CALL apoc.util.validate(NOT(EXISTS(this.id) AND this.id = $this_password_auth_allow0_id), "@neo4j/graphql/FORBIDDEN", [0]) +CALL apoc.util.validate(NOT(this.id IS NOT NULL AND this.id = $this_password_auth_allow0_id), "@neo4j/graphql/FORBIDDEN", [0]) RETURN this { .password } as this ``` @@ -170,10 +170,10 @@ RETURN this { .password } as this ```cypher MATCH (this:User) -CALL apoc.util.validate(NOT(EXISTS(this.id) AND this.id = $this_auth_allow0_id), "@neo4j/graphql/FORBIDDEN", [0]) +CALL apoc.util.validate(NOT(this.id IS NOT NULL AND this.id = $this_auth_allow0_id), "@neo4j/graphql/FORBIDDEN", [0]) RETURN this { .id, - posts: [ (this)-[:HAS_POST]->(this_posts:Post) WHERE apoc.util.validatePredicate(NOT(EXISTS((this_posts)<-[:HAS_POST]-(:User)) AND ANY(creator IN [(this_posts)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this_posts_auth_allow0_creator_id)), "@neo4j/graphql/FORBIDDEN", [0]) | this_posts { .content } ] + posts: [ (this)-[:HAS_POST]->(this_posts:Post) WHERE apoc.util.validatePredicate(NOT(EXISTS((this_posts)<-[:HAS_POST]-(:User)) AND ANY(creator IN [(this_posts)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this_posts_auth_allow0_creator_id)), "@neo4j/graphql/FORBIDDEN", [0]) | this_posts { .content } ] } as this ``` @@ -215,12 +215,12 @@ RETURN this { ```cypher MATCH (this:Post) -CALL apoc.util.validate(NOT(EXISTS((this)<-[:HAS_POST]-(:User)) AND ANY(creator IN [(this)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this_auth_allow0_creator_id)), "@neo4j/graphql/FORBIDDEN", [0]) +CALL apoc.util.validate(NOT(EXISTS((this)<-[:HAS_POST]-(:User)) AND ANY(creator IN [(this)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this_auth_allow0_creator_id)), "@neo4j/graphql/FORBIDDEN", [0]) RETURN this { creator: head([ (this)<-[:HAS_POST]-(this_creator:User) - WHERE apoc.util.validatePredicate(NOT(EXISTS(this_creator.id) AND this_creator.id = $this_creator_auth_allow0_id), "@neo4j/graphql/FORBIDDEN", [0]) AND apoc.util.validatePredicate(NOT(EXISTS(this_creator.id) AND this_creator.id = $this_creator_password_auth_allow0_id), "@neo4j/graphql/FORBIDDEN", [0]) | this_creator { + WHERE apoc.util.validatePredicate(NOT(this_creator.id IS NOT NULL AND this_creator.id = $this_creator_auth_allow0_id), "@neo4j/graphql/FORBIDDEN", [0]) AND apoc.util.validatePredicate(NOT(this_creator.id IS NOT NULL AND this_creator.id = $this_creator_password_auth_allow0_id), "@neo4j/graphql/FORBIDDEN", [0]) | this_creator { .password } ]) } as this @@ -269,10 +269,10 @@ RETURN this { ```cypher MATCH (this:User) WHERE this.id = $this_id -CALL apoc.util.validate(NOT(EXISTS(this.id) AND this.id = $this_auth_allow0_id), "@neo4j/graphql/FORBIDDEN", [0]) +CALL apoc.util.validate(NOT(this.id IS NOT NULL AND this.id = $this_auth_allow0_id), "@neo4j/graphql/FORBIDDEN", [0]) RETURN this { .id, - posts: [ (this)-[:HAS_POST]->(this_posts:Post) WHERE this_posts.id = $this_posts_id AND apoc.util.validatePredicate(NOT(EXISTS((this_posts)<-[:HAS_POST]-(:User)) AND ANY(creator IN [(this_posts)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this_posts_auth_allow0_creator_id)), "@neo4j/graphql/FORBIDDEN", [0]) | this_posts { comments: [ (this_posts)-[:HAS_COMMENT]->(this_posts_comments:Comment) WHERE this_posts_comments.id = $this_posts_comments_id AND apoc.util.validatePredicate(NOT(EXISTS((this_posts_comments)<-[:HAS_COMMENT]-(:User)) AND ANY(creator IN [(this_posts_comments)<-[:HAS_COMMENT]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this_posts_comments_auth_allow0_creator_id)), "@neo4j/graphql/FORBIDDEN", [0]) | this_posts_comments { + posts: [ (this)-[:HAS_POST]->(this_posts:Post) WHERE this_posts.id = $this_posts_id AND apoc.util.validatePredicate(NOT(EXISTS((this_posts)<-[:HAS_POST]-(:User)) AND ANY(creator IN [(this_posts)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this_posts_auth_allow0_creator_id)), "@neo4j/graphql/FORBIDDEN", [0]) | this_posts { comments: [ (this_posts)-[:HAS_COMMENT]->(this_posts_comments:Comment) WHERE this_posts_comments.id = $this_posts_comments_id AND apoc.util.validatePredicate(NOT(EXISTS((this_posts_comments)<-[:HAS_COMMENT]-(:User)) AND ANY(creator IN [(this_posts_comments)<-[:HAS_COMMENT]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this_posts_comments_auth_allow0_creator_id)), "@neo4j/graphql/FORBIDDEN", [0]) | this_posts_comments { .content } ] } ] } as this @@ -323,7 +323,7 @@ MATCH (this:User) WHERE this.id = $this_id WITH this -CALL apoc.util.validate(NOT(EXISTS(this.id) AND this.id = $this_auth_allow0_id), "@neo4j/graphql/FORBIDDEN", [0]) +CALL apoc.util.validate(NOT(this.id IS NOT NULL AND this.id = $this_auth_allow0_id), "@neo4j/graphql/FORBIDDEN", [0]) SET this.id = $this_update_id @@ -372,7 +372,7 @@ MATCH (this:User) WHERE this.id = $this_id WITH this -CALL apoc.util.validate(NOT(EXISTS(this.id) AND this.id = $this_auth_allow0_id AND EXISTS(this.id) AND this.id = $this_update_password_auth_allow0_id), "@neo4j/graphql/FORBIDDEN", [0]) +CALL apoc.util.validate(NOT(this.id IS NOT NULL AND this.id = $this_auth_allow0_id AND this.id IS NOT NULL AND this.id = $this_update_password_auth_allow0_id), "@neo4j/graphql/FORBIDDEN", [0]) SET this.password = $this_update_password @@ -425,12 +425,12 @@ MATCH (this:Post) WHERE this.id = $this_id WITH this -CALL apoc.util.validate(NOT(EXISTS((this)<-[:HAS_POST]-(:User)) AND ANY(creator IN [(this)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this_auth_allow0_creator_id)), "@neo4j/graphql/FORBIDDEN", [0]) +CALL apoc.util.validate(NOT(EXISTS((this)<-[:HAS_POST]-(:User)) AND ANY(creator IN [(this)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this_auth_allow0_creator_id)), "@neo4j/graphql/FORBIDDEN", [0]) WITH this OPTIONAL MATCH (this)<-[:HAS_POST]-(this_creator0:User) CALL apoc.do.when(this_creator0 IS NOT NULL, " WITH this, this_creator0 - CALL apoc.util.validate(NOT(EXISTS(this_creator0.id) AND this_creator0.id = $this_creator0_auth_allow0_id), \"@neo4j/graphql/FORBIDDEN\", [0]) + CALL apoc.util.validate(NOT(this_creator0.id IS NOT NULL AND this_creator0.id = $this_creator0_auth_allow0_id), \"@neo4j/graphql/FORBIDDEN\", [0]) SET this_creator0.id = $this_update_creator0_id RETURN count(*) ", "", @@ -498,11 +498,11 @@ MATCH (this:Post) WHERE this.id = $this_id WITH this -CALL apoc.util.validate(NOT(EXISTS((this)<-[:HAS_POST]-(:User)) AND ANY(creator IN [(this)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this_auth_allow0_creator_id)), "@neo4j/graphql/FORBIDDEN", [0]) WITH this OPTIONAL MATCH (this)<-[:HAS_POST]-(this_creator0:User) +CALL apoc.util.validate(NOT(EXISTS((this)<-[:HAS_POST]-(:User)) AND ANY(creator IN [(this)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this_auth_allow0_creator_id)), "@neo4j/graphql/FORBIDDEN", [0]) WITH this OPTIONAL MATCH (this)<-[:HAS_POST]-(this_creator0:User) CALL apoc.do.when(this_creator0 IS NOT NULL, " WITH this, this_creator0 - CALL apoc.util.validate(NOT(EXISTS(this_creator0.id) AND this_creator0.id = $this_creator0_auth_allow0_id AND EXISTS(this_creator0.id) AND this_creator0.id = $this_update_creator0_password_auth_allow0_id), \"@neo4j/graphql/FORBIDDEN\", [0]) + CALL apoc.util.validate(NOT(this_creator0.id IS NOT NULL AND this_creator0.id = $this_creator0_auth_allow0_id AND this_creator0.id IS NOT NULL AND this_creator0.id = $this_update_creator0_password_auth_allow0_id), \"@neo4j/graphql/FORBIDDEN\", [0]) SET this_creator0.password = $this_update_creator0_password RETURN count(*) ", @@ -563,7 +563,7 @@ mutation { ```cypher MATCH (this:User) WHERE this.id = $this_id WITH this -CALL apoc.util.validate(NOT(EXISTS(this.id) AND this.id = $this_auth_allow0_id), "@neo4j/graphql/FORBIDDEN", [0]) +CALL apoc.util.validate(NOT(this.id IS NOT NULL AND this.id = $this_auth_allow0_id), "@neo4j/graphql/FORBIDDEN", [0]) DETACH DELETE this ``` @@ -611,13 +611,13 @@ WITH this OPTIONAL MATCH (this)-[:HAS_POST]->(this_posts0:Post) WHERE this_posts0.id = $this_posts0_id WITH this, this_posts0 -CALL apoc.util.validate(NOT(EXISTS((this_posts0)<-[:HAS_POST]-(:User)) AND ANY(creator IN [(this_posts0)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this_posts0_auth_allow0_creator_id)), "@neo4j/graphql/FORBIDDEN", [0]) +CALL apoc.util.validate(NOT(EXISTS((this_posts0)<-[:HAS_POST]-(:User)) AND ANY(creator IN [(this_posts0)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this_posts0_auth_allow0_creator_id)), "@neo4j/graphql/FORBIDDEN", [0]) FOREACH(_ IN CASE this_posts0 WHEN NULL THEN [] ELSE [1] END | DETACH DELETE this_posts0 ) WITH this -CALL apoc.util.validate(NOT(EXISTS(this.id) AND this.id = $this_auth_allow0_id), "@neo4j/graphql/FORBIDDEN", [0]) +CALL apoc.util.validate(NOT(this.id IS NOT NULL AND this.id = $this_auth_allow0_id), "@neo4j/graphql/FORBIDDEN", [0]) DETACH DELETE this ``` @@ -671,7 +671,7 @@ WHERE this_disconnect_posts0.id = $this_disconnect_posts0_id WITH this, this_disconnect_posts0, this_disconnect_posts0_rel -CALL apoc.util.validate(NOT(EXISTS(this_disconnect_posts0.id) AND this_disconnect_posts0.id = $this_disconnect_posts0User0_allow_auth_allow0_id AND EXISTS((this_disconnect_posts0)<-[:HAS_POST]-(:User)) AND ANY(creator IN [(this_disconnect_posts0)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this_disconnect_posts0Post1_allow_auth_allow0_creator_id)), "@neo4j/graphql/FORBIDDEN", [0]) +CALL apoc.util.validate(NOT(this_disconnect_posts0.id IS NOT NULL AND this_disconnect_posts0.id = $this_disconnect_posts0User0_allow_auth_allow0_id AND EXISTS((this_disconnect_posts0)<-[:HAS_POST]-(:User)) AND ANY(creator IN [(this_disconnect_posts0)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this_disconnect_posts0Post1_allow_auth_allow0_creator_id)), "@neo4j/graphql/FORBIDDEN", [0]) FOREACH(_ IN CASE this_disconnect_posts0 WHEN NULL THEN [] ELSE [1] END | DELETE this_disconnect_posts0_rel @@ -732,13 +732,13 @@ MATCH (this:Comment) WHERE this.id = $this_id WITH this -CALL apoc.util.validate(NOT(EXISTS((this)<-[:HAS_COMMENT]-(:User)) AND ANY(creator IN [(this)<-[:HAS_COMMENT]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this_auth_allow0_creator_id)), "@neo4j/graphql/FORBIDDEN", [0]) +CALL apoc.util.validate(NOT(EXISTS((this)<-[:HAS_COMMENT]-(:User)) AND ANY(creator IN [(this)<-[:HAS_COMMENT]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this_auth_allow0_creator_id)), "@neo4j/graphql/FORBIDDEN", [0]) WITH this OPTIONAL MATCH (this)<-[this_post0_disconnect0_rel:HAS_COMMENT]-(this_post0_disconnect0:Post) WITH this, this_post0_disconnect0, this_post0_disconnect0_rel -CALL apoc.util.validate(NOT(EXISTS((this_post0_disconnect0)<-[:HAS_COMMENT]-(:User)) AND ANY(creator IN [(this_post0_disconnect0)<-[:HAS_COMMENT]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this_post0_disconnect0Comment0_allow_auth_allow0_creator_id) AND EXISTS((this_post0_disconnect0)<-[:HAS_POST]-(:User)) AND ANY(creator IN [(this_post0_disconnect0)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this_post0_disconnect0Post1_allow_auth_allow0_creator_id)), "@neo4j/graphql/FORBIDDEN", [0]) +CALL apoc.util.validate(NOT(EXISTS((this_post0_disconnect0)<-[:HAS_COMMENT]-(:User)) AND ANY(creator IN [(this_post0_disconnect0)<-[:HAS_COMMENT]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this_post0_disconnect0Comment0_allow_auth_allow0_creator_id) AND EXISTS((this_post0_disconnect0)<-[:HAS_POST]-(:User)) AND ANY(creator IN [(this_post0_disconnect0)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this_post0_disconnect0Post1_allow_auth_allow0_creator_id)), "@neo4j/graphql/FORBIDDEN", [0]) FOREACH(_ IN CASE this_post0_disconnect0 WHEN NULL THEN [] ELSE [1] END | DELETE this_post0_disconnect0_rel @@ -749,7 +749,7 @@ OPTIONAL MATCH (this_post0_disconnect0)<-[this_post0_disconnect0_creator0_rel:HA WHERE this_post0_disconnect0_creator0.id = $this_post0_disconnect0_creator0_id WITH this, this_post0_disconnect0, this_post0_disconnect0_creator0, this_post0_disconnect0_creator0_rel -CALL apoc.util.validate(NOT(EXISTS((this_post0_disconnect0_creator0)<-[:HAS_POST]-(:User)) AND ANY(creator IN [(this_post0_disconnect0_creator0)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this_post0_disconnect0_creator0Post0_allow_auth_allow0_creator_id) AND EXISTS(this_post0_disconnect0_creator0.id) AND this_post0_disconnect0_creator0.id = $this_post0_disconnect0_creator0User1_allow_auth_allow0_id), "@neo4j/graphql/FORBIDDEN", [0]) +CALL apoc.util.validate(NOT(EXISTS((this_post0_disconnect0_creator0)<-[:HAS_POST]-(:User)) AND ANY(creator IN [(this_post0_disconnect0_creator0)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this_post0_disconnect0_creator0Post0_allow_auth_allow0_creator_id) AND this_post0_disconnect0_creator0.id IS NOT NULL AND this_post0_disconnect0_creator0.id = $this_post0_disconnect0_creator0User1_allow_auth_allow0_id), "@neo4j/graphql/FORBIDDEN", [0]) FOREACH(_ IN CASE this_post0_disconnect0_creator0 WHEN NULL THEN [] ELSE [1] END | DELETE this_post0_disconnect0_creator0_rel @@ -813,7 +813,7 @@ CALL { WHERE this_connect_posts0.id = $this_connect_posts0_id WITH this, this_connect_posts0 - CALL apoc.util.validate(NOT(EXISTS(this_connect_posts0.id) AND this_connect_posts0.id = $this_connect_posts0User0_allow_auth_allow0_id AND EXISTS((this_connect_posts0)<-[:HAS_POST]-(:User)) AND ANY(creator IN [(this_connect_posts0)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this_connect_posts0Post1_allow_auth_allow0_creator_id)), "@neo4j/graphql/FORBIDDEN", [0]) + CALL apoc.util.validate(NOT(this_connect_posts0.id IS NOT NULL AND this_connect_posts0.id = $this_connect_posts0User0_allow_auth_allow0_id AND EXISTS((this_connect_posts0)<-[:HAS_POST]-(:User)) AND ANY(creator IN [(this_connect_posts0)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this_connect_posts0Post1_allow_auth_allow0_creator_id)), "@neo4j/graphql/FORBIDDEN", [0]) FOREACH(_ IN CASE this_connect_posts0 WHEN NULL THEN [] ELSE [1] END | MERGE (this)-[:HAS_POST]->(this_connect_posts0) diff --git a/packages/graphql/tests/tck/tck-test-files/cypher/directives/auth/arguments/bind.md b/packages/graphql/tests/tck/tck-test-files/cypher/directives/auth/arguments/bind.md index 1b7c0e704a..455b4537ff 100644 --- a/packages/graphql/tests/tck/tck-test-files/cypher/directives/auth/arguments/bind.md +++ b/packages/graphql/tests/tck/tck-test-files/cypher/directives/auth/arguments/bind.md @@ -61,7 +61,7 @@ CALL { SET this0.id = $this0_id SET this0.name = $this0_name WITH this0 - CALL apoc.util.validate(NOT(EXISTS(this0.id) AND this0.id = $this0_auth_bind0_id), "@neo4j/graphql/FORBIDDEN", [0]) + CALL apoc.util.validate(NOT(this0.id IS NOT NULL AND this0.id = $this0_auth_bind0_id), "@neo4j/graphql/FORBIDDEN", [0]) RETURN this0 } RETURN this0 { .id } AS this0 @@ -134,17 +134,17 @@ CALL { SET this0_posts0_creator0.id = $this0_posts0_creator0_id WITH this0, this0_posts0, this0_posts0_creator0 - CALL apoc.util.validate(NOT(EXISTS(this0_posts0_creator0.id) AND this0_posts0_creator0.id = $this0_posts0_creator0_auth_bind0_id), "@neo4j/graphql/FORBIDDEN", [0]) + CALL apoc.util.validate(NOT(this0_posts0_creator0.id IS NOT NULL AND this0_posts0_creator0.id = $this0_posts0_creator0_auth_bind0_id), "@neo4j/graphql/FORBIDDEN", [0]) MERGE (this0_posts0)<-[:HAS_POST]-(this0_posts0_creator0) WITH this0, this0_posts0 - CALL apoc.util.validate(NOT(EXISTS((this0_posts0)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this0_posts0)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this0_posts0_auth_bind0_creator_id)), "@neo4j/graphql/FORBIDDEN", [0]) + CALL apoc.util.validate(NOT(EXISTS((this0_posts0)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this0_posts0)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this0_posts0_auth_bind0_creator_id)), "@neo4j/graphql/FORBIDDEN", [0]) MERGE (this0)-[:HAS_POST]->(this0_posts0) WITH this0 - CALL apoc.util.validate(NOT(EXISTS(this0.id) AND this0.id = $this0_auth_bind0_id), "@neo4j/graphql/FORBIDDEN", [0]) + CALL apoc.util.validate(NOT(this0.id IS NOT NULL AND this0.id = $this0_auth_bind0_id), "@neo4j/graphql/FORBIDDEN", [0]) RETURN this0 } @@ -199,7 +199,7 @@ WHERE this.id = $this_id SET this.id = $this_update_id WITH this -CALL apoc.util.validate(NOT(EXISTS(this.id) AND this.id = $this_auth_bind0_id), "@neo4j/graphql/FORBIDDEN", [0]) +CALL apoc.util.validate(NOT(this.id IS NOT NULL AND this.id = $this_auth_bind0_id), "@neo4j/graphql/FORBIDDEN", [0]) RETURN this { .id } AS this ``` @@ -264,7 +264,7 @@ CALL apoc.do.when(this_posts0 IS NOT NULL, SET this_posts0_creator0.id = $this_update_posts0_creator0_id WITH this, this_posts0, this_posts0_creator0 - CALL apoc.util.validate(NOT(EXISTS(this_posts0_creator0.id) AND this_posts0_creator0.id = $this_posts0_creator0_auth_bind0_id), \"@neo4j/graphql/FORBIDDEN\", [0]) + CALL apoc.util.validate(NOT(this_posts0_creator0.id IS NOT NULL AND this_posts0_creator0.id = $this_posts0_creator0_auth_bind0_id), \"@neo4j/graphql/FORBIDDEN\", [0]) RETURN count(*) \", @@ -276,7 +276,7 @@ CALL apoc.do.when(this_posts0 IS NOT NULL, {this:this, this_posts0:this_posts0, auth:$auth,this_update_posts0_creator0_id:$this_update_posts0_creator0_id,this_posts0_creator0_auth_bind0_id:$this_posts0_creator0_auth_bind0_id}) YIELD value as _ WITH this -CALL apoc.util.validate(NOT(EXISTS(this.id) AND this.id = $this_auth_bind0_id), "@neo4j/graphql/FORBIDDEN", [0]) +CALL apoc.util.validate(NOT(this.id IS NOT NULL AND this.id = $this_auth_bind0_id), "@neo4j/graphql/FORBIDDEN", [0]) RETURN this { .id } AS this ``` @@ -345,7 +345,7 @@ CALL { ) WITH this, this_connect_creator0 - CALL apoc.util.validate(NOT(EXISTS((this_connect_creator0)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_connect_creator0)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this_connect_creator0Post0_bind_auth_bind0_creator_id) AND EXISTS(this_connect_creator0.id) AND this_connect_creator0.id = $this_connect_creator0User1_bind_auth_bind0_id), "@neo4j/graphql/FORBIDDEN", [0]) + CALL apoc.util.validate(NOT(EXISTS((this_connect_creator0)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_connect_creator0)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this_connect_creator0Post0_bind_auth_bind0_creator_id) AND this_connect_creator0.id IS NOT NULL AND this_connect_creator0.id = $this_connect_creator0User1_bind_auth_bind0_id), "@neo4j/graphql/FORBIDDEN", [0]) RETURN count(*) } @@ -406,7 +406,7 @@ FOREACH(_ IN CASE this_disconnect_creator0 WHEN NULL THEN [] ELSE [1] END | ) WITH this, this_disconnect_creator0 -CALL apoc.util.validate(NOT(EXISTS((this_disconnect_creator0)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_disconnect_creator0)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this_disconnect_creator0Post0_bind_auth_bind0_creator_id) AND EXISTS(this_disconnect_creator0.id) AND this_disconnect_creator0.id = $this_disconnect_creator0User1_bind_auth_bind0_id), "@neo4j/graphql/FORBIDDEN", [0]) +CALL apoc.util.validate(NOT(EXISTS((this_disconnect_creator0)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_disconnect_creator0)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this_disconnect_creator0Post0_bind_auth_bind0_creator_id) AND this_disconnect_creator0.id IS NOT NULL AND this_disconnect_creator0.id = $this_disconnect_creator0User1_bind_auth_bind0_id), "@neo4j/graphql/FORBIDDEN", [0]) RETURN this { .id } AS this ``` diff --git a/packages/graphql/tests/tck/tck-test-files/cypher/directives/auth/arguments/where.md b/packages/graphql/tests/tck/tck-test-files/cypher/directives/auth/arguments/where.md index 1bb6e655f8..1bbb30496f 100644 --- a/packages/graphql/tests/tck/tck-test-files/cypher/directives/auth/arguments/where.md +++ b/packages/graphql/tests/tck/tck-test-files/cypher/directives/auth/arguments/where.md @@ -83,7 +83,7 @@ extend type Post ```cypher MATCH (this:User) -WHERE EXISTS(this.id) AND this.id = $this_auth_where0_id +WHERE this.id IS NOT NULL AND this.id = $this_auth_where0_id RETURN this { .id } as this ``` @@ -122,7 +122,7 @@ RETURN this { .id } as this ```cypher MATCH (this:User) -WHERE this.name = $this_name AND EXISTS(this.id) AND this.id = $this_auth_where0_id +WHERE this.name = $this_name AND this.id IS NOT NULL AND this.id = $this_auth_where0_id RETURN this { .id } as this ``` @@ -165,10 +165,10 @@ RETURN this { .id } as this ```cypher MATCH (this:User) -WHERE EXISTS(this.id) AND this.id = $this_auth_where0_id +WHERE this.id IS NOT NULL AND this.id = $this_auth_where0_id RETURN this { .id, - posts: [ (this)-[:HAS_POST]->(this_posts:Post) WHERE EXISTS((this_posts)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_posts)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this_posts_auth_where0_creator_id) | this_posts { .content } ] + posts: [ (this)-[:HAS_POST]->(this_posts:Post) WHERE EXISTS((this_posts)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_posts)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this_posts_auth_where0_creator_id) | this_posts { .content } ] } as this ``` @@ -211,11 +211,11 @@ RETURN this { ```cypher MATCH (this:User) -WHERE EXISTS(this.id) AND this.id = $this_auth_where0_id +WHERE this.id IS NOT NULL AND this.id = $this_auth_where0_id RETURN this { .id, - posts: [ (this)-[:HAS_POST]->(this_posts:Post) WHERE this_posts.content = $this_posts_content AND EXISTS((this_posts)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_posts)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this_posts_auth_where0_creator_id) | this_posts { .content } ] + posts: [ (this)-[:HAS_POST]->(this_posts:Post) WHERE this_posts.content = $this_posts_content AND EXISTS((this_posts)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_posts)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this_posts_auth_where0_creator_id) | this_posts { .content } ] } as this ``` @@ -261,10 +261,10 @@ RETURN this { ```cypher MATCH (this:User) -WHERE EXISTS(this.id) AND this.id = $this_auth_where0_id +WHERE this.id IS NOT NULL AND this.id = $this_auth_where0_id RETURN this { .id, - content: [(this)-[:HAS_POST]->(this_content) WHERE "Post" IN labels(this_content) | head( [ this_content IN [this_content] WHERE "Post" IN labels (this_content) AND EXISTS((this_content)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_content)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this_content_Post_auth_where0_creator_id) | this_content { __resolveType: "Post", .id } ] ) ] + content: [(this)-[:HAS_POST]->(this_content) WHERE "Post" IN labels(this_content) | head( [ this_content IN [this_content] WHERE "Post" IN labels (this_content) AND EXISTS((this_content)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_content)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this_content_Post_auth_where0_creator_id) | this_content { __resolveType: "Post", .id } ] ) ] } as this ``` @@ -306,7 +306,7 @@ mutation { ```cypher MATCH (this:User) -WHERE EXISTS(this.id) AND this.id = $this_auth_where0_id +WHERE this.id IS NOT NULL AND this.id = $this_auth_where0_id SET this.name = $this_update_name RETURN this { .id } AS this ``` @@ -349,7 +349,7 @@ mutation { ```cypher MATCH (this:User) -WHERE this.name = $this_name AND EXISTS(this.id) AND this.id = $this_auth_where0_id +WHERE this.name = $this_name AND this.id IS NOT NULL AND this.id = $this_auth_where0_id SET this.name = $this_update_name RETURN this { .id } AS this ``` @@ -396,16 +396,16 @@ mutation { ```cypher MATCH (this:User) -WHERE EXISTS(this.id) AND this.id = $this_auth_where0_id +WHERE this.id IS NOT NULL AND this.id = $this_auth_where0_id WITH this OPTIONAL MATCH (this)-[:HAS_POST]->(this_posts0:Post) -WHERE EXISTS((this_posts0)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_posts0)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this_posts0_auth_where0_creator_id) +WHERE EXISTS((this_posts0)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_posts0)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this_posts0_auth_where0_creator_id) CALL apoc.do.when(this_posts0 IS NOT NULL, " SET this_posts0.id = $this_update_posts0_id RETURN count(*) ", "", {this:this, this_posts0:this_posts0, auth:$auth,this_update_posts0_id:$this_update_posts0_id}) YIELD value as _ RETURN this { .id, - posts: [ (this)-[:HAS_POST]->(this_posts:Post) WHERE EXISTS((this_posts)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_posts)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this_posts_auth_where0_creator_id) | this_posts { .id } ] + posts: [ (this)-[:HAS_POST]->(this_posts:Post) WHERE EXISTS((this_posts)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_posts)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this_posts_auth_where0_creator_id) | this_posts { .id } ] } AS this ``` @@ -459,7 +459,7 @@ mutation { ```cypher MATCH (this:User) -WHERE EXISTS(this.id) AND this.id = $this_auth_where0_id +WHERE this.id IS NOT NULL AND this.id = $this_auth_where0_id DETACH DELETE this ``` @@ -498,7 +498,7 @@ mutation { ```cypher MATCH (this:User) -WHERE this.name = $this_name AND EXISTS(this.id) AND this.id = $this_auth_where0_id +WHERE this.name = $this_name AND this.id IS NOT NULL AND this.id = $this_auth_where0_id DETACH DELETE this ``` @@ -538,11 +538,11 @@ mutation { ```cypher MATCH (this:User) -WHERE EXISTS(this.id) AND this.id = $this_auth_where0_id +WHERE this.id IS NOT NULL AND this.id = $this_auth_where0_id WITH this OPTIONAL MATCH (this)-[:HAS_POST]->(this_posts0:Post) -WHERE EXISTS((this_posts0)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_posts0)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this_posts0_auth_where0_creator_id) +WHERE EXISTS((this_posts0)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_posts0)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this_posts0_auth_where0_creator_id) FOREACH(_ IN CASE this_posts0 WHEN NULL THEN [] ELSE [1] END | DETACH DELETE this_posts0 ) @@ -605,7 +605,7 @@ CALL { CALL { WITH this0 OPTIONAL MATCH (this0_posts_connect0:Post) - WHERE EXISTS((this0_posts_connect0)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this0_posts_connect0)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this0_posts_connect0_auth_where0_creator_id) + WHERE EXISTS((this0_posts_connect0)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this0_posts_connect0)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this0_posts_connect0_auth_where0_creator_id) FOREACH(_ IN CASE this0_posts_connect0 WHEN NULL THEN [] ELSE [1] END | MERGE (this0)-[:HAS_POST]->(this0_posts_connect0) ) @@ -675,7 +675,7 @@ CALL { CALL { WITH this0 OPTIONAL MATCH (this0_posts_connect0:Post) - WHERE this0_posts_connect0.id = $this0_posts_connect0_id AND EXISTS((this0_posts_connect0)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this0_posts_connect0)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this0_posts_connect0_auth_where0_creator_id) + WHERE this0_posts_connect0.id = $this0_posts_connect0_id AND EXISTS((this0_posts_connect0)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this0_posts_connect0)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this0_posts_connect0_auth_where0_creator_id) FOREACH(_ IN CASE this0_posts_connect0 WHEN NULL THEN [] ELSE [1] END | MERGE (this0)-[:HAS_POST]->(this0_posts_connect0) ) @@ -729,16 +729,16 @@ mutation { ```cypher MATCH (this:User) -WHERE EXISTS(this.id) AND this.id = $this_auth_where0_id +WHERE this.id IS NOT NULL AND this.id = $this_auth_where0_id WITH this -WHERE EXISTS(this.id) AND this.id = $this_auth_where0_id +WHERE this.id IS NOT NULL AND this.id = $this_auth_where0_id WITH this CALL { WITH this OPTIONAL MATCH (this_posts0_connect0:Post) - WHERE EXISTS((this_posts0_connect0)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_posts0_connect0)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this_posts0_connect0_auth_where0_creator_id) + WHERE EXISTS((this_posts0_connect0)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_posts0_connect0)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this_posts0_connect0_auth_where0_creator_id) FOREACH(_ IN CASE this_posts0_connect0 WHEN NULL THEN [] ELSE [1] END | MERGE (this)-[:HAS_POST]->(this_posts0_connect0) ) @@ -786,16 +786,16 @@ mutation { ```cypher MATCH (this:User) -WHERE EXISTS(this.id) AND this.id = $this_auth_where0_id +WHERE this.id IS NOT NULL AND this.id = $this_auth_where0_id WITH this -WHERE EXISTS(this.id) AND this.id = $this_auth_where0_id +WHERE this.id IS NOT NULL AND this.id = $this_auth_where0_id WITH this CALL { WITH this OPTIONAL MATCH (this_posts0_connect0:Post) - WHERE this_posts0_connect0.id = $this_posts0_connect0_id AND EXISTS((this_posts0_connect0)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_posts0_connect0)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this_posts0_connect0_auth_where0_creator_id) + WHERE this_posts0_connect0.id = $this_posts0_connect0_id AND EXISTS((this_posts0_connect0)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_posts0_connect0)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this_posts0_connect0_auth_where0_creator_id) FOREACH(_ IN CASE this_posts0_connect0 WHEN NULL THEN [] ELSE [1] END | MERGE (this)-[:HAS_POST]->(this_posts0_connect0) ) @@ -844,16 +844,16 @@ mutation { ```cypher MATCH (this:User) -WHERE EXISTS(this.id) AND this.id = $this_auth_where0_id +WHERE this.id IS NOT NULL AND this.id = $this_auth_where0_id WITH this -WHERE EXISTS(this.id) AND this.id = $this_auth_where0_id +WHERE this.id IS NOT NULL AND this.id = $this_auth_where0_id WITH this CALL { WITH this OPTIONAL MATCH (this_connect_posts0:Post) - WHERE EXISTS((this_connect_posts0)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_connect_posts0)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this_connect_posts0_auth_where0_creator_id) + WHERE EXISTS((this_connect_posts0)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_connect_posts0)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this_connect_posts0_auth_where0_creator_id) FOREACH(_ IN CASE this_connect_posts0 WHEN NULL THEN [] ELSE [1] END | MERGE (this)-[:HAS_POST]->(this_connect_posts0) ) @@ -901,16 +901,16 @@ mutation { ```cypher MATCH (this:User) -WHERE EXISTS(this.id) AND this.id = $this_auth_where0_id +WHERE this.id IS NOT NULL AND this.id = $this_auth_where0_id WITH this -WHERE EXISTS(this.id) AND this.id = $this_auth_where0_id +WHERE this.id IS NOT NULL AND this.id = $this_auth_where0_id WITH this CALL { WITH this OPTIONAL MATCH (this_connect_posts0:Post) - WHERE this_connect_posts0.id = $this_connect_posts0_id AND EXISTS((this_connect_posts0)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_connect_posts0)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this_connect_posts0_auth_where0_creator_id) + WHERE this_connect_posts0.id = $this_connect_posts0_id AND EXISTS((this_connect_posts0)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_connect_posts0)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this_connect_posts0_auth_where0_creator_id) FOREACH(_ IN CASE this_connect_posts0 WHEN NULL THEN [] ELSE [1] END | MERGE (this)-[:HAS_POST]->(this_connect_posts0) ) @@ -959,14 +959,14 @@ mutation { ```cypher MATCH (this:User) -WHERE EXISTS(this.id) AND this.id = $this_auth_where0_id +WHERE this.id IS NOT NULL AND this.id = $this_auth_where0_id WITH this -WHERE EXISTS(this.id) AND this.id = $this_auth_where0_id +WHERE this.id IS NOT NULL AND this.id = $this_auth_where0_id WITH this OPTIONAL MATCH (this)-[this_posts0_disconnect0_rel:HAS_POST]->(this_posts0_disconnect0:Post) -WHERE EXISTS((this_posts0_disconnect0)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_posts0_disconnect0)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this_posts0_disconnect0_auth_where0_creator_id) +WHERE EXISTS((this_posts0_disconnect0)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_posts0_disconnect0)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this_posts0_disconnect0_auth_where0_creator_id) FOREACH(_ IN CASE this_posts0_disconnect0 WHEN NULL THEN [] ELSE [1] END | DELETE this_posts0_disconnect0_rel ) @@ -1013,13 +1013,13 @@ mutation { ```cypher MATCH (this:User) -WHERE EXISTS(this.id) AND this.id = $this_auth_where0_id +WHERE this.id IS NOT NULL AND this.id = $this_auth_where0_id WITH this -WHERE EXISTS(this.id) AND this.id = $this_auth_where0_id +WHERE this.id IS NOT NULL AND this.id = $this_auth_where0_id WITH this -OPTIONAL MATCH (this)-[this_posts0_disconnect0_rel:HAS_POST]->(this_posts0_disconnect0:Post) WHERE this_posts0_disconnect0.id = $this_posts0_disconnect0_id AND EXISTS((this_posts0_disconnect0)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_posts0_disconnect0)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this_posts0_disconnect0_auth_where0_creator_id) +OPTIONAL MATCH (this)-[this_posts0_disconnect0_rel:HAS_POST]->(this_posts0_disconnect0:Post) WHERE this_posts0_disconnect0.id = $this_posts0_disconnect0_id AND EXISTS((this_posts0_disconnect0)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_posts0_disconnect0)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this_posts0_disconnect0_auth_where0_creator_id) FOREACH(_ IN CASE this_posts0_disconnect0 WHEN NULL THEN [] ELSE [1] END | DELETE this_posts0_disconnect0_rel ) @@ -1065,10 +1065,10 @@ mutation { ```cypher MATCH (this:User) -WHERE EXISTS(this.id) AND this.id = $this_auth_where0_id +WHERE this.id IS NOT NULL AND this.id = $this_auth_where0_id WITH this -WHERE EXISTS(this.id) AND this.id = $this_auth_where0_id WITH this OPTIONAL MATCH (this)-[this_disconnect_posts0_rel:HAS_POST]->(this_disconnect_posts0:Post) WHERE EXISTS((this_disconnect_posts0)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_disconnect_posts0)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this_disconnect_posts0_auth_where0_creator_id) +WHERE this.id IS NOT NULL AND this.id = $this_auth_where0_id WITH this OPTIONAL MATCH (this)-[this_disconnect_posts0_rel:HAS_POST]->(this_disconnect_posts0:Post) WHERE EXISTS((this_disconnect_posts0)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_disconnect_posts0)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this_disconnect_posts0_auth_where0_creator_id) FOREACH(_ IN CASE this_disconnect_posts0 WHEN NULL THEN [] ELSE [1] END | DELETE this_disconnect_posts0_rel @@ -1115,9 +1115,9 @@ mutation { ```cypher MATCH (this:User) -WHERE EXISTS(this.id) AND this.id = $this_auth_where0_id +WHERE this.id IS NOT NULL AND this.id = $this_auth_where0_id WITH this -WHERE EXISTS(this.id) AND this.id = $this_auth_where0_id WITH this OPTIONAL MATCH (this)-[this_disconnect_posts0_rel:HAS_POST]->(this_disconnect_posts0:Post) WHERE this_disconnect_posts0.id = $this_disconnect_posts0_id AND EXISTS((this_disconnect_posts0)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_disconnect_posts0)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this_disconnect_posts0_auth_where0_creator_id) +WHERE this.id IS NOT NULL AND this.id = $this_auth_where0_id WITH this OPTIONAL MATCH (this)-[this_disconnect_posts0_rel:HAS_POST]->(this_disconnect_posts0:Post) WHERE this_disconnect_posts0.id = $this_disconnect_posts0_id AND EXISTS((this_disconnect_posts0)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_disconnect_posts0)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this_disconnect_posts0_auth_where0_creator_id) FOREACH(_ IN CASE this_disconnect_posts0 WHEN NULL THEN [] ELSE [1] END | DELETE this_disconnect_posts0_rel diff --git a/packages/graphql/tests/tck/tck-test-files/cypher/directives/auth/projection.md b/packages/graphql/tests/tck/tck-test-files/cypher/directives/auth/projection.md index 760cf9ce3b..4617bfbcf6 100644 --- a/packages/graphql/tests/tck/tck-test-files/cypher/directives/auth/projection.md +++ b/packages/graphql/tests/tck/tck-test-files/cypher/directives/auth/projection.md @@ -36,10 +36,10 @@ mutation { ```cypher MATCH (this:User) WITH this -CALL apoc.util.validate(NOT(EXISTS(this.id) AND this.id = $this_update_id_auth_allow0_id), "@neo4j/graphql/FORBIDDEN", [0]) +CALL apoc.util.validate(NOT(this.id IS NOT NULL AND this.id = $this_update_id_auth_allow0_id), "@neo4j/graphql/FORBIDDEN", [0]) SET this.id = $this_update_id WITH this -CALL apoc.util.validate(NOT(EXISTS(this.id) AND this.id = $this_id_auth_allow0_id), "@neo4j/graphql/FORBIDDEN", [0]) +CALL apoc.util.validate(NOT(this.id IS NOT NULL AND this.id = $this_id_auth_allow0_id), "@neo4j/graphql/FORBIDDEN", [0]) RETURN this { .id } AS this ``` @@ -93,8 +93,8 @@ CALL { RETURN this1 } -CALL apoc.util.validate(NOT(EXISTS(this0.id) AND this0.id = $projection_id_auth_allow0_id), "@neo4j/graphql/FORBIDDEN", [0]) -CALL apoc.util.validate(NOT(EXISTS(this1.id) AND this1.id = $projection_id_auth_allow0_id), "@neo4j/graphql/FORBIDDEN", [0]) +CALL apoc.util.validate(NOT(this0.id IS NOT NULL AND this0.id = $projection_id_auth_allow0_id), "@neo4j/graphql/FORBIDDEN", [0]) +CALL apoc.util.validate(NOT(this1.id IS NOT NULL AND this1.id = $projection_id_auth_allow0_id), "@neo4j/graphql/FORBIDDEN", [0]) RETURN this0 { .id } AS this0, this1 { .id } AS this1 ``` diff --git a/packages/graphql/tests/tck/tck-test-files/cypher/union.md b/packages/graphql/tests/tck/tck-test-files/cypher/union.md index ce4b94ce09..b711efc1f3 100644 --- a/packages/graphql/tests/tck/tck-test-files/cypher/union.md +++ b/packages/graphql/tests/tck/tck-test-files/cypher/union.md @@ -62,7 +62,7 @@ RETURN this { [ this_search IN [this_search] WHERE "Genre" IN labels (this_search) AND this_search.name = $this_search_Genre_name AND - apoc.util.validatePredicate(NOT(EXISTS(this_search.name) AND this_search.name = $this_search_Genre_auth_allow0_name), "@neo4j/graphql/FORBIDDEN", [0]) | + apoc.util.validatePredicate(NOT(this_search.name IS NOT NULL AND this_search.name = $this_search_Genre_auth_allow0_name), "@neo4j/graphql/FORBIDDEN", [0]) | this_search { __resolveType: "Genre", .name From 94b725f450ae82049a77333d1181ee04b1187374 Mon Sep 17 00:00:00 2001 From: dmoree Date: Wed, 28 Jul 2021 00:55:13 -0700 Subject: [PATCH 5/6] Fix for field with multiple aliases (#359) * add: override default field resolver * fix: allow for multiple aliases on single field * fix: allow alias for response of {create,update} * fix: alias tck test add: tck test related to issue#350 add: integration test related to issue#350 * fix: dangling underscores * fix: {datetime,point} proj to use field.name * add: alias {datetime,point} tck test * change: contents of comments for snapshot * update: remove clearing of database * Update packages/graphql/tests/integration/issues/350.int.test.ts Co-authored-by: Daniel Starns --- packages/graphql/package.json | 2 +- .../src/schema/make-augmented-schema.ts | 18 ++- .../graphql/src/schema/resolvers/create.ts | 11 +- .../src/schema/resolvers/defaultField.ts | 36 +++++ .../graphql/src/schema/resolvers/index.ts | 1 + .../graphql/src/schema/resolvers/update.ts | 17 ++- .../translate/create-projection-and-params.ts | 35 +++-- .../graphql/src/translate/translate-create.ts | 9 +- .../graphql/src/translate/translate-update.ts | 10 +- .../tests/integration/issues/350.int.test.ts | 136 ++++++++++++++++++ .../tests/tck/tck-test-files/cypher/alias.md | 125 +++++++++++++++- 11 files changed, 362 insertions(+), 38 deletions(-) create mode 100644 packages/graphql/src/schema/resolvers/defaultField.ts create mode 100644 packages/graphql/tests/integration/issues/350.int.test.ts diff --git a/packages/graphql/package.json b/packages/graphql/package.json index b02a0631f9..c3ec9dde87 100644 --- a/packages/graphql/package.json +++ b/packages/graphql/package.json @@ -36,7 +36,6 @@ }, "author": "Neo4j Inc.", "devDependencies": { - "@graphql-tools/utils": "^7.7.3", "@types/deep-equal": "1.0.1", "@types/faker": "5.1.7", "@types/is-uuid": "1.0.0", @@ -62,6 +61,7 @@ "dependencies": { "@graphql-tools/merge": "^6.2.13", "@graphql-tools/schema": "^7.1.3", + "@graphql-tools/utils": "^7.7.3", "camelcase": "^6.2.0", "debug": "^4.3.1", "deep-equal": "^2.0.5", diff --git a/packages/graphql/src/schema/make-augmented-schema.ts b/packages/graphql/src/schema/make-augmented-schema.ts index cb08bf447b..3d59b24be8 100644 --- a/packages/graphql/src/schema/make-augmented-schema.ts +++ b/packages/graphql/src/schema/make-augmented-schema.ts @@ -19,6 +19,7 @@ import { mergeTypeDefs } from "@graphql-tools/merge"; import { IExecutableSchemaDefinition, makeExecutableSchema } from "@graphql-tools/schema"; +import { forEachField } from "@graphql-tools/utils"; import camelCase from "camelcase"; import { DefinitionNode, @@ -44,7 +45,14 @@ import pluralize from "pluralize"; import { Node, Exclude } from "../classes"; import getAuth from "./get-auth"; import { PrimitiveField, Auth } from "../types"; -import { findResolver, createResolver, deleteResolver, cypherResolver, updateResolver } from "./resolvers"; +import { + findResolver, + createResolver, + deleteResolver, + cypherResolver, + updateResolver, + defaultFieldResolver, +} from "./resolvers"; import checkNodeImplementsInterfaces from "./check-node-implements-interfaces"; import * as Scalars from "./scalars"; import parseExcludeDirective from "./parse-exclude-directive"; @@ -760,6 +768,14 @@ function makeAugmentedSchema( resolvers: generatedResolvers, }); + // Assign a default field resolver to account for aliasing of fields + forEachField(schema, (field) => { + if (!field.resolve) { + // eslint-disable-next-line no-param-reassign + field.resolve = defaultFieldResolver; + } + }); + return { nodes, schema, diff --git a/packages/graphql/src/schema/resolvers/create.ts b/packages/graphql/src/schema/resolvers/create.ts index e2ab877371..d1a64b6dc9 100644 --- a/packages/graphql/src/schema/resolvers/create.ts +++ b/packages/graphql/src/schema/resolvers/create.ts @@ -19,13 +19,14 @@ import camelCase from "camelcase"; import pluralize from "pluralize"; +import { FieldNode, GraphQLResolveInfo } from "graphql"; import { execute } from "../../utils"; import { translateCreate } from "../../translate"; import { Node } from "../../classes"; import { Context } from "../../types"; export default function createResolver({ node }: { node: Node }) { - async function resolve(_root: any, _args: any, _context: unknown) { + async function resolve(_root: any, _args: any, _context: unknown, info: GraphQLResolveInfo) { const context = _context as Context; const [cypher, params] = translateCreate({ context, node }); @@ -36,8 +37,14 @@ export default function createResolver({ node }: { node: Node }) { context, }); + const responseField = info.fieldNodes[0].selectionSet?.selections.find( + (selection) => selection.kind === "Field" && selection.name.value === pluralize(camelCase(node.name)) + ) as FieldNode; // Field exist by construction and must be selected as it is the only field. + + const responseKey = responseField.alias ? responseField.alias.value : responseField.name.value; + return { - [pluralize(camelCase(node.name))]: Object.values(result[0] || {}), + [responseKey]: Object.values(result[0] || {}), }; } diff --git a/packages/graphql/src/schema/resolvers/defaultField.ts b/packages/graphql/src/schema/resolvers/defaultField.ts new file mode 100644 index 0000000000..01dfc6606a --- /dev/null +++ b/packages/graphql/src/schema/resolvers/defaultField.ts @@ -0,0 +1,36 @@ +/* + * 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 { GraphQLResolveInfo } from "graphql"; + +/** + * Based on the default field resolver used by graphql-js that accounts for aliased fields + * @link https://github.com/graphql/graphql-js/blob/main/src/execution/execute.ts#L999-L1015 + */ +// eslint-disable-next-line consistent-return +export default function defaultFieldResolver(source: any, args: any, context: unknown, info: GraphQLResolveInfo) { + const responseKey = info.fieldNodes[0].alias ? info.fieldNodes[0].alias.value : info.fieldNodes[0].name.value; + if ((typeof source === "object" && source !== null) || typeof source === "function") { + const property = source[responseKey]; + if (typeof property === "function") { + return source[responseKey](args, context, info); + } + return property; + } +} diff --git a/packages/graphql/src/schema/resolvers/index.ts b/packages/graphql/src/schema/resolvers/index.ts index 5bf65f9c50..b6df70bdcd 100644 --- a/packages/graphql/src/schema/resolvers/index.ts +++ b/packages/graphql/src/schema/resolvers/index.ts @@ -22,3 +22,4 @@ export { default as findResolver } from "./read"; export { default as updateResolver } from "./update"; export { default as deleteResolver } from "./delete"; export { default as cypherResolver } from "./cypher"; +export { default as defaultFieldResolver } from "./defaultField"; diff --git a/packages/graphql/src/schema/resolvers/update.ts b/packages/graphql/src/schema/resolvers/update.ts index 7e75f93bf1..d0896caa1e 100644 --- a/packages/graphql/src/schema/resolvers/update.ts +++ b/packages/graphql/src/schema/resolvers/update.ts @@ -19,13 +19,14 @@ import camelCase from "camelcase"; import pluralize from "pluralize"; +import { FieldNode, GraphQLResolveInfo } from "graphql"; +import { execute } from "../../utils"; +import { translateUpdate } from "../../translate"; import { Node } from "../../classes"; import { Context } from "../../types"; -import { translateUpdate } from "../../translate"; -import { execute } from "../../utils"; export default function updateResolver({ node }: { node: Node }) { - async function resolve(_root: any, _args: any, _context: unknown) { + async function resolve(_root: any, _args: any, _context: unknown, info: GraphQLResolveInfo) { const context = _context as Context; const [cypher, params] = translateUpdate({ context, node }); const result = await execute({ @@ -35,7 +36,15 @@ export default function updateResolver({ node }: { node: Node }) { context, }); - return { [pluralize(camelCase(node.name))]: result.map((x) => x.this) }; + const responseField = info.fieldNodes[0].selectionSet?.selections.find( + (selection) => selection.kind === "Field" && selection.name.value === pluralize(camelCase(node.name)) + ) as FieldNode; // Field exist by construction and must be selected as it is the only field. + + const responseKey = responseField.alias ? responseField.alias.value : responseField.name.value; + + return { + [responseKey]: result.map((x) => x.this), + }; } return { diff --git a/packages/graphql/src/translate/create-projection-and-params.ts b/packages/graphql/src/translate/create-projection-and-params.ts index aab26fa07a..3601cf87f2 100644 --- a/packages/graphql/src/translate/create-projection-and-params.ts +++ b/packages/graphql/src/translate/create-projection-and-params.ts @@ -139,14 +139,7 @@ function createProjectionAndParams({ chainStr?: string; varName: string; }): [string, any, ProjectionMeta?] { - function reducer(res: Res, [k, field]: [string, any]): Res { - let key = k; - const alias: string | undefined = field.alias !== field.name ? field.alias : undefined; - - if (alias) { - key = field.name as string; - } - + function reducer(res: Res, [key, field]: [string, any]): Res { let param = ""; if (chainStr) { param = `${chainStr}_${key}`; @@ -157,11 +150,11 @@ function createProjectionAndParams({ const whereInput = field.args.where as GraphQLWhereArg; const optionsInput = field.args.options as GraphQLOptionsArg; const fieldFields = (field.fieldsByTypeName as unknown) as FieldsByTypeName; - const cypherField = node.cypherFields.find((x) => x.fieldName === key); - const relationField = node.relationFields.find((x) => x.fieldName === key); - const pointField = node.pointFields.find((x) => x.fieldName === key); - const dateTimeField = node.dateTimeFields.find((x) => x.fieldName === key); - const authableField = node.authableFields.find((x) => x.fieldName === key); + const cypherField = node.cypherFields.find((x) => x.fieldName === field.name); + const relationField = node.relationFields.find((x) => x.fieldName === field.name); + const pointField = node.pointFields.find((x) => x.fieldName === field.name); + const dateTimeField = node.dateTimeFields.find((x) => x.fieldName === field.name); + const authableField = node.authableFields.find((x) => x.fieldName === field.name); if (authableField) { if (authableField.auth) { @@ -403,26 +396,30 @@ function createProjectionAndParams({ // Sadly need to select the whole point object due to the risk of height/z // being selected on a 2D point, to which the database will throw an error if (point) { - fields.push(isArray ? "point:p" : `point: ${varName}.${key}`); + fields.push(isArray ? "point:p" : `point: ${varName}.${field.name}`); } if (crs) { - fields.push(isArray ? "crs: p.crs" : `crs: ${varName}.${key}.crs`); + fields.push(isArray ? "crs: p.crs" : `crs: ${varName}.${field.name}.crs`); } res.projection.push( isArray - ? `${key}: [p in ${varName}.${key} | { ${fields.join(", ")} }]` + ? `${key}: [p in ${varName}.${field.name} | { ${fields.join(", ")} }]` : `${key}: { ${fields.join(", ")} }` ); } else if (dateTimeField) { res.projection.push( dateTimeField.typeMeta.array - ? `${key}: [ dt in ${varName}.${key} | apoc.date.convertFormat(toString(dt), "iso_zoned_date_time", "iso_offset_date_time") ]` - : `${key}: apoc.date.convertFormat(toString(${varName}.${key}), "iso_zoned_date_time", "iso_offset_date_time")` + ? `${key}: [ dt in ${varName}.${field.name} | apoc.date.convertFormat(toString(dt), "iso_zoned_date_time", "iso_offset_date_time") ]` + : `${key}: apoc.date.convertFormat(toString(${varName}.${field.name}), "iso_zoned_date_time", "iso_offset_date_time")` ); } else { - res.projection.push(`.${key}`); + // If field is aliased, rename projected field to alias and set to varName.fieldName + // e.g. RETURN varname { .fieldName } -> RETURN varName { alias: varName.fieldName } + const aliasedProj = field.alias !== field.name ? `${field.alias}: ${varName}` : ""; + + res.projection.push(`${aliasedProj}.${field.name}`); } return res; diff --git a/packages/graphql/src/translate/translate-create.ts b/packages/graphql/src/translate/translate-create.ts index acf8493a50..b71c26bed6 100644 --- a/packages/graphql/src/translate/translate-create.ts +++ b/packages/graphql/src/translate/translate-create.ts @@ -28,9 +28,12 @@ import { AUTH_FORBIDDEN_ERROR } from "../constants"; function translateCreate({ context, node }: { context: Context; node: Node }): [string, any] { const { resolveTree } = context; - const { fieldsByTypeName } = resolveTree.fieldsByTypeName[`Create${pluralize(node.name)}MutationResponse`][ - pluralize(camelCase(node.name)) - ]; + // Due to potential aliasing of returned object in response we look through fields of CreateMutationResponse + // and find field where field.name ~ node.name which exists by construction + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const { fieldsByTypeName } = Object.values( + resolveTree.fieldsByTypeName[`Create${pluralize(node.name)}MutationResponse`] + ).find((field) => field.name === pluralize(camelCase(node.name)))!; const { createStrs, params } = (resolveTree.args.input as any[]).reduce( (res, input, index) => { diff --git a/packages/graphql/src/translate/translate-update.ts b/packages/graphql/src/translate/translate-update.ts index 9a9c16c568..dbd009ef9d 100644 --- a/packages/graphql/src/translate/translate-update.ts +++ b/packages/graphql/src/translate/translate-update.ts @@ -51,9 +51,13 @@ function translateUpdate({ node, context }: { node: Node; context: Context }): [ let projStr = ""; let cypherParams: { [k: string]: any } = {}; const whereStrs: string[] = []; - const { fieldsByTypeName } = resolveTree.fieldsByTypeName[`Update${pluralize(node.name)}MutationResponse`][ - pluralize(camelCase(node.name)) - ]; + + // Due to potential aliasing of returned object in response we look through fields of UpdateMutationResponse + // and find field where field.name ~ node.name which exists by construction + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const { fieldsByTypeName } = Object.values( + resolveTree.fieldsByTypeName[`Update${pluralize(node.name)}MutationResponse`] + ).find((field) => field.name === pluralize(camelCase(node.name)))!; if (whereInput) { const where = createWhereAndParams({ diff --git a/packages/graphql/tests/integration/issues/350.int.test.ts b/packages/graphql/tests/integration/issues/350.int.test.ts new file mode 100644 index 0000000000..fbf3cae0ce --- /dev/null +++ b/packages/graphql/tests/integration/issues/350.int.test.ts @@ -0,0 +1,136 @@ +/* + * 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 { gql } from "apollo-server"; +import { generate } from "randomstring"; +import neo4j from "../neo4j"; +import { Neo4jGraphQL } from "../../../src/classes"; + +describe("https://github.com/neo4j/graphql/issues/350", () => { + let driver: Driver; + + beforeAll(async () => { + driver = await neo4j(); + }); + + afterAll(async () => { + await driver.close(); + }); + + test("Retain attributes when aliasing the same field multiple times in a single query", async () => { + const session = driver.session(); + const typeDefs = gql` + type Post { + id: ID! + title: String! + content: String! + comments: [Comment!]! @relationship(type: "HAS_COMMENT", direction: OUT) + } + type Comment { + id: ID! + flagged: Boolean! + content: String! + post: Post! @relationship(type: "HAS_COMMENT", direction: IN) + canEdit: Boolean! @cypher(statement: "RETURN false") + } + `; + + const neoSchema = new Neo4jGraphQL({ typeDefs }); + + const postId = generate({ + charset: "alphabetic", + }); + + const postTitle = generate({ + charset: "alphabetic", + }); + + const postContent = generate({ + charset: "alphabetic", + }); + + const comment1Id = generate({ + charset: "alphabetic", + }); + + const comment1Content = "comment 1 content"; + + const comment2Id = generate({ + charset: "alphabetic", + }); + + const comment2Content = "comment 2 content"; + + const query = ` + query { + posts(where: { id: "${postId}" }) { + flaggedComments: comments(where: { flagged: true }) { + content + flagged + } + unflaggedComments: comments(where: {flagged: false}) { + content + flagged + } + } + } + `; + + try { + await session.run( + ` + CREATE (post:Post {id: $postId, title: $postTitle, content: $postContent}) + CREATE (comment1:Comment {id: $comment1Id, content: $comment1Content, flagged: true}) + CREATE (comment2:Comment {id: $comment2Id, content: $comment2Content, flagged: false}) + MERGE (post)-[:HAS_COMMENT]->(comment1) + MERGE (post)-[:HAS_COMMENT]->(comment2) + + `, + { + postId, + postTitle, + postContent, + comment1Id, + comment1Content, + comment2Id, + comment2Content, + } + ); + + const result = await graphql({ + schema: neoSchema.schema, + source: query, + contextValue: { driver }, + }); + expect(result.errors).toBeFalsy(); + expect(result?.data?.posts[0].flaggedComments).toContainEqual({ + content: comment1Content, + flagged: true, + }); + expect(result?.data?.posts[0].unflaggedComments).toContainEqual({ + content: comment2Content, + flagged: false, + }); + } finally { + await session.close(); + } + }); +}); diff --git a/packages/graphql/tests/tck/tck-test-files/cypher/alias.md b/packages/graphql/tests/tck/tck-test-files/cypher/alias.md index f43d158c0c..d6f3b1febb 100644 --- a/packages/graphql/tests/tck/tck-test-files/cypher/alias.md +++ b/packages/graphql/tests/tck/tck-test-files/cypher/alias.md @@ -6,13 +6,15 @@ Schema: ```schema type Actor { - name: String + name: String! } type Movie { id: ID - actors: [Actor] @relationship(type: "ACTED_IN", direction: IN) - custom: [Movie] @cypher(statement: """ + releaseDate: DateTime! + location: Point! + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN) + custom: [Movie!]! @cypher(statement: """ MATCH (m:Movie) RETURN m """) @@ -41,12 +43,125 @@ type Movie { **Expected Cypher output** +```cypher +MATCH (this:Movie) +RETURN this { + movieId: this.id, + actors: [ (this)<-[:ACTED_IN]-(this_actors:Actor) | this_actors { aliasActorsName: this_actors.name } ], + custom: [this_custom IN apoc.cypher.runFirstColumn("MATCH (m:Movie) RETURN m", {this: this, auth: $auth}, true) | this_custom { aliasCustomId: this_custom.id }] +} as this +``` + +**Expected Cypher params** + +```cypher-params +{ + "auth": { + "isAuthenticated": true, + "roles": [], + "jwt": {} + } +} +``` + +### Multiple aliases for single field with arguments + +**GraphQL input** + +```graphql +{ + movies { + id + keanu: actors(where: { name: "Keanu" }) { + name + } + carrie: actors(where: { name: "Carrie" }) { + name + } + } +} +``` + +**Expected Cypher output** + ```cypher MATCH (this:Movie) RETURN this { .id, - actors: [ (this)<-[:ACTED_IN]-(this_actors:Actor) | this_actors { .name } ], - custom: [this_custom IN apoc.cypher.runFirstColumn("MATCH (m:Movie) RETURN m", {this: this, auth: $auth}, true) | this_custom { .id }] + keanu: [ (this)<-[:ACTED_IN]-(this_keanu:Actor) WHERE this_keanu.name = $this_keanu_name | this_keanu { .name } ], + carrie: [ (this)<-[:ACTED_IN]-(this_carrie:Actor) WHERE this_carrie.name = $this_carrie_name | this_carrie { .name } ], +} as this +``` + +**Expected Cypher params** + +```cypher-params +{ + "auth": { + "isAuthenticated": true, + "roles": [], + "jwt": {} + }, + "this_keanu_name": "Keanu", + "this_carrie_name": "Carrie" +} +``` + +### Alias datetime field + +**GraphQL input** + +```graphql +{ + movies { + d1: releaseDate + d2: releaseDate + } +} +``` + +**Expected Cypher output** + +```cypher +MATCH (this:Movie) +RETURN this { + d1: apoc.date.convertFormat(toString(this.releaseDate), "iso_zoned_date_time", "iso_offset_date_time"), + d2: apoc.date.convertFormat(toString(this.releaseDate), "iso_zoned_date_time", "iso_offset_date_time") +} as this +``` + +**Expected Cypher params** + +```cypher-params +{ + "auth": { + "isAuthenticated": true, + "roles": [], + "jwt": {} + } +} +``` + +### Alias point field + +**GraphQL input** + +```graphql +{ + movies { + p1: location + p2: location + } +} +``` + +**Expected Cypher output** + +```cypher +MATCH (this:Movie) +RETURN this { + p1: { point: this.location }, + p2: { point: this.location } } as this ``` From ca8406e501a6e55fde25e7a1f278331a01d0c9d9 Mon Sep 17 00:00:00 2001 From: Neo Technology Build Agent Date: Wed, 28 Jul 2021 10:46:46 +0000 Subject: [PATCH 6/6] Version update --- examples/migration/package.json | 2 +- examples/neo-push/server/package.json | 4 ++-- packages/graphql/package.json | 2 +- packages/ogm/package.json | 4 ++-- yarn.lock | 12 ++++++------ 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/examples/migration/package.json b/examples/migration/package.json index 90c988b5d9..800c026c5d 100644 --- a/examples/migration/package.json +++ b/examples/migration/package.json @@ -4,7 +4,7 @@ "start": "node src/index.js" }, "dependencies": { - "@neo4j/graphql": "^1.2.3", + "@neo4j/graphql": "^1.2.4", "apollo-server": "^2.23.0", "graphql": "^15.0.0", "neo4j-driver": "^4.2.0" diff --git a/examples/neo-push/server/package.json b/examples/neo-push/server/package.json index 9e919f13f5..9c235f560d 100644 --- a/examples/neo-push/server/package.json +++ b/examples/neo-push/server/package.json @@ -12,8 +12,8 @@ "author": "", "license": "ISC", "dependencies": { - "@neo4j/graphql": "^1.2.3", - "@neo4j/graphql-ogm": "^1.2.3", + "@neo4j/graphql": "^1.2.4", + "@neo4j/graphql-ogm": "^1.2.4", "apollo-server-express": "2.19.0", "bcrypt": "5.0.1", "debug": "4.3.1", diff --git a/packages/graphql/package.json b/packages/graphql/package.json index c3ec9dde87..0cc55e9967 100644 --- a/packages/graphql/package.json +++ b/packages/graphql/package.json @@ -1,6 +1,6 @@ { "name": "@neo4j/graphql", - "version": "1.2.3", + "version": "1.2.4", "description": "A GraphQL to Cypher query execution layer for Neo4j and JavaScript GraphQL implementations", "keywords": [ "neo4j", diff --git a/packages/ogm/package.json b/packages/ogm/package.json index 7a3d31a565..f2a8ab6e4c 100644 --- a/packages/ogm/package.json +++ b/packages/ogm/package.json @@ -1,6 +1,6 @@ { "name": "@neo4j/graphql-ogm", - "version": "1.2.3", + "version": "1.2.4", "description": "GraphQL powered OGM for Neo4j and Javascript applications", "keywords": [ "neo4j", @@ -27,7 +27,7 @@ "author": "Neo4j Inc.", "dependencies": { "@graphql-tools/merge": "^6.2.13", - "@neo4j/graphql": "^1.2.3", + "@neo4j/graphql": "^1.2.4", "camelcase": "^6.2.0", "pluralize": "^8.0.0" }, diff --git a/yarn.lock b/yarn.lock index db1cb90f84..130e029477 100644 --- a/yarn.lock +++ b/yarn.lock @@ -899,12 +899,12 @@ __metadata: languageName: node linkType: hard -"@neo4j/graphql-ogm@^1.2.3, @neo4j/graphql-ogm@workspace:packages/ogm": +"@neo4j/graphql-ogm@^1.2.4, @neo4j/graphql-ogm@workspace:packages/ogm": version: 0.0.0-use.local resolution: "@neo4j/graphql-ogm@workspace:packages/ogm" dependencies: "@graphql-tools/merge": ^6.2.13 - "@neo4j/graphql": ^1.2.3 + "@neo4j/graphql": ^1.2.4 "@types/jest": 26.0.8 "@types/node": 14.0.27 camelcase: ^6.2.0 @@ -923,7 +923,7 @@ __metadata: languageName: unknown linkType: soft -"@neo4j/graphql@^1.2.3, @neo4j/graphql@workspace:packages/graphql": +"@neo4j/graphql@^1.2.4, @neo4j/graphql@workspace:packages/graphql": version: 0.0.0-use.local resolution: "@neo4j/graphql@workspace:packages/graphql" dependencies: @@ -9335,7 +9335,7 @@ fsevents@^1.2.7: version: 0.0.0-use.local resolution: "migration@workspace:examples/migration" dependencies: - "@neo4j/graphql": ^1.2.3 + "@neo4j/graphql": ^1.2.4 apollo-server: ^2.23.0 graphql: ^15.0.0 neo4j-driver: ^4.2.0 @@ -9686,8 +9686,8 @@ fsevents@^1.2.7: version: 0.0.0-use.local resolution: "neo-push-server@workspace:examples/neo-push/server" dependencies: - "@neo4j/graphql": ^1.2.3 - "@neo4j/graphql-ogm": ^1.2.3 + "@neo4j/graphql": ^1.2.4 + "@neo4j/graphql-ogm": ^1.2.4 "@types/bcrypt": 3.0.0 "@types/debug": 4.1.5 "@types/dotenv": 8.2.0