diff --git a/.changeset/soft-socks-count.md b/.changeset/soft-socks-count.md new file mode 100644 index 0000000000..cada9436ec --- /dev/null +++ b/.changeset/soft-socks-count.md @@ -0,0 +1,22 @@ +--- +"@neo4j/graphql": patch +--- + +Add `version` to `cypherQueryOptions` in context to add a Cypher version with `CYPHER` before each query: + +```js +{ + cypherQueryOptions: { + addVersionPrefix: true, + }, +} +``` + +This prepends all Cypher queries with a `CYPHER [version]` statement: + +```cypher +CYPHER 5 +MATCH (this:Movie) +WHERE this.title = $param0 +RETURN this { .title } AS this +``` diff --git a/packages/graphql/package.json b/packages/graphql/package.json index efc3b619e9..1b539e2eb5 100644 --- a/packages/graphql/package.json +++ b/packages/graphql/package.json @@ -79,7 +79,7 @@ "@graphql-tools/resolvers-composition": "^7.0.0", "@graphql-tools/schema": "^10.0.0", "@graphql-tools/utils": "10.6.1", - "@neo4j/cypher-builder": "2.2.1", + "@neo4j/cypher-builder": "2.3.0", "camelcase": "^6.3.0", "debug": "^4.3.4", "dot-prop": "^6.0.1", diff --git a/packages/graphql/src/classes/Executor.ts b/packages/graphql/src/classes/Executor.ts index d970ccbd85..13f51728b4 100644 --- a/packages/graphql/src/classes/Executor.ts +++ b/packages/graphql/src/classes/Executor.ts @@ -40,6 +40,7 @@ import { } from "../constants"; import { debugCypherAndParams } from "../debug/debug-cypher-and-params"; import type { CypherQueryOptions } from "../types"; +import { isInArray } from "../utils/is-in-array"; import { Neo4jGraphQLAuthenticationError, Neo4jGraphQLConstraintValidationError, @@ -49,6 +50,8 @@ import { const debug = Debug(DEBUG_EXECUTE); +const SUPPORTED_CYPHER_VERSION = "5"; + interface DriverLike { session(config); } @@ -107,7 +110,6 @@ export class Executor { }: ExecutorConstructorParam) { this.executionContext = executionContext; this.cypherQueryOptions = cypherQueryOptions; - this.cypherQueryOptions = cypherQueryOptions; this.sessionConfig = sessionConfig; this.cypherParams = cypherParams; this.transactionMetadata = transactionMetadata; @@ -173,16 +175,34 @@ export class Executor { return error; } - private generateQuery(query: string): string { - if (this.cypherQueryOptions && Object.keys(this.cypherQueryOptions).length) { - const cypherQueryOptions = `CYPHER ${Object.entries(this.cypherQueryOptions) - .map(([key, value]) => `${key}=${value}`) - .join(" ")}`; + private addCypherOptionsToQuery(query: string): string { + const cypherVersion = this.getCypherVersionStatement(); + + const cypherQueryOptions = this.getCypherQueryOptionsStatement(); + + return `${cypherVersion}${cypherQueryOptions}${query}`; + } - return `${cypherQueryOptions}\n${query}`; + private getCypherVersionStatement(): string { + if (this.cypherQueryOptions?.addVersionPrefix) { + return `CYPHER ${SUPPORTED_CYPHER_VERSION}\n`; } + return ""; + } - return query; + private getCypherQueryOptionsStatement(): string { + const ignoredCypherQueryOptions: Array = ["addVersionPrefix"]; + const cypherQueryOptions = Object.entries(this.cypherQueryOptions ?? []).filter(([key, _value]) => { + return !isInArray(ignoredCypherQueryOptions, key); + }); + if (cypherQueryOptions.length) { + return `CYPHER ${cypherQueryOptions + .map(([key, value]) => { + return `${key}=${value}`; + }) + .join(" ")}\n`; + } + return ""; } private getTransactionConfig(info?: GraphQLResolveInfo): TransactionConfig { @@ -276,7 +296,7 @@ export class Executor { parameters: Record, transaction: Transaction | ManagedTransaction ): Result { - const queryToRun = this.generateQuery(query); + const queryToRun = this.addCypherOptionsToQuery(query); debugCypherAndParams(debug, queryToRun, parameters); diff --git a/packages/graphql/src/translate/translate-resolve-reference.ts b/packages/graphql/src/translate/translate-resolve-reference.ts index 1fc547d3d9..b3658b5c19 100644 --- a/packages/graphql/src/translate/translate-resolve-reference.ts +++ b/packages/graphql/src/translate/translate-resolve-reference.ts @@ -18,11 +18,11 @@ */ import type Cypher from "@neo4j/cypher-builder"; -import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-translation-context"; import Debug from "debug"; -import { QueryASTFactory } from "./queryAST/factory/QueryASTFactory"; -import type { EntityAdapter } from "../schema-model/entity/EntityAdapter"; import { DEBUG_TRANSLATE } from "../constants"; +import type { EntityAdapter } from "../schema-model/entity/EntityAdapter"; +import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-translation-context"; +import { QueryASTFactory } from "./queryAST/factory/QueryASTFactory"; const debug = Debug(DEBUG_TRANSLATE); diff --git a/packages/graphql/src/types/index.ts b/packages/graphql/src/types/index.ts index 795c7e5b5c..c492c11b99 100644 --- a/packages/graphql/src/types/index.ts +++ b/packages/graphql/src/types/index.ts @@ -276,6 +276,7 @@ export interface CypherQueryOptions { operatorEngine?: "default" | "interpreted" | "compiled"; interpretedPipesFallback?: "default" | "disabled" | "whitelisted_plans_only" | "all"; replan?: "default" | "force" | "skip"; + addVersionPrefix?: boolean; } /** Input field for graphql-compose */ diff --git a/packages/graphql/tests/integration/config-options/query-options.int.test.ts b/packages/graphql/tests/integration/config-options/query-options.int.test.ts index cac91a87fb..3db6adb016 100644 --- a/packages/graphql/tests/integration/config-options/query-options.int.test.ts +++ b/packages/graphql/tests/integration/config-options/query-options.int.test.ts @@ -87,4 +87,36 @@ describe("query options", () => { expect(result?.data?.[Movie.plural]).toEqual([{ id }, { id }, { id }]); }); + + test("queries should work with version set to Cypher version", async () => { + const id = generate({ + charset: "alphabetic", + }); + + const query = ` + query($id: ID){ + ${Movie.plural}(where: {id_EQ: $id}){ + id + } + } + `; + + await neoSchema.checkNeo4jCompat(); + + await testHelper.executeCypher( + ` + CREATE (:${Movie} {id: $id}), (:${Movie} {id: $id}), (:${Movie} {id: $id}) + `, + { id } + ); + + const result = await testHelper.executeGraphQL(query, { + variableValues: { id }, + contextValue: { cypherQueryOptions: { runtime: "interpreted", addVersionPrefix: true } }, + }); + + expect(result.errors).toBeFalsy(); + + expect(result?.data?.[Movie.plural]).toEqual([{ id }, { id }, { id }]); + }); }); diff --git a/yarn.lock b/yarn.lock index 222d58202c..a33c81c0fd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2082,10 +2082,10 @@ __metadata: languageName: node linkType: hard -"@neo4j/cypher-builder@npm:2.2.1": - version: 2.2.1 - resolution: "@neo4j/cypher-builder@npm:2.2.1" - checksum: 10c0/5d3af9a6b9bc7db5dc36ca7cfe6e0ec2d104a65538afca20aa47245cfb87681275e26d99e802f9ac80f2597b3535133c3be97cd3b5db35ba1495e6ef4f230dfb +"@neo4j/cypher-builder@npm:2.3.0": + version: 2.3.0 + resolution: "@neo4j/cypher-builder@npm:2.3.0" + checksum: 10c0/bd7d98b8c3a84e9562ff88b9f2ad10a9258aabc2204dba247d4aa83944bce6f5100a3e14199a688dea62de2313910b90e03da7a9b1a423d0a279b022e5909fc3 languageName: node linkType: hard @@ -2106,7 +2106,7 @@ __metadata: "@graphql-tools/resolvers-composition": "npm:^7.0.0" "@graphql-tools/schema": "npm:^10.0.0" "@graphql-tools/utils": "npm:10.6.1" - "@neo4j/cypher-builder": "npm:2.2.1" + "@neo4j/cypher-builder": "npm:2.3.0" "@types/deep-equal": "npm:1.0.4" "@types/is-uuid": "npm:1.0.2" "@types/jest": "npm:29.5.14"