From c8e1afe5537ff09b4ad11e100765f583d23eb692 Mon Sep 17 00:00:00 2001 From: Darrell Warde Date: Tue, 15 Jun 2021 12:57:07 +0100 Subject: [PATCH 01/20] Missing copyright headers --- .../get-relationship-field-meta.test.ts | 19 +++++++++++++++++++ .../graphql/src/schema/get-where-fields.ts | 19 +++++++++++++++++++ .../create-connection-and-params.test.ts | 19 +++++++++++++++++++ .../create-connection-and-params.ts | 19 +++++++++++++++++++ ...-set-relationship-properties-and-params.ts | 19 +++++++++++++++++++ .../create-set-relationship-properties.ts | 19 +++++++++++++++++++ .../elements/create-datetime-element.test.ts | 19 +++++++++++++++++++ .../elements/create-datetime-element.ts | 19 +++++++++++++++++++ .../elements/create-point-element.test.ts | 19 +++++++++++++++++++ .../elements/create-point-element.ts | 19 +++++++++++++++++++ ...eate-relationship-property-element.test.ts | 19 +++++++++++++++++++ .../create-relationship-property-element.ts | 19 +++++++++++++++++++ .../create-connection-where-and-params.ts | 19 +++++++++++++++++++ .../src/translate/where/create-filter.test.ts | 19 +++++++++++++++++++ .../src/translate/where/create-filter.ts | 19 +++++++++++++++++++ .../create-relationship-where-and-params.ts | 19 +++++++++++++++++++ 16 files changed, 304 insertions(+) diff --git a/packages/graphql/src/schema/get-relationship-field-meta.test.ts b/packages/graphql/src/schema/get-relationship-field-meta.test.ts index 5df75cf4ca..25dc5e6e67 100644 --- a/packages/graphql/src/schema/get-relationship-field-meta.test.ts +++ b/packages/graphql/src/schema/get-relationship-field-meta.test.ts @@ -1,3 +1,22 @@ +/* + * 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 { mergeTypeDefs } from "@graphql-tools/merge"; import { InterfaceTypeDefinitionNode } from "graphql"; import getRelationshipFieldMeta from "./get-relationship-field-meta"; diff --git a/packages/graphql/src/schema/get-where-fields.ts b/packages/graphql/src/schema/get-where-fields.ts index 0f9fffb98c..a8e62eef63 100644 --- a/packages/graphql/src/schema/get-where-fields.ts +++ b/packages/graphql/src/schema/get-where-fields.ts @@ -1,3 +1,22 @@ +/* + * 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 { CustomEnumField, CustomScalarField, DateTimeField, PointField, PrimitiveField } from "../types"; interface Fields { diff --git a/packages/graphql/src/translate/connection/create-connection-and-params.test.ts b/packages/graphql/src/translate/connection/create-connection-and-params.test.ts index 5212760edc..4de82883c3 100644 --- a/packages/graphql/src/translate/connection/create-connection-and-params.test.ts +++ b/packages/graphql/src/translate/connection/create-connection-and-params.test.ts @@ -1,3 +1,22 @@ +/* + * 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 { ResolveTree } from "graphql-parse-resolve-info"; import dedent from "dedent"; import { mocked } from "ts-jest/utils"; diff --git a/packages/graphql/src/translate/connection/create-connection-and-params.ts b/packages/graphql/src/translate/connection/create-connection-and-params.ts index 7cd91aa021..619750426d 100644 --- a/packages/graphql/src/translate/connection/create-connection-and-params.ts +++ b/packages/graphql/src/translate/connection/create-connection-and-params.ts @@ -1,3 +1,22 @@ +/* + * 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 { FieldsByTypeName, ResolveTree } from "graphql-parse-resolve-info"; import { ConnectionField, ConnectionOptionsArg, ConnectionWhereArg, Context } from "../../types"; import { Node } from "../../classes"; diff --git a/packages/graphql/src/translate/create-set-relationship-properties-and-params.ts b/packages/graphql/src/translate/create-set-relationship-properties-and-params.ts index 0cdf656f09..395bc8f280 100644 --- a/packages/graphql/src/translate/create-set-relationship-properties-and-params.ts +++ b/packages/graphql/src/translate/create-set-relationship-properties-and-params.ts @@ -1,3 +1,22 @@ +/* + * 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. + */ + /* eslint-disable prefer-destructuring */ import { Relationship } from "../classes"; import { BaseField, DateTimeField, PrimitiveField } from "../types"; diff --git a/packages/graphql/src/translate/create-set-relationship-properties.ts b/packages/graphql/src/translate/create-set-relationship-properties.ts index ad447a2ea5..6497898cb5 100644 --- a/packages/graphql/src/translate/create-set-relationship-properties.ts +++ b/packages/graphql/src/translate/create-set-relationship-properties.ts @@ -1,3 +1,22 @@ +/* + * 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 { Relationship } from "../classes"; import { BaseField, DateTimeField, PrimitiveField } from "../types"; diff --git a/packages/graphql/src/translate/projection/elements/create-datetime-element.test.ts b/packages/graphql/src/translate/projection/elements/create-datetime-element.test.ts index 14fbcc208e..be1dbd669b 100644 --- a/packages/graphql/src/translate/projection/elements/create-datetime-element.test.ts +++ b/packages/graphql/src/translate/projection/elements/create-datetime-element.test.ts @@ -1,3 +1,22 @@ +/* + * 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 { ResolveTree } from "graphql-parse-resolve-info"; import { DateTimeField } from "../../../types"; import createDatetimeElement from "./create-datetime-element"; diff --git a/packages/graphql/src/translate/projection/elements/create-datetime-element.ts b/packages/graphql/src/translate/projection/elements/create-datetime-element.ts index 59d930680a..20e5facb6d 100644 --- a/packages/graphql/src/translate/projection/elements/create-datetime-element.ts +++ b/packages/graphql/src/translate/projection/elements/create-datetime-element.ts @@ -1,3 +1,22 @@ +/* + * 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 { ResolveTree } from "graphql-parse-resolve-info"; import { DateTimeField } from "../../../types"; diff --git a/packages/graphql/src/translate/projection/elements/create-point-element.test.ts b/packages/graphql/src/translate/projection/elements/create-point-element.test.ts index 151452c88b..3a0116f4fc 100644 --- a/packages/graphql/src/translate/projection/elements/create-point-element.test.ts +++ b/packages/graphql/src/translate/projection/elements/create-point-element.test.ts @@ -1,3 +1,22 @@ +/* + * 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 { ResolveTree } from "graphql-parse-resolve-info"; import { PointField } from "../../../types"; import createPointElement from "./create-point-element"; diff --git a/packages/graphql/src/translate/projection/elements/create-point-element.ts b/packages/graphql/src/translate/projection/elements/create-point-element.ts index 0d28785e00..3bfe861de9 100644 --- a/packages/graphql/src/translate/projection/elements/create-point-element.ts +++ b/packages/graphql/src/translate/projection/elements/create-point-element.ts @@ -1,3 +1,22 @@ +/* + * 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 { ResolveTree } from "graphql-parse-resolve-info"; import { PointField } from "../../../types"; diff --git a/packages/graphql/src/translate/projection/elements/create-relationship-property-element.test.ts b/packages/graphql/src/translate/projection/elements/create-relationship-property-element.test.ts index 82b2664654..0cbc895606 100644 --- a/packages/graphql/src/translate/projection/elements/create-relationship-property-element.test.ts +++ b/packages/graphql/src/translate/projection/elements/create-relationship-property-element.test.ts @@ -1,3 +1,22 @@ +/* + * 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 { ResolveTree } from "graphql-parse-resolve-info"; import Relationship from "../../../classes/Relationship"; import { DateTimeField, PointField, PrimitiveField } from "../../../types"; diff --git a/packages/graphql/src/translate/projection/elements/create-relationship-property-element.ts b/packages/graphql/src/translate/projection/elements/create-relationship-property-element.ts index 4bf839dbea..791b5bf36b 100644 --- a/packages/graphql/src/translate/projection/elements/create-relationship-property-element.ts +++ b/packages/graphql/src/translate/projection/elements/create-relationship-property-element.ts @@ -1,3 +1,22 @@ +/* + * 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 { ResolveTree } from "graphql-parse-resolve-info"; import Relationship from "../../../classes/Relationship"; import createDatetimeElement from "./create-datetime-element"; diff --git a/packages/graphql/src/translate/where/create-connection-where-and-params.ts b/packages/graphql/src/translate/where/create-connection-where-and-params.ts index d7832dc2d4..35e7f2f5b2 100644 --- a/packages/graphql/src/translate/where/create-connection-where-and-params.ts +++ b/packages/graphql/src/translate/where/create-connection-where-and-params.ts @@ -1,3 +1,22 @@ +/* + * 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 { Node, Relationship } from "../../classes"; import { ConnectionWhereArg, Context } from "../../types"; import createRelationshipWhereAndParams from "./create-relationship-where-and-params"; diff --git a/packages/graphql/src/translate/where/create-filter.test.ts b/packages/graphql/src/translate/where/create-filter.test.ts index 9770c8b091..ea57776122 100644 --- a/packages/graphql/src/translate/where/create-filter.test.ts +++ b/packages/graphql/src/translate/where/create-filter.test.ts @@ -1,3 +1,22 @@ +/* + * 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 createFilter, { Operator } from "./create-filter"; describe("createFilter", () => { diff --git a/packages/graphql/src/translate/where/create-filter.ts b/packages/graphql/src/translate/where/create-filter.ts index 4030ee4e84..372797c1cf 100644 --- a/packages/graphql/src/translate/where/create-filter.ts +++ b/packages/graphql/src/translate/where/create-filter.ts @@ -1,3 +1,22 @@ +/* + * 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. + */ + export enum Operator { INCLUDES = "IN", IN = "IN", diff --git a/packages/graphql/src/translate/where/create-relationship-where-and-params.ts b/packages/graphql/src/translate/where/create-relationship-where-and-params.ts index 327ad8d34b..cf0e811a08 100644 --- a/packages/graphql/src/translate/where/create-relationship-where-and-params.ts +++ b/packages/graphql/src/translate/where/create-relationship-where-and-params.ts @@ -1,3 +1,22 @@ +/* + * 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 Relationship from "../../classes/Relationship"; import { GraphQLWhereArg, Context, PrimitiveField } from "../../types"; import createFilter from "./create-filter"; From 900bb4bb5d92520bc29c6568c726379c56cabb35 Mon Sep 17 00:00:00 2001 From: Darrell Warde Date: Tue, 15 Jun 2021 13:00:28 +0100 Subject: [PATCH 02/20] Point to prerelease documentation for 2.0.0 branch --- README.md | 3 ++- packages/graphql/README.md | 2 +- packages/ogm/README.md | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1878aec0d7..acc57d8091 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ 💡 Welcome to the Monorepo for [Neo4j](https://neo4j.com/) + [GraphQL](https://graphql.org/). ![Neo4j + GraphQL](./docs/images/banner.png) +

Discord @@ -18,7 +19,7 @@ Want to contribute to `@neo4j/graphql`? See our [contributing guide](./docs/mark ## Links -1. [Documentation](https://neo4j.com/docs/graphql-manual/current/) +1. [Documentation](https://neo4j.com/docs/graphql-manual/2.0/) 2. [Discord](https://discord.gg/neo4j) 3. [Examples](./examples) diff --git a/packages/graphql/README.md b/packages/graphql/README.md index 454493b72b..19d8936047 100644 --- a/packages/graphql/README.md +++ b/packages/graphql/README.md @@ -14,7 +14,7 @@ A GraphQL to Cypher query execution layer for Neo4j and JavaScript GraphQL implementations. -1. [Documentation](https://neo4j.com/docs/graphql-manual/current/) +1. [Documentation](https://neo4j.com/docs/graphql-manual/2.0/) ## Installation diff --git a/packages/ogm/README.md b/packages/ogm/README.md index 2206abf1db..05de511b66 100644 --- a/packages/ogm/README.md +++ b/packages/ogm/README.md @@ -14,7 +14,7 @@ GraphQL powered OGM for Neo4j and Javascript applications. -1. [Documentation](https://neo4j.com/docs/graphql-manual/current/) +1. [Documentation](https://neo4j.com/docs/graphql-manual/2.0/ogm/) ## Installation From 20ff60f55c4ffe7e13688e074c51d5feda4d5801 Mon Sep 17 00:00:00 2001 From: Darrell Warde Date: Tue, 15 Jun 2021 13:11:56 +0100 Subject: [PATCH 03/20] General housekeeping before release --- .../graphql/src/schema/get-where-fields.ts | 7 --- .../src/schema/make-augmented-schema.ts | 32 +---------- .../create-connection-and-params.ts | 53 ------------------- .../src/translate/create-delete-and-params.ts | 4 +- .../graphql/src/translate/translate-update.ts | 3 -- .../create-connection-where-and-params.ts | 7 --- .../where/create-node-where-and-params.ts | 11 ---- .../create-relationship-where-and-params.ts | 1 - 8 files changed, 3 insertions(+), 115 deletions(-) diff --git a/packages/graphql/src/schema/get-where-fields.ts b/packages/graphql/src/schema/get-where-fields.ts index a8e62eef63..2d32cdb0f7 100644 --- a/packages/graphql/src/schema/get-where-fields.ts +++ b/packages/graphql/src/schema/get-where-fields.ts @@ -38,13 +38,6 @@ function getWhereFields({ typeName, fields, enableRegex }: { typeName: string; f }, {}), ...[...fields.primitiveFields, ...fields.dateTimeFields, ...fields.enumFields, ...fields.pointFields].reduce( (res, f) => { - // This is the only sensible place to flag whether Point and CartesianPoint are used - // if (f.typeMeta.name === "Point") { - // pointInTypeDefs = true; - // } else if (f.typeMeta.name === "CartesianPoint") { - // cartesianPointInTypeDefs = true; - // } - res[f.fieldName] = f.typeMeta.input.where.pretty; res[`${f.fieldName}_NOT`] = f.typeMeta.input.where.pretty; diff --git a/packages/graphql/src/schema/make-augmented-schema.ts b/packages/graphql/src/schema/make-augmented-schema.ts index 113aba1a4a..028e395287 100644 --- a/packages/graphql/src/schema/make-augmented-schema.ts +++ b/packages/graphql/src/schema/make-augmented-schema.ts @@ -310,7 +310,7 @@ function makeAugmentedSchema( composer.createInputTC({ name: `${relationship.name.value}CreateInput`, - // TODO - Duplicated with rel properties + // TODO - Duplicated with node properties on line 492 fields: relationshipFieldMeta.reduce((res, f) => { if ((f as PrimitiveField)?.autogenerate) { return res; @@ -489,7 +489,7 @@ function makeAugmentedSchema( const nodeInput = composer.createInputTC({ name: `${node.name}CreateInput`, - // TODO - Duplicated with rel properties + // TODO - Duplicated with relationship properties on line 313 fields: [ ...node.primitiveFields, ...node.scalarFields, @@ -573,24 +573,6 @@ function makeAugmentedSchema( }, }); - // composer.createInputTC({ - // name: `${node.name}DisconnectFieldInput`, - // fields: { - // where: `${node.name}Where`, - // ...(node.relationFields.length ? { disconnect: nodeDisconnectInput } : {}), - // }, - // }); - - // if (!composer.has(`${node.name}DeleteFieldInput`)) { - // composer.createInputTC({ - // name: `${node.name}DeleteFieldInput`, - // fields: { - // where: `${node.name}Where`, - // ...(node.relationFields.length ? { delete: nodeDeleteInput } : {}), - // }, - // }); - // } - node.relationFields.forEach((rel) => { if (rel.union) { const refNodes = nodes.filter((x) => rel.union?.nodes?.includes(x.name)); @@ -614,13 +596,6 @@ function makeAugmentedSchema( const nodeFieldDeleteInputName = `${unionPrefix}DeleteFieldInput`; const nodeFieldDisconnectInputName = `${unionPrefix}DisconnectFieldInput`; - // const disconnectField = rel.typeMeta.array - // ? `[${n.name}DisconnectFieldInput!]` - // : `${n.name}DisconnectFieldInput`; - // const deleteField = rel.typeMeta.array - // ? `[${n.name}DeleteFieldInput!]` - // : `${n.name}DeleteFieldInput`; - composeNode.addFieldArgs(rel.fieldName, { [n.name]: `${n.name}Where`, }); @@ -760,9 +735,6 @@ function makeAugmentedSchema( const nodeFieldUpdateInputName = `${node.name}${upperFirstLetter(rel.fieldName)}UpdateFieldInput`; const nodeFieldDeleteInputName = `${node.name}${upperFirstLetter(rel.fieldName)}DeleteFieldInput`; const nodeFieldDisconnectInputName = `${node.name}${upperFirstLetter(rel.fieldName)}DisconnectFieldInput`; - // const disconnectField = rel.typeMeta.array - // ? `[${n.name}DisconnectFieldInput!]` - // : `${n.name}DisconnectFieldInput`; whereInput.addFields({ ...{ [rel.fieldName]: `${n.name}Where`, [`${rel.fieldName}_NOT`]: `${n.name}Where` }, diff --git a/packages/graphql/src/translate/connection/create-connection-and-params.ts b/packages/graphql/src/translate/connection/create-connection-and-params.ts index 619750426d..cf446c36ef 100644 --- a/packages/graphql/src/translate/connection/create-connection-and-params.ts +++ b/packages/graphql/src/translate/connection/create-connection-and-params.ts @@ -25,54 +25,6 @@ import Relationship from "../../classes/Relationship"; import createRelationshipPropertyElement from "../projection/elements/create-relationship-property-element"; import createConnectionWhereAndParams from "../where/create-connection-where-and-params"; -/* -input: - -{ - actorsConnection: { - alias: "actorsConnection" - name: "actorsConnection" - args: { where, options }???? - fieldsByTypeName: { - MovieActorsConnection: { - edges: { - alias: "edges" - name: "edges" - args: { } - fieldsByTypeName: { - MovieActorsRelationship: { - screenTime: { - alias: "screenTime" - name: "screenTime" - } - node: { - alias: "node" - name: "node" - fieldsByTypeName: { PASS ME BACK TO create-projection-and-params - .......... - } - } - } - } - } - } - } - } -} - -output: - -actorsConnection: apoc.cypher.runFirstColumn( - " - MATCH (this)<-[this_acted_in:ACTED_IN]-(this_actors:Actor) - WITH collect({ screenTime: this_acted_in.screenTime, node: { name: this_actors.name }}) as edges - RETURN { edges: edges } - ", - {this: this}, - true - ) - -*/ function createConnectionAndParams({ resolveTree, field, @@ -231,11 +183,6 @@ function createConnectionAndParams({ const nodeOutStr = `(${relatedNodeVariable}:${field.relationship.typeMeta.name})`; const relatedNode = context.neoSchema.nodes.find((x) => x.name === field.relationship.typeMeta.name) as Node; - /* - MATCH clause, example: - - MATCH (this)<-[this_acted_in:ACTED_IN]-(this_actor:Actor) - */ subquery.push(`MATCH (${nodeVariable})${inStr}${relTypeStr}${outStr}${nodeOutStr}`); if (whereInput) { diff --git a/packages/graphql/src/translate/create-delete-and-params.ts b/packages/graphql/src/translate/create-delete-and-params.ts index 860aa60c95..1e5f817d67 100644 --- a/packages/graphql/src/translate/create-delete-and-params.ts +++ b/packages/graphql/src/translate/create-delete-and-params.ts @@ -96,13 +96,11 @@ function createDeleteAndParams({ relationshipVariable, relationship, parameterPrefix: `${parameterPrefix}${!recursing ? `.${key}` : ""}${ - // used here relationField.typeMeta.array ? `[${index}]` : "" }.where`, }); if (whereAndParams[0]) { whereStrs.push(whereAndParams[0]); - // res.params = { ...res.params, ...whereAndParams[1] }; } } const whereAuth = createAuthAndParams({ @@ -145,7 +143,7 @@ function createDeleteAndParams({ parentVar: _varName, parameterPrefix: `${parameterPrefix}${!recursing ? `.${key}` : ""}${ relationField.typeMeta.array ? `[${index}]` : "" - }.delete`, // TODO + }.delete`, recursing: false, }); res.strs.push(deleteAndParams[0]); diff --git a/packages/graphql/src/translate/translate-update.ts b/packages/graphql/src/translate/translate-update.ts index 8f24a4ae6b..48f3a3c0b2 100644 --- a/packages/graphql/src/translate/translate-update.ts +++ b/packages/graphql/src/translate/translate-update.ts @@ -102,8 +102,6 @@ function translateUpdate({ node, context }: { node: Node; context: Context }): [ [updateStr] = updateAndParams; cypherParams = { ...cypherParams, - // Crude check if parameter is actually used before adding it - // ...(updateStr.includes(resolveTree.name) ? { [resolveTree.name]: { args: { update: updateInput } } } : {}), ...updateAndParams[1], }; updateArgs = { @@ -234,7 +232,6 @@ function translateUpdate({ node, context }: { node: Node; context: Context }): [ [deleteStr] = deleteAndParams; cypherParams = { ...cypherParams, - // ...(deleteStr.includes(resolveTree.name) ? { [resolveTree.name]: { args: { delete: deleteInput } } } : {}), ...deleteAndParams[1], }; updateArgs = { diff --git a/packages/graphql/src/translate/where/create-connection-where-and-params.ts b/packages/graphql/src/translate/where/create-connection-where-and-params.ts index 35e7f2f5b2..033f0339d7 100644 --- a/packages/graphql/src/translate/where/create-connection-where-and-params.ts +++ b/packages/graphql/src/translate/where/create-connection-where-and-params.ts @@ -60,9 +60,6 @@ function createConnectionWhereAndParams({ innerParams.push(or[1]); }); - // whereStrs.push(`(${innerClauses.join(` ${k} `)})`); - // params = { ...params, [k]: innerParams }; - const whereStrs = [...res.whereStrs, `(${innerClauses.filter((clause) => !!clause).join(` ${k} `)})`]; const params = { ...res.params, [k]: innerParams }; res = { whereStrs, params }; @@ -77,8 +74,6 @@ function createConnectionWhereAndParams({ context, parameterPrefix: `${parameterPrefix}.${k}`, }); - // whereStrs.push(k === "relationship_NOT" ? `(NOT ${relationshipWhere[0]})` : relationshipWhere[0]); - // params = { ...params, [k]: relationshipWhere[1] }; const whereStrs = [ ...res.whereStrs, @@ -97,8 +92,6 @@ function createConnectionWhereAndParams({ context, parameterPrefix: `${parameterPrefix}.${k}`, }); - // whereStrs.push(k.endsWith("_NOT") ? `(NOT ${nodeWhere[0]})` : nodeWhere[0]); - // params = { ...params, [k]: nodeWhere[1] }; const whereStrs = [...res.whereStrs, k.endsWith("_NOT") ? `(NOT ${nodeWhere[0]})` : nodeWhere[0]]; const params = { ...res.params, [k]: nodeWhere[1] }; diff --git a/packages/graphql/src/translate/where/create-node-where-and-params.ts b/packages/graphql/src/translate/where/create-node-where-and-params.ts index 65b8665c3e..76a916d20c 100644 --- a/packages/graphql/src/translate/where/create-node-where-and-params.ts +++ b/packages/graphql/src/translate/where/create-node-where-and-params.ts @@ -87,14 +87,11 @@ function createNodeWhereAndParams({ whereInput: v, node, nodeVariable, - // chainStr: `${param}${i > 0 ? i : ""}`, context, - // recursing: true, parameterPrefix: `${parameterPrefix}.${fieldName}[${i}]`, }); innerClauses.push(`(${recurse[0]})`); - // res.params = { ...res.params, ...recurse[1] }; nestedParams.push(recurse[1]); }); @@ -211,14 +208,7 @@ function createNodeWhereAndParams({ resultStr += ")"; // close ALL res.clauses.push(resultStr); res.params = { ...res.params, fieldName: nestedParams }; - // } else if (pointField) { - // let clause = `${property} IN [p in $${param} | point(p)]`; - // if (not) clause = `(NOT ${clause})`; - // res.clauses.push(clause); - // res.params[key] = value; } else { - // let clause = `${property} IN $${param}`; - // if (not) clause = `(NOT ${clause})`; const clause = createFilter({ left: property, operator, @@ -280,7 +270,6 @@ function createNodeWhereAndParams({ } const { clauses, params } = Object.entries(whereInput).reduce(reducer, { clauses: [], params: {} }); - // let where = `${!recursing ? "WHERE " : ""}`; const where = clauses.join(" AND ").replace(/INNER_WHERE/gi, "WHERE"); return [where, params]; diff --git a/packages/graphql/src/translate/where/create-relationship-where-and-params.ts b/packages/graphql/src/translate/where/create-relationship-where-and-params.ts index cf0e811a08..b82303b587 100644 --- a/packages/graphql/src/translate/where/create-relationship-where-and-params.ts +++ b/packages/graphql/src/translate/where/create-relationship-where-and-params.ts @@ -171,7 +171,6 @@ function createRelationshipWhereAndParams({ } const { clauses, params } = Object.entries(whereInput).reduce(reducer, { clauses: [], params: {} }); - // let where = `${!recursing ? "WHERE " : ""}`; const where = clauses.join(" AND ").replace(/INNER_WHERE/gi, "WHERE"); return [where, params]; From fb03f39b34d2b5cbe3f1bd4fd4b0bc97daaa2717 Mon Sep 17 00:00:00 2001 From: Darrell Warde Date: Tue, 15 Jun 2021 13:52:28 +0100 Subject: [PATCH 04/20] Update code comment --- packages/graphql/src/schema/make-augmented-schema.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/graphql/src/schema/make-augmented-schema.ts b/packages/graphql/src/schema/make-augmented-schema.ts index 028e395287..eb46694c54 100644 --- a/packages/graphql/src/schema/make-augmented-schema.ts +++ b/packages/graphql/src/schema/make-augmented-schema.ts @@ -310,7 +310,7 @@ function makeAugmentedSchema( composer.createInputTC({ name: `${relationship.name.value}CreateInput`, - // TODO - Duplicated with node properties on line 492 + // TODO - This reduce duplicated when creating node CreateInput - put into shared function? fields: relationshipFieldMeta.reduce((res, f) => { if ((f as PrimitiveField)?.autogenerate) { return res; @@ -489,7 +489,7 @@ function makeAugmentedSchema( const nodeInput = composer.createInputTC({ name: `${node.name}CreateInput`, - // TODO - Duplicated with relationship properties on line 313 + // TODO - This reduce duplicated when creating relationship CreateInput - put into shared function? fields: [ ...node.primitiveFields, ...node.scalarFields, From 25689029f9ef6be7a57e9f78af1384643723c270 Mon Sep 17 00:00:00 2001 From: Darrell Warde Date: Wed, 16 Jun 2021 11:05:49 +0100 Subject: [PATCH 05/20] Fix TCK tests broken from merge --- .../tests/tck/tck-test-files/schema/connections/enums.md | 4 ++-- .../tests/tck/tck-test-files/schema/connections/unions.md | 6 +++--- .../tck/tck-test-files/schema/relationship-properties.md | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/graphql/tests/tck/tck-test-files/schema/connections/enums.md b/packages/graphql/tests/tck/tck-test-files/schema/connections/enums.md index 9a84ad55aa..cb314595be 100644 --- a/packages/graphql/tests/tck/tck-test-files/schema/connections/enums.md +++ b/packages/graphql/tests/tck/tck-test-files/schema/connections/enums.md @@ -361,8 +361,8 @@ type Mutation { } type Query { - actors(where: ActorWhere, options: ActorOptions): [Actor]! - movies(where: MovieWhere, options: MovieOptions): [Movie]! + actors(where: ActorWhere, options: ActorOptions): [Actor!]! + movies(where: MovieWhere, options: MovieOptions): [Movie!]! } enum SortDirection { diff --git a/packages/graphql/tests/tck/tck-test-files/schema/connections/unions.md b/packages/graphql/tests/tck/tck-test-files/schema/connections/unions.md index 4c96799666..0fdd6b9884 100644 --- a/packages/graphql/tests/tck/tck-test-files/schema/connections/unions.md +++ b/packages/graphql/tests/tck/tck-test-files/schema/connections/unions.md @@ -495,9 +495,9 @@ type Mutation { union Publication = Book | Journal type Query { - authors(where: AuthorWhere, options: AuthorOptions): [Author]! - books(where: BookWhere, options: BookOptions): [Book]! - journals(where: JournalWhere, options: JournalOptions): [Journal]! + authors(where: AuthorWhere, options: AuthorOptions): [Author!]! + books(where: BookWhere, options: BookOptions): [Book!]! + journals(where: JournalWhere, options: JournalOptions): [Journal!]! } input QueryOptions { diff --git a/packages/graphql/tests/tck/tck-test-files/schema/relationship-properties.md b/packages/graphql/tests/tck/tck-test-files/schema/relationship-properties.md index cad6f82ca3..148b7fe848 100644 --- a/packages/graphql/tests/tck/tck-test-files/schema/relationship-properties.md +++ b/packages/graphql/tests/tck/tck-test-files/schema/relationship-properties.md @@ -335,8 +335,8 @@ type Mutation { } type Query { - actors(where: ActorWhere, options: ActorOptions): [Actor]! - movies(where: MovieWhere, options: MovieOptions): [Movie]! + actors(where: ActorWhere, options: ActorOptions): [Actor!]! + movies(where: MovieWhere, options: MovieOptions): [Movie!]! } enum SortDirection { From 5965c7554b8a06eb591c40191d8917864986abf5 Mon Sep 17 00:00:00 2001 From: gaspard Date: Wed, 16 Jun 2021 17:17:27 +0200 Subject: [PATCH 06/20] fix(jwt): req.cookies might be undefined this fix prevents the app from crashing id req.cookies is undefined --- packages/graphql/src/auth/get-jwt.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/graphql/src/auth/get-jwt.ts b/packages/graphql/src/auth/get-jwt.ts index 6d6de9558f..721aa4f3b5 100644 --- a/packages/graphql/src/auth/get-jwt.ts +++ b/packages/graphql/src/auth/get-jwt.ts @@ -49,7 +49,7 @@ function getJWT(context: Context): any { return result; } - const authorization = (req.headers.authorization || req.headers.Authorization || req.cookies.token) as string; + const authorization = (req.headers.authorization || req.headers.Authorization || req.cookies?.token) as string; if (!authorization) { debug("Could not get .authorization, .Authorization or .cookies.token from req"); From b479c856b23b32d9f9d71f1a99162226a90ce2ff Mon Sep 17 00:00:00 2001 From: Darrell Warde Date: Fri, 18 Jun 2021 11:03:13 +0100 Subject: [PATCH 07/20] Add scalars earlier in schema augmentation for use in types and interfaces without throwing Error (fixes DateTime relationship properties) --- packages/graphql/src/schema/make-augmented-schema.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/graphql/src/schema/make-augmented-schema.ts b/packages/graphql/src/schema/make-augmented-schema.ts index 29462e6917..d0912f72a5 100644 --- a/packages/graphql/src/schema/make-augmented-schema.ts +++ b/packages/graphql/src/schema/make-augmented-schema.ts @@ -155,6 +155,8 @@ function makeAugmentedSchema( composer.addTypeDefs(print({ kind: "Document", definitions: extraDefinitions })); } + Object.keys(Scalars).forEach((scalar) => composer.addTypeDefs(`scalar ${scalar}`)); + const nodes = objectNodes.map((definition) => { if (definition.name.value === "PageInfo") { throw new Error( @@ -1054,8 +1056,6 @@ function makeAugmentedSchema( }); }); - Object.keys(Scalars).forEach((scalar) => composer.addTypeDefs(`scalar ${scalar}`)); - if (!Object.values(composer.Mutation.getFields()).length) { composer.delete("Mutation"); } From ebc95e86c36bea5cba89d1903f48c1a2a8931075 Mon Sep 17 00:00:00 2001 From: Darrell Warde Date: Fri, 18 Jun 2021 11:46:07 +0100 Subject: [PATCH 08/20] Changes to accomodate merge from master --- .../src/schema/make-augmented-schema.ts | 24 +++++++++++++++---- .../connect.int.test.ts | 12 +++++----- .../create.int.test.ts | 16 ++++++------- 3 files changed, 33 insertions(+), 19 deletions(-) diff --git a/packages/graphql/src/schema/make-augmented-schema.ts b/packages/graphql/src/schema/make-augmented-schema.ts index 8bbcc31516..974ea92abf 100644 --- a/packages/graphql/src/schema/make-augmented-schema.ts +++ b/packages/graphql/src/schema/make-augmented-schema.ts @@ -39,8 +39,10 @@ import { InputTypeComposer, ObjectTypeComposer, InputTypeComposerFieldConfigAsObjectDefinition, + ObjectTypeComposerFieldConfigAsObjectDefinition, } from "graphql-compose"; import pluralize from "pluralize"; +import { Integer, isInt } from "neo4j-driver"; import { Node, Exclude } from "../classes"; import getAuth from "./get-auth"; import { PrimitiveField, Auth, CustomEnumField } from "../types"; @@ -257,12 +259,25 @@ function makeAugmentedSchema( ...relationship.fields?.reduce((res, f) => { const typeMeta = getFieldTypeMeta(f); + const newField = { + description: f.description?.value, + type: typeMeta.pretty, + } as ObjectTypeComposerFieldConfigAsObjectDefinition; + + if (["Int", "Float"].includes(typeMeta.name)) { + newField.resolve = (source) => { + // @ts-ignore: outputValue is unknown, and to cast to object would be an antipattern + if (isInt(source[f.name.value])) { + return (source[f.name.value] as Integer).toNumber(); + } + + return source[f.name.value]; + }; + } + return { ...res, - [f.name.value]: { - description: f.description?.value, - type: typeMeta.pretty, - }, + [f.name.value]: newField, }; }, {}), }, @@ -1080,7 +1095,6 @@ function makeAugmentedSchema( } unions.forEach((union) => { - // eslint-disable-next-line no-underscore-dangle if (!generatedResolvers[union.name.value]) { generatedResolvers[union.name.value] = { __resolveType: (root) => root.__resolveType }; } diff --git a/packages/graphql/tests/integration/relationship-properties/connect.int.test.ts b/packages/graphql/tests/integration/relationship-properties/connect.int.test.ts index 266b890108..359e75910d 100644 --- a/packages/graphql/tests/integration/relationship-properties/connect.int.test.ts +++ b/packages/graphql/tests/integration/relationship-properties/connect.int.test.ts @@ -59,7 +59,7 @@ describe("Relationship properties - connect", () => { const session = driver.session(); const movieTitle = generate({ charset: "alphabetic" }); const actorName = generate({ charset: "alphabetic" }); - const screenTime = Math.floor((Math.random() * 1e10) / Math.random()); + const screenTime = Math.floor((Math.random() * 1e3) / Math.random()); const source = ` mutation($movieTitle: String!, $screenTime: Int!, $actorName: String!) { @@ -68,9 +68,9 @@ describe("Relationship properties - connect", () => { { title: $movieTitle actors: { - connect: [{ + connect: [{ where: { name: $actorName }, - properties: { screenTime: $screenTime }, + properties: { screenTime: $screenTime }, }] } } @@ -157,7 +157,7 @@ describe("Relationship properties - connect", () => { const session = driver.session(); const movieTitle = generate({ charset: "alphabetic" }); const actorName = generate({ charset: "alphabetic" }); - const screenTime = Math.floor((Math.random() * 1e10) / Math.random()); + const screenTime = Math.floor((Math.random() * 1e3) / Math.random()); const source = ` mutation($movieTitle: String!, $screenTime: Int!, $actorName: String!) { @@ -240,7 +240,7 @@ describe("Relationship properties - connect", () => { const session = driver.session(); const movieTitle = generate({ charset: "alphabetic" }); const actorName = generate({ charset: "alphabetic" }); - const screenTime = Math.floor((Math.random() * 1e10) / Math.random()); + const screenTime = Math.floor((Math.random() * 1e3) / Math.random()); const source = ` mutation($movieTitle: String!, $screenTime: Int!, $actorName: String!) { @@ -335,7 +335,7 @@ describe("Relationship properties - connect", () => { const session = driver.session(); const movieTitle = generate({ charset: "alphabetic" }); const actorName = generate({ charset: "alphabetic" }); - const screenTime = Math.floor((Math.random() * 1e10) / Math.random()); + const screenTime = Math.floor((Math.random() * 1e3) / Math.random()); const source = ` mutation($movieTitle: String!, $screenTime: Int!, $actorName: String!) { diff --git a/packages/graphql/tests/integration/relationship-properties/create.int.test.ts b/packages/graphql/tests/integration/relationship-properties/create.int.test.ts index 8a346ffd23..a289766da1 100644 --- a/packages/graphql/tests/integration/relationship-properties/create.int.test.ts +++ b/packages/graphql/tests/integration/relationship-properties/create.int.test.ts @@ -59,7 +59,7 @@ describe("Relationship properties - create", () => { const session = driver.session(); const movieTitle = generate({ charset: "alphabetic" }); const actorName = generate({ charset: "alphabetic" }); - const screenTime = Math.floor((Math.random() * 1e10) / Math.random()); + const screenTime = Math.floor((Math.random() * 1e3) / Math.random()); const source = ` mutation($movieTitle: String!, $screenTime: Int!, $actorName: String!) { @@ -68,9 +68,9 @@ describe("Relationship properties - create", () => { { title: $movieTitle actors: { - create: [{ - properties: { screenTime: $screenTime }, - node: { name: $actorName } + create: [{ + properties: { screenTime: $screenTime }, + node: { name: $actorName } }] } } @@ -145,7 +145,7 @@ describe("Relationship properties - create", () => { const session = driver.session(); const movieTitle = generate({ charset: "alphabetic" }); const actorName = generate({ charset: "alphabetic" }); - const words = Math.floor((Math.random() * 1e10) / Math.random()); + const words = Math.floor((Math.random() * 1e3) / Math.random()); const source = ` mutation($actorName: String!, $words: Int!, $movieTitle: String!) { @@ -154,9 +154,9 @@ describe("Relationship properties - create", () => { { name: $actorName publications_Movie: { - create: [{ - properties: { words: $words }, - node: { title: $movieTitle } + create: [{ + properties: { words: $words }, + node: { title: $movieTitle } }] } } From 6d1853dc29a62cd01ea63d7f232e2d8e498b812a Mon Sep 17 00:00:00 2001 From: Daniel Starns Date: Tue, 22 Jun 2021 08:50:34 +0100 Subject: [PATCH 09/20] fix: use package json for useragent name and version (#271) * fix: use package json for useragent name and version * fix: add userAgent support for <=4.2 and >=4.3 drivers --- .gitignore | 2 ++ packages/graphql/src/environment.ts | 6 ++++-- packages/graphql/src/tsconfig.build.json | 5 ++++- packages/graphql/src/tsconfig.json | 5 ++++- packages/graphql/src/utils/execute.test.ts | 9 +++++++++ packages/graphql/src/utils/execute.ts | 13 ++++++++++--- packages/graphql/tests/tsconfig.json | 3 ++- packages/graphql/tsconfig.json | 9 +++++++++ tsconfig.base.json | 3 ++- 9 files changed, 46 insertions(+), 9 deletions(-) create mode 100644 packages/graphql/tsconfig.json diff --git a/.gitignore b/.gitignore index c042c0813d..910ba88d66 100644 --- a/.gitignore +++ b/.gitignore @@ -89,3 +89,5 @@ packages/package-tests/**/package-lock.json !.yarn/sdks !.yarn/versions .pnp.* + +tsconfig.tsbuildinfo \ No newline at end of file diff --git a/packages/graphql/src/environment.ts b/packages/graphql/src/environment.ts index d8794aea79..4ce0a636b3 100644 --- a/packages/graphql/src/environment.ts +++ b/packages/graphql/src/environment.ts @@ -17,9 +17,11 @@ * limitations under the License. */ +import * as pack from "../package.json"; + const environment = { - NPM_PACKAGE_VERSION: process.env.NPM_PACKAGE_VERSION as string, - NPM_PACKAGE_NAME: process.env.NPM_PACKAGE_NAME as string, + NPM_PACKAGE_VERSION: pack.version, + NPM_PACKAGE_NAME: pack.name, }; export default environment; diff --git a/packages/graphql/src/tsconfig.build.json b/packages/graphql/src/tsconfig.build.json index fd1ffc9e58..77dcf7d6c7 100644 --- a/packages/graphql/src/tsconfig.build.json +++ b/packages/graphql/src/tsconfig.build.json @@ -3,5 +3,8 @@ "compilerOptions": { "outDir": "../dist" }, - "exclude": ["**/*.test.ts"] + "exclude": ["**/*.test.ts"], + "references": [ + { "path": "../" } // for package.json import + ] } diff --git a/packages/graphql/src/tsconfig.json b/packages/graphql/src/tsconfig.json index cefd165b28..da081ea0e6 100644 --- a/packages/graphql/src/tsconfig.json +++ b/packages/graphql/src/tsconfig.json @@ -2,5 +2,8 @@ "extends": "../../../tsconfig.base.json", "compilerOptions": { "outDir": "../dist" - } + }, + "references": [ + { "path": "../" } // for package.json import + ] } diff --git a/packages/graphql/src/utils/execute.test.ts b/packages/graphql/src/utils/execute.test.ts index 64d64e9660..66b2ac3cff 100644 --- a/packages/graphql/src/utils/execute.test.ts +++ b/packages/graphql/src/utils/execute.test.ts @@ -21,6 +21,7 @@ import { Driver } from "neo4j-driver"; import { Neo4jGraphQL } from "../classes"; import { Context } from "../types"; import execute from "./execute"; +import environment from "../environment"; describe("execute", () => { test("should execute return records.toObject", async () => { @@ -66,6 +67,8 @@ describe("execute", () => { close: () => true, }; }, + // @ts-ignore + _config: {}, }; // @ts-ignore @@ -82,6 +85,12 @@ describe("execute", () => { }); expect(result).toEqual([{ title }]); + // @ts-ignore + expect(driver._userAgent).toEqual(`${environment.NPM_PACKAGE_NAME}/${environment.NPM_PACKAGE_VERSION}`); + // @ts-ignore + expect(driver._config.userAgent).toEqual( + `${environment.NPM_PACKAGE_NAME}/${environment.NPM_PACKAGE_VERSION}` + ); }) ); }); diff --git a/packages/graphql/src/utils/execute.ts b/packages/graphql/src/utils/execute.ts index f69c2eba63..bde33eeb6c 100644 --- a/packages/graphql/src/utils/execute.ts +++ b/packages/graphql/src/utils/execute.ts @@ -52,8 +52,16 @@ async function execute(input: { } } - // @ts-ignore: Required to set connection user agent - input.context.driver._userAgent = `${environment.NPM_PACKAGE_VERSION}/${environment.NPM_PACKAGE_NAME}`; // eslint-disable-line no-underscore-dangle + const userAgent = `${environment.NPM_PACKAGE_NAME}/${environment.NPM_PACKAGE_VERSION}`; + + // @ts-ignore: below + if (input.context.driver?._config) { + // @ts-ignore: (driver >= 4.3) + input.context.driver._config.userAgent = userAgent; // eslint-disable-line no-underscore-dangle + } + + // @ts-ignore: (driver <= 4.2) + input.context.driver._userAgent = userAgent; // eslint-disable-line no-underscore-dangle const session = input.context.driver.session(sessionParams); @@ -67,7 +75,6 @@ async function execute(input: { } try { - // input.context.neoSchema.debug(`Cypher: ${input.cypher}\nParams: ${JSON.stringify(input.params, null, 2)}`); debug( "%s", `About to execute Cypher:\nCypher:\n${input.cypher}\nParams:\n${JSON.stringify(input.params, null, 2)}` diff --git a/packages/graphql/tests/tsconfig.json b/packages/graphql/tests/tsconfig.json index 7f6af9d66e..9f31a4ae81 100644 --- a/packages/graphql/tests/tsconfig.json +++ b/packages/graphql/tests/tsconfig.json @@ -1,7 +1,8 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "types": ["node", "jest"] + "types": ["node", "jest"], + "resolveJsonModule": true }, "references": [{ "path": "../src/tsconfig.json" }] } diff --git a/packages/graphql/tsconfig.json b/packages/graphql/tsconfig.json new file mode 100644 index 0000000000..8ba1689616 --- /dev/null +++ b/packages/graphql/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": ".", + "outDir": ".", + "composite": true + }, + "files": ["package.json"] +} diff --git a/tsconfig.base.json b/tsconfig.base.json index 3416d45b57..423b79e32f 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -10,6 +10,7 @@ "strict": true, "target": "es6", // TODO Refactor so that noImplicitAny is no longer needed - "noImplicitAny": false + "noImplicitAny": false, + "resolveJsonModule": true } } From 689c7788198e8787476c877a90c796a3d6d5640f Mon Sep 17 00:00:00 2001 From: Daniel Starns Date: Tue, 22 Jun 2021 08:50:47 +0100 Subject: [PATCH 10/20] config: remove codeowners (#277) --- .github/CODEOWNERS | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index 301c5f3494..0000000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1 +0,0 @@ -* @danstarns @darrellwarde From 283b179f36d373f82ccfa26c6a80a5d53f0133a2 Mon Sep 17 00:00:00 2001 From: Neo Technology Build Agent Date: Tue, 22 Jun 2021 08:53:21 +0000 Subject: [PATCH 11/20] Version update --- examples/migration/package.json | 2 +- examples/neo-push/server/package.json | 4 ++-- packages/graphql/package.json | 2 +- packages/ogm/package.json | 4 ++-- yarn.lock | 12 ++++++------ 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/examples/migration/package.json b/examples/migration/package.json index b5d9b9a2a6..873bee6299 100644 --- a/examples/migration/package.json +++ b/examples/migration/package.json @@ -4,7 +4,7 @@ "start": "node src/index.js" }, "dependencies": { - "@neo4j/graphql": "^1.0.2", + "@neo4j/graphql": "^1.0.3", "apollo-server": "^2.23.0", "graphql": "^15.0.0", "neo4j-driver": "^4.2.0" diff --git a/examples/neo-push/server/package.json b/examples/neo-push/server/package.json index 82fb390070..144aac7613 100644 --- a/examples/neo-push/server/package.json +++ b/examples/neo-push/server/package.json @@ -12,8 +12,8 @@ "author": "", "license": "ISC", "dependencies": { - "@neo4j/graphql": "^1.0.2", - "@neo4j/graphql-ogm": "^1.0.2", + "@neo4j/graphql": "^1.0.3", + "@neo4j/graphql-ogm": "^1.0.3", "apollo-server-express": "2.19.0", "bcrypt": "5.0.1", "debug": "4.3.1", diff --git a/packages/graphql/package.json b/packages/graphql/package.json index 2d36b2d1c2..3646883832 100644 --- a/packages/graphql/package.json +++ b/packages/graphql/package.json @@ -1,6 +1,6 @@ { "name": "@neo4j/graphql", - "version": "1.0.2", + "version": "1.0.3", "description": "A GraphQL to Cypher query execution layer for Neo4j and JavaScript GraphQL implementations", "keywords": [ "neo4j", diff --git a/packages/ogm/package.json b/packages/ogm/package.json index 73fcb6e626..4d67f979c5 100644 --- a/packages/ogm/package.json +++ b/packages/ogm/package.json @@ -1,6 +1,6 @@ { "name": "@neo4j/graphql-ogm", - "version": "1.0.2", + "version": "1.0.3", "description": "GraphQL powered OGM for Neo4j and Javascript applications", "keywords": [ "neo4j", @@ -27,7 +27,7 @@ "author": "Neo4j Inc.", "dependencies": { "@graphql-tools/merge": "^6.2.13", - "@neo4j/graphql": "^1.0.2", + "@neo4j/graphql": "^1.0.3", "camelcase": "^6.2.0", "pluralize": "^8.0.0" }, diff --git a/yarn.lock b/yarn.lock index 5925e6b9ba..ac9bacb471 100644 --- a/yarn.lock +++ b/yarn.lock @@ -899,12 +899,12 @@ __metadata: languageName: node linkType: hard -"@neo4j/graphql-ogm@^1.0.2, @neo4j/graphql-ogm@workspace:packages/ogm": +"@neo4j/graphql-ogm@^1.0.3, @neo4j/graphql-ogm@workspace:packages/ogm": version: 0.0.0-use.local resolution: "@neo4j/graphql-ogm@workspace:packages/ogm" dependencies: "@graphql-tools/merge": ^6.2.13 - "@neo4j/graphql": ^1.0.2 + "@neo4j/graphql": ^1.0.3 "@types/jest": 26.0.8 "@types/node": 14.0.27 camelcase: ^6.2.0 @@ -923,7 +923,7 @@ __metadata: languageName: unknown linkType: soft -"@neo4j/graphql@^1.0.2, @neo4j/graphql@workspace:packages/graphql": +"@neo4j/graphql@^1.0.3, @neo4j/graphql@workspace:packages/graphql": version: 0.0.0-use.local resolution: "@neo4j/graphql@workspace:packages/graphql" dependencies: @@ -9306,7 +9306,7 @@ fsevents@^1.2.7: version: 0.0.0-use.local resolution: "migration@workspace:examples/migration" dependencies: - "@neo4j/graphql": ^1.0.2 + "@neo4j/graphql": ^1.0.3 apollo-server: ^2.23.0 graphql: ^15.0.0 neo4j-driver: ^4.2.0 @@ -9657,8 +9657,8 @@ fsevents@^1.2.7: version: 0.0.0-use.local resolution: "neo-push-server@workspace:examples/neo-push/server" dependencies: - "@neo4j/graphql": ^1.0.2 - "@neo4j/graphql-ogm": ^1.0.2 + "@neo4j/graphql": ^1.0.3 + "@neo4j/graphql-ogm": ^1.0.3 "@types/bcrypt": 3.0.0 "@types/debug": 4.1.5 "@types/dotenv": 8.2.0 From 00871c8a3a7dd5655f9469c1ea5475d6eee74c55 Mon Sep 17 00:00:00 2001 From: Gaspard Beernaert Date: Mon, 28 Jun 2021 11:42:29 +0200 Subject: [PATCH 12/20] fix(login): avoid confusion caused by secondary button (#265) --- examples/neo-push/client/src/components/SignIn.tsx | 10 +++++++--- examples/neo-push/client/src/components/SignUp.tsx | 10 +++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/examples/neo-push/client/src/components/SignIn.tsx b/examples/neo-push/client/src/components/SignIn.tsx index 2a9c0cecde..4f30a33bb2 100644 --- a/examples/neo-push/client/src/components/SignIn.tsx +++ b/examples/neo-push/client/src/components/SignIn.tsx @@ -74,9 +74,6 @@ function SignIn() { onChange={(e) => setPassword(e.target.value)} /> - @@ -90,6 +87,13 @@ function SignIn() { {error} )} +


+

+ Go to{" "} + history.push(constants.SIGN_UP_PAGE)}> + Sign Up + instead +

diff --git a/examples/neo-push/client/src/components/SignUp.tsx b/examples/neo-push/client/src/components/SignUp.tsx index bf43cb6d7f..ed7c49ac58 100644 --- a/examples/neo-push/client/src/components/SignUp.tsx +++ b/examples/neo-push/client/src/components/SignUp.tsx @@ -87,9 +87,6 @@ function SignUp() { onChange={(e) => setPasswordConfirm(e.target.value)} /> - @@ -103,6 +100,13 @@ function SignUp() { {error} )} +
+

+ Go to{" "} + history.push(constants.SIGN_IN_PAGE)}> + Sign In + instead +

From ba9ee0268dc68aa24ec53323af4e3716039534c3 Mon Sep 17 00:00:00 2001 From: Arnaud Gissinger <37625778+mathix420@users.noreply.github.com> Date: Wed, 30 Jun 2021 15:28:41 +0200 Subject: [PATCH 13/20] fix: losing params while creating Auth Predicate (#281) * fix: loosing params while creating Auth Predicate * fix: typos * fix: typo --- .../translate/create-auth-and-params.test.ts | 142 ++++++++++++++++++ .../src/translate/create-auth-and-params.ts | 6 +- 2 files changed, 147 insertions(+), 1 deletion(-) 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 a14b78847a..24213fd6d1 100644 --- a/packages/graphql/src/translate/create-auth-and-params.test.ts +++ b/packages/graphql/src/translate/create-auth-and-params.test.ts @@ -548,4 +548,146 @@ describe("createAuthAndParams", () => { }); }); }); + + describe("params", () => { + test("should convert undefined $jwt parameters to null", () => { + 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" } }, + { 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(result[1]).toMatchObject({ + this_auth_allow0_id: null, + }); + }); + + test("should convert undefined $context parameters to null", () => { + 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" } }, + { 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(result[1]).toMatchObject({ + this_auth_allow0_id: null, + }); + }); + }); }); diff --git a/packages/graphql/src/translate/create-auth-and-params.ts b/packages/graphql/src/translate/create-auth-and-params.ts index c57da6550f..5b028cb574 100644 --- a/packages/graphql/src/translate/create-auth-and-params.ts +++ b/packages/graphql/src/translate/create-auth-and-params.ts @@ -93,13 +93,17 @@ function createAuthPredicate({ if (authableField) { const [, jwtPath] = (value as string).split("$jwt."); const [, ctxPath] = (value as string).split("$context."); - let paramValue: string = value as string; + let paramValue: string | null = value as string; if (jwtPath) { paramValue = dotProp.get({ value: jwt }, `value.${jwtPath}`) as string; } else if (ctxPath) { paramValue = dotProp.get({ value: context }, `value.${ctxPath}`) as string; } + // To avoid losing params on query execution + if ((jwtPath || ctxPath) && paramValue === undefined) { + paramValue = null; + } const param = `${chainStr}_${key}`; res.params[param] = paramValue; From 6fdaedabe4fd5935ba8dc53a5029a8742de6e61c Mon Sep 17 00:00:00 2001 From: Daniel Starns Date: Wed, 30 Jun 2021 14:47:17 +0100 Subject: [PATCH 14/20] feat: add projection to top level cypher directive (#251) * feat: add projection to top level queries and mutations using cypher directive * fix: add missing cypherParams --- packages/graphql/src/constants.ts | 3 +- .../src/schema/make-augmented-schema.ts | 1 + .../src/schema/resolvers/cypher.test.ts | 2 +- .../graphql/src/schema/resolvers/cypher.ts | 71 ++- .../integration/custom-resolvers-int.test.ts | 8 +- .../tests/integration/cypher.int.test.ts | 547 ++++++++++++++++++ 6 files changed, 623 insertions(+), 9 deletions(-) create mode 100644 packages/graphql/tests/integration/cypher.int.test.ts diff --git a/packages/graphql/src/constants.ts b/packages/graphql/src/constants.ts index 149c0c57be..69c41560d5 100644 --- a/packages/graphql/src/constants.ts +++ b/packages/graphql/src/constants.ts @@ -28,8 +28,9 @@ export const REQUIRED_APOC_FUNCTIONS = [ "apoc.cypher.runFirstColumn", "apoc.coll.sortMulti", "apoc.date.convertFormat", + "apoc.map.values", ]; -export const REQUIRED_APOC_PROCEDURES = ["apoc.util.validate", "apoc.do.when"]; +export const REQUIRED_APOC_PROCEDURES = ["apoc.util.validate", "apoc.do.when", "apoc.cypher.doIt"]; export const DEBUG_AUTH = `${DEBUG_PREFIX}:auth`; export const DEBUG_GRAPHQL = `${DEBUG_PREFIX}:graphql`; export const DEBUG_EXECUTE = `${DEBUG_PREFIX}:execute`; diff --git a/packages/graphql/src/schema/make-augmented-schema.ts b/packages/graphql/src/schema/make-augmented-schema.ts index d05841243c..1dd953cd0f 100644 --- a/packages/graphql/src/schema/make-augmented-schema.ts +++ b/packages/graphql/src/schema/make-augmented-schema.ts @@ -676,6 +676,7 @@ function makeAugmentedSchema( const customResolver = cypherResolver({ field, statement: field.statement, + type: type as "Query" | "Mutation", }); const composedField = objectFieldsToComposeFields([field])[field.fieldName]; diff --git a/packages/graphql/src/schema/resolvers/cypher.test.ts b/packages/graphql/src/schema/resolvers/cypher.test.ts index 88315088f6..ed6d13a594 100644 --- a/packages/graphql/src/schema/resolvers/cypher.test.ts +++ b/packages/graphql/src/schema/resolvers/cypher.test.ts @@ -29,7 +29,7 @@ describe("Cypher resolver", () => { arguments: [], }; - const result = cypherResolver({ field, statement: "" }); + const result = cypherResolver({ field, statement: "", type: "Query" }); expect(result.type).toEqual(field.typeMeta.pretty); expect(result.resolve).toBeInstanceOf(Function); expect(result.args).toMatchObject({}); diff --git a/packages/graphql/src/schema/resolvers/cypher.ts b/packages/graphql/src/schema/resolvers/cypher.ts index 2f30b1799d..6d387fcd0b 100644 --- a/packages/graphql/src/schema/resolvers/cypher.ts +++ b/packages/graphql/src/schema/resolvers/cypher.ts @@ -24,12 +24,29 @@ import { graphqlArgsToCompose } from "../to-compose"; import createAuthAndParams from "../../translate/create-auth-and-params"; import createAuthParam from "../../translate/create-auth-param"; import { AUTH_FORBIDDEN_ERROR } from "../../constants"; +import createProjectionAndParams from "../../translate/create-projection-and-params"; -export default function cypherResolver({ field, statement }: { field: BaseField; statement: string }) { +export default function cypherResolver({ + field, + statement, + type, +}: { + field: BaseField; + statement: string; + type: "Query" | "Mutation"; +}) { async function resolve(_root: any, args: any, _context: unknown) { const context = _context as Context; + const { + resolveTree: { fieldsByTypeName }, + } = context; const cypherStrs: string[] = []; let params = { ...args, auth: createAuthParam({ context }), cypherParams: context.cypherParams }; + let projectionStr = ""; + let projectionAuthStr = ""; + const isPrimitive = ["ID", "String", "Boolean", "Float", "Int", "DateTime", "BigInt"].includes( + field.typeMeta.name + ); const preAuth = createAuthAndParams({ entity: field, context }); if (preAuth[0]) { @@ -37,7 +54,57 @@ export default function cypherResolver({ field, statement }: { field: BaseField; cypherStrs.push(`CALL apoc.util.validate(NOT(${preAuth[0]}), "${AUTH_FORBIDDEN_ERROR}", [0])`); } - cypherStrs.push(statement); + const referenceNode = context.neoSchema.nodes.find((x) => x.name === field.typeMeta.name); + if (referenceNode) { + const recurse = createProjectionAndParams({ + fieldsByTypeName, + node: referenceNode, + context, + varName: `this`, + }); + [projectionStr] = recurse; + params = { ...params, ...recurse[1] }; + if (recurse[2]?.authValidateStrs?.length) { + projectionAuthStr = recurse[2].authValidateStrs.join(" AND "); + } + } + + const initApocParamsStrs = ["auth: $auth", ...(context.cypherParams ? ["cypherParams: $cypherParams"] : [])]; + const apocParams = Object.entries(args).reduce( + (r: { strs: string[]; params: any }, entry) => { + return { + strs: [...r.strs, `${entry[0]}: $${entry[0]}`], + params: { ...r.params, [entry[0]]: entry[1] }, + }; + }, + { strs: initApocParamsStrs, params } + ) as { strs: string[]; params: any }; + const apocParamsStr = `{${apocParams.strs.length ? `${apocParams.strs.join(", ")}` : ""}}`; + + const expectMultipleValues = referenceNode && field.typeMeta.array ? "true" : "false"; + if (type === "Query") { + cypherStrs.push(` + WITH apoc.cypher.runFirstColumn("${statement}", ${apocParamsStr}, ${expectMultipleValues}) as x + UNWIND x as this + `); + } else { + cypherStrs.push(` + CALL apoc.cypher.doIt("${statement}", ${apocParamsStr}) YIELD value + WITH apoc.map.values(value, [keys(value)[0]])[0] AS this + `); + } + + if (projectionAuthStr) { + cypherStrs.push( + `WHERE apoc.util.validatePredicate(NOT(${projectionAuthStr}), "${AUTH_FORBIDDEN_ERROR}", [0])` + ); + } + + if (!isPrimitive) { + cypherStrs.push(`RETURN this ${projectionStr} AS this`); + } else { + cypherStrs.push(`RETURN this`); + } const result = await execute({ cypher: cypherStrs.join("\n"), diff --git a/packages/graphql/tests/integration/custom-resolvers-int.test.ts b/packages/graphql/tests/integration/custom-resolvers-int.test.ts index 3236c2f0c7..a75aeaf300 100644 --- a/packages/graphql/tests/integration/custom-resolvers-int.test.ts +++ b/packages/graphql/tests/integration/custom-resolvers-int.test.ts @@ -17,7 +17,6 @@ * limitations under the License. */ -/* eslint-disable no-useless-escape */ import { Driver } from "neo4j-driver"; import { graphql, createSourceEventStream, parse } from "graphql"; import { generate } from "randomstring"; @@ -236,7 +235,7 @@ describe("Custom Resolvers", () => { typeDefs = ` type Query { test: ${type}! @cypher(statement: """ - RETURN \"${id}\" + RETURN \\"${id}\\" """) } `; @@ -304,7 +303,7 @@ describe("Custom Resolvers", () => { type Query { test: Test! @cypher(statement: """ - RETURN {id: \"${id}\"} + RETURN {id: \\"${id}\\"} """) } `; @@ -404,7 +403,7 @@ describe("Custom Resolvers", () => { type Mutation { test(id: ID!): ID! @cypher(statement: """ - RETURN \"${id}\" + $id + RETURN \\"${id}\\" + $id """) } `; @@ -684,4 +683,3 @@ describe("Custom Resolvers", () => { }); }); }); -/* eslint-enable */ diff --git a/packages/graphql/tests/integration/cypher.int.test.ts b/packages/graphql/tests/integration/cypher.int.test.ts new file mode 100644 index 0000000000..a42454f0bd --- /dev/null +++ b/packages/graphql/tests/integration/cypher.int.test.ts @@ -0,0 +1,547 @@ +/* + * 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 jsonwebtoken from "jsonwebtoken"; +import { Driver } from "neo4j-driver"; +import { graphql } from "graphql"; +import { Socket } from "net"; +import { IncomingMessage } from "http"; +import { generate } from "randomstring"; +import neo4j from "./neo4j"; +import { Neo4jGraphQL } from "../../src/classes"; + +describe("cypher", () => { + let driver: Driver; + + beforeAll(async () => { + driver = await neo4j(); + }); + + afterAll(async () => { + await driver.close(); + }); + + describe("Top level cypher", () => { + describe("Query", () => { + test("should query custom query and return relationship data", async () => { + const session = driver.session(); + + const movieTitle = generate({ + charset: "alphabetic", + }); + const actorName = generate({ + charset: "alphabetic", + }); + + const typeDefs = ` + type Movie { + title: String! + actors: [Actor] @relationship(type: "ACTED_IN", direction: IN) + } + + type Actor { + name: String! + movies: [Movie] @relationship(type: "ACTED_IN", direction: OUT) + } + + type Query { + customMovies(title: String!): [Movie] @cypher(statement: """ + MATCH (m:Movie {title: $title}) + RETURN m + """) + } + `; + + const neoSchema = new Neo4jGraphQL({ typeDefs }); + + const source = ` + query($title: String!) { + customMovies(title: $title) { + title + actors { + name + } + } + } + `; + + try { + await session.run( + ` + CREATE (:Movie {title: $title})<-[:ACTED_IN]-(:Actor {name: $name}) + `, + { + title: movieTitle, + name: actorName, + } + ); + + const gqlResult = await graphql({ + schema: neoSchema.schema, + source, + contextValue: { driver }, + variableValues: { title: movieTitle }, + }); + + expect(gqlResult.errors).toBeFalsy(); + + expect((gqlResult?.data as any).customMovies).toEqual([ + { title: movieTitle, actors: [{ name: actorName }] }, + ]); + } finally { + await session.close(); + } + }); + + test("should query custom query and return relationship data with custom where on field", async () => { + const session = driver.session(); + + const movieTitle = generate({ + charset: "alphabetic", + }); + const actorName = generate({ + charset: "alphabetic", + }); + + const typeDefs = ` + type Movie { + title: String! + actors: [Actor] @relationship(type: "ACTED_IN", direction: IN) + } + + type Actor { + name: String! + movies: [Movie] @relationship(type: "ACTED_IN", direction: OUT) + } + + type Query { + customMovies(title: String!): [Movie] @cypher(statement: """ + MATCH (m:Movie {title: $title}) + RETURN m + """) + } + `; + + const neoSchema = new Neo4jGraphQL({ typeDefs }); + + const source = ` + query($title: String!, $name: String) { + customMovies(title: $title) { + title + actors(where: {name: $name}) { + name + } + } + } + `; + + try { + await session.run( + ` + CREATE (:Movie {title: $title})<-[:ACTED_IN]-(:Actor {name: $name}) + `, + { + title: movieTitle, + name: actorName, + } + ); + + const gqlResult = await graphql({ + schema: neoSchema.schema, + source, + contextValue: { driver }, + variableValues: { title: movieTitle, name: actorName }, + }); + + expect(gqlResult.errors).toBeFalsy(); + + expect((gqlResult?.data as any).customMovies).toEqual([ + { title: movieTitle, actors: [{ name: actorName }] }, + ]); + } finally { + await session.close(); + } + }); + + test("should query custom query and return relationship data with auth", async () => { + const session = driver.session(); + + const movieTitle = generate({ + charset: "alphabetic", + }); + const actorName = generate({ + charset: "alphabetic", + }); + + const typeDefs = ` + type Movie { + title: String! + actors: [Actor] @relationship(type: "ACTED_IN", direction: IN) + } + + type Actor @auth(rules: [{operations: [READ], roles: ["admin"]}]) { + name: String! + movies: [Movie] @relationship(type: "ACTED_IN", direction: OUT) + } + + type Query { + customMovies(title: String!): [Movie] @cypher(statement: """ + MATCH (m:Movie {title: $title}) + RETURN m + """) + } + `; + + const secret = "secret"; + + const token = jsonwebtoken.sign( + { + roles: [], + }, + secret + ); + + const neoSchema = new Neo4jGraphQL({ typeDefs, config: { jwt: { secret } } }); + + const source = ` + query($title: String!, $name: String) { + customMovies(title: $title) { + title + actors(where: {name: $name}) { + name + } + } + } + `; + + try { + await session.run( + ` + CREATE (:Movie {title: $title})<-[:ACTED_IN]-(:Actor {name: $name}) + `, + { + title: movieTitle, + name: actorName, + } + ); + + const socket = new Socket({ readable: true }); + const req = new IncomingMessage(socket); + req.headers.authorization = `Bearer ${token}`; + + const gqlResult = await graphql({ + schema: neoSchema.schema, + source, + contextValue: { driver, req }, + variableValues: { title: movieTitle, name: actorName }, + }); + + expect((gqlResult.errors as any[])[0].message).toEqual("Forbidden"); + } finally { + await session.close(); + } + }); + }); + + describe("Mutation", () => { + test("should query custom mutation and return relationship data", async () => { + const session = driver.session(); + + const movieTitle = generate({ + charset: "alphabetic", + }); + const actorName = generate({ + charset: "alphabetic", + }); + + const typeDefs = ` + type Movie { + title: String! + actors: [Actor] @relationship(type: "ACTED_IN", direction: IN) + } + + type Actor { + name: String! + movies: [Movie] @relationship(type: "ACTED_IN", direction: OUT) + } + + type Mutation { + customMovies(title: String!): [Movie] @cypher(statement: """ + MATCH (m:Movie {title: $title}) + RETURN m + """) + } + `; + + const neoSchema = new Neo4jGraphQL({ typeDefs }); + + const source = ` + mutation($title: String!) { + customMovies(title: $title) { + title + actors { + name + } + } + } + `; + + try { + await session.run( + ` + CREATE (:Movie {title: $title})<-[:ACTED_IN]-(:Actor {name: $name}) + `, + { + title: movieTitle, + name: actorName, + } + ); + + const gqlResult = await graphql({ + schema: neoSchema.schema, + source, + contextValue: { driver }, + variableValues: { title: movieTitle }, + }); + + expect(gqlResult.errors).toBeFalsy(); + + expect((gqlResult?.data as any).customMovies).toEqual([ + { title: movieTitle, actors: [{ name: actorName }] }, + ]); + } finally { + await session.close(); + } + }); + + test("should query custom mutation and return relationship data with custom where on field", async () => { + const session = driver.session(); + + const movieTitle = generate({ + charset: "alphabetic", + }); + const actorName = generate({ + charset: "alphabetic", + }); + + const typeDefs = ` + type Movie { + title: String! + actors: [Actor] @relationship(type: "ACTED_IN", direction: IN) + } + + type Actor { + name: String! + movies: [Movie] @relationship(type: "ACTED_IN", direction: OUT) + } + + type Mutation { + customMovies(title: String!): [Movie] @cypher(statement: """ + MATCH (m:Movie {title: $title}) + RETURN m + """) + } + `; + + const neoSchema = new Neo4jGraphQL({ typeDefs }); + + const source = ` + mutation($title: String!, $name: String) { + customMovies(title: $title) { + title + actors(where: {name: $name}) { + name + } + } + } + `; + + try { + await session.run( + ` + CREATE (:Movie {title: $title})<-[:ACTED_IN]-(:Actor {name: $name}) + `, + { + title: movieTitle, + name: actorName, + } + ); + + const gqlResult = await graphql({ + schema: neoSchema.schema, + source, + contextValue: { driver }, + variableValues: { title: movieTitle }, + }); + + expect(gqlResult.errors).toBeFalsy(); + + expect((gqlResult?.data as any).customMovies).toEqual([ + { title: movieTitle, actors: [{ name: actorName }] }, + ]); + } finally { + await session.close(); + } + }); + + test("should query custom mutation and return relationship data with auth", async () => { + const session = driver.session(); + + const movieTitle = generate({ + charset: "alphabetic", + }); + const actorName = generate({ + charset: "alphabetic", + }); + + const typeDefs = ` + type Movie { + title: String! + actors: [Actor] @relationship(type: "ACTED_IN", direction: IN) + } + + type Actor @auth(rules: [{operations: [READ], roles: ["admin"]}]) { + name: String! + movies: [Movie] @relationship(type: "ACTED_IN", direction: OUT) + } + + type Mutation { + customMovies(title: String!): [Movie] @cypher(statement: """ + MATCH (m:Movie {title: $title}) + RETURN m + """) + } + `; + + const neoSchema = new Neo4jGraphQL({ typeDefs }); + + const source = ` + mutation($title: String!, $name: String) { + customMovies(title: $title) { + title + actors(where: {name: $name}) { + name + } + } + } + `; + + try { + await session.run( + ` + CREATE (:Movie {title: $title})<-[:ACTED_IN]-(:Actor {name: $name}) + `, + { + title: movieTitle, + name: actorName, + } + ); + + const gqlResult = await graphql({ + schema: neoSchema.schema, + source, + contextValue: { driver }, + variableValues: { title: movieTitle }, + }); + + expect((gqlResult.errors as any[])[0].message).toEqual("Forbidden"); + } finally { + await session.close(); + } + }); + }); + + describe("Issues", () => { + // https://github.com/neo4j/graphql/issues/227 + test("227", async () => { + const session = driver.session(); + + const memberId = generate({ + charset: "alphabetic", + }); + const gender = generate({ + charset: "alphabetic", + }); + const townId = generate({ + charset: "alphabetic", + }); + + const typeDefs = ` + type Member { + id: ID! + gender: Gender @relationship(type: "HAS_GENDER", direction: OUT) + } + + type Gender { + gender: String! + } + + type Query { + townMemberList(id: ID!): [Member] @cypher(statement: """ + MATCH (town:Town {id:$id}) + OPTIONAL MATCH (town)<-[:BELONGS_TO]-(member:Member) + RETURN member + """) + } + `; + + const neoSchema = new Neo4jGraphQL({ typeDefs }); + + const source = ` + query($id: ID!) { + townMemberList(id: $id) { + id + gender { + gender + } + } + } + `; + + try { + await session.run( + ` + CREATE (t:Town {id: $townId}) + MERGE (t)<-[:BELONGS_TO]-(m:Member {id: $memberId}) + MERGE (m)-[:HAS_GENDER]->(:Gender {gender: $gender}) + `, + { + memberId, + gender, + townId, + } + ); + + const gqlResult = await graphql({ + schema: neoSchema.schema, + source, + contextValue: { driver }, + variableValues: { id: townId }, + }); + + expect(gqlResult.errors).toBeFalsy(); + + expect((gqlResult?.data as any).townMemberList).toEqual([{ id: memberId, gender: { gender } }]); + } finally { + await session.close(); + } + }); + }); + }); +}); From 662acc30e35af7e10801648ed006159bcebe3b52 Mon Sep 17 00:00:00 2001 From: Darrell Warde <8117355+darrellwarde@users.noreply.github.com> Date: Tue, 6 Jul 2021 09:15:43 +0100 Subject: [PATCH 15/20] Fix for loss of scalar and field level resolvers (#297) * wrapCustomResolvers removed in favour of schema level resolver auth injection * Add test cases for this fix --- packages/graphql/package.json | 1 + packages/graphql/src/classes/Neo4jGraphQL.ts | 22 ++- .../src/schema/make-augmented-schema.ts | 16 +-- .../src/schema/wrap-custom-resolvers.ts | 132 ------------------ .../tests/integration/issues/#207.int.test.ts | 112 +++++++++++++++ .../tests/integration/issues/#283.int.test.ts | 103 ++++++++++++++ .../tests/integration/types/point.int.test.ts | 12 +- yarn.lock | 63 +++++++++ 8 files changed, 311 insertions(+), 150 deletions(-) delete mode 100644 packages/graphql/src/schema/wrap-custom-resolvers.ts create mode 100644 packages/graphql/tests/integration/issues/#207.int.test.ts create mode 100644 packages/graphql/tests/integration/issues/#283.int.test.ts diff --git a/packages/graphql/package.json b/packages/graphql/package.json index 3646883832..221dfae160 100644 --- a/packages/graphql/package.json +++ b/packages/graphql/package.json @@ -56,6 +56,7 @@ "rimraf": "3.0.2", "semver": "7.3.5", "ts-jest": "26.1.4", + "ts-node": "^10.0.0", "typescript": "3.9.7" }, "dependencies": { diff --git a/packages/graphql/src/classes/Neo4jGraphQL.ts b/packages/graphql/src/classes/Neo4jGraphQL.ts index 0c82d29fff..20920a7fe2 100644 --- a/packages/graphql/src/classes/Neo4jGraphQL.ts +++ b/packages/graphql/src/classes/Neo4jGraphQL.ts @@ -20,7 +20,7 @@ import Debug from "debug"; import { Driver } from "neo4j-driver"; import { DocumentNode, GraphQLResolveInfo, GraphQLSchema, parse, printSchema, print } from "graphql"; -import { addSchemaLevelResolver, IExecutableSchemaDefinition } from "@graphql-tools/schema"; +import { addResolversToSchema, addSchemaLevelResolver, IExecutableSchemaDefinition } from "@graphql-tools/schema"; import type { DriverConfig } from "../types"; import { makeAugmentedSchema } from "../schema"; import Node from "./Node"; @@ -28,6 +28,7 @@ import { checkNeo4jCompat } from "../utils"; import { getJWT } from "../auth/index"; import { DEBUG_GRAPHQL } from "../constants"; import getNeo4jResolveTree from "../utils/get-neo4j-resolve-tree"; +import createAuthParam from "../translate/create-auth-param"; const debug = Debug(DEBUG_GRAPHQL); @@ -60,13 +61,26 @@ class Neo4jGraphQL { public config?: Neo4jGraphQLConfig; constructor(input: Neo4jGraphQLConstructor) { - const { config = {}, driver, ...schemaDefinition } = input; + const { config = {}, driver, resolvers, ...schemaDefinition } = input; const { nodes, schema } = makeAugmentedSchema(schemaDefinition, { enableRegex: config.enableRegex }); this.driver = driver; this.config = config; this.nodes = nodes; - this.schema = this.createWrappedSchema({ schema, config }); + this.schema = schema; + /* + addResolversToSchema must be first, so that custom resolvers also get schema level resolvers + */ + if (resolvers) { + if (Array.isArray(resolvers)) { + resolvers.forEach((r) => { + this.schema = addResolversToSchema(this.schema, r); + }); + } else { + this.schema = addResolversToSchema(this.schema, resolvers); + } + } + this.schema = this.createWrappedSchema({ schema: this.schema, config }); this.document = parse(printSchema(schema)); } @@ -118,6 +132,8 @@ class Neo4jGraphQL { context.resolveTree = getNeo4jResolveTree(resolveInfo); context.jwt = getJWT(context); + + context.auth = createAuthParam({ context }); }); } diff --git a/packages/graphql/src/schema/make-augmented-schema.ts b/packages/graphql/src/schema/make-augmented-schema.ts index 1dd953cd0f..cb08bf447b 100644 --- a/packages/graphql/src/schema/make-augmented-schema.ts +++ b/packages/graphql/src/schema/make-augmented-schema.ts @@ -48,7 +48,6 @@ import { findResolver, createResolver, deleteResolver, cypherResolver, updateRes import checkNodeImplementsInterfaces from "./check-node-implements-interfaces"; import * as Scalars from "./scalars"; import parseExcludeDirective from "./parse-exclude-directive"; -import wrapCustomResolvers from "./wrap-custom-resolvers"; import getCustomResolvers from "./get-custom-resolvers"; import getObjFieldMeta from "./get-obj-field-meta"; import * as point from "./point"; @@ -56,7 +55,7 @@ import { graphqlDirectivesToCompose, objectFieldsToComposeFields } from "./to-co // import validateTypeDefs from "./validation"; function makeAugmentedSchema( - { typeDefs, resolvers, ...schemaDefinition }: IExecutableSchemaDefinition, + { typeDefs, ...schemaDefinition }: IExecutableSchemaDefinition, { enableRegex }: { enableRegex?: boolean } = {} ): { schema: GraphQLSchema; nodes: Node[] } { const document = mergeTypeDefs(Array.isArray(typeDefs) ? (typeDefs as string[]) : [typeDefs as string]); @@ -175,8 +174,6 @@ function makeAugmentedSchema( return node; }); - const nodeNames = nodes.map((x) => x.name); - nodes.forEach((node) => { const nodeFields = objectFieldsToComposeFields([ ...node.primitiveFields, @@ -741,7 +738,7 @@ function makeAugmentedSchema( } const generatedTypeDefs = composer.toSDL(); - let generatedResolvers: any = { + const generatedResolvers = { ...composer.getResolveMethods(), ...Object.entries(Scalars).reduce((res, [name, scalar]) => { if (generatedTypeDefs.includes(`scalar ${name}\n`)) { @@ -751,16 +748,7 @@ function makeAugmentedSchema( }, {}), }; - if (resolvers) { - generatedResolvers = wrapCustomResolvers({ - generatedResolvers, - nodeNames, - resolvers, - }); - } - unions.forEach((union) => { - // eslint-disable-next-line no-underscore-dangle if (!generatedResolvers[union.name.value]) { generatedResolvers[union.name.value] = { __resolveType: (root) => root.__resolveType }; } diff --git a/packages/graphql/src/schema/wrap-custom-resolvers.ts b/packages/graphql/src/schema/wrap-custom-resolvers.ts deleted file mode 100644 index 764f52807a..0000000000 --- a/packages/graphql/src/schema/wrap-custom-resolvers.ts +++ /dev/null @@ -1,132 +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 { IExecutableSchemaDefinition } from "@graphql-tools/schema"; -import createAuthParam from "../translate/create-auth-param"; - -type IResolvers = IExecutableSchemaDefinition["resolvers"]; - -function wrapCustomResolvers({ - resolvers, - generatedResolvers, - nodeNames, -}: { - resolvers: IResolvers; - nodeNames: string[]; - generatedResolvers: any; -}): IResolvers { - let newResolvers: any = {}; - - const { - Query: customQueries = {}, - Mutation: customMutations = {}, - Subscription: customSubscriptions = {}, - ...rest - } = resolvers as Record; - - if (customQueries) { - if (generatedResolvers.Query) { - newResolvers.Query = { ...generatedResolvers.Query, ...customQueries }; - } else { - newResolvers.Query = customQueries; - } - } - - if (customMutations) { - if (generatedResolvers.Mutation) { - newResolvers.Mutation = { ...generatedResolvers.Mutation, ...customMutations }; - } else { - newResolvers.Mutation = customMutations; - } - } - - if (Object.keys(customSubscriptions).length) { - newResolvers.Subscription = customSubscriptions; - } - - const typeResolvers = Object.entries(rest).reduce((r, entry) => { - const [key, value] = entry; - - if (!nodeNames.includes(key)) { - return r; - } - - return { - ...r, - [key]: { - ...generatedResolvers[key], - ...value, - }, - }; - }, {}); - newResolvers = { - ...newResolvers, - ...typeResolvers, - }; - - (function wrapResolvers(obj) { - Object.entries(obj).forEach(([key, value]) => { - if (typeof value === "function") { - obj[key] = (...args) => { - const { driver } = args[2]; - if (!driver) { - throw new Error("context.diver missing"); - } - - const auth = createAuthParam({ context: args[2] }); - - args[2] = { ...args[2], auth }; - - return value(...args); - }; - - return; - } - - if (typeof value === "object") { - obj[key] = value; - wrapResolvers(value); - } - }); - - return obj; - })(newResolvers); - - // Not to wrap the scalars and directives - const otherResolvers = Object.entries(rest).reduce((r, entry) => { - const [key, value] = entry; - - if (nodeNames.includes(key)) { - return r; - } - - return { - ...r, - [key]: value, - }; - }, {}); - newResolvers = { - ...newResolvers, - ...otherResolvers, - }; - - return newResolvers; -} - -export default wrapCustomResolvers; diff --git a/packages/graphql/tests/integration/issues/#207.int.test.ts b/packages/graphql/tests/integration/issues/#207.int.test.ts new file mode 100644 index 0000000000..abe4135e77 --- /dev/null +++ b/packages/graphql/tests/integration/issues/#207.int.test.ts @@ -0,0 +1,112 @@ +/* + * 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 neo4j from "../neo4j"; +import { Neo4jGraphQL } from "../../../src/classes"; + +describe("https://github.com/neo4j/graphql/issues/207", () => { + let driver: Driver; + const typeDefs = gql` + union Result = Book | Author + + type Book { + title: String + } + + type Author { + name: String + } + + type Query { + search: [Result] + } + `; + const resolvers = { + Result: { + __resolveType(obj) { + if (obj.name) { + return "Author"; + } + if (obj.title) { + return "Book"; + } + return null; // GraphQLError is thrown + }, + }, + Query: { + search: () => [{ title: "GraphQL Unions for Dummies" }, { name: "Darrell" }], + }, + }; + + beforeAll(async () => { + driver = await neo4j(); + }); + + afterAll(async () => { + await driver.close(); + }); + + test("__resolveType resolvers are correctly evaluated", async () => { + const session = driver.session(); + + const neoSchema = new Neo4jGraphQL({ typeDefs, resolvers, driver }); + + const mutation = ` + query GetSearchResults { + search { + __typename + ... on Book { + title + } + ... on Author { + name + } + } + } + `; + + try { + await neoSchema.checkNeo4jCompat(); + + const result = await graphql({ + schema: neoSchema.schema, + source: mutation, + contextValue: { driver }, + }); + + expect(result.errors).toBeFalsy(); + + expect(result?.data?.search).toEqual([ + { + __typename: "Book", + title: "GraphQL Unions for Dummies", + }, + { + __typename: "Author", + name: "Darrell", + }, + ]); + } finally { + await session.close(); + } + }); +}); diff --git a/packages/graphql/tests/integration/issues/#283.int.test.ts b/packages/graphql/tests/integration/issues/#283.int.test.ts new file mode 100644 index 0000000000..724908dcd9 --- /dev/null +++ b/packages/graphql/tests/integration/issues/#283.int.test.ts @@ -0,0 +1,103 @@ +/* + * 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/283", () => { + let driver: Driver; + const typeDefs = gql` + type Mutation { + login: String + createPost(input: PostCreateInput!): Post! + @cypher( + statement: """ + CREATE (post:Post) + SET + post = $input, + post.datetime = datetime(), + post.id = randomUUID() + RETURN post + """ + ) + } + + type Post { + id: ID! @id + title: String! + datetime: DateTime @readonly @timestamp(operations: [CREATE]) + } + `; + // Presence of a custom resolver was causing the bug + const resolvers = { + Mutation: { + login: () => { + return { token: "token" }; + }, + }, + }; + + beforeAll(async () => { + driver = await neo4j(); + }); + + afterAll(async () => { + await driver.close(); + }); + + test("DateTime values return correctly when using custom resolvers in the schema", async () => { + const session = driver.session(); + + const neoSchema = new Neo4jGraphQL({ typeDefs, resolvers, driver }); + + const title = generate({ charset: "alphabetic" }); + + const mutation = ` + mutation { + createPost(input: { title: "${title}" }) { + id + title + datetime + } + } + `; + + try { + await neoSchema.checkNeo4jCompat(); + + const result = await graphql({ + schema: neoSchema.schema, + source: mutation, + contextValue: { driver }, + }); + + expect(result.errors).toBeFalsy(); + + expect(typeof result?.data?.createPost?.datetime).toBe("string"); + + await session.run(`MATCH (p:Post) WHERE p.title = "${title}" DELETE p`); + } finally { + await session.close(); + } + }); +}); diff --git a/packages/graphql/tests/integration/types/point.int.test.ts b/packages/graphql/tests/integration/types/point.int.test.ts index 8213498206..779dfcc290 100644 --- a/packages/graphql/tests/integration/types/point.int.test.ts +++ b/packages/graphql/tests/integration/types/point.int.test.ts @@ -36,8 +36,18 @@ describe("Point", () => { size: Int! location: Point! } + + type Query { + custom: String! + } `; - neoSchema = new Neo4jGraphQL({ typeDefs }); + // Dummy custom resolvers to validate fix for https://github.com/neo4j/graphql/issues/278 + const resolvers = { + Query: { + custom: () => "hello", + }, + }; + neoSchema = new Neo4jGraphQL({ typeDefs, resolvers }); }); beforeEach(() => { diff --git a/yarn.lock b/yarn.lock index ac9bacb471..e2921d6aaf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -957,6 +957,7 @@ __metadata: rimraf: 3.0.2 semver: 7.3.5 ts-jest: 26.1.4 + ts-node: ^10.0.0 typescript: 3.9.7 peerDependencies: graphql: ^15.0.0 @@ -1160,6 +1161,34 @@ __metadata: languageName: node linkType: hard +"@tsconfig/node10@npm:^1.0.7": + version: 1.0.8 + resolution: "@tsconfig/node10@npm:1.0.8" + checksum: 0336493b89fb7c06409a1247a3fb00fac2755f21f3f8ae4b9dd2457859abfc5e8ca42b6d9ca5a279fe81bc70fe1f3450eef61e5dd5a63a7b4a6946ff31874816 + languageName: node + linkType: hard + +"@tsconfig/node12@npm:^1.0.7": + version: 1.0.9 + resolution: "@tsconfig/node12@npm:1.0.9" + checksum: 5532bfb5df47ed3a507da533c731a2fb80ee2e886edadbf20e664dcd3172d5c159577a281d15733b8d0c30bfa4e6b48496bef0704192c085520bc76bb9938068 + languageName: node + linkType: hard + +"@tsconfig/node14@npm:^1.0.0": + version: 1.0.1 + resolution: "@tsconfig/node14@npm:1.0.1" + checksum: d0068287dba46dc98e7d49c229b0fee034fbac2bb4bc2efe12cc67227a1c68ec0728ca1e535dff7f033f7455de6c67e9b8f9d90f4fc3bb07c0d9ac08186fe65c + languageName: node + linkType: hard + +"@tsconfig/node16@npm:^1.0.1": + version: 1.0.1 + resolution: "@tsconfig/node16@npm:1.0.1" + checksum: c389a4a81c291a27b96705de7fbe46d29aa4eb771450a41dfc075d89e1fdd63141898043a0d9f627460a1c409d06635a044dc4b3a4516173769a7d0a1558c51d + languageName: node + linkType: hard + "@types/accepts@npm:*, @types/accepts@npm:^1.3.5": version: 1.3.5 resolution: "@types/accepts@npm:1.3.5" @@ -13158,6 +13187,40 @@ resolve@^2.0.0-next.3: languageName: node linkType: hard +"ts-node@npm:^10.0.0": + version: 10.0.0 + resolution: "ts-node@npm:10.0.0" + dependencies: + "@tsconfig/node10": ^1.0.7 + "@tsconfig/node12": ^1.0.7 + "@tsconfig/node14": ^1.0.0 + "@tsconfig/node16": ^1.0.1 + arg: ^4.1.0 + create-require: ^1.1.0 + diff: ^4.0.1 + make-error: ^1.1.1 + source-map-support: ^0.5.17 + yn: 3.1.1 + peerDependencies: + "@swc/core": ">=1.2.45" + "@swc/wasm": ">=1.2.45" + "@types/node": "*" + typescript: ">=2.7" + peerDependenciesMeta: + "@swc/core": + optional: true + "@swc/wasm": + optional: true + bin: + ts-node: dist/bin.js + ts-node-cwd: dist/bin-cwd.js + ts-node-script: dist/bin-script.js + ts-node-transpile-only: dist/bin-transpile.js + ts-script: dist/bin-script-deprecated.js + checksum: dc461e2b9b931b00ff065530a0247f86da1d035e72a7ef6d7ed072dd8e6b236d1879f113dcc73a354d240c81b6b845445c3d32b16eeb68022ed27ab6d130c049 + languageName: node + linkType: hard + "tsconfig-paths@npm:^3.9.0": version: 3.9.0 resolution: "tsconfig-paths@npm:3.9.0" From e55bd536442fe26f5810281cfb1a1f720bff416e Mon Sep 17 00:00:00 2001 From: Darrell Warde Date: Tue, 6 Jul 2021 09:42:48 +0100 Subject: [PATCH 16/20] Mention double escaping for @cypher directive --- docs/asciidoc/type-definitions/cypher.adoc | 33 ++++++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/docs/asciidoc/type-definitions/cypher.adoc b/docs/asciidoc/type-definitions/cypher.adoc index 35e30a5c01..90d570624d 100644 --- a/docs/asciidoc/type-definitions/cypher.adoc +++ b/docs/asciidoc/type-definitions/cypher.adoc @@ -14,6 +14,33 @@ directive @cypher( ) on FIELD_DEFINITION ---- +== Character Escaping + +All double quotes must be _double escaped_ when used in a @cypher directive - once for GraphQL and once for the function in which we run the Cypher. For example, at its simplest: + +[source, graphql] +---- +type Example { + string: String! + @cypher( + statement: """ + RETURN \\"field-level string\\" + """ + ) +} + +type Query { + string: String! + @cypher( + statement: """ + RETURN \\"Query-level string\\" + """ + ) +} +---- + +Note the double-backslash (`\\`) before each double quote (`"`). + == Globals Global variables are available for use within the Cypher statement. @@ -59,12 +86,12 @@ type Query { === `cypherParams` Use to inject values into the cypher query from the GraphQL context function. -Inject into context: +Inject into context: [source, typescript] ---- const server = new ApolloServer({ - typeDefs, + typeDefs, context: () => { return { cypherParams: { userId: "user-id-01" } @@ -73,7 +100,7 @@ const server = new ApolloServer({ }); ---- -Use in cypher query: +Use in cypher query: [source, graphql] ---- From 46f64c6e849e7fe57d39d3fc639e9d93b2f54193 Mon Sep 17 00:00:00 2001 From: Neo Technology Build Agent Date: Tue, 6 Jul 2021 10:31:53 +0000 Subject: [PATCH 17/20] Version update --- examples/migration/package.json | 2 +- examples/neo-push/server/package.json | 4 ++-- packages/graphql/package.json | 2 +- packages/ogm/package.json | 4 ++-- yarn.lock | 12 ++++++------ 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/examples/migration/package.json b/examples/migration/package.json index 873bee6299..5a0faf4dfa 100644 --- a/examples/migration/package.json +++ b/examples/migration/package.json @@ -4,7 +4,7 @@ "start": "node src/index.js" }, "dependencies": { - "@neo4j/graphql": "^1.0.3", + "@neo4j/graphql": "^1.1.0", "apollo-server": "^2.23.0", "graphql": "^15.0.0", "neo4j-driver": "^4.2.0" diff --git a/examples/neo-push/server/package.json b/examples/neo-push/server/package.json index 144aac7613..ffa04b00da 100644 --- a/examples/neo-push/server/package.json +++ b/examples/neo-push/server/package.json @@ -12,8 +12,8 @@ "author": "", "license": "ISC", "dependencies": { - "@neo4j/graphql": "^1.0.3", - "@neo4j/graphql-ogm": "^1.0.3", + "@neo4j/graphql": "^1.1.0", + "@neo4j/graphql-ogm": "^1.1.0", "apollo-server-express": "2.19.0", "bcrypt": "5.0.1", "debug": "4.3.1", diff --git a/packages/graphql/package.json b/packages/graphql/package.json index 221dfae160..263259305c 100644 --- a/packages/graphql/package.json +++ b/packages/graphql/package.json @@ -1,6 +1,6 @@ { "name": "@neo4j/graphql", - "version": "1.0.3", + "version": "1.1.0", "description": "A GraphQL to Cypher query execution layer for Neo4j and JavaScript GraphQL implementations", "keywords": [ "neo4j", diff --git a/packages/ogm/package.json b/packages/ogm/package.json index 4d67f979c5..7ff9338692 100644 --- a/packages/ogm/package.json +++ b/packages/ogm/package.json @@ -1,6 +1,6 @@ { "name": "@neo4j/graphql-ogm", - "version": "1.0.3", + "version": "1.1.0", "description": "GraphQL powered OGM for Neo4j and Javascript applications", "keywords": [ "neo4j", @@ -27,7 +27,7 @@ "author": "Neo4j Inc.", "dependencies": { "@graphql-tools/merge": "^6.2.13", - "@neo4j/graphql": "^1.0.3", + "@neo4j/graphql": "^1.1.0", "camelcase": "^6.2.0", "pluralize": "^8.0.0" }, diff --git a/yarn.lock b/yarn.lock index e2921d6aaf..201f95e5b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -899,12 +899,12 @@ __metadata: languageName: node linkType: hard -"@neo4j/graphql-ogm@^1.0.3, @neo4j/graphql-ogm@workspace:packages/ogm": +"@neo4j/graphql-ogm@^1.1.0, @neo4j/graphql-ogm@workspace:packages/ogm": version: 0.0.0-use.local resolution: "@neo4j/graphql-ogm@workspace:packages/ogm" dependencies: "@graphql-tools/merge": ^6.2.13 - "@neo4j/graphql": ^1.0.3 + "@neo4j/graphql": ^1.1.0 "@types/jest": 26.0.8 "@types/node": 14.0.27 camelcase: ^6.2.0 @@ -923,7 +923,7 @@ __metadata: languageName: unknown linkType: soft -"@neo4j/graphql@^1.0.3, @neo4j/graphql@workspace:packages/graphql": +"@neo4j/graphql@^1.1.0, @neo4j/graphql@workspace:packages/graphql": version: 0.0.0-use.local resolution: "@neo4j/graphql@workspace:packages/graphql" dependencies: @@ -9335,7 +9335,7 @@ fsevents@^1.2.7: version: 0.0.0-use.local resolution: "migration@workspace:examples/migration" dependencies: - "@neo4j/graphql": ^1.0.3 + "@neo4j/graphql": ^1.1.0 apollo-server: ^2.23.0 graphql: ^15.0.0 neo4j-driver: ^4.2.0 @@ -9686,8 +9686,8 @@ fsevents@^1.2.7: version: 0.0.0-use.local resolution: "neo-push-server@workspace:examples/neo-push/server" dependencies: - "@neo4j/graphql": ^1.0.3 - "@neo4j/graphql-ogm": ^1.0.3 + "@neo4j/graphql": ^1.1.0 + "@neo4j/graphql-ogm": ^1.1.0 "@types/bcrypt": 3.0.0 "@types/debug": 4.1.5 "@types/dotenv": 8.2.0 From 5e928f6e3944ebac24b4f3cc4a6d5726ae5c004c Mon Sep 17 00:00:00 2001 From: Darrell Warde <8117355+darrellwarde@users.noreply.github.com> Date: Fri, 9 Jul 2021 15:18:55 +0100 Subject: [PATCH 18/20] Allows users to pass in decoded JWT (#303) * Allows users to pass in decoded JWT - needs more testing * More tests for decoded JWTs * Updates to auth documentation * Fix relationships documentation examples (#296) --- docs/asciidoc/auth/{auth.adoc => index.adoc} | 0 docs/asciidoc/auth/setup.adoc | 198 ++++++----- .../type-definitions/relationships.adoc | 4 +- packages/graphql/package.json | 2 +- packages/graphql/src/classes/Neo4jGraphQL.ts | 4 +- .../src/translate/create-auth-and-params.ts | 4 + .../src/translate/create-auth-param.ts | 6 +- packages/graphql/src/types.ts | 10 +- .../auth/custom-cypher.int.test.ts | 333 ++++++++++++++++++ .../auth/custom-resolvers.int.test.ts | 48 +++ .../auth/is-authenticated.int.test.ts | 42 +++ ...t.test.ts => custom-resolvers.int.test.ts} | 157 --------- yarn.lock | 18 +- 13 files changed, 560 insertions(+), 266 deletions(-) rename docs/asciidoc/auth/{auth.adoc => index.adoc} (100%) create mode 100644 packages/graphql/tests/integration/auth/custom-cypher.int.test.ts rename packages/graphql/tests/integration/{custom-resolvers-int.test.ts => custom-resolvers.int.test.ts} (73%) diff --git a/docs/asciidoc/auth/auth.adoc b/docs/asciidoc/auth/index.adoc similarity index 100% rename from docs/asciidoc/auth/auth.adoc rename to docs/asciidoc/auth/index.adoc diff --git a/docs/asciidoc/auth/setup.adoc b/docs/asciidoc/auth/setup.adoc index 4c3a59865d..debfa0b109 100644 --- a/docs/asciidoc/auth/setup.adoc +++ b/docs/asciidoc/auth/setup.adoc @@ -1,35 +1,43 @@ [[auth-setup]] = Setup -The auth implementation uses JWT tokens. You are expected to pass a JWT into the request. The accepted token type should be Bearer where the header should be authorization; +== Configuration -[source] ----- -POST / HTTP/1.1 -authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyb2xlcyI6WyJ1c2VyX2FkbWluIiwicG9zdF9hZG1pbiIsImdyb3VwX2FkbWluIl19.IY0LWqgHcjEtOsOw60mqKazhuRFKroSXFQkpCtWpgQI -content-type: application/json ----- +If you want the Neo4j GraphQL Library to perform JWT decoding and verification for you, you must pass the configuration option `jwt` into the `Neo4jGraphQL` or `OGM` constructor, which has the following arguments: -== Config +- `secret` - The secret to be used to decode and verify JWTs +- `noVerify` (optional) - Disable verification of JWTs, defaults to _false_ +- `rolesPath` (optional) - A string key to specify where to find roles in the JWT, defaults to "roles" -Auth centric values on the Config object passed to Neo4jGraphQL or OGM. +The simplest construction of a `Neo4jGraphQL` instance would be: -.Auth Config -|=== -|Variable | Usage +[source, javascript] +---- +const neoSchema = new Neo4jGraphQL({ + typeDefs, + config: { + jwt: { + secret + } + } +}); +---- -|`secret` -| Specify JWT secret +It is also possible to pass in JWTs which have already been decoded, in which case the `jwt` option is _not necessary_. This will be covered in the section <>. -|`noVerify` -| Disable the verification of the JW +=== Auth Roles Object Paths +If you are using a 3rd party auth provider such as Auth0 you may find your roles property being nested inside an object: -|`rolesPath` -| Specify where on the JWT the roles key is -|=== +[source, json] +---- +{ + "https://auth0.mysite.com/claims": { + "https://auth0.mysite.com/claims/roles": ["admin"] + } +} +---- -== Server Construction -Request object needs to be injected into the context before you can use auth. Here is an example using Apollo Server; +In order to make use of this, you must pass it in as a "dot path" into the `rolesPath` option: [source, javascript] ---- @@ -37,18 +45,75 @@ const neoSchema = new Neo4jGraphQL({ typeDefs, config: { jwt: { - secret + secret, + rolesPath: "https://auth0.mysite.com/claims\\.https://auth0.mysite.com/claims/roles" } } }); +---- + +[[auth-setup-passing-in]] +== Passing in JWTs + +If you wish to pass in an encoded JWT, this must be included in the `Authorization` header of your requests, in the format: + +[source] +---- +POST / HTTP/1.1 +authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyb2xlcyI6WyJ1c2VyX2FkbWluIiwicG9zdF9hZG1pbiIsImdyb3VwX2FkbWluIl19.IY0LWqgHcjEtOsOw60mqKazhuRFKroSXFQkpCtWpgQI +content-type: application/json +---- + +Note the string "Bearer" before the inclusion of the JWT. +Then, using Apollo Server as an example, you must include the request in the GraphQL context, as follows (using the `neoSchema` instance from the example above): + +[source, javascript] +---- const server = new ApolloServer({ schema: neoSchema.schema, context: ({ req }) => ({ req }), }); ---- -== `rules` +Note that the request key `req` is appropriate for Express servers, but different middlewares use different keys for request objects. You can more details at https://www.apollographql.com/docs/apollo-server/api/apollo-server/#middleware-specific-context-fields. + +=== Decoded JWTs + +Alternatively, you can pass a key `jwt` of type `JwtPayload` into the context, which has the following definition: + +[source, typescript] +---- +// standard claims https://datatracker.ietf.org/doc/html/rfc7519#section-4.1 +interface JwtPayload { + [key: string]: any; + iss?: string | undefined; + sub?: string | undefined; + aud?: string | string[] | undefined; + exp?: number | undefined; + nbf?: number | undefined; + iat?: number | undefined; + jti?: string | undefined; +} +---- + +_Do not_ pass in the header or the signature. + +For example, you might have a function `decodeJWT` which returns a decoded JWT: + +[source, javascript] +---- +const decodedJWT = decodeJWT(encodedJWT) + +const server = new ApolloServer({ + schema: neoSchema.schema, + context: { jwt: decodedJWT.payload }, +}); +---- + +== `@auth` directive + +=== `rules` You can have many rules for many operations. We fall through each rule, on the corresponding operation, until we find a match. On no match found, an error is thrown. You can think of rules as a big OR. @@ -61,9 +126,9 @@ You can have many rules for many operations. We fall through each rule, on the c ]) ---- -== `operations` +=== `operations` -Operations is an array, you can re-use the same rule for many operations. +Operations is an array which allows you to re-use the same rule for many operations. [source, graphql] ---- @@ -73,9 +138,9 @@ Operations is an array, you can re-use the same rule for many operations. ]) ---- -> The absence of an `operations` argument will imply all operations +NOTE: Note that the absence of an `operations` argument will imply _all_ operations. -Many different operations can be called in one query take the below mutation; +Many different operations can be called at once, for example in the following Mutation: [source, graphql] ---- @@ -95,44 +160,16 @@ mutation { } ---- -In the above example; First we do a `create` operation then we do a `connect` operation. - -The full list of operations are; - -1. read - `MATCH` -2. create - `CREATE` -3. update - `SET` -4. delete - `DELETE` -5. connect - `MATCH` & `MERGE` -6. disconnect - `MATCH` & `DELETE` - - -== Auth Roles Object Paths -If you are using 3rd party Auth solutions such as Auth0 you may find your roles property being nested inside an object; - -[source, json] ----- -{ - "https://auth0.mysite.com/claims": { - "https://auth0.mysite.com/claims/roles": ["admin"] - } -} ----- +In the above example, we perform a `CREATE` followed by a `CONNECT`, so our auth rule must allow our user to perform both of these operations. -Specify the key at construction: +The full list of operations are: -[source, javascript] ----- -const neoSchema = new Neo4jGraphQL({ - typeDefs, - config: { - jwt: { - secret, - rolesPath: "https://auth0.mysite.com/claims\\.https://auth0.mysite.com/claims/roles" - } - } -}); ----- +- read - `MATCH` +- create - `CREATE` +- update - `SET` +- delete - `DELETE` +- connect - `MATCH` & `MERGE` +- disconnect - `MATCH` & `DELETE` == Auth Value Plucking @@ -141,30 +178,16 @@ const neoSchema = new Neo4jGraphQL({ == Auth Custom Resolvers -You cant put the auth directive on a custom resolver. We do make life easier by injecting the auth param into it. It will be available under the `context.auth` property; +You can't use the `@auth` directive on a custom resolver, however, we do make life easier by injecting the auth parameter into it. It will be available under the `context.auth` property. For example, the following custom resolver returns the `sub` field from the JWT: [source, javascript] ---- -const { Neo4jGraphQL } = require("@neo4j/graphql") -const neo4j = require("neo4j-driver"); -const { ApolloServer } = require("apollo-server") - const typeDefs = ` - type User { - id: ID! - email: String! - password: String! - } type Query { myId: ID! } `; -const driver = neo4j.driver( - "bolt://localhost:7687", - neo4j.auth.basic("admin", "password") -); - const resolvers = { Query: { myId(root, args, context) { @@ -172,20 +195,13 @@ const resolvers = { } } }; - -const neoSchema = new Neo4jGraphQL({ typeDefs, resolvers, config: { jwt } }); - -const server = new ApolloServer({ - schema: neo4jGraphQL.schema, - context: ({ req }) => ({ req, driver }), -}); - -server.listen(4000).then(() => console.log("online")); ---- == Auth on `@cypher` -You can put the `@auth` directive on a field with the `@cypher` directive. Functionality like allow and bind will not work but you can still utilize `isAuthenticated` and `roles`. +You can put the `@auth` directive on a field with the `@cypher` directive. Functionality like `allow` and `bind` will not work but you can still utilize `isAuthenticated` and `roles`. Additionally, you don't need to specify operations for `@auth` directives on `@cypher` fields. + +The following example uses the `isAuthenticated` rule to ensure a user is authenticated, before returning the `User` associated with the JWT: [source, graphql] ---- @@ -193,18 +209,20 @@ type User @exclude { id: ID name: String } + type Query { - users: [User] @cypher(statement: "MATCH (a:User) RETURN a") @auth(rules: [{ isAuthenticated: true }]) + me: User @cypher(statement: "MATCH (u:User { id: $auth.jwt.sub }) RETURN u") @auth(rules: [{ isAuthenticated: true }]) } ---- -Notice you don't need to specify operations for `@auth` directives on `@cypher` fields. +In the following example, the current user must have role "admin" in order to query the `history` field on the type `User`: [source, graphql] ---- type History @exclude { website: String! } + type User { id: ID name: String diff --git a/docs/asciidoc/type-definitions/relationships.adoc b/docs/asciidoc/type-definitions/relationships.adoc index d810b3e1a0..6cce576c81 100644 --- a/docs/asciidoc/type-definitions/relationships.adoc +++ b/docs/asciidoc/type-definitions/relationships.adoc @@ -87,11 +87,11 @@ We then need to create the actor in our example, and connect them to the new Mov [source, graphql] ---- mutation CreateActor { - createActors(input: [ + createPeople(input: [ { name: "Tom Hanks" born: 1956 - movies: { + actedInMovies: { connect: { where: { title: "Forrest Gump" diff --git a/packages/graphql/package.json b/packages/graphql/package.json index 263259305c..2a601a6d73 100644 --- a/packages/graphql/package.json +++ b/packages/graphql/package.json @@ -41,7 +41,7 @@ "@types/faker": "5.1.7", "@types/is-uuid": "1.0.0", "@types/jest": "26.0.8", - "@types/jsonwebtoken": "8.5.0", + "@types/jsonwebtoken": "^8.5.4", "@types/node": "14.0.27", "@types/pluralize": "0.0.29", "@types/randomstring": "1.1.6", diff --git a/packages/graphql/src/classes/Neo4jGraphQL.ts b/packages/graphql/src/classes/Neo4jGraphQL.ts index 20920a7fe2..86439f0bf1 100644 --- a/packages/graphql/src/classes/Neo4jGraphQL.ts +++ b/packages/graphql/src/classes/Neo4jGraphQL.ts @@ -131,7 +131,9 @@ class Neo4jGraphQL { context.resolveTree = getNeo4jResolveTree(resolveInfo); - context.jwt = getJWT(context); + if (!context.jwt) { + context.jwt = getJWT(context); + } context.auth = createAuthParam({ context }); }); diff --git a/packages/graphql/src/translate/create-auth-and-params.ts b/packages/graphql/src/translate/create-auth-and-params.ts index 5b028cb574..2838687f47 100644 --- a/packages/graphql/src/translate/create-auth-and-params.ts +++ b/packages/graphql/src/translate/create-auth-and-params.ts @@ -67,6 +67,10 @@ function createAuthPredicate({ } const { jwt } = context; + if (!jwt) { + throw new Error("Can't generate auth predicate - no JWT in context"); + } + const result = Object.entries(rule[kind] as any).reduce( (res: Res, [key, value]) => { if (key === "AND" || key === "OR") { diff --git a/packages/graphql/src/translate/create-auth-param.ts b/packages/graphql/src/translate/create-auth-param.ts index cb94160df0..e203945dcb 100644 --- a/packages/graphql/src/translate/create-auth-param.ts +++ b/packages/graphql/src/translate/create-auth-param.ts @@ -34,11 +34,7 @@ function createAuthParam({ context }: { context: Context }) { const jwtConfig = context.neoSchema.config?.jwt; - if (!jwtConfig) { - return param; - } - - if (jwtConfig.rolesPath) { + if (jwtConfig?.rolesPath) { param.roles = dotProp.get(jwt, jwtConfig.rolesPath); } else if (jwt.roles) { param.roles = jwt.roles; diff --git a/packages/graphql/src/types.ts b/packages/graphql/src/types.ts index 07d15732ba..c51b10734d 100644 --- a/packages/graphql/src/types.ts +++ b/packages/graphql/src/types.ts @@ -19,6 +19,7 @@ import { InputValueDefinitionNode, DirectiveNode } from "graphql"; import { ResolveTree } from "graphql-parse-resolve-info"; +import { JwtPayload } from "jsonwebtoken"; import { Driver } from "neo4j-driver"; import { Neo4jGraphQL } from "./classes"; @@ -27,12 +28,19 @@ export type DriverConfig = { bookmarks?: string | string[]; }; +interface AuthContext { + isAuthenticated: boolean; + roles: [string]; + jwt: JwtPayload; +} + export interface Context { driver: Driver; driverConfig?: DriverConfig; resolveTree: ResolveTree; neoSchema: Neo4jGraphQL; - jwt?: any; + jwt?: JwtPayload; + auth?: AuthContext; [k: string]: any; } diff --git a/packages/graphql/tests/integration/auth/custom-cypher.int.test.ts b/packages/graphql/tests/integration/auth/custom-cypher.int.test.ts new file mode 100644 index 0000000000..feed393657 --- /dev/null +++ b/packages/graphql/tests/integration/auth/custom-cypher.int.test.ts @@ -0,0 +1,333 @@ +/* + * 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 { IncomingMessage } from "http"; +import { Socket } from "net"; +import jsonwebtoken from "jsonwebtoken"; +import { Neo4jGraphQL } from "../../../src/classes"; +import neo4j from "../neo4j"; + +describe("should inject the auth into cypher directive", () => { + let driver: Driver; + + beforeAll(async () => { + driver = await neo4j(); + }); + + afterAll(async () => { + await driver.close(); + }); + + test("query", async () => { + const session = driver.session(); + + const typeDefs = ` + type Query { + userId: ID @cypher(statement: """ + RETURN $auth.jwt.sub + """) + } + `; + + const userId = generate({ + charset: "alphabetic", + }); + + const secret = "secret"; + + const token = jsonwebtoken.sign({ sub: userId }, secret); + + const neoSchema = new Neo4jGraphQL({ typeDefs, config: { jwt: { secret } } }); + + const query = ` + { + userId + } + `; + + try { + const socket = new Socket({ readable: true }); + const req = new IncomingMessage(socket); + req.headers.authorization = `Bearer ${token}`; + + const gqlResult = await graphql({ + schema: neoSchema.schema, + source: query as string, + contextValue: { driver, req }, + }); + + expect(gqlResult.errors).toBeUndefined(); + + expect((gqlResult.data as any).userId).toEqual(userId); + } finally { + await session.close(); + } + }); + + test("query (decoded JWT)", async () => { + const session = driver.session(); + + const typeDefs = ` + type Query { + userId: ID @cypher(statement: """ + RETURN $auth.jwt.sub + """) + } + `; + + const userId = generate({ + charset: "alphabetic", + }); + + const neoSchema = new Neo4jGraphQL({ typeDefs }); + + const query = ` + { + userId + } + `; + + const jwt = { + sub: userId, + name: "John Doe", + iat: 1516239022, + }; + + try { + const gqlResult = await graphql({ + schema: neoSchema.schema, + source: query as string, + contextValue: { driver, jwt }, + }); + + expect(gqlResult.errors).toBeUndefined(); + + expect((gqlResult.data as any).userId).toEqual(userId); + } finally { + await session.close(); + } + }); + + test("mutation", async () => { + const session = driver.session(); + + const typeDefs = ` + type User { + id: ID + } + + type Mutation { + userId: ID @cypher(statement: """ + RETURN $auth.jwt.sub + """) + } + `; + + const userId = generate({ + charset: "alphabetic", + }); + + const secret = "secret"; + + const token = jsonwebtoken.sign({ sub: userId }, secret); + + const neoSchema = new Neo4jGraphQL({ typeDefs, config: { jwt: { secret } } }); + + const query = ` + mutation { + userId + } + `; + + try { + const socket = new Socket({ readable: true }); + const req = new IncomingMessage(socket); + req.headers.authorization = `Bearer ${token}`; + + const gqlResult = await graphql({ + schema: neoSchema.schema, + source: query as string, + contextValue: { driver, req }, + }); + + expect(gqlResult.errors).toBeUndefined(); + + expect((gqlResult.data as any).userId).toEqual(userId); + } finally { + await session.close(); + } + }); + + test("mutation (decoded JWT)", async () => { + const session = driver.session(); + + const typeDefs = ` + type User { + id: ID + } + + type Mutation { + userId: ID @cypher(statement: """ + RETURN $auth.jwt.sub + """) + } + `; + + const userId = generate({ + charset: "alphabetic", + }); + + const jwt = { + sub: userId, + name: "John Doe", + iat: 1516239022, + }; + + const neoSchema = new Neo4jGraphQL({ typeDefs }); + + const query = ` + mutation { + userId + } + `; + + try { + const gqlResult = await graphql({ + schema: neoSchema.schema, + source: query as string, + contextValue: { driver, jwt }, + }); + + expect(gqlResult.errors).toBeUndefined(); + + expect((gqlResult.data as any).userId).toEqual(userId); + } finally { + await session.close(); + } + }); + + test("should inject the auth into cypher directive on fields", async () => { + const session = driver.session(); + + const typeDefs = ` + type User { + id: ID + userId: ID @cypher(statement: """ + WITH $auth.jwt.sub as a + RETURN a + """) + } + `; + + const userId = generate({ + charset: "alphabetic", + }); + + const secret = "secret"; + + const token = jsonwebtoken.sign({ sub: userId }, secret); + + const neoSchema = new Neo4jGraphQL({ typeDefs, config: { jwt: { secret } } }); + + const query = ` + { + users(where: {id: "${userId}"}){ + userId + } + } + `; + + try { + await session.run(` + CREATE (:User {id: "${userId}"}) + `); + + const socket = new Socket({ readable: true }); + const req = new IncomingMessage(socket); + req.headers.authorization = `Bearer ${token}`; + + const gqlResult = await graphql({ + schema: neoSchema.schema, + source: query, + contextValue: { driver, req }, + }); + + expect(gqlResult.errors).toBeUndefined(); + + expect((gqlResult.data as any).users[0].userId).toEqual(userId); + } finally { + await session.close(); + } + }); + + test("should inject the auth into cypher directive on fields (decoded JWT)", async () => { + const session = driver.session(); + + const typeDefs = ` + type User { + id: ID + userId: ID @cypher(statement: """ + WITH $auth.jwt.sub as a + RETURN a + """) + } + `; + + const userId = generate({ + charset: "alphabetic", + }); + + const jwt = { + sub: userId, + name: "John Doe", + iat: 1516239022, + }; + + const neoSchema = new Neo4jGraphQL({ typeDefs }); + + const query = ` + { + users(where: {id: "${userId}"}){ + userId + } + } + `; + + try { + await session.run(` + CREATE (:User {id: "${userId}"}) + `); + + const gqlResult = await graphql({ + schema: neoSchema.schema, + source: query, + contextValue: { driver, jwt }, + }); + + expect(gqlResult.errors).toBeUndefined(); + + expect((gqlResult.data as any).users[0].userId).toEqual(userId); + } finally { + await session.close(); + } + }); +}); diff --git a/packages/graphql/tests/integration/auth/custom-resolvers.int.test.ts b/packages/graphql/tests/integration/auth/custom-resolvers.int.test.ts index 419a727d31..625afb4495 100644 --- a/packages/graphql/tests/integration/auth/custom-resolvers.int.test.ts +++ b/packages/graphql/tests/integration/auth/custom-resolvers.int.test.ts @@ -207,5 +207,53 @@ describe("auth/custom-resolvers", () => { expect(gqlResult.errors).toBeUndefined(); expect((gqlResult.data as any).me.customId).toEqual(userId); }); + + test("should inject auth in context of custom Query when decoded JWT passed in", async () => { + const typeDefs = ` + type User { + id: ID + } + + type Query { + me: User + } + `; + + const userId = generate({ + charset: "alphabetic", + }); + + const query = ` + { + me { + id + } + } + `; + + const jwt = { + sub: userId, + name: "John Doe", + iat: 1516239022, + }; + + const neoSchema = new Neo4jGraphQL({ + typeDefs, + resolvers: { + Query: { + me: (_, __, ctx) => ({ id: ctx.auth.jwt.sub }), + }, + }, + }); + + const gqlResult = await graphql({ + schema: neoSchema.schema, + source: query, + contextValue: { driver, jwt }, + }); + + expect(gqlResult.errors).toBeUndefined(); + expect((gqlResult.data as any).me.id).toEqual(userId); + }); }); }); diff --git a/packages/graphql/tests/integration/auth/is-authenticated.int.test.ts b/packages/graphql/tests/integration/auth/is-authenticated.int.test.ts index c4466d4737..74bf2df982 100644 --- a/packages/graphql/tests/integration/auth/is-authenticated.int.test.ts +++ b/packages/graphql/tests/integration/auth/is-authenticated.int.test.ts @@ -688,5 +688,47 @@ describe("auth/is-authenticated", () => { await session.close(); } }); + + test("should not throw if decoded JWT passed in context", async () => { + const session = driver.session({ defaultAccessMode: "READ" }); + + const typeDefs = ` + type Product @auth(rules: [{ + operations: [READ], + isAuthenticated: true + }]) { + id: ID + name: String + } + `; + + const neoSchema = new Neo4jGraphQL({ typeDefs }); + + const query = ` + { + products { + id + } + } + `; + + const jwt = { + sub: "1234567890", + name: "John Doe", + iat: 1516239022, + }; + + try { + const gqlResult = await graphql({ + schema: neoSchema.schema, + source: query, + contextValue: { driver, jwt }, + }); + + expect(gqlResult.errors).toBeFalsy(); + } finally { + await session.close(); + } + }); }); }); diff --git a/packages/graphql/tests/integration/custom-resolvers-int.test.ts b/packages/graphql/tests/integration/custom-resolvers.int.test.ts similarity index 73% rename from packages/graphql/tests/integration/custom-resolvers-int.test.ts rename to packages/graphql/tests/integration/custom-resolvers.int.test.ts index a75aeaf300..0f58bec346 100644 --- a/packages/graphql/tests/integration/custom-resolvers-int.test.ts +++ b/packages/graphql/tests/integration/custom-resolvers.int.test.ts @@ -20,9 +20,6 @@ import { Driver } from "neo4j-driver"; import { graphql, createSourceEventStream, parse } from "graphql"; import { generate } from "randomstring"; -import { IncomingMessage } from "http"; -import { Socket } from "net"; -import jsonwebtoken from "jsonwebtoken"; import { Neo4jGraphQL } from "../../src/classes"; import neo4j from "./neo4j"; @@ -435,160 +432,6 @@ describe("Custom Resolvers", () => { } }); - describe("auth", () => { - describe("should inject the auth into cypher directive on queries and mutations", () => { - test("query", async () => { - const session = driver.session(); - - const typeDefs = ` - type Query { - userId: ID @cypher(statement: """ - RETURN $auth.jwt.sub - """) - } - `; - - const userId = generate({ - charset: "alphabetic", - }); - - const secret = "secret"; - - const token = jsonwebtoken.sign({ sub: userId }, secret); - - const neoSchema = new Neo4jGraphQL({ typeDefs, config: { jwt: { secret } } }); - - const query = ` - { - userId - } - `; - - try { - const socket = new Socket({ readable: true }); - const req = new IncomingMessage(socket); - req.headers.authorization = `Bearer ${token}`; - - const gqlResult = await graphql({ - schema: neoSchema.schema, - source: query as string, - contextValue: { driver, req }, - }); - - expect(gqlResult.errors).toBeUndefined(); - - expect((gqlResult.data as any).userId).toEqual(userId); - } finally { - await session.close(); - } - }); - - test("mutation", async () => { - const session = driver.session(); - - const typeDefs = ` - type User { - id: ID - } - - type Mutation { - userId: ID @cypher(statement: """ - RETURN $auth.jwt.sub - """) - } - `; - - const userId = generate({ - charset: "alphabetic", - }); - - const secret = "secret"; - - const token = jsonwebtoken.sign({ sub: userId }, secret); - - const neoSchema = new Neo4jGraphQL({ typeDefs, config: { jwt: { secret } } }); - - const query = ` - mutation { - userId - } - `; - - try { - const socket = new Socket({ readable: true }); - const req = new IncomingMessage(socket); - req.headers.authorization = `Bearer ${token}`; - - const gqlResult = await graphql({ - schema: neoSchema.schema, - source: query as string, - contextValue: { driver, req }, - }); - - expect(gqlResult.errors).toBeUndefined(); - - expect((gqlResult.data as any).userId).toEqual(userId); - } finally { - await session.close(); - } - }); - }); - - test("should inject the auth into cypher directive on fields", async () => { - const session = driver.session(); - - const typeDefs = ` - type User { - id: ID - userId: ID @cypher(statement: """ - WITH $auth.jwt.sub as a - RETURN a - """) - } - `; - - const userId = generate({ - charset: "alphabetic", - }); - - const secret = "secret"; - - const token = jsonwebtoken.sign({ sub: userId }, secret); - - const neoSchema = new Neo4jGraphQL({ typeDefs, config: { jwt: { secret } } }); - - const query = ` - { - users(where: {id: "${userId}"}){ - userId - } - } - `; - - try { - await session.run(` - CREATE (:User {id: "${userId}"}) - `); - - const socket = new Socket({ readable: true }); - const req = new IncomingMessage(socket); - req.headers.authorization = `Bearer ${token}`; - - const gqlResult = await graphql({ - schema: neoSchema.schema, - source: query, - contextValue: { driver, req }, - }); - - expect(gqlResult.errors).toBeUndefined(); - - expect((gqlResult.data as any).users[0].userId).toEqual(userId); - } finally { - await session.close(); - } - }); - }); - test("should return an enum from a cypher directive (top level)", async () => { const typeDefs = ` enum Status { diff --git a/yarn.lock b/yarn.lock index 201f95e5b4..67a7e96852 100644 --- a/yarn.lock +++ b/yarn.lock @@ -934,7 +934,7 @@ __metadata: "@types/faker": 5.1.7 "@types/is-uuid": 1.0.0 "@types/jest": 26.0.8 - "@types/jsonwebtoken": 8.5.0 + "@types/jsonwebtoken": ^8.5.4 "@types/node": 14.0.27 "@types/pluralize": 0.0.29 "@types/randomstring": 1.1.6 @@ -1594,21 +1594,21 @@ __metadata: languageName: node linkType: hard -"@types/jsonwebtoken@npm:8.5.0": - version: 8.5.0 - resolution: "@types/jsonwebtoken@npm:8.5.0" +"@types/jsonwebtoken@npm:8.5.1": + version: 8.5.1 + resolution: "@types/jsonwebtoken@npm:8.5.1" dependencies: "@types/node": "*" - checksum: d1a6b28f826898bc4f5596d91705aa864419c4a5ca49215507260644af8ceff9edb6db1901a108207cfb22558d035655b55309f38b75fa081da01be9f5408b21 + checksum: f66ee4a12584a699a6dba5daa79d0f87ae77950a332f918c7f485b6d33501be84547fcac992d7f5a8960d04f77f206b118d9a558d8dd49f1aea99c64a17182b0 languageName: node linkType: hard -"@types/jsonwebtoken@npm:8.5.1": - version: 8.5.1 - resolution: "@types/jsonwebtoken@npm:8.5.1" +"@types/jsonwebtoken@npm:^8.5.4": + version: 8.5.4 + resolution: "@types/jsonwebtoken@npm:8.5.4" dependencies: "@types/node": "*" - checksum: f66ee4a12584a699a6dba5daa79d0f87ae77950a332f918c7f485b6d33501be84547fcac992d7f5a8960d04f77f206b118d9a558d8dd49f1aea99c64a17182b0 + checksum: 605ab698b007f04a1d1665b6af0dd31b0787c700c44dbca4474cebff6ae0e8435cc1166ae7d6895de3b6241f5a9ab62bea154ae5fdc7ac09cb13ab59dea4864d languageName: node linkType: hard From decaf85058c867801a21478f59f9db5e7a00bb81 Mon Sep 17 00:00:00 2001 From: Neo Technology Build Agent Date: Mon, 12 Jul 2021 09:49:25 +0000 Subject: [PATCH 19/20] Version update --- examples/migration/package.json | 2 +- examples/neo-push/server/package.json | 4 ++-- packages/graphql/package.json | 2 +- packages/ogm/package.json | 4 ++-- yarn.lock | 12 ++++++------ 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/examples/migration/package.json b/examples/migration/package.json index 5a0faf4dfa..514aa7a592 100644 --- a/examples/migration/package.json +++ b/examples/migration/package.json @@ -4,7 +4,7 @@ "start": "node src/index.js" }, "dependencies": { - "@neo4j/graphql": "^1.1.0", + "@neo4j/graphql": "^1.2.0", "apollo-server": "^2.23.0", "graphql": "^15.0.0", "neo4j-driver": "^4.2.0" diff --git a/examples/neo-push/server/package.json b/examples/neo-push/server/package.json index ffa04b00da..e15534f13c 100644 --- a/examples/neo-push/server/package.json +++ b/examples/neo-push/server/package.json @@ -12,8 +12,8 @@ "author": "", "license": "ISC", "dependencies": { - "@neo4j/graphql": "^1.1.0", - "@neo4j/graphql-ogm": "^1.1.0", + "@neo4j/graphql": "^1.2.0", + "@neo4j/graphql-ogm": "^1.2.0", "apollo-server-express": "2.19.0", "bcrypt": "5.0.1", "debug": "4.3.1", diff --git a/packages/graphql/package.json b/packages/graphql/package.json index 2a601a6d73..b0a6589965 100644 --- a/packages/graphql/package.json +++ b/packages/graphql/package.json @@ -1,6 +1,6 @@ { "name": "@neo4j/graphql", - "version": "1.1.0", + "version": "1.2.0", "description": "A GraphQL to Cypher query execution layer for Neo4j and JavaScript GraphQL implementations", "keywords": [ "neo4j", diff --git a/packages/ogm/package.json b/packages/ogm/package.json index 7ff9338692..3f5b1fc100 100644 --- a/packages/ogm/package.json +++ b/packages/ogm/package.json @@ -1,6 +1,6 @@ { "name": "@neo4j/graphql-ogm", - "version": "1.1.0", + "version": "1.2.0", "description": "GraphQL powered OGM for Neo4j and Javascript applications", "keywords": [ "neo4j", @@ -27,7 +27,7 @@ "author": "Neo4j Inc.", "dependencies": { "@graphql-tools/merge": "^6.2.13", - "@neo4j/graphql": "^1.1.0", + "@neo4j/graphql": "^1.2.0", "camelcase": "^6.2.0", "pluralize": "^8.0.0" }, diff --git a/yarn.lock b/yarn.lock index 67a7e96852..e879ef3d7d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -899,12 +899,12 @@ __metadata: languageName: node linkType: hard -"@neo4j/graphql-ogm@^1.1.0, @neo4j/graphql-ogm@workspace:packages/ogm": +"@neo4j/graphql-ogm@^1.2.0, @neo4j/graphql-ogm@workspace:packages/ogm": version: 0.0.0-use.local resolution: "@neo4j/graphql-ogm@workspace:packages/ogm" dependencies: "@graphql-tools/merge": ^6.2.13 - "@neo4j/graphql": ^1.1.0 + "@neo4j/graphql": ^1.2.0 "@types/jest": 26.0.8 "@types/node": 14.0.27 camelcase: ^6.2.0 @@ -923,7 +923,7 @@ __metadata: languageName: unknown linkType: soft -"@neo4j/graphql@^1.1.0, @neo4j/graphql@workspace:packages/graphql": +"@neo4j/graphql@^1.2.0, @neo4j/graphql@workspace:packages/graphql": version: 0.0.0-use.local resolution: "@neo4j/graphql@workspace:packages/graphql" dependencies: @@ -9335,7 +9335,7 @@ fsevents@^1.2.7: version: 0.0.0-use.local resolution: "migration@workspace:examples/migration" dependencies: - "@neo4j/graphql": ^1.1.0 + "@neo4j/graphql": ^1.2.0 apollo-server: ^2.23.0 graphql: ^15.0.0 neo4j-driver: ^4.2.0 @@ -9686,8 +9686,8 @@ fsevents@^1.2.7: version: 0.0.0-use.local resolution: "neo-push-server@workspace:examples/neo-push/server" dependencies: - "@neo4j/graphql": ^1.1.0 - "@neo4j/graphql-ogm": ^1.1.0 + "@neo4j/graphql": ^1.2.0 + "@neo4j/graphql-ogm": ^1.2.0 "@types/bcrypt": 3.0.0 "@types/debug": 4.1.5 "@types/dotenv": 8.2.0 From 53187d14b2ef74a5267a458f28cfbea220e348fb Mon Sep 17 00:00:00 2001 From: Darrell Warde Date: Mon, 12 Jul 2021 14:08:11 +0100 Subject: [PATCH 20/20] Replace instances of "properties" with "relationship" --- .../asciidoc/type-definitions/relationships.adoc | 4 ++-- .../graphql/src/schema/make-augmented-schema.ts | 8 ++++---- .../src/translate/create-connect-and-params.ts | 6 +++--- .../src/translate/create-create-and-params.ts | 6 +++--- .../src/translate/create-update-and-params.ts | 6 +++--- .../graphql/src/translate/translate-update.ts | 6 +++--- .../integration/connection-resolvers-int.test.ts | 16 ++++++++-------- .../integration/connections/enums.int.test.ts | 2 +- .../relationship-properties/connect.int.test.ts | 8 ++++---- .../relationship-properties/create.int.test.ts | 4 ++-- .../relationship_properties/connect.md | 8 ++++---- .../relationship_properties/create.md | 2 +- .../relationship_properties/update.md | 4 ++-- .../tck-test-files/schema/connections/enums.md | 8 ++++---- .../tck-test-files/schema/connections/unions.md | 16 ++++++++-------- .../schema/relationship-properties.md | 8 ++++---- 16 files changed, 56 insertions(+), 56 deletions(-) diff --git a/docs/asciidoc/type-definitions/relationships.adoc b/docs/asciidoc/type-definitions/relationships.adoc index 32f80ec89a..64e3a65bd5 100644 --- a/docs/asciidoc/type-definitions/relationships.adoc +++ b/docs/asciidoc/type-definitions/relationships.adoc @@ -128,7 +128,7 @@ mutation CreateActor { where: { node: { title: "Forrest Gump" } } - properties: { + relationship: { roles: ["Forrest"] } } @@ -182,7 +182,7 @@ mutation CreateMovieDirectorAndActor { name: "Tom Hanks" born: 1956 } - properties: { + relationship: { roles: ["Forrest"] } } diff --git a/packages/graphql/src/schema/make-augmented-schema.ts b/packages/graphql/src/schema/make-augmented-schema.ts index 9094f22e30..ee228562c5 100644 --- a/packages/graphql/src/schema/make-augmented-schema.ts +++ b/packages/graphql/src/schema/make-augmented-schema.ts @@ -662,7 +662,7 @@ function makeAugmentedSchema( name: createName, fields: { node: `${n.name}CreateInput!`, - ...(rel.properties ? { properties: `${rel.properties}CreateInput!` } : {}), + ...(rel.properties ? { relationship: `${rel.properties}CreateInput!` } : {}), }, }); @@ -699,7 +699,7 @@ function makeAugmentedSchema( : `${n.name}ConnectInput`, } : {}), - ...(rel.properties ? { properties: `${rel.properties}CreateInput!` } : {}), + ...(rel.properties ? { relationship: `${rel.properties}CreateInput!` } : {}), }, }); @@ -859,7 +859,7 @@ function makeAugmentedSchema( fields: { node: `${n.name}CreateInput!`, ...(rel.properties - ? { properties: `${rel.properties}CreateInput${anyNonNullRelProperties ? `!` : ""}` } + ? { relationship: `${rel.properties}CreateInput${anyNonNullRelProperties ? `!` : ""}` } : {}), }, }); @@ -886,7 +886,7 @@ function makeAugmentedSchema( ? { connect: rel.typeMeta.array ? `[${n.name}ConnectInput!]` : `${n.name}ConnectInput` } : {}), ...(rel.properties - ? { properties: `${rel.properties}CreateInput${anyNonNullRelProperties ? `!` : ""}` } + ? { relationship: `${rel.properties}CreateInput${anyNonNullRelProperties ? `!` : ""}` } : {}), }, }); diff --git a/packages/graphql/src/translate/create-connect-and-params.ts b/packages/graphql/src/translate/create-connect-and-params.ts index 6f7e7f8a30..81074337fb 100644 --- a/packages/graphql/src/translate/create-connect-and-params.ts +++ b/packages/graphql/src/translate/create-connect-and-params.ts @@ -60,7 +60,7 @@ function createConnectAndParams({ const relationshipName = `${baseName}_relationship`; const inStr = relationField.direction === "IN" ? "<-" : "-"; const outStr = relationField.direction === "OUT" ? "->" : "-"; - const relTypeStr = `[${connect.properties ? relationshipName : ""}:${relationField.type}]`; + const relTypeStr = `[${connect.relationship ? relationshipName : ""}:${relationField.type}]`; if (parentNode.auth && !fromCreate) { const whereAuth = createAuthAndParams({ @@ -155,13 +155,13 @@ function createConnectAndParams({ res.connects.push(`FOREACH(_ IN CASE ${nodeName} WHEN NULL THEN [] ELSE [1] END | `); res.connects.push(`MERGE (${parentVar})${inStr}${relTypeStr}${outStr}(${nodeName})`); - if (connect.properties) { + if (connect.relationship) { const relationship = (context.neoSchema.relationships.find( (x) => x.properties === relationField.properties ) as unknown) as Relationship; const setA = createSetRelationshipPropertiesAndParams({ - properties: connect.properties, + properties: connect.relationship, varName: relationshipName, relationship, operation: "CREATE", diff --git a/packages/graphql/src/translate/create-create-and-params.ts b/packages/graphql/src/translate/create-create-and-params.ts index b6fd47d059..d018feed41 100644 --- a/packages/graphql/src/translate/create-create-and-params.ts +++ b/packages/graphql/src/translate/create-create-and-params.ts @@ -96,16 +96,16 @@ function createCreateAndParams({ const inStr = relationField.direction === "IN" ? "<-" : "-"; const outStr = relationField.direction === "OUT" ? "->" : "-"; - const relTypeStr = `[${create.properties ? propertiesName : ""}:${relationField.type}]`; + const relTypeStr = `[${create.relationship ? propertiesName : ""}:${relationField.type}]`; res.creates.push(`MERGE (${varName})${inStr}${relTypeStr}${outStr}(${nodeName})`); - if (create.properties) { + if (create.relationship) { const relationship = (context.neoSchema.relationships.find( (x) => x.properties === relationField.properties ) as unknown) as Relationship; const setA = createSetRelationshipPropertiesAndParams({ - properties: create.properties, + properties: create.relationship, varName: propertiesName, relationship, operation: "CREATE", diff --git a/packages/graphql/src/translate/create-update-and-params.ts b/packages/graphql/src/translate/create-update-and-params.ts index ba0a32d496..7b465931c4 100644 --- a/packages/graphql/src/translate/create-update-and-params.ts +++ b/packages/graphql/src/translate/create-update-and-params.ts @@ -216,7 +216,7 @@ function createUpdateAndParams({ } else { updateStrs.push(`", "", ${apocArgs})`); } - updateStrs.push(`YIELD value as ${relationshipVariable}_${key}${index}_properties`); + updateStrs.push(`YIELD value as ${relationshipVariable}_${key}${index}_relationship`); res.strs.push(updateStrs.join("\n")); } } @@ -306,9 +306,9 @@ function createUpdateAndParams({ }]${outStr}(${nodeName})` ); - if (create.properties) { + if (create.relationship) { const setA = createSetRelationshipPropertiesAndParams({ - properties: create.properties, + properties: create.relationship, varName: propertiesName, relationship, operation: "CREATE", diff --git a/packages/graphql/src/translate/translate-update.ts b/packages/graphql/src/translate/translate-update.ts index d2988cb081..3ebff9ad98 100644 --- a/packages/graphql/src/translate/translate-update.ts +++ b/packages/graphql/src/translate/translate-update.ts @@ -205,7 +205,7 @@ function translateUpdate({ node, context }: { node: Node; context: Context }): [ }${index}`; const nodeName = `${baseName}_node`; const propertiesName = `${baseName}_relationship`; - const relTypeStr = `[${create.properties ? propertiesName : ""}:${relationField.type}]`; + const relTypeStr = `[${create.relationship ? propertiesName : ""}:${relationField.type}]`; const createAndParams = createCreateAndParams({ context, @@ -218,13 +218,13 @@ function translateUpdate({ node, context }: { node: Node; context: Context }): [ cypherParams = { ...cypherParams, ...createAndParams[1] }; createStrs.push(`MERGE (${varName})${inStr}${relTypeStr}${outStr}(${nodeName})`); - if (create.properties) { + if (create.relationship) { const relationship = (context.neoSchema.relationships.find( (x) => x.properties === relationField.properties ) as unknown) as Relationship; const setA = createSetRelationshipPropertiesAndParams({ - properties: create.properties, + properties: create.relationship, varName: propertiesName, relationship, operation: "CREATE", diff --git a/packages/graphql/tests/integration/connection-resolvers-int.test.ts b/packages/graphql/tests/integration/connection-resolvers-int.test.ts index 10874eb4c8..467e5cce5a 100644 --- a/packages/graphql/tests/integration/connection-resolvers-int.test.ts +++ b/packages/graphql/tests/integration/connection-resolvers-int.test.ts @@ -79,7 +79,7 @@ describe("Connection Resolvers", () => { id: "${actorId}", name: "Keanu Reeves" }, - properties: { + relationship: { screenTime: 100 } }] @@ -201,7 +201,7 @@ describe("Connection Resolvers", () => { id: x.toString(), name: String.fromCharCode(x + 1 + 64) + generate({ charset: "alphabetic" }), }, - properties: { + relationship: { screenTime: Math.floor(Math.random() * 200), }, })); @@ -237,9 +237,9 @@ describe("Connection Resolvers", () => { title: movieTitle, actorsConnection: { totalCount: 20, - edges: actors.slice(0, 5).map(({ node, properties }) => ({ + edges: actors.slice(0, 5).map(({ node, relationship }) => ({ node, - screenTime: properties.screenTime, + screenTime: relationship.screenTime, cursor: expect.any(String), })), pageInfo: { @@ -294,10 +294,10 @@ describe("Connection Resolvers", () => { title: movieTitle, actorsConnection: { totalCount: 20, - edges: actors.slice(5, 10).map(({ node, properties }) => ({ + edges: actors.slice(5, 10).map(({ node, relationship }) => ({ node, cursor: expect.any(String), - screenTime: properties.screenTime, + screenTime: relationship.screenTime, })), pageInfo: { hasPreviousPage: true, @@ -325,10 +325,10 @@ describe("Connection Resolvers", () => { title: movieTitle, actorsConnection: { totalCount: 20, - edges: actors.slice(10, 15).map(({ node, properties }) => ({ + edges: actors.slice(10, 15).map(({ node, relationship }) => ({ node, cursor: expect.any(String), - screenTime: properties.screenTime, + screenTime: relationship.screenTime, })), pageInfo: { hasPreviousPage: true, diff --git a/packages/graphql/tests/integration/connections/enums.int.test.ts b/packages/graphql/tests/integration/connections/enums.int.test.ts index 6f7564fddc..e9d50664d6 100644 --- a/packages/graphql/tests/integration/connections/enums.int.test.ts +++ b/packages/graphql/tests/integration/connections/enums.int.test.ts @@ -85,7 +85,7 @@ describe("Enum Relationship Properties", () => { actors: { create: [ { - properties: { roleType: LEADING } + relationship: { roleType: LEADING } node: { name: $name } } ] diff --git a/packages/graphql/tests/integration/relationship-properties/connect.int.test.ts b/packages/graphql/tests/integration/relationship-properties/connect.int.test.ts index ef4808e152..57c13ac138 100644 --- a/packages/graphql/tests/integration/relationship-properties/connect.int.test.ts +++ b/packages/graphql/tests/integration/relationship-properties/connect.int.test.ts @@ -70,7 +70,7 @@ describe("Relationship properties - connect", () => { actors: { connect: [{ where: { node: { name: $actorName } }, - properties: { screenTime: $screenTime }, + relationship: { screenTime: $screenTime }, }] } } @@ -169,7 +169,7 @@ describe("Relationship properties - connect", () => { where: { node: { title: $movieTitle } }, - properties: { + relationship: { screenTime: $screenTime } } @@ -251,7 +251,7 @@ describe("Relationship properties - connect", () => { connect: { actors: { where: { node: { name: $actorName } } - properties: { screenTime: $screenTime } + relationship: { screenTime: $screenTime } } } ) { @@ -343,7 +343,7 @@ describe("Relationship properties - connect", () => { actedIn: { Movie: { where: { node: { title: $movieTitle } } - properties: { screenTime: $screenTime } + relationship: { screenTime: $screenTime } } } } diff --git a/packages/graphql/tests/integration/relationship-properties/create.int.test.ts b/packages/graphql/tests/integration/relationship-properties/create.int.test.ts index aec52fffc9..51b59e3372 100644 --- a/packages/graphql/tests/integration/relationship-properties/create.int.test.ts +++ b/packages/graphql/tests/integration/relationship-properties/create.int.test.ts @@ -69,7 +69,7 @@ describe("Relationship properties - create", () => { title: $movieTitle actors: { create: [{ - properties: { screenTime: $screenTime }, + relationship: { screenTime: $screenTime }, node: { name: $actorName } }] } @@ -156,7 +156,7 @@ describe("Relationship properties - create", () => { publications: { Movie: { create: [{ - properties: { words: $words }, + relationship: { words: $words }, node: { title: $movieTitle } }] } diff --git a/packages/graphql/tests/tck/tck-test-files/cypher/connections/relationship_properties/connect.md b/packages/graphql/tests/tck/tck-test-files/cypher/connections/relationship_properties/connect.md index 947aded9da..3423b8aa64 100644 --- a/packages/graphql/tests/tck/tck-test-files/cypher/connections/relationship_properties/connect.md +++ b/packages/graphql/tests/tck/tck-test-files/cypher/connections/relationship_properties/connect.md @@ -30,7 +30,7 @@ mutation { input: [ { title: "Forrest Gump" - actors: { connect: [{ properties: { screenTime: 60 } }] } + actors: { connect: [{ relationship: { screenTime: 60 } }] } } ] ) { @@ -102,7 +102,7 @@ mutation { connect: [ { where: { node: { name: "Tom Hanks" } } - properties: { screenTime: 60 } + relationship: { screenTime: 60 } } ] } @@ -173,7 +173,7 @@ this0 { .title, actorsConnection } AS this0 mutation { updateMovies( where: { title: "Forrest Gump" } - connect: { actors: { properties: { screenTime: 60 } } } + connect: { actors: { relationship: { screenTime: 60 } } } ) { movies { title @@ -236,7 +236,7 @@ mutation { connect: { actors: { where: { node: { name: "Tom Hanks" } } - properties: { screenTime: 60 } + relationship: { screenTime: 60 } } } ) { diff --git a/packages/graphql/tests/tck/tck-test-files/cypher/connections/relationship_properties/create.md b/packages/graphql/tests/tck/tck-test-files/cypher/connections/relationship_properties/create.md index 1c490bc2b9..b9f9f9201c 100644 --- a/packages/graphql/tests/tck/tck-test-files/cypher/connections/relationship_properties/create.md +++ b/packages/graphql/tests/tck/tck-test-files/cypher/connections/relationship_properties/create.md @@ -34,7 +34,7 @@ mutation { create: [ { node: { name: "Tom Hanks" } - properties: { screenTime: 60 } + relationship: { screenTime: 60 } } ] } diff --git a/packages/graphql/tests/tck/tck-test-files/cypher/connections/relationship_properties/update.md b/packages/graphql/tests/tck/tck-test-files/cypher/connections/relationship_properties/update.md index 3c3de1cb93..8b773798ee 100644 --- a/packages/graphql/tests/tck/tck-test-files/cypher/connections/relationship_properties/update.md +++ b/packages/graphql/tests/tck/tck-test-files/cypher/connections/relationship_properties/update.md @@ -58,7 +58,7 @@ CALL apoc.do.when(this_acted_in0 IS NOT NULL, " SET this_acted_in0.screenTime = $updateMovies.args.update.actors[0].update.relationship.screenTime RETURN count(*) ", "", {this_acted_in0:this_acted_in0, updateMovies: $updateMovies}) -YIELD value as this_acted_in0_actors0_properties +YIELD value as this_acted_in0_actors0_relationship RETURN this { .title } AS this ``` @@ -143,7 +143,7 @@ CALL apoc.do.when(this_acted_in0 IS NOT NULL, " SET this_acted_in0.screenTime = $updateMovies.args.update.actors[0].update.relationship.screenTime RETURN count(*) ", "", {this_acted_in0:this_acted_in0, updateMovies: $updateMovies}) -YIELD value as this_acted_in0_actors0_properties +YIELD value as this_acted_in0_actors0_relationship RETURN this { .title } AS this ``` diff --git a/packages/graphql/tests/tck/tck-test-files/schema/connections/enums.md b/packages/graphql/tests/tck/tck-test-files/schema/connections/enums.md index 9e071641f9..7b15c4e3c5 100644 --- a/packages/graphql/tests/tck/tck-test-files/schema/connections/enums.md +++ b/packages/graphql/tests/tck/tck-test-files/schema/connections/enums.md @@ -107,7 +107,7 @@ input MovieConnectWhere { input ActorMoviesConnectFieldInput { where: MovieConnectWhere connect: [MovieConnectInput!] - properties: ActedInCreateInput! + relationship: ActedInCreateInput! } type ActorMoviesConnection { @@ -132,7 +132,7 @@ input ActorMoviesConnectionWhere { input ActorMoviesCreateFieldInput { node: MovieCreateInput! - properties: ActedInCreateInput! + relationship: ActedInCreateInput! } input ActorMoviesFieldInput { @@ -233,7 +233,7 @@ input ActorConnectWhere { input MovieActorsConnectFieldInput { where: ActorConnectWhere connect: [ActorConnectInput!] - properties: ActedInCreateInput! + relationship: ActedInCreateInput! } type MovieActorsConnection { @@ -258,7 +258,7 @@ input MovieActorsConnectionWhere { input MovieActorsCreateFieldInput { node: ActorCreateInput! - properties: ActedInCreateInput! + relationship: ActedInCreateInput! } input MovieActorsFieldInput { diff --git a/packages/graphql/tests/tck/tck-test-files/schema/connections/unions.md b/packages/graphql/tests/tck/tck-test-files/schema/connections/unions.md index 1bad2fe854..432dbbd0eb 100644 --- a/packages/graphql/tests/tck/tck-test-files/schema/connections/unions.md +++ b/packages/graphql/tests/tck/tck-test-files/schema/connections/unions.md @@ -82,7 +82,7 @@ input AuthorPublicationsBookConnectionWhere { input AuthorPublicationsBookCreateFieldInput { node: BookCreateInput! - properties: WroteCreateInput! + relationship: WroteCreateInput! } input AuthorPublicationsBookDeleteFieldInput { @@ -125,13 +125,13 @@ input JournalConnectWhere { input AuthorPublicationsBookConnectFieldInput { where: BookConnectWhere connect: [BookConnectInput!] - properties: WroteCreateInput! + relationship: WroteCreateInput! } input AuthorPublicationsJournalConnectFieldInput { where: JournalConnectWhere connect: [JournalConnectInput!] - properties: WroteCreateInput! + relationship: WroteCreateInput! } type AuthorPublicationsConnection { @@ -192,7 +192,7 @@ input AuthorPublicationsJournalConnectionWhere { input AuthorPublicationsJournalCreateFieldInput { node: JournalCreateInput! - properties: WroteCreateInput! + relationship: WroteCreateInput! } input AuthorPublicationsJournalDeleteFieldInput { @@ -279,7 +279,7 @@ input AuthorConnectWhere { input BookAuthorConnectFieldInput { where: AuthorConnectWhere connect: [AuthorConnectInput!] - properties: WroteCreateInput! + relationship: WroteCreateInput! } type BookAuthorConnection { @@ -304,7 +304,7 @@ input BookAuthorConnectionWhere { input BookAuthorCreateFieldInput { node: AuthorCreateInput! - properties: WroteCreateInput! + relationship: WroteCreateInput! } input BookAuthorDeleteFieldInput { @@ -432,7 +432,7 @@ type Journal { input JournalAuthorConnectFieldInput { where: AuthorConnectWhere connect: [AuthorConnectInput!] - properties: WroteCreateInput! + relationship: WroteCreateInput! } type JournalAuthorConnection { @@ -457,7 +457,7 @@ input JournalAuthorConnectionWhere { input JournalAuthorCreateFieldInput { node: AuthorCreateInput! - properties: WroteCreateInput! + relationship: WroteCreateInput! } input JournalAuthorDeleteFieldInput { diff --git a/packages/graphql/tests/tck/tck-test-files/schema/relationship-properties.md b/packages/graphql/tests/tck/tck-test-files/schema/relationship-properties.md index 2dd3be293c..71680295ce 100644 --- a/packages/graphql/tests/tck/tck-test-files/schema/relationship-properties.md +++ b/packages/graphql/tests/tck/tck-test-files/schema/relationship-properties.md @@ -101,7 +101,7 @@ input MovieConnectWhere { input ActorMoviesConnectFieldInput { where: MovieConnectWhere connect: [MovieConnectInput!] - properties: ActedInCreateInput! + relationship: ActedInCreateInput! } type ActorMoviesConnection { @@ -126,7 +126,7 @@ input ActorMoviesConnectionWhere { input ActorMoviesCreateFieldInput { node: MovieCreateInput! - properties: ActedInCreateInput! + relationship: ActedInCreateInput! } input ActorMoviesFieldInput { @@ -227,7 +227,7 @@ type Movie { input MovieActorsConnectFieldInput { where: ActorConnectWhere connect: [ActorConnectInput!] - properties: ActedInCreateInput! + relationship: ActedInCreateInput! } type MovieActorsConnection { @@ -252,7 +252,7 @@ input MovieActorsConnectionWhere { input MovieActorsCreateFieldInput { node: ActorCreateInput! - properties: ActedInCreateInput! + relationship: ActedInCreateInput! } input MovieActorsFieldInput {