From 70f8efa540cbea5d6d9ee7d8c8a060d89dbda8b3 Mon Sep 17 00:00:00 2001 From: Daniel Starns Date: Tue, 15 Jun 2021 12:52:53 +0100 Subject: [PATCH] feat: add cypherParams --- docs/asciidoc/type-definitions/cypher.adoc | 30 +++ .../graphql/src/schema/resolvers/cypher.ts | 2 +- .../translate/create-projection-and-params.ts | 8 +- .../integration/cypher-params.int.test.ts | 186 ++++++++++++++++++ 4 files changed, 223 insertions(+), 3 deletions(-) create mode 100644 packages/graphql/tests/integration/cypher-params.int.test.ts diff --git a/docs/asciidoc/type-definitions/cypher.adoc b/docs/asciidoc/type-definitions/cypher.adoc index 7321dc8cce..35e30a5c01 100644 --- a/docs/asciidoc/type-definitions/cypher.adoc +++ b/docs/asciidoc/type-definitions/cypher.adoc @@ -55,6 +55,36 @@ type Query { } ---- + +=== `cypherParams` +Use to inject values into the cypher query from the GraphQL context function. + +Inject into context: + +[source, typescript] +---- +const server = new ApolloServer({ + typeDefs, + context: () => { + return { + cypherParams: { userId: "user-id-01" } + } + } +}); +---- + +Use in cypher query: + +[source, graphql] +---- +type Query { + userPosts: [Post] @cypher(statement: """ + MATCH (:User {id: $cypherParams.userId})-[:POSTED]->(p:Post) + RETURN p + """) +} +---- + == Return values The return value of the Cypher statement must be of the same type to which the directive is applied. diff --git a/packages/graphql/src/schema/resolvers/cypher.ts b/packages/graphql/src/schema/resolvers/cypher.ts index 8bb0fc6856..2f30b1799d 100644 --- a/packages/graphql/src/schema/resolvers/cypher.ts +++ b/packages/graphql/src/schema/resolvers/cypher.ts @@ -29,7 +29,7 @@ export default function cypherResolver({ field, statement }: { field: BaseField; async function resolve(_root: any, args: any, _context: unknown) { const context = _context as Context; const cypherStrs: string[] = []; - let params = { ...args, auth: createAuthParam({ context }) }; + let params = { ...args, auth: createAuthParam({ context }), cypherParams: context.cypherParams }; const preAuth = createAuthAndParams({ entity: field, context }); if (preAuth[0]) { diff --git a/packages/graphql/src/translate/create-projection-and-params.ts b/packages/graphql/src/translate/create-projection-and-params.ts index 6ca3b987a3..6fd9e04965 100644 --- a/packages/graphql/src/translate/create-projection-and-params.ts +++ b/packages/graphql/src/translate/create-projection-and-params.ts @@ -207,6 +207,10 @@ function createProjectionAndParams({ } } + const initApocParamsStrs = [ + "auth: $auth", + ...(context.cypherParams ? ["cypherParams: $cypherParams"] : []), + ]; const apocParams = Object.entries(field.args).reduce( (r: { strs: string[]; params: any }, entry) => { const argName = `${param}_${entry[0]}`; @@ -216,9 +220,9 @@ function createProjectionAndParams({ params: { ...r.params, [argName]: entry[1] }, }; }, - { strs: ["auth: $auth"], params: {} } + { strs: initApocParamsStrs, params: {} } ) as { strs: string[]; params: any }; - res.params = { ...res.params, ...apocParams.params }; + res.params = { ...res.params, ...apocParams.params, cypherParams: context.cypherParams }; const expectMultipleValues = referenceNode && cypherField.typeMeta.array ? "true" : "false"; const apocWhere = `${ diff --git a/packages/graphql/tests/integration/cypher-params.int.test.ts b/packages/graphql/tests/integration/cypher-params.int.test.ts new file mode 100644 index 0000000000..4ea806e130 --- /dev/null +++ b/packages/graphql/tests/integration/cypher-params.int.test.ts @@ -0,0 +1,186 @@ +/* + * 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 { Neo4jGraphQL } from "../../src/classes"; +import neo4j from "./neo4j"; + +describe("cypherParams", () => { + let driver: Driver; + + beforeAll(async () => { + driver = await neo4j(); + }); + + afterAll(async () => { + await driver.close(); + }); + + test("should inject cypherParams on top-level cypher query", async () => { + const session = driver.session(); + + const typeDefs = ` + type Movie { + id: ID + } + + type Query { + id: String! @cypher(statement: "RETURN $cypherParams.id") + } + `; + + const neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + + const id = generate({ + charset: "alphabetic", + }); + + const source = ` + { + id + } + `; + + try { + const gqlResult = await graphql({ + schema: neoSchema.schema, + source, + contextValue: { driver, cypherParams: { id } }, + }); + + expect(gqlResult.errors).toBeFalsy(); + + expect((gqlResult.data as any).id).toEqual(id); + } finally { + await session.close(); + } + }); + + test("should inject cypherParams on field level nested query", async () => { + const session = driver.session(); + + const typeDefs = ` + type CypherParams { + id: ID + } + + type Movie { + id: ID + cypherParams: CypherParams @cypher(statement: "RETURN $cypherParams") + } + `; + + const neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + + const movieId = generate({ + charset: "alphabetic", + }); + const cypherParamsId = generate({ + charset: "alphabetic", + }); + + const source = ` + query($id: ID) { + movies(where: {id: $id}) { + id + cypherParams { + id + } + } + } + `; + + try { + await session.run( + ` + CREATE (:Movie {id: $movieId}) + `, + { movieId } + ); + + const gqlResult = await graphql({ + schema: neoSchema.schema, + source, + variableValues: { + id: movieId, + }, + contextValue: { driver, cypherParams: { id: cypherParamsId } }, + }); + + expect(gqlResult.errors).toBeFalsy(); + + expect((gqlResult.data as any).movies[0]).toEqual({ + id: movieId, + cypherParams: { + id: cypherParamsId, + }, + }); + } finally { + await session.close(); + } + }); + + test("should inject cypherParams on top-level cypher mutation", async () => { + const session = driver.session(); + + const typeDefs = ` + type Movie { + id: ID + } + + type Mutation { + id: String! @cypher(statement: "RETURN $cypherParams.id") + } + `; + + const neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + + const id = generate({ + charset: "alphabetic", + }); + + const source = ` + mutation { + id + } + `; + + try { + const gqlResult = await graphql({ + schema: neoSchema.schema, + source, + contextValue: { driver, cypherParams: { id } }, + }); + + expect(gqlResult.errors).toBeFalsy(); + + expect((gqlResult.data as any).id).toEqual(id); + } finally { + await session.close(); + } + }); +});