diff --git a/.changeset/shy-penguins-sin.md b/.changeset/shy-penguins-sin.md new file mode 100644 index 0000000000..273a3acd1f --- /dev/null +++ b/.changeset/shy-penguins-sin.md @@ -0,0 +1,5 @@ +--- +"@neo4j/graphql": patch +--- + +fix: Pass the cypherParams from the top-level context to the translate functions. diff --git a/packages/graphql/src/translate/translate-aggregate.ts b/packages/graphql/src/translate/translate-aggregate.ts index dd8e6e26f2..ae1d591c6a 100644 --- a/packages/graphql/src/translate/translate-aggregate.ts +++ b/packages/graphql/src/translate/translate-aggregate.ts @@ -27,7 +27,7 @@ import { translateTopLevelMatch } from "./translate-top-level-match"; function translateAggregate({ node, context }: { node: Node; context: Context }): [string, any] { const { fieldsByTypeName } = context.resolveTree; const varName = "this"; - let cypherParams: { [k: string]: any } = {}; + let cypherParams: { [k: string]: any } = context.cypherParams ? { cypherParams: context.cypherParams } : {}; const cypherStrs: string[] = []; const topLevelMatch = translateTopLevelMatch({ node, context, varName, operation: "READ" }); diff --git a/packages/graphql/src/translate/translate-delete.ts b/packages/graphql/src/translate/translate-delete.ts index d3b448fc5c..6b18597c01 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 let matchAndWhereStr = ""; let allowStr = ""; let deleteStr = ""; - let cypherParams: { [k: string]: any } = {}; + let cypherParams: { [k: string]: any } = context.cypherParams ? { 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 e8ba5e197e..115d7e4d87 100644 --- a/packages/graphql/src/translate/translate-read.ts +++ b/packages/graphql/src/translate/translate-read.ts @@ -46,7 +46,7 @@ export function translateRead({ let authStr = ""; let projAuth = ""; - let cypherParams: { [k: string]: any } = {}; + let cypherParams: { [k: string]: any } = context.cypherParams ? { cypherParams: context.cypherParams } : {}; const connectionStrs: string[] = []; const interfaceStrs: string[] = []; diff --git a/packages/graphql/src/translate/translate-update.ts b/packages/graphql/src/translate/translate-update.ts index 11c84a283b..98c2046662 100644 --- a/packages/graphql/src/translate/translate-update.ts +++ b/packages/graphql/src/translate/translate-update.ts @@ -65,7 +65,7 @@ export default async function translateUpdate({ let deleteStr = ""; let projAuth = ""; let projStr = ""; - let cypherParams: { [k: string]: any } = {}; + let cypherParams: { [k: string]: any } = context.cypherParams ? { cypherParams: context.cypherParams } : {}; const assumeReconnecting = Boolean(connectInput) && Boolean(disconnectInput); const topLevelMatch = translateTopLevelMatch({ node, context, varName, operation: "UPDATE" }); @@ -363,7 +363,7 @@ export default async function translateUpdate({ refNode, context, withVars, - callbackBucket + callbackBucket, }); connectStrs.push(cypher); cypherParams = { ...cypherParams, ...params }; diff --git a/packages/graphql/tests/integration/issues/1249.int.test.ts b/packages/graphql/tests/integration/issues/1249.int.test.ts new file mode 100644 index 0000000000..9cdf098608 --- /dev/null +++ b/packages/graphql/tests/integration/issues/1249.int.test.ts @@ -0,0 +1,105 @@ +/* + * 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 type { GraphQLSchema } from "graphql"; +import { graphql } from "graphql"; +import type { Driver } from "neo4j-driver"; +import Neo4j from "../neo4j"; +import { Neo4jGraphQL } from "../../../src"; + +describe("https://github.com/neo4j/graphql/issues/1249", () => { + let schema: GraphQLSchema; + let driver: Driver; + let neo4j: Neo4j; + + const typeDefs = ` + type Bulk + @exclude(operations: [CREATE, DELETE, UPDATE]) + @node(additionalLabels: ["$context.cypherParams.tenant"]) { + id: ID! + supplierMaterialNumber: String! + material: Material! @relationship(type: "MATERIAL_BULK", direction: OUT) + } + + type Material @exclude(operations: [CREATE, DELETE, UPDATE]) { + id: ID! + itemNumber: String! + + suppliers: [Supplier!]! + @relationship(type: "MATERIAL_SUPPLIER", properties: "RelationMaterialSupplier", direction: OUT) + } + + type Supplier @exclude(operations: [CREATE, DELETE, UPDATE]) { + id: ID! + name: String + supplierId: String! + } + + interface RelationMaterialSupplier @relationshipProperties { + supplierMaterialNumber: String! + } + `; + + beforeAll(async () => { + neo4j = new Neo4j(); + driver = await neo4j.getDriver(); + }); + + afterAll(async () => { + await driver.close(); + }); + + test("should pass the cypherParams from the context correctly at the top level translate", async () => { + const neoGraphql = new Neo4jGraphQL({ + typeDefs, + driver, + }); + schema = await neoGraphql.getSchema(); + + const query = ` + query { + bulks { + supplierMaterialNumber + material { + id + suppliersConnection { + edges { + supplierMaterialNumber + node { + supplierId + } + } + } + } + } + } + `; + + const res = await graphql({ + schema, + source: query, + contextValue: neo4j.getContextValues({ cypherParams: { tenant: "BULK" } }), + }); + + expect(res.errors).toBeUndefined(); + expect(res.data).toEqual({ + bulks: [], + }); + }); +}); diff --git a/packages/graphql/tests/tck/tck-test-files/issues/1249.test.ts b/packages/graphql/tests/tck/tck-test-files/issues/1249.test.ts new file mode 100644 index 0000000000..f1db08b835 --- /dev/null +++ b/packages/graphql/tests/tck/tck-test-files/issues/1249.test.ts @@ -0,0 +1,109 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { gql } from "apollo-server"; +import type { DocumentNode } from "graphql"; +import { Neo4jGraphQL } from "../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../utils/tck-test-utils"; + +describe("https://github.com/neo4j/graphql/issues/1429", () => { + let typeDefs: DocumentNode; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = gql` + type Bulk + @exclude(operations: [CREATE, DELETE, UPDATE]) + @node(additionalLabels: ["$context.cypherParams.tenant"]) { + id: ID! + supplierMaterialNumber: String! + material: Material! @relationship(type: "MATERIAL_BULK", direction: OUT) + } + + type Material @exclude(operations: [CREATE, DELETE, UPDATE]) { + id: ID! + itemNumber: String! + + suppliers: [Supplier!]! + @relationship(type: "MATERIAL_SUPPLIER", properties: "RelationMaterialSupplier", direction: OUT) + } + + type Supplier @exclude(operations: [CREATE, DELETE, UPDATE]) { + id: ID! + name: String + supplierId: String! + } + + interface RelationMaterialSupplier @relationshipProperties { + supplierMaterialNumber: String! + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("should contain the cypherParams that are passed via the context", async () => { + const query = gql` + query { + bulks { + supplierMaterialNumber + material { + id + suppliersConnection { + edges { + supplierMaterialNumber + node { + supplierId + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { contextValues: { cypherParams: { tenant: "BULK" } } }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this:\`Bulk\`:\`BULK\`) + RETURN this { .supplierMaterialNumber, material: head([ (this)-[:MATERIAL_BULK]->(this_material:Material) | this_material { .id, suppliersConnection: apoc.cypher.runFirstColumnSingle(\\"CALL { + WITH this_material + MATCH (this_material)-[this_material_material_supplier_relationship:MATERIAL_SUPPLIER]->(this_material_supplier:Supplier) + WITH collect({ supplierMaterialNumber: this_material_material_supplier_relationship.supplierMaterialNumber, node: { supplierId: this_material_supplier.supplierId } }) AS edges + UNWIND edges as edge + WITH collect(edge) AS edges, size(collect(edge)) AS totalCount + RETURN { edges: edges, totalCount: totalCount } AS suppliersConnection + } RETURN suppliersConnection\\", { this_material: this_material, auth: $auth, cypherParams: $cypherParams }) } ]) } as this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"cypherParams\\": { + \\"tenant\\": \\"BULK\\" + }, + \\"auth\\": { + \\"isAuthenticated\\": false, + \\"roles\\": [] + } + }" + `); + }); +}); diff --git a/packages/graphql/tests/tck/utils/tck-test-utils.ts b/packages/graphql/tests/tck/utils/tck-test-utils.ts index a3b10cd38d..d67ab6f0e5 100644 --- a/packages/graphql/tests/tck/utils/tck-test-utils.ts +++ b/packages/graphql/tests/tck/utils/tck-test-utils.ts @@ -77,15 +77,20 @@ export async function translateQuery( options?: { req?: IncomingMessage; variableValues?: Record; + contextValues?: Record; } ): Promise<{ cypher: string; params: Record }> { const driverBuilder = new DriverBuilder(); - const contextValue: Record = { driver: driverBuilder.instance() }; + let contextValue: Record = { driver: driverBuilder.instance() }; if (options?.req) { contextValue.req = options.req; } + if (options?.contextValues) { + contextValue = { ...contextValue, ...options.contextValues }; + } + const graphqlArgs: GraphQLArgs = { schema: await neoSchema.getSchema(), source: getQuerySource(query),