From 2a4c5980ee4b76e80723fbc73094c2cd0bfb9708 Mon Sep 17 00:00:00 2001 From: Darrell Warde Date: Tue, 25 May 2021 12:57:12 +0100 Subject: [PATCH 1/2] Current progress on getting Neo4j values into resolve tree --- packages/graphql/src/classes/Neo4jGraphQL.ts | 8 +- packages/graphql/src/schema/scalars/index.ts | 2 - .../src/utils/get-neo4j-resolve-tree.ts | 140 ++++++++++++++++++ packages/graphql/tests/tck/tck.test.ts | 15 +- 4 files changed, 156 insertions(+), 9 deletions(-) create mode 100644 packages/graphql/src/utils/get-neo4j-resolve-tree.ts diff --git a/packages/graphql/src/classes/Neo4jGraphQL.ts b/packages/graphql/src/classes/Neo4jGraphQL.ts index 1cb0b67198..ec35c0bb0f 100644 --- a/packages/graphql/src/classes/Neo4jGraphQL.ts +++ b/packages/graphql/src/classes/Neo4jGraphQL.ts @@ -21,13 +21,14 @@ 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 { parseResolveInfo, ResolveTree } from "graphql-parse-resolve-info"; +// import { parseResolveInfo, ResolveTree } from "graphql-parse-resolve-info"; import type { DriverConfig } from "../types"; import { makeAugmentedSchema } from "../schema"; import Node from "./Node"; import { checkNeo4jCompat } from "../utils"; import { getJWT } from "../auth/index"; import { DEBUG_GRAPHQL } from "../constants"; +import getNeo4jResolveTree from "../utils/get-neo4j-resolve-tree"; const debug = Debug(DEBUG_GRAPHQL); @@ -114,7 +115,10 @@ class Neo4jGraphQL { } context.neoSchema = this; - context.resolveTree = parseResolveInfo(resolveInfo) as ResolveTree; + // context.resolveTree = parseResolveInfo(resolveInfo) as ResolveTree; + + context.resolveTree = getNeo4jResolveTree(resolveInfo); + context.jwt = getJWT(context); }); } diff --git a/packages/graphql/src/schema/scalars/index.ts b/packages/graphql/src/schema/scalars/index.ts index 3e62cf5f1e..92b6325cb3 100644 --- a/packages/graphql/src/schema/scalars/index.ts +++ b/packages/graphql/src/schema/scalars/index.ts @@ -17,8 +17,6 @@ * limitations under the License. */ -export { default as Float } from "./Float"; -export { default as Int } from "./Int"; export { default as BigInt } from "./BigInt"; export { default as ID } from "./ID"; export { default as DateTime } from "./DateTime"; diff --git a/packages/graphql/src/utils/get-neo4j-resolve-tree.ts b/packages/graphql/src/utils/get-neo4j-resolve-tree.ts new file mode 100644 index 0000000000..127090447c --- /dev/null +++ b/packages/graphql/src/utils/get-neo4j-resolve-tree.ts @@ -0,0 +1,140 @@ +import { + ArgumentNode, + FieldNode, + GraphQLResolveInfo, + ObjectFieldNode, + SelectionNode, + ValueNode, + VariableDefinitionNode, +} from "graphql"; +import { parseResolveInfo, ResolveTree } from "graphql-parse-resolve-info"; +import neo4j from "neo4j-driver"; + +function getNeo4jArgumentValue( + value: any, + valueNode: ValueNode, + variableDefinitions: readonly VariableDefinitionNode[] | undefined +) { + switch (valueNode.kind) { + case "BooleanValue": + case "EnumValue": + case "FloatValue": + case "NullValue": + case "StringValue": + return value; + case "IntValue": + return neo4j.int(value); + case "Variable": + // eslint-disable-next-line no-case-declarations + const variable = variableDefinitions?.find((v) => v.variable.name.value === valueNode.name.value); + + switch (variable?.type.kind) { + case "ListType": + break; + case "NamedType": + if (variable.type.name.value === "Int") { + return neo4j.int(value); + } + return value; + case "NonNullType": + break; + default: + throw new Error(); + } + + return value; + case "ListValue": + return value.map((v, i) => getNeo4jArgumentValue(v, valueNode.values[i], variableDefinitions)); + case "ObjectValue": + return Object.entries(value).reduce((res, [k, v]) => { + const vNode = valueNode.fields.find((f) => f.name.value === k) as ObjectFieldNode; + + if (Array.isArray(v) && vNode.value.kind !== "ListValue") { + return { ...res, [k]: v.map((v1) => getNeo4jArgumentValue(v1, vNode.value, variableDefinitions)) }; + } + + return { ...res, [k]: getNeo4jArgumentValue(v, vNode.value, variableDefinitions) }; + }, {}); + default: + throw new Error(); + } +} + +function getNeo4jFieldsByTypeName( + resolveTree: ResolveTree, + fieldNodes: readonly SelectionNode[], + variableDefinitions: readonly VariableDefinitionNode[] | undefined +) { + const fieldNode = fieldNodes.find((n) => n.kind === "Field" && n.name.value === resolveTree.name) as FieldNode; + + if (!fieldNode) return resolveTree; + + const args = Object.entries(resolveTree.args).reduce((a, [name, value]) => { + const argumentNode = fieldNode.arguments?.find((argument) => argument.name.value === name) as ArgumentNode; + + return { + ...a, + [name]: getNeo4jArgumentValue(value, argumentNode?.value, variableDefinitions), + }; + }, {}); + + const fieldsByTypeName = Object.entries(resolveTree.fieldsByTypeName).reduce((f, [k, v]) => { + const resolveTrees = Object.entries(v).reduce((t, [k1, v1]) => { + return { + ...t, + [k1]: getNeo4jFieldsByTypeName( + v1, + fieldNode.selectionSet?.selections as FieldNode[], + variableDefinitions + ), + }; + }, {}); + + return { + ...f, + [k]: resolveTrees, + }; + }, {}); + + const { alias, name } = resolveTree; + + return { alias, args, fieldsByTypeName, name } as ResolveTree; +} + +function getNeo4jResolveTree(resolveInfo: GraphQLResolveInfo) { + const resolveTree = parseResolveInfo(resolveInfo) as ResolveTree; + const fieldNode = resolveInfo.fieldNodes.find((n) => n.name.value === resolveTree.name) as FieldNode; + + const args = Object.entries(resolveTree.args).reduce((a, [name, value]) => { + const argumentNode = fieldNode.arguments?.find((argument) => argument.name.value === name) as ArgumentNode; + + return { + ...a, + [name]: getNeo4jArgumentValue(value, argumentNode?.value, resolveInfo.operation.variableDefinitions), + }; + }, {}); + + const fieldsByTypeName = Object.entries(resolveTree.fieldsByTypeName).reduce((f, [k, v]) => { + const resolveTrees = Object.entries(v).reduce((t, [k1, v1]) => { + return { + ...t, + [k1]: getNeo4jFieldsByTypeName( + v1, + fieldNode.selectionSet?.selections as FieldNode[], + resolveInfo.operation.variableDefinitions + ), + }; + }, {}); + + return { + ...f, + [k]: resolveTrees, + }; + }, {}); + + const { alias, name } = resolveTree; + + return { alias, args, fieldsByTypeName, name } as ResolveTree; +} + +export default getNeo4jResolveTree; diff --git a/packages/graphql/tests/tck/tck.test.ts b/packages/graphql/tests/tck/tck.test.ts index c5ea50a1d6..cee1e6f65c 100644 --- a/packages/graphql/tests/tck/tck.test.ts +++ b/packages/graphql/tests/tck/tck.test.ts @@ -34,7 +34,7 @@ import pluralize from "pluralize"; import jsonwebtoken from "jsonwebtoken"; import { IncomingMessage } from "http"; import { Socket } from "net"; -import { parseResolveInfo, ResolveTree } from "graphql-parse-resolve-info"; +// import { parseResolveInfo, ResolveTree } from "graphql-parse-resolve-info"; import { SchemaDirectiveVisitor, printSchemaWithDirectives } from "@graphql-tools/utils"; import { translateCreate, translateDelete, translateRead, translateUpdate } from "../../src/translate"; import { Context } from "../../src/types"; @@ -50,6 +50,7 @@ import { trimmer } from "../../src/utils"; import * as Scalars from "../../src/schema/scalars"; import { Node } from "../../src/classes"; import createAuthParam from "../../src/translate/create-auth-param"; +import getNeo4jResolveTree from "../../src/utils/get-neo4j-resolve-tree"; const TCK_DIR = path.join(__dirname, "tck-test-files"); @@ -140,7 +141,8 @@ describe("TCK Generated tests", () => { context: Context, info: GraphQLResolveInfo ) => { - const resolveTree = parseResolveInfo(info) as ResolveTree; + // const resolveTree = parseResolveInfo(info) as ResolveTree; + const resolveTree = getNeo4jResolveTree(info); context.neoSchema = neoSchema; context.resolveTree = resolveTree; @@ -176,7 +178,8 @@ describe("TCK Generated tests", () => { context: any, info: GraphQLResolveInfo ) => { - const resolveTree = parseResolveInfo(info) as ResolveTree; + // const resolveTree = parseResolveInfo(info) as ResolveTree; + const resolveTree = getNeo4jResolveTree(info); context.neoSchema = neoSchema; context.resolveTree = resolveTree; @@ -204,7 +207,8 @@ describe("TCK Generated tests", () => { context: any, info: GraphQLResolveInfo ) => { - const resolveTree = parseResolveInfo(info) as ResolveTree; + // const resolveTree = parseResolveInfo(info) as ResolveTree; + const resolveTree = getNeo4jResolveTree(info); context.neoSchema = neoSchema; context.resolveTree = resolveTree; @@ -227,7 +231,8 @@ describe("TCK Generated tests", () => { }; }, [`delete${pluralize(def.name.value)}`]: (_root: any, _params: any, context: any, info) => { - const resolveTree = parseResolveInfo(info) as ResolveTree; + // const resolveTree = parseResolveInfo(info) as ResolveTree; + const resolveTree = getNeo4jResolveTree(info); context.neoSchema = neoSchema; context.resolveTree = resolveTree; From f38481a95bfd7e0bd4b5370850944d9363bbe659 Mon Sep 17 00:00:00 2001 From: Darrell Warde Date: Wed, 2 Jun 2021 11:23:39 +0100 Subject: [PATCH 2/2] Fix for integer input parsing --- packages/graphql/src/classes/Neo4jGraphQL.ts | 2 - packages/graphql/src/schema/scalars/Int.ts | 18 +- packages/graphql/src/schema/scalars/index.ts | 4 +- .../src/utils/get-neo4j-resolve-tree.ts | 217 ++++++++++-------- packages/graphql/tests/tck/tck.test.ts | 4 - 5 files changed, 126 insertions(+), 119 deletions(-) diff --git a/packages/graphql/src/classes/Neo4jGraphQL.ts b/packages/graphql/src/classes/Neo4jGraphQL.ts index ec35c0bb0f..0c82d29fff 100644 --- a/packages/graphql/src/classes/Neo4jGraphQL.ts +++ b/packages/graphql/src/classes/Neo4jGraphQL.ts @@ -21,7 +21,6 @@ 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 { parseResolveInfo, ResolveTree } from "graphql-parse-resolve-info"; import type { DriverConfig } from "../types"; import { makeAugmentedSchema } from "../schema"; import Node from "./Node"; @@ -115,7 +114,6 @@ class Neo4jGraphQL { } context.neoSchema = this; - // context.resolveTree = parseResolveInfo(resolveInfo) as ResolveTree; context.resolveTree = getNeo4jResolveTree(resolveInfo); diff --git a/packages/graphql/src/schema/scalars/Int.ts b/packages/graphql/src/schema/scalars/Int.ts index 78a0cc2fac..568c583030 100644 --- a/packages/graphql/src/schema/scalars/Int.ts +++ b/packages/graphql/src/schema/scalars/Int.ts @@ -18,22 +18,16 @@ */ import { GraphQLScalarType } from "graphql"; -import { int, Integer } from "neo4j-driver"; +import { isInt, Integer } from "neo4j-driver"; export default new GraphQLScalarType({ name: "Int", - parseValue(value) { - if (typeof value !== "number") { - throw new Error("Cannot represent non number as Int"); + serialize(outputValue: unknown) { + // @ts-ignore: outputValue is unknown, and to cast to object would be an antipattern + if (isInt(outputValue)) { + return (outputValue as Integer).toNumber(); } - return int(value); - }, - serialize(value: Integer) { - if (value.toNumber) { - return value.toNumber(); - } - - return value; + return outputValue; }, }); diff --git a/packages/graphql/src/schema/scalars/index.ts b/packages/graphql/src/schema/scalars/index.ts index 92b6325cb3..4c5bc784c3 100644 --- a/packages/graphql/src/schema/scalars/index.ts +++ b/packages/graphql/src/schema/scalars/index.ts @@ -18,5 +18,7 @@ */ export { default as BigInt } from "./BigInt"; -export { default as ID } from "./ID"; export { default as DateTime } from "./DateTime"; +export { default as Float } from "./Float"; +export { default as ID } from "./ID"; +export { default as Int } from "./Int"; diff --git a/packages/graphql/src/utils/get-neo4j-resolve-tree.ts b/packages/graphql/src/utils/get-neo4j-resolve-tree.ts index 127090447c..56541e19ef 100644 --- a/packages/graphql/src/utils/get-neo4j-resolve-tree.ts +++ b/packages/graphql/src/utils/get-neo4j-resolve-tree.ts @@ -1,134 +1,151 @@ +/* + * 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 { - ArgumentNode, - FieldNode, + GraphQLField, + GraphQLInterfaceType, + GraphQLNamedType, + GraphQLNonNull, + GraphQLObjectType, GraphQLResolveInfo, - ObjectFieldNode, - SelectionNode, - ValueNode, - VariableDefinitionNode, + GraphQLInputObjectType, + GraphQLInputType, + GraphQLList, + GraphQLScalarType, } from "graphql"; import { parseResolveInfo, ResolveTree } from "graphql-parse-resolve-info"; import neo4j from "neo4j-driver"; -function getNeo4jArgumentValue( - value: any, - valueNode: ValueNode, - variableDefinitions: readonly VariableDefinitionNode[] | undefined -) { - switch (valueNode.kind) { - case "BooleanValue": - case "EnumValue": - case "FloatValue": - case "NullValue": - case "StringValue": - return value; - case "IntValue": - return neo4j.int(value); - case "Variable": - // eslint-disable-next-line no-case-declarations - const variable = variableDefinitions?.find((v) => v.variable.name.value === valueNode.name.value); - - switch (variable?.type.kind) { - case "ListType": - break; - case "NamedType": - if (variable.type.name.value === "Int") { - return neo4j.int(value); - } - return value; - case "NonNullType": - break; - default: - throw new Error(); - } - - return value; - case "ListValue": - return value.map((v, i) => getNeo4jArgumentValue(v, valueNode.values[i], variableDefinitions)); - case "ObjectValue": - return Object.entries(value).reduce((res, [k, v]) => { - const vNode = valueNode.fields.find((f) => f.name.value === k) as ObjectFieldNode; - - if (Array.isArray(v) && vNode.value.kind !== "ListValue") { - return { ...res, [k]: v.map((v1) => getNeo4jArgumentValue(v1, vNode.value, variableDefinitions)) }; - } - - return { ...res, [k]: getNeo4jArgumentValue(v, vNode.value, variableDefinitions) }; - }, {}); - default: - throw new Error(); +function getNeo4jArgumentValue({ argument, type }: { argument: unknown | unknown[]; type: GraphQLInputType }) { + if (argument === null) { + return argument; } -} -function getNeo4jFieldsByTypeName( - resolveTree: ResolveTree, - fieldNodes: readonly SelectionNode[], - variableDefinitions: readonly VariableDefinitionNode[] | undefined -) { - const fieldNode = fieldNodes.find((n) => n.kind === "Field" && n.name.value === resolveTree.name) as FieldNode; + if (type.toString().endsWith("!")) { + return getNeo4jArgumentValue({ argument, type: (type as GraphQLNonNull).ofType }); + } - if (!fieldNode) return resolveTree; + if (type.toString().startsWith("[") && type.toString().endsWith("]")) { + return (argument as unknown[]).map((a) => + getNeo4jArgumentValue({ argument: a, type: (type as GraphQLList).ofType }) + ); + } - const args = Object.entries(resolveTree.args).reduce((a, [name, value]) => { - const argumentNode = fieldNode.arguments?.find((argument) => argument.name.value === name) as ArgumentNode; + if (type instanceof GraphQLInputObjectType) { + return Object.entries(argument as Record).reduce((res, [key, value]) => { + const field = Object.values(type.getFields()).find((f) => f.name === key); - return { - ...a, - [name]: getNeo4jArgumentValue(value, argumentNode?.value, variableDefinitions), - }; - }, {}); + if (!field) { + throw new Error( + `Error whilst generating Neo4j resolve tree: could not find field ${key} in type ${type.name}` + ); + } - const fieldsByTypeName = Object.entries(resolveTree.fieldsByTypeName).reduce((f, [k, v]) => { - const resolveTrees = Object.entries(v).reduce((t, [k1, v1]) => { return { - ...t, - [k1]: getNeo4jFieldsByTypeName( - v1, - fieldNode.selectionSet?.selections as FieldNode[], - variableDefinitions - ), + ...res, + [key]: getNeo4jArgumentValue({ argument: value, type: field.type }), }; }, {}); + } - return { - ...f, - [k]: resolveTrees, - }; - }, {}); + if (type instanceof GraphQLScalarType) { + return type.name === "Int" ? neo4j.int(argument as number) : argument; + } - const { alias, name } = resolveTree; + return argument; +} - return { alias, args, fieldsByTypeName, name } as ResolveTree; +interface GetNeo4jResolveTreeOptions { + resolveTree: ResolveTree; + field: GraphQLField; } -function getNeo4jResolveTree(resolveInfo: GraphQLResolveInfo) { - const resolveTree = parseResolveInfo(resolveInfo) as ResolveTree; - const fieldNode = resolveInfo.fieldNodes.find((n) => n.name.value === resolveTree.name) as FieldNode; +function getNeo4jResolveTree(resolveInfo: GraphQLResolveInfo, options?: GetNeo4jResolveTreeOptions) { + const resolveTree = options?.resolveTree || (parseResolveInfo(resolveInfo) as ResolveTree); + + let field: GraphQLField; + + if (options?.field) { + field = options.field; + } else { + const queryType = resolveInfo.schema.getQueryType(); + const mutationType = resolveInfo.schema.getMutationType(); + + field = Object.values({ ...queryType?.getFields(), ...mutationType?.getFields() }).find( + (f) => f.name === resolveTree.name + ) as GraphQLField; + } + + const args = Object.entries(resolveTree.args).reduce((res, [name, value]) => { + const argument = field.args.find((arg) => arg.name === name); - const args = Object.entries(resolveTree.args).reduce((a, [name, value]) => { - const argumentNode = fieldNode.arguments?.find((argument) => argument.name.value === name) as ArgumentNode; + if (!argument) { + throw new Error( + `Error whilst generating Neo4j resolve tree: could not find argument ${name} on field ${field.name}` + ); + } return { - ...a, - [name]: getNeo4jArgumentValue(value, argumentNode?.value, resolveInfo.operation.variableDefinitions), + ...res, + [name]: getNeo4jArgumentValue({ argument: value, type: argument.type }), }; }, {}); - const fieldsByTypeName = Object.entries(resolveTree.fieldsByTypeName).reduce((f, [k, v]) => { - const resolveTrees = Object.entries(v).reduce((t, [k1, v1]) => { + const fieldsByTypeName = Object.entries(resolveTree.fieldsByTypeName).reduce((res, [typeName, fields]) => { + let type: GraphQLObjectType | GraphQLInterfaceType; + + // eslint-disable-next-line no-underscore-dangle,@typescript-eslint/naming-convention + const _type = resolveInfo.schema.getType(typeName) as GraphQLNamedType; + + if (!_type) { + throw new Error( + `Error whilst generating Neo4j resolve tree: could not find type with name ${typeName} in schema` + ); + } + + /* isTypeOf and resolveType are defining for GraphQLObjectType and GraphQLInterfaceType */ + if ((_type as GraphQLObjectType).isTypeOf) { + type = _type as GraphQLObjectType; + } else if ((_type as GraphQLInterfaceType).resolveType) { + type = _type as GraphQLInterfaceType; + } else { + return { + ...res, + [typeName]: fields, + }; + } + + const resolveTrees = Object.entries(fields).reduce((trees, [fieldName, f]) => { return { - ...t, - [k1]: getNeo4jFieldsByTypeName( - v1, - fieldNode.selectionSet?.selections as FieldNode[], - resolveInfo.operation.variableDefinitions - ), + ...trees, + [fieldName]: getNeo4jResolveTree(resolveInfo, { + resolveTree: f, + field: Object.values(type.getFields()).find( + (typeField) => typeField.name === f.name + ) as GraphQLField, + }), }; }, {}); return { - ...f, - [k]: resolveTrees, + ...res, + [typeName]: resolveTrees, }; }, {}); diff --git a/packages/graphql/tests/tck/tck.test.ts b/packages/graphql/tests/tck/tck.test.ts index cee1e6f65c..631f2ec26d 100644 --- a/packages/graphql/tests/tck/tck.test.ts +++ b/packages/graphql/tests/tck/tck.test.ts @@ -141,7 +141,6 @@ describe("TCK Generated tests", () => { context: Context, info: GraphQLResolveInfo ) => { - // const resolveTree = parseResolveInfo(info) as ResolveTree; const resolveTree = getNeo4jResolveTree(info); context.neoSchema = neoSchema; @@ -178,7 +177,6 @@ describe("TCK Generated tests", () => { context: any, info: GraphQLResolveInfo ) => { - // const resolveTree = parseResolveInfo(info) as ResolveTree; const resolveTree = getNeo4jResolveTree(info); context.neoSchema = neoSchema; @@ -207,7 +205,6 @@ describe("TCK Generated tests", () => { context: any, info: GraphQLResolveInfo ) => { - // const resolveTree = parseResolveInfo(info) as ResolveTree; const resolveTree = getNeo4jResolveTree(info); context.neoSchema = neoSchema; @@ -231,7 +228,6 @@ describe("TCK Generated tests", () => { }; }, [`delete${pluralize(def.name.value)}`]: (_root: any, _params: any, context: any, info) => { - // const resolveTree = parseResolveInfo(info) as ResolveTree; const resolveTree = getNeo4jResolveTree(info); context.neoSchema = neoSchema;