diff --git a/.changeset/twelve-forks-kneel.md b/.changeset/twelve-forks-kneel.md new file mode 100644 index 0000000000..0e59458977 --- /dev/null +++ b/.changeset/twelve-forks-kneel.md @@ -0,0 +1,5 @@ +--- +"@neo4j/graphql": patch +--- + +`cypherParams` added to the `Neo4jGraphQLContext` type, and the fields within it can be referred to directly. diff --git a/packages/graphql/src/classes/NodeDirective.ts b/packages/graphql/src/classes/NodeDirective.ts index adda132485..9d4cdfc61c 100644 --- a/packages/graphql/src/classes/NodeDirective.ts +++ b/packages/graphql/src/classes/NodeDirective.ts @@ -17,9 +17,9 @@ * limitations under the License. */ +import dotProp from "dot-prop"; import { Neo4jGraphQLError } from "./Error"; import type { Context } from "../types"; -import ContextParser from "../utils/context-parser"; import Cypher from "@neo4j/cypher-builder"; export interface NodeDirectiveConstructor { @@ -48,35 +48,30 @@ export class NodeDirective { private mapLabelsWithContext(labels: string[], context: Context): string[] { return labels.map((label: string) => { - const jwtPath = ContextParser.parseTag(label, "jwt"); - let ctxPath = ContextParser.parseTag(label, "context"); - - if (jwtPath) { - ctxPath = `jwt.${jwtPath}`; - } - - if (ctxPath) { - let mappedLabel = ContextParser.getProperty(ctxPath, context); - if (mappedLabel) { - return mappedLabel; - } - - // Try the new authorization path - this will become default in 4.0.0 - if (jwtPath) { - ctxPath = `authorization.jwt.${jwtPath}`; - mappedLabel = ContextParser.getProperty(ctxPath, context); - if (mappedLabel) { - return mappedLabel; - } + if (label.startsWith("$")) { + // Trim $context. OR $ off the beginning of the string + const path = label.substring(label.startsWith("$context") ? 9 : 1); + const labelValue = this.searchLabel(context, path); + if (!labelValue) { + throw new Error(`Label value not found in context.`); } - - throw new Error(`Label value not found in context.`); + return labelValue; } return label; }); } + private searchLabel(context, path): string | undefined { + // Search for the key at the root of the context + let labelValue = dotProp.get(context, path); + if (!labelValue) { + // Search for the key in cypherParams + labelValue = dotProp.get(context.cypherParams, path); + } + return labelValue; + } + private escapeLabel(label: string): string { return Cypher.utils.escapeLabel(label); } diff --git a/packages/graphql/src/schema/resolvers/wrapper.ts b/packages/graphql/src/schema/resolvers/wrapper.ts index dfc75099a3..dc3fe3a925 100644 --- a/packages/graphql/src/schema/resolvers/wrapper.ts +++ b/packages/graphql/src/schema/resolvers/wrapper.ts @@ -96,12 +96,12 @@ export const wrapResolver = if (!context.jwt) { if (authorization) { try { - const jwt = await authorization.decode(context); + context.jwt = await authorization.decode(context); const isAuthenticated = true; context.authorization = { isAuthenticated, - jwt, - jwtParam: new Cypher.NamedParam("jwt", jwt), + jwt: context.jwt, + jwtParam: new Cypher.NamedParam("jwt", context.jwt), isAuthenticatedParam: new Cypher.NamedParam("isAuthenticated", isAuthenticated), claims: jwtPayloadFieldsMap, }; diff --git a/packages/graphql/src/translate/translate-aggregate.ts b/packages/graphql/src/translate/translate-aggregate.ts index 54c3c20d50..f509f09a08 100644 --- a/packages/graphql/src/translate/translate-aggregate.ts +++ b/packages/graphql/src/translate/translate-aggregate.ts @@ -28,7 +28,7 @@ import { compileCypher } from "../utils/compile-cypher"; function translateAggregate({ node, context }: { node: Node; context: Context }): [Cypher.Clause, any] { const { fieldsByTypeName } = context.resolveTree; const varName = "this"; - let cypherParams: { [k: string]: any } = context.cypherParams ? { cypherParams: context.cypherParams } : {}; + let cypherParams: Record = context.cypherParams ? { ...context.cypherParams } : {}; const cypherStrs: Cypher.Clause[] = []; const matchNode = new Cypher.NamedNode(varName, { labels: node.getLabels(context) }); const where = context.resolveTree.args.where as GraphQLWhereArg | undefined; diff --git a/packages/graphql/src/translate/translate-delete.ts b/packages/graphql/src/translate/translate-delete.ts index 528e62837c..bfd5c9e94a 100644 --- a/packages/graphql/src/translate/translate-delete.ts +++ b/packages/graphql/src/translate/translate-delete.ts @@ -33,7 +33,7 @@ export function translateDelete({ context, node }: { context: Context; node: Nod const varName = "this"; let matchAndWhereStr = ""; let deleteStr = ""; - let cypherParams: { [k: string]: any } = context.cypherParams ? { cypherParams: context.cypherParams } : {}; + let cypherParams: Record = context.cypherParams ? { ...context.cypherParams } : {}; const withVars = [varName]; diff --git a/packages/graphql/src/translate/translate-read.ts b/packages/graphql/src/translate/translate-read.ts index 3ae8d064c8..363bc6de23 100644 --- a/packages/graphql/src/translate/translate-read.ts +++ b/packages/graphql/src/translate/translate-read.ts @@ -191,7 +191,7 @@ export function translateRead( projectionSubqueries, projectionClause ); - const result = readQuery.build(undefined, context.cypherParams ? { cypherParams: context.cypherParams } : {}); + const result = readQuery.build(undefined, context.cypherParams ? { ...context.cypherParams } : {}); return result; } diff --git a/packages/graphql/src/translate/translate-resolve-reference.ts b/packages/graphql/src/translate/translate-resolve-reference.ts index 5e36c660e1..205e89dad1 100644 --- a/packages/graphql/src/translate/translate-resolve-reference.ts +++ b/packages/graphql/src/translate/translate-resolve-reference.ts @@ -91,5 +91,5 @@ export function translateResolveReference({ returnClause ); - return readQuery.build(undefined, context.cypherParams ? { cypherParams: context.cypherParams } : {}); + return readQuery.build(undefined, context.cypherParams ? { ...context.cypherParams } : {}); } diff --git a/packages/graphql/src/translate/translate-top-level-cypher.ts b/packages/graphql/src/translate/translate-top-level-cypher.ts index fa1a0038c3..3bf4a6373b 100644 --- a/packages/graphql/src/translate/translate-top-level-cypher.ts +++ b/packages/graphql/src/translate/translate-top-level-cypher.ts @@ -170,8 +170,6 @@ export function translateTopLevelCypher({ ); } - const initApocParamsStrs = ["auth: $auth", ...(context.cypherParams ? ["cypherParams: $cypherParams"] : [])]; - // Null default argument values are not passed into the resolve tree therefore these are not being passed to // `apocParams` below causing a runtime error when executing. const nullArgumentValues = field.arguments.reduce( @@ -183,18 +181,13 @@ export function translateTopLevelCypher({ ); const apocParams = Object.entries({ ...nullArgumentValues, ...resolveTree.args }).reduce( - (result: { strs: string[]; params: { [key: string]: unknown } }, entry) => ({ - strs: [...result.strs, `${entry[0]}: $${entry[0]}`], + (result: { params: { [key: string]: unknown } }, entry) => ({ params: { ...result.params, [entry[0]]: entry[1] }, }), - { strs: initApocParamsStrs, params } + { params } ); - if (statement.includes("$jwt")) { - apocParams.strs.push("jwt: $jwt"); - } - - params = { ...params, ...apocParams.params }; + params = { ...params, ...apocParams.params, ...(context.cypherParams || {}) }; if (type === "Query") { const cypherStatement = createCypherDirectiveSubquery({ diff --git a/packages/graphql/src/translate/translate-update.ts b/packages/graphql/src/translate/translate-update.ts index 24b79e19e0..00194c3fac 100644 --- a/packages/graphql/src/translate/translate-update.ts +++ b/packages/graphql/src/translate/translate-update.ts @@ -66,7 +66,7 @@ export default async function translateUpdate({ const createStrs: string[] = []; let deleteStr = ""; let projAuth: Cypher.Clause | undefined = undefined; - let cypherParams: { [k: string]: any } = context.cypherParams ? { cypherParams: context.cypherParams } : {}; + let cypherParams: Record = context.cypherParams ? { ...context.cypherParams } : {}; const assumeReconnecting = Boolean(connectInput) && Boolean(disconnectInput); const matchNode = new Cypher.NamedNode(varName, { labels: node.getLabels(context) }); const where = resolveTree.args.where as GraphQLWhereArg | undefined; diff --git a/packages/graphql/src/types/neo4j-graphql-context.ts b/packages/graphql/src/types/neo4j-graphql-context.ts index 7f5da1f7e9..18204fdfd8 100644 --- a/packages/graphql/src/types/neo4j-graphql-context.ts +++ b/packages/graphql/src/types/neo4j-graphql-context.ts @@ -22,6 +22,19 @@ import type { JWTPayload } from "jose"; import type { ExecutionContext, Neo4jGraphQLSessionConfig } from "../classes/Executor"; export interface Neo4jGraphQLContext { + /** + * Parameters to be used when querying with Cypher. + * + * To be used with directives such as `@node` and `@cypher`, and can be used directly as named here. + * + * @example + * Given a `cypherParams` value as follows: + * ``` + * { title: "The Matrix" } + * ``` + * This can be referred to like `@cypher(statement: "RETURN $title AS title", columnName: "title")`. + */ + cypherParams?: Record; /** * Configures which {@link https://neo4j.com/docs/cypher-manual/current/query-tuning/query-options/ | Cypher query options} to use when executing the translated query. */ diff --git a/packages/graphql/src/utils/context-parser.ts b/packages/graphql/src/utils/context-parser.ts deleted file mode 100644 index b13c069422..0000000000 --- a/packages/graphql/src/utils/context-parser.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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 dotProp from "dot-prop"; -import type { Context } from "../types"; - -export default class ContextParser { - public static parseTag(value: string, tagName: "context" | "jwt"): string | undefined { - const [, path] = value?.split?.(`$${tagName}.`) || []; - return path; - } - - public static getProperty(path: string, context: Context): string | undefined { - return dotProp.get({ value: context }, `value.${path}`); - } -} diff --git a/packages/graphql/tests/integration/cypher-params.int.test.ts b/packages/graphql/tests/integration/cypher-params.int.test.ts index 7171b32d07..d3a3168fa9 100644 --- a/packages/graphql/tests/integration/cypher-params.int.test.ts +++ b/packages/graphql/tests/integration/cypher-params.int.test.ts @@ -45,7 +45,7 @@ describe("cypherParams", () => { } type Query { - id: String! @cypher(statement: "RETURN $cypherParams.id AS id", columnName: "id") + id: String! @cypher(statement: "RETURN $id AS id", columnName: "id") } `; @@ -128,7 +128,7 @@ describe("cypherParams", () => { variableValues: { id: movieId, }, - contextValue: neo4j.getContextValues({ cypherParams: { id: cypherParamsId } }), + contextValue: neo4j.getContextValues({ cypherParams: { cypherParams: { id: cypherParamsId } } }), }); expect(gqlResult.errors).toBeFalsy(); @@ -153,7 +153,7 @@ describe("cypherParams", () => { } type Mutation { - id: String! @cypher(statement: "RETURN $cypherParams.id AS id", columnName:"id") + id: String! @cypher(statement: "RETURN $id AS id", columnName:"id") } `; diff --git a/packages/graphql/tests/integration/issues/1249.int.test.ts b/packages/graphql/tests/integration/issues/1249.int.test.ts index ee767d2106..0ea6e2924b 100644 --- a/packages/graphql/tests/integration/issues/1249.int.test.ts +++ b/packages/graphql/tests/integration/issues/1249.int.test.ts @@ -31,7 +31,7 @@ describe("https://github.com/neo4j/graphql/issues/1249", () => { const typeDefs = ` type Bulk @exclude(operations: [CREATE, DELETE, UPDATE]) - @node(labels: ["Bulk", "$context.cypherParams.tenant"]) { + @node(labels: ["Bulk", "$tenant"]) { id: ID! supplierMaterialNumber: String! material: Material! @relationship(type: "MATERIAL_BULK", direction: OUT) diff --git a/packages/graphql/tests/tck/issues/1249.test.ts b/packages/graphql/tests/tck/issues/1249.test.ts index 912270d6a0..ddd30c2e06 100644 --- a/packages/graphql/tests/tck/issues/1249.test.ts +++ b/packages/graphql/tests/tck/issues/1249.test.ts @@ -28,9 +28,7 @@ describe("https://github.com/neo4j/graphql/issues/1249", () => { beforeAll(() => { typeDefs = gql` - type Bulk - @exclude(operations: [CREATE, DELETE, UPDATE]) - @node(labels: ["Bulk", "$context.cypherParams.tenant"]) { + type Bulk @exclude(operations: [CREATE, DELETE, UPDATE]) @node(labels: ["Bulk", "$tenant"]) { id: ID! supplierMaterialNumber: String! material: Material! @relationship(type: "MATERIAL_BULK", direction: OUT) @@ -103,9 +101,7 @@ describe("https://github.com/neo4j/graphql/issues/1249", () => { expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"cypherParams\\": { - \\"tenant\\": \\"BULK\\" - } + \\"tenant\\": \\"BULK\\" }" `); });