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..2bb8f0e2d7 100644 --- a/packages/graphql/src/schema/get-auth.ts +++ b/packages/graphql/src/schema/get-auth.ts @@ -21,7 +21,17 @@ 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/schema/make-augmented-schema.ts b/packages/graphql/src/schema/make-augmented-schema.ts index b58ddc8bb9..2efdab7df9 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, @@ -50,12 +51,13 @@ import { Node, Exclude } from "../classes"; import getAuth from "./get-auth"; import { PrimitiveField, Auth, CustomEnumField, ConnectionQueryArgs } from "../types"; import { - findResolver, + countResolver, createResolver, - deleteResolver, cypherResolver, + defaultFieldResolver, + deleteResolver, + findResolver, updateResolver, - countResolver, } from "./resolvers"; import * as Scalars from "./scalars"; import parseExcludeDirective from "./parse-exclude-directive"; @@ -1307,6 +1309,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, relationships, 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 b5ffdfed2e..36d09cce7e 100644 --- a/packages/graphql/src/schema/resolvers/index.ts +++ b/packages/graphql/src/schema/resolvers/index.ts @@ -17,9 +17,10 @@ * limitations under the License. */ +export { default as countResolver } from "./count"; export { default as createResolver } from "./create"; +export { default as cypherResolver } from "./cypher"; +export { default as defaultFieldResolver } from "./defaultField"; +export { default as deleteResolver } from "./delete"; 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 countResolver } from "./count"; 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-auth-and-params.test.ts b/packages/graphql/src/translate/create-auth-and-params.test.ts index 430239fb29..9e0ab6e2f7 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,147 @@ 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..3f4fc8e143 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/translate/create-projection-and-params.ts b/packages/graphql/src/translate/create-projection-and-params.ts index 4a08257b75..c4b362f546 100644 --- a/packages/graphql/src/translate/create-projection-and-params.ts +++ b/packages/graphql/src/translate/create-projection-and-params.ts @@ -131,14 +131,7 @@ function createProjectionAndParams({ resolveType?: boolean; inRelationshipProjection?: boolean; }): [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}`; @@ -149,12 +142,12 @@ 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 connectionField = node.connectionFields.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 connectionField = node.connectionFields.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) { @@ -429,7 +422,19 @@ function createProjectionAndParams({ } else if (dateTimeField) { res.projection.push(createDatetimeElement({ resolveTree: field, field: dateTimeField, variable: varName })); } else { - res.projection.push(literalElements ? `${key}: ${varName}.${key}` : `.${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 } + let aliasedProj: string; + + if (field.alias !== field.name) { + aliasedProj = `${field.alias}: ${varName}`; + } else if (literalElements) { + aliasedProj = `${key}: ${varName}`; + } else { + aliasedProj = ""; + } + + res.projection.push(`${aliasedProj}.${field.name}`); } return res; diff --git a/packages/graphql/src/translate/create-where-and-params.ts b/packages/graphql/src/translate/create-where-and-params.ts index 2c5a833eab..4b03e3b4d1 100644 --- a/packages/graphql/src/translate/create-where-and-params.ts +++ b/packages/graphql/src/translate/create-where-and-params.ts @@ -625,12 +625,15 @@ function createWhereAndParams({ context, recursing: true, }); - - innerClauses.push(`${recurse[0]}`); - res.params = { ...res.params, ...recurse[1] }; + if (recurse[0]) { + innerClauses.push(`${recurse[0]}`); + res.params = { ...res.params, ...recurse[1] }; + } }); - res.clauses.push(`(${innerClauses.join(` ${key} `)})`); + if (innerClauses.length) { + res.clauses.push(`(${innerClauses.join(` ${key} `)})`); + } return res; } diff --git a/packages/graphql/src/translate/translate-create.ts b/packages/graphql/src/translate/translate-create.ts index 156560441c..2aca13503d 100644 --- a/packages/graphql/src/translate/translate-create.ts +++ b/packages/graphql/src/translate/translate-create.ts @@ -32,9 +32,12 @@ function translateCreate({ context, node }: { context: Context; node: Node }): [ 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 c5746827ab..86c264e3e4 100644 --- a/packages/graphql/src/translate/translate-update.ts +++ b/packages/graphql/src/translate/translate-update.ts @@ -56,9 +56,12 @@ function translateUpdate({ node, context }: { node: Node; context: Context }): [ const connectionStrs: string[] = []; let updateArgs = {}; - 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/src/types.ts b/packages/graphql/src/types.ts index 60590196a3..1a1fff66b1 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..d38a60d9d3 --- /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/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/integration/issues/360.int.test.ts b/packages/graphql/tests/integration/issues/360.int.test.ts new file mode 100644 index 0000000000..67403e7851 --- /dev/null +++ b/packages/graphql/tests/integration/issues/360.int.test.ts @@ -0,0 +1,205 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Driver } from "neo4j-driver"; +import { graphql } from "graphql"; +import { generate } from "randomstring"; +import camelcase from "camelcase"; +import pluralize from "pluralize"; +import { Neo4jGraphQL } from "../../../src/classes"; +import neo4j from "../neo4j"; + +describe("360", () => { + let driver: Driver; + + beforeAll(async () => { + driver = await neo4j(); + }); + + afterAll(async () => { + await driver.close(); + }); + + test("should return all nodes when AND is used and members are optional", async () => { + const session = driver.session(); + + const type = `${generate({ + charset: "alphabetic", + readable: true, + })}Event`; + + const pluralType = pluralize(camelcase(type)); + + const typeDefs = ` + type ${type} { + id: ID! + name: String + start: DateTime + end: DateTime + activity: String + } + `; + + const neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + + const query = ` + query ($rangeStart: DateTime, $rangeEnd: DateTime, $activity: String) { + ${pluralType}(where: { AND: [{ start_GTE: $rangeStart }, { start_LTE: $rangeEnd }, { activity: $activity }] }) { + id + } + } + `; + + try { + await session.run( + ` + CREATE (:${type} {id: randomUUID(), name: randomUUID(), start: datetime(), end: datetime()}) + CREATE (:${type} {id: randomUUID(), name: randomUUID(), start: datetime(), end: datetime()}) + CREATE (:${type} {id: randomUUID(), name: randomUUID(), start: datetime(), end: datetime()}) + ` + ); + + const gqlResult = await graphql({ + schema: neoSchema.schema, + source: query, + contextValue: { driver }, + }); + + expect(gqlResult.errors).toBeUndefined(); + 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).toBeUndefined(); + expect((gqlResult.data as any)[pluralType]).toHaveLength(3); + } finally { + await session.close(); + } + }); + + test("should recreate given test in issue and return correct results", async () => { + const session = driver.session(); + + const type = `${generate({ + charset: "alphabetic", + readable: true, + })}Event`; + + const pluralType = pluralize(camelcase(type)); + + const typeDefs = ` + type ${type} { + id: ID! + name: String + start: DateTime + end: DateTime + activity: String + } + `; + + const neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + + const rangeStart = new Date().toISOString(); + const rangeEnd = new Date().toISOString(); + + const query = ` + query ($rangeStart: DateTime, $rangeEnd: DateTime, $activity: String) { + ${pluralType}(where: { OR: [{ start_GTE: $rangeStart }, { start_LTE: $rangeEnd }, { activity: $activity }] }) { + id + } + } + `; + + try { + await session.run( + ` + CREATE (:${type} {id: randomUUID(), name: randomUUID(), start: datetime($rangeStart), end: datetime($rangeEnd)}) + CREATE (:${type} {id: randomUUID(), name: randomUUID(), start: datetime($rangeStart), end: datetime($rangeEnd)}) + CREATE (:${type} {id: randomUUID(), name: randomUUID(), start: datetime(), end: datetime()}) + `, + { rangeStart, rangeEnd } + ); + + const gqlResult = await graphql({ + schema: neoSchema.schema, + source: query, + contextValue: { driver }, + variableValues: { rangeStart, rangeEnd }, + }); + + expect(gqlResult.errors).toBeUndefined(); + expect((gqlResult.data as any)[pluralType]).toHaveLength(3); + } 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 b3478c040e..b3bb18aa4f 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: ```graphql type Actor { - name: String + name: String! } type Movie { id: ID - actors: [Actor] @relationship(type: "ACTED_IN", direction: IN) - custom: [Movie] + releaseDate: DateTime! + location: Point! + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN) + custom: [Movie!]! @cypher( statement: """ MATCH (m:Movie) @@ -44,12 +46,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 ``` 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 ad94b9f0fb..d9490afc82 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)<-[this_has_post0_relationship: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(*) ", "", @@ -507,12 +507,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)<-[this_has_post0_relationship: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(*) ", @@ -582,7 +582,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 ``` @@ -630,13 +630,13 @@ WITH this OPTIONAL MATCH (this)-[this_posts0_relationship:HAS_POST]->(this_posts0:Post) WHERE this_posts0.id = $this_deleteUsers.args.delete.posts[0].where.node.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 ``` @@ -703,7 +703,7 @@ OPTIONAL MATCH (this)-[this_disconnect_posts0_rel:HAS_POST]->(this_disconnect_po WHERE this_disconnect_posts0.id = $updateUsers.args.disconnect.posts[0].where.node.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 @@ -780,13 +780,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 @@ -797,7 +797,7 @@ OPTIONAL MATCH (this_post0_disconnect0)<-[this_post0_disconnect0_creator0_rel:HA WHERE this_post0_disconnect0_creator0.id = $updateComments.args.update.post.disconnect.disconnect.creator.where.node.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 @@ -878,7 +878,7 @@ CALL { WHERE this_connect_posts0_node.id = $this_connect_posts0_node_id WITH this, this_connect_posts0_node - CALL apoc.util.validate(NOT(EXISTS(this_connect_posts0_node.id) AND this_connect_posts0_node.id = $this_connect_posts0_nodeUser0_allow_auth_allow0_id AND EXISTS((this_connect_posts0_node)<-[:HAS_POST]-(:User)) AND ANY(creator IN [(this_connect_posts0_node)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this_connect_posts0_nodePost1_allow_auth_allow0_creator_id)), "@neo4j/graphql/FORBIDDEN", [0]) + CALL apoc.util.validate(NOT(this_connect_posts0_node.id IS NOT NULL AND this_connect_posts0_node.id = $this_connect_posts0_nodeUser0_allow_auth_allow0_id AND EXISTS((this_connect_posts0_node)<-[:HAS_POST]-(:User)) AND ANY(creator IN [(this_connect_posts0_node)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this_connect_posts0_nodePost1_allow_auth_allow0_creator_id)), "@neo4j/graphql/FORBIDDEN", [0]) FOREACH(_ IN CASE this_connect_posts0_node WHEN NULL THEN [] ELSE [1] END | MERGE (this)-[:HAS_POST]->(this_connect_posts0_node) 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 af4c30fc69..f9b754e05f 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 @@ -138,17 +138,17 @@ CALL { SET this0_posts0_node_creator0_node.id = $this0_posts0_node_creator0_node_id WITH this0, this0_posts0_node, this0_posts0_node_creator0_node - CALL apoc.util.validate(NOT(EXISTS(this0_posts0_node_creator0_node.id) AND this0_posts0_node_creator0_node.id = $this0_posts0_node_creator0_node_auth_bind0_id), "@neo4j/graphql/FORBIDDEN", [0]) + CALL apoc.util.validate(NOT(this0_posts0_node_creator0_node.id IS NOT NULL AND this0_posts0_node_creator0_node.id = $this0_posts0_node_creator0_node_auth_bind0_id), "@neo4j/graphql/FORBIDDEN", [0]) MERGE (this0_posts0_node)<-[:HAS_POST]-(this0_posts0_node_creator0_node) WITH this0, this0_posts0_node - CALL apoc.util.validate(NOT(EXISTS((this0_posts0_node)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this0_posts0_node)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this0_posts0_node_auth_bind0_creator_id)), "@neo4j/graphql/FORBIDDEN", [0]) + CALL apoc.util.validate(NOT(EXISTS((this0_posts0_node)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this0_posts0_node)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this0_posts0_node_auth_bind0_creator_id)), "@neo4j/graphql/FORBIDDEN", [0]) MERGE (this0)-[:HAS_POST]->(this0_posts0_node) 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 } @@ -203,7 +203,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 ``` @@ -270,7 +270,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(*) \", @@ -282,7 +282,7 @@ CALL apoc.do.when(this_posts0 IS NOT NULL, {this:this, updateUsers: $updateUsers, 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 ``` @@ -376,7 +376,7 @@ CALL { ) WITH this, this_connect_creator0_node - CALL apoc.util.validate(NOT(EXISTS((this_connect_creator0_node)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_connect_creator0_node)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this_connect_creator0_nodePost0_bind_auth_bind0_creator_id) AND EXISTS(this_connect_creator0_node.id) AND this_connect_creator0_node.id = $this_connect_creator0_nodeUser1_bind_auth_bind0_id), "@neo4j/graphql/FORBIDDEN", [0]) + CALL apoc.util.validate(NOT(EXISTS((this_connect_creator0_node)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_connect_creator0_node)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this_connect_creator0_nodePost0_bind_auth_bind0_creator_id) AND this_connect_creator0_node.id IS NOT NULL AND this_connect_creator0_node.id = $this_connect_creator0_nodeUser1_bind_auth_bind0_id), "@neo4j/graphql/FORBIDDEN", [0]) RETURN count(*) } @@ -437,7 +437,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 afb6b7ea5b..6e5b97365d 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 @@ -73,7 +73,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 ``` @@ -112,7 +112,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 ``` @@ -155,10 +155,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 ``` @@ -205,10 +205,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 CALL { WITH this MATCH (this)-[this_has_post:HAS_POST]->(this_post:Post) - WHERE EXISTS((this_post)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_post)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this_post_auth_where0_creator_id) + WHERE EXISTS((this_post)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_post)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this_post_auth_where0_creator_id) WITH collect({ node: { content: this_post.content } }) AS edges RETURN { edges: edges, totalCount: size(edges) } AS postsConnection } @@ -258,10 +258,10 @@ RETURN this { .id, postsConnection } 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 CALL { WITH this MATCH (this)-[this_has_post:HAS_POST]->(this_post:Post) - WHERE this_post.id = $this_postsConnection.args.where.node.id AND EXISTS((this_post)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_post)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this_post_auth_where0_creator_id) + WHERE this_post.id = $this_postsConnection.args.where.node.id AND EXISTS((this_post)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_post)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this_post_auth_where0_creator_id) WITH collect({ node: { content: this_post.content } }) AS edges RETURN { edges: edges, totalCount: size(edges) } AS postsConnection } @@ -316,11 +316,11 @@ RETURN this { .id, postsConnection } 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 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 ``` @@ -366,10 +366,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 ``` @@ -418,13 +418,13 @@ 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 CALL { WITH this CALL { WITH this OPTIONAL MATCH (this)-[this_has_post:HAS_POST]->(this_Post:Post) - WHERE EXISTS((this_Post)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_Post)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this_Post_auth_where0_creator_id) + WHERE EXISTS((this_Post)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_Post)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this_Post_auth_where0_creator_id) WITH { node: { __resolveType: "Post", id: this_Post.id } } AS edge RETURN edge } @@ -479,13 +479,13 @@ RETURN this { .id, contentConnection } 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 CALL { WITH this CALL { WITH this OPTIONAL MATCH (this)-[this_has_post:HAS_POST]->(this_Post:Post) - WHERE this_Post.id = $this_contentConnection.args.where.Post.node.id AND EXISTS((this_Post)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_Post)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this_Post_auth_where0_creator_id) + WHERE this_Post.id = $this_contentConnection.args.where.Post.node.id AND EXISTS((this_Post)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_Post)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this_Post_auth_where0_creator_id) WITH { node: { __resolveType: "Post", id: this_Post.id } } AS edge RETURN edge } @@ -544,7 +544,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 ``` @@ -587,7 +587,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 ``` @@ -634,16 +634,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)-[this_has_post0_relationship: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, updateUsers: $updateUsers, 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 ``` @@ -708,7 +708,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 ``` @@ -747,7 +747,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 ``` @@ -787,11 +787,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)-[this_posts0_relationship: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 ) @@ -854,7 +854,7 @@ CALL { CALL { WITH this0 OPTIONAL MATCH (this0_posts_connect0_node:Post) - WHERE EXISTS((this0_posts_connect0_node)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this0_posts_connect0_node)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this0_posts_connect0_node_auth_where0_creator_id) + WHERE EXISTS((this0_posts_connect0_node)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this0_posts_connect0_node)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this0_posts_connect0_node_auth_where0_creator_id) FOREACH(_ IN CASE this0_posts_connect0_node WHEN NULL THEN [] ELSE [1] END | MERGE (this0)-[:HAS_POST]->(this0_posts_connect0_node) ) @@ -924,7 +924,7 @@ CALL { CALL { WITH this0 OPTIONAL MATCH (this0_posts_connect0_node:Post) - WHERE this0_posts_connect0_node.id = $this0_posts_connect0_node_id AND EXISTS((this0_posts_connect0_node)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this0_posts_connect0_node)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this0_posts_connect0_node_auth_where0_creator_id) + WHERE this0_posts_connect0_node.id = $this0_posts_connect0_node_id AND EXISTS((this0_posts_connect0_node)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this0_posts_connect0_node)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this0_posts_connect0_node_auth_where0_creator_id) FOREACH(_ IN CASE this0_posts_connect0_node WHEN NULL THEN [] ELSE [1] END | MERGE (this0)-[:HAS_POST]->(this0_posts_connect0_node) ) @@ -978,16 +978,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_node:Post) - WHERE EXISTS((this_posts0_connect0_node)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_posts0_connect0_node)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this_posts0_connect0_node_auth_where0_creator_id) + WHERE EXISTS((this_posts0_connect0_node)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_posts0_connect0_node)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this_posts0_connect0_node_auth_where0_creator_id) FOREACH(_ IN CASE this_posts0_connect0_node WHEN NULL THEN [] ELSE [1] END | MERGE (this)-[:HAS_POST]->(this_posts0_connect0_node) ) @@ -1037,16 +1037,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_node:Post) - WHERE this_posts0_connect0_node.id = $this_posts0_connect0_node_id AND EXISTS((this_posts0_connect0_node)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_posts0_connect0_node)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this_posts0_connect0_node_auth_where0_creator_id) + WHERE this_posts0_connect0_node.id = $this_posts0_connect0_node_id AND EXISTS((this_posts0_connect0_node)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_posts0_connect0_node)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this_posts0_connect0_node_auth_where0_creator_id) FOREACH(_ IN CASE this_posts0_connect0_node WHEN NULL THEN [] ELSE [1] END | MERGE (this)-[:HAS_POST]->(this_posts0_connect0_node) ) @@ -1095,16 +1095,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_node:Post) - WHERE EXISTS((this_connect_posts0_node)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_connect_posts0_node)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this_connect_posts0_node_auth_where0_creator_id) + WHERE EXISTS((this_connect_posts0_node)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_connect_posts0_node)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this_connect_posts0_node_auth_where0_creator_id) FOREACH(_ IN CASE this_connect_posts0_node WHEN NULL THEN [] ELSE [1] END | MERGE (this)-[:HAS_POST]->(this_connect_posts0_node) ) @@ -1152,16 +1152,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_node:Post) - WHERE this_connect_posts0_node.id = $this_connect_posts0_node_id AND EXISTS((this_connect_posts0_node)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_connect_posts0_node)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this_connect_posts0_node_auth_where0_creator_id) + WHERE this_connect_posts0_node.id = $this_connect_posts0_node_id AND EXISTS((this_connect_posts0_node)<-[:HAS_POST]-(:User)) AND ALL(creator IN [(this_connect_posts0_node)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this_connect_posts0_node_auth_where0_creator_id) FOREACH(_ IN CASE this_connect_posts0_node WHEN NULL THEN [] ELSE [1] END | MERGE (this)-[:HAS_POST]->(this_connect_posts0_node) ) @@ -1210,14 +1210,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 ) @@ -1266,13 +1266,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 = $updateUsers.args.update.posts[0].disconnect[0].where.node.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 = $updateUsers.args.update.posts[0].disconnect[0].where.node.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 ) @@ -1336,10 +1336,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 @@ -1397,9 +1397,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 this_disconnect_posts0.id = $updateUsers.args.disconnect.posts[0].where.node.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 = $updateUsers.args.disconnect.posts[0].where.node.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-connection-union.md b/packages/graphql/tests/tck/tck-test-files/cypher/directives/auth/projection-connection-union.md index 9cb18e4c6b..6737943f76 100644 --- a/packages/graphql/tests/tck/tck-test-files/cypher/directives/auth/projection-connection-union.md +++ b/packages/graphql/tests/tck/tck-test-files/cypher/directives/auth/projection-connection-union.md @@ -55,17 +55,17 @@ extend type Post @auth(rules: [{ allow: { creator: { id: "$jwt.sub" } } }]) ```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]) CALL { WITH this CALL { WITH this OPTIONAL MATCH (this)-[this_published:PUBLISHED]->(this_Post:Post) - CALL apoc.util.validate(NOT(EXISTS((this_Post)<-[:HAS_POST]-(:User)) AND ANY(creator IN [(this_Post)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this_Post_auth_allow0_creator_id)), "@neo4j/graphql/FORBIDDEN", [0]) + CALL apoc.util.validate(NOT(EXISTS((this_Post)<-[:HAS_POST]-(:User)) AND ANY(creator IN [(this_Post)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this_Post_auth_allow0_creator_id)), "@neo4j/graphql/FORBIDDEN", [0]) CALL { WITH this_Post MATCH (this_Post)<-[this_Post_has_post:HAS_POST]-(this_Post_user:User) - CALL apoc.util.validate(NOT(EXISTS(this_Post_user.id) AND this_Post_user.id = $this_Post_user_auth_allow0_id), "@neo4j/graphql/FORBIDDEN", [0]) + CALL apoc.util.validate(NOT(this_Post_user.id IS NOT NULL AND this_Post_user.id = $this_Post_user_auth_allow0_id), "@neo4j/graphql/FORBIDDEN", [0]) WITH collect({ node: { name: this_Post_user.name } }) AS edges RETURN { edges: edges, totalCount: size(edges) } AS creatorConnection } diff --git a/packages/graphql/tests/tck/tck-test-files/cypher/directives/auth/projection-connection.md b/packages/graphql/tests/tck/tck-test-files/cypher/directives/auth/projection-connection.md index 63a735e049..d05074f6a9 100644 --- a/packages/graphql/tests/tck/tck-test-files/cypher/directives/auth/projection-connection.md +++ b/packages/graphql/tests/tck/tck-test-files/cypher/directives/auth/projection-connection.md @@ -45,11 +45,11 @@ extend type Post @auth(rules: [{ allow: { creator: { id: "$jwt.sub" } } }]) ```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]) CALL { WITH this MATCH (this)-[this_has_post:HAS_POST]->(this_post:Post) - CALL apoc.util.validate(NOT(EXISTS((this_post)<-[:HAS_POST]-(:User)) AND ANY(creator IN [(this_post)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this_post_auth_allow0_creator_id)), "@neo4j/graphql/FORBIDDEN", [0]) + CALL apoc.util.validate(NOT(EXISTS((this_post)<-[:HAS_POST]-(:User)) AND ANY(creator IN [(this_post)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this_post_auth_allow0_creator_id)), "@neo4j/graphql/FORBIDDEN", [0]) WITH collect({ node: { content: this_post.content } }) AS edges RETURN { edges: edges, totalCount: size(edges) } AS postsConnection } @@ -105,15 +105,15 @@ RETURN this { .name, postsConnection } 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]) CALL { WITH this MATCH (this)-[this_has_post:HAS_POST]->(this_post:Post) - CALL apoc.util.validate(NOT(EXISTS((this_post)<-[:HAS_POST]-(:User)) AND ANY(creator IN [(this_post)<-[:HAS_POST]-(creator:User) | creator] WHERE EXISTS(creator.id) AND creator.id = $this_post_auth_allow0_creator_id)), "@neo4j/graphql/FORBIDDEN", [0]) + CALL apoc.util.validate(NOT(EXISTS((this_post)<-[:HAS_POST]-(:User)) AND ANY(creator IN [(this_post)<-[:HAS_POST]-(creator:User) | creator] WHERE creator.id IS NOT NULL AND creator.id = $this_post_auth_allow0_creator_id)), "@neo4j/graphql/FORBIDDEN", [0]) CALL { WITH this_post MATCH (this_post)<-[this_post_has_post:HAS_POST]-(this_post_user:User) - CALL apoc.util.validate(NOT(EXISTS(this_post_user.id) AND this_post_user.id = $this_post_user_auth_allow0_id), "@neo4j/graphql/FORBIDDEN", [0]) + CALL apoc.util.validate(NOT(this_post_user.id IS NOT NULL AND this_post_user.id = $this_post_user_auth_allow0_id), "@neo4j/graphql/FORBIDDEN", [0]) WITH collect({ node: { name: this_post_user.name } }) AS edges RETURN { edges: edges, totalCount: size(edges) } AS creatorConnection } 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 c9507aba1c..1ce7441711 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/issues/360.md b/packages/graphql/tests/tck/tck-test-files/cypher/issues/360.md new file mode 100644 index 0000000000..2ad7c9f2e5 --- /dev/null +++ b/packages/graphql/tests/tck/tck-test-files/cypher/issues/360.md @@ -0,0 +1,161 @@ +# #360 + + + +Schema: + +```graphql +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 Input + +```json +{ + "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 + +```json +{ + "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 Input + +```json +{ + "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 + +```json +{ + "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 + } +} +``` + +--- 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 e3c082fec9..f9760ca77d 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