From a6fbe2f5807fab8f6c3cdb872afdf45e788e810a Mon Sep 17 00:00:00 2001 From: Darrell Warde <8117355+darrellwarde@users.noreply.github.com> Date: Wed, 2 Jun 2021 15:06:59 +0100 Subject: [PATCH] Fix integer input parsing (#230) * Current progress on getting Neo4j values into resolve tree * Fix for integer input parsing --- packages/graphql/src/classes/Neo4jGraphQL.ts | 6 +- packages/graphql/src/schema/scalars/Int.ts | 18 +- packages/graphql/src/schema/scalars/index.ts | 6 +- .../src/utils/get-neo4j-resolve-tree.ts | 157 ++++++++++++++++++ packages/graphql/tests/tck/tck.test.ts | 11 +- 5 files changed, 176 insertions(+), 22 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..0c82d29fff 100644 --- a/packages/graphql/src/classes/Neo4jGraphQL.ts +++ b/packages/graphql/src/classes/Neo4jGraphQL.ts @@ -21,13 +21,13 @@ 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"; 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 +114,9 @@ class Neo4jGraphQL { } context.neoSchema = this; - context.resolveTree = parseResolveInfo(resolveInfo) as ResolveTree; + + context.resolveTree = getNeo4jResolveTree(resolveInfo); + context.jwt = getJWT(context); }); } 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 3e62cf5f1e..4c5bc784c3 100644 --- a/packages/graphql/src/schema/scalars/index.ts +++ b/packages/graphql/src/schema/scalars/index.ts @@ -17,8 +17,8 @@ * 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"; +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 new file mode 100644 index 0000000000..56541e19ef --- /dev/null +++ b/packages/graphql/src/utils/get-neo4j-resolve-tree.ts @@ -0,0 +1,157 @@ +/* + * 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 { + GraphQLField, + GraphQLInterfaceType, + GraphQLNamedType, + GraphQLNonNull, + GraphQLObjectType, + GraphQLResolveInfo, + GraphQLInputObjectType, + GraphQLInputType, + GraphQLList, + GraphQLScalarType, +} from "graphql"; +import { parseResolveInfo, ResolveTree } from "graphql-parse-resolve-info"; +import neo4j from "neo4j-driver"; + +function getNeo4jArgumentValue({ argument, type }: { argument: unknown | unknown[]; type: GraphQLInputType }) { + if (argument === null) { + return argument; + } + + if (type.toString().endsWith("!")) { + return getNeo4jArgumentValue({ argument, type: (type as GraphQLNonNull).ofType }); + } + + if (type.toString().startsWith("[") && type.toString().endsWith("]")) { + return (argument as unknown[]).map((a) => + getNeo4jArgumentValue({ argument: a, type: (type as GraphQLList).ofType }) + ); + } + + 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); + + if (!field) { + throw new Error( + `Error whilst generating Neo4j resolve tree: could not find field ${key} in type ${type.name}` + ); + } + + return { + ...res, + [key]: getNeo4jArgumentValue({ argument: value, type: field.type }), + }; + }, {}); + } + + if (type instanceof GraphQLScalarType) { + return type.name === "Int" ? neo4j.int(argument as number) : argument; + } + + return argument; +} + +interface GetNeo4jResolveTreeOptions { + resolveTree: ResolveTree; + field: GraphQLField; +} + +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); + + if (!argument) { + throw new Error( + `Error whilst generating Neo4j resolve tree: could not find argument ${name} on field ${field.name}` + ); + } + + return { + ...res, + [name]: getNeo4jArgumentValue({ argument: value, type: argument.type }), + }; + }, {}); + + 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 { + ...trees, + [fieldName]: getNeo4jResolveTree(resolveInfo, { + resolveTree: f, + field: Object.values(type.getFields()).find( + (typeField) => typeField.name === f.name + ) as GraphQLField, + }), + }; + }, {}); + + return { + ...res, + [typeName]: 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..631f2ec26d 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,7 @@ describe("TCK Generated tests", () => { context: Context, info: GraphQLResolveInfo ) => { - const resolveTree = parseResolveInfo(info) as ResolveTree; + const resolveTree = getNeo4jResolveTree(info); context.neoSchema = neoSchema; context.resolveTree = resolveTree; @@ -176,7 +177,7 @@ describe("TCK Generated tests", () => { context: any, info: GraphQLResolveInfo ) => { - const resolveTree = parseResolveInfo(info) as ResolveTree; + const resolveTree = getNeo4jResolveTree(info); context.neoSchema = neoSchema; context.resolveTree = resolveTree; @@ -204,7 +205,7 @@ describe("TCK Generated tests", () => { context: any, info: GraphQLResolveInfo ) => { - const resolveTree = parseResolveInfo(info) as ResolveTree; + const resolveTree = getNeo4jResolveTree(info); context.neoSchema = neoSchema; context.resolveTree = resolveTree; @@ -227,7 +228,7 @@ 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; context.resolveTree = resolveTree;