From b0fe269e6fee296d1392ba6b77aa71443876cbd4 Mon Sep 17 00:00:00 2001 From: Darrell Warde <8117355+darrellwarde@users.noreply.github.com> Date: Thu, 8 Jul 2021 10:56:29 +0100 Subject: [PATCH] Merge most recent changes from master into 2.0.0 (#306) * Missing copyright headers * Point to prerelease documentation for 2.0.0 branch * General housekeeping before release * Update code comment * Fix TCK tests broken from merge * fix(jwt): req.cookies might be undefined this fix prevents the app from crashing id req.cookies is undefined * Add scalars earlier in schema augmentation for use in types and interfaces without throwing Error (fixes DateTime relationship properties) * Changes to accomodate merge from master * 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 * config: remove codeowners (#277) * Version update * fix(login): avoid confusion caused by secondary button (#265) * fix: losing params while creating Auth Predicate (#281) * fix: loosing params while creating Auth Predicate * fix: typos * fix: typo * 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 * 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 * Mention double escaping for @cypher directive * Version update Co-authored-by: gaspard Co-authored-by: Oskar Hane Co-authored-by: Daniel Starns Co-authored-by: Neo Technology Build Agent Co-authored-by: Arnaud Gissinger <37625778+mathix420@users.noreply.github.com> --- .github/CODEOWNERS | 1 - .gitignore | 2 + docs/asciidoc/type-definitions/cypher.adoc | 33 +- .../neo-push/client/src/components/SignIn.tsx | 10 +- .../neo-push/client/src/components/SignUp.tsx | 10 +- packages/graphql/package.json | 1 + packages/graphql/src/auth/get-jwt.ts | 2 +- packages/graphql/src/classes/Neo4jGraphQL.ts | 22 +- packages/graphql/src/constants.ts | 3 +- packages/graphql/src/environment.ts | 6 +- .../src/schema/make-augmented-schema.ts | 16 +- .../src/schema/resolvers/cypher.test.ts | 2 +- .../graphql/src/schema/resolvers/cypher.ts | 71 ++- .../src/schema/wrap-custom-resolvers.ts | 132 ----- .../translate/create-auth-and-params.test.ts | 142 +++++ .../src/translate/create-auth-and-params.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 +- .../integration/custom-resolvers-int.test.ts | 8 +- .../tests/integration/cypher.int.test.ts | 547 ++++++++++++++++++ .../tests/integration/issues/#207.int.test.ts | 112 ++++ .../tests/integration/issues/#283.int.test.ts | 103 ++++ .../tests/integration/types/point.int.test.ts | 12 +- packages/graphql/tests/tsconfig.json | 3 +- packages/graphql/tsconfig.json | 9 + tsconfig.base.json | 3 +- yarn.lock | 63 ++ 29 files changed, 1172 insertions(+), 179 deletions(-) delete mode 100644 .github/CODEOWNERS delete mode 100644 packages/graphql/src/schema/wrap-custom-resolvers.ts create mode 100644 packages/graphql/tests/integration/cypher.int.test.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 create mode 100644 packages/graphql/tsconfig.json 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 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/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] ---- 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 +

diff --git a/packages/graphql/package.json b/packages/graphql/package.json index d9752140ed..033a455c43 100644 --- a/packages/graphql/package.json +++ b/packages/graphql/package.json @@ -57,6 +57,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/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"); diff --git a/packages/graphql/src/classes/Neo4jGraphQL.ts b/packages/graphql/src/classes/Neo4jGraphQL.ts index 98eb14029c..feaf368db1 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"; @@ -29,6 +29,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); @@ -63,7 +64,7 @@ class Neo4jGraphQL { public config?: Neo4jGraphQLConfig; constructor(input: Neo4jGraphQLConstructor) { - const { config = {}, driver, ...schemaDefinition } = input; + const { config = {}, driver, resolvers, ...schemaDefinition } = input; const { nodes, relationships, schema } = makeAugmentedSchema(schemaDefinition, { enableRegex: config.enableRegex, }); @@ -72,7 +73,20 @@ class Neo4jGraphQL { this.config = config; this.nodes = nodes; this.relationships = relationships; - 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)); } @@ -124,6 +138,8 @@ class Neo4jGraphQL { context.resolveTree = getNeo4jResolveTree(resolveInfo); context.jwt = getJWT(context); + + context.auth = createAuthParam({ context }); }); } 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/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/schema/make-augmented-schema.ts b/packages/graphql/src/schema/make-augmented-schema.ts index 3b278c6e5d..87d81d8aee 100644 --- a/packages/graphql/src/schema/make-augmented-schema.ts +++ b/packages/graphql/src/schema/make-augmented-schema.ts @@ -50,7 +50,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"; @@ -63,7 +62,7 @@ import { createConnectionWithEdgeProperties } from "./pagination"; // import validateTypeDefs from "./validation"; function makeAugmentedSchema( - { typeDefs, resolvers, ...schemaDefinition }: IExecutableSchemaDefinition, + { typeDefs, ...schemaDefinition }: IExecutableSchemaDefinition, { enableRegex }: { enableRegex?: boolean } = {} ): { schema: GraphQLSchema; nodes: Node[]; relationships: Relationship[] } { const document = mergeTypeDefs(Array.isArray(typeDefs) ? (typeDefs as string[]) : [typeDefs as string]); @@ -249,8 +248,6 @@ function makeAugmentedSchema( const relationshipProperties = interfaces.filter((i) => relationshipPropertyInterfaceNames.has(i.name.value)); interfaces = interfaces.filter((i) => !relationshipPropertyInterfaceNames.has(i.name.value)); - const nodeNames = nodes.map((x) => x.name); - const relationshipFields = new Map(); relationshipProperties.forEach((relationship) => { @@ -1108,6 +1105,7 @@ function makeAugmentedSchema( const customResolver = cypherResolver({ field, statement: field.statement, + type: type as "Query" | "Mutation", }); const composedField = objectFieldsToComposeFields([field])[field.fieldName]; @@ -1138,7 +1136,7 @@ function makeAugmentedSchema( composer.delete("Mutation"); } const generatedTypeDefs = composer.toSDL(); - let generatedResolvers: any = { + const generatedResolvers = { ...composer.getResolveMethods(), ...Object.entries(Scalars).reduce((res, [name, scalar]) => { if (generatedTypeDefs.includes(`scalar ${name}\n`)) { @@ -1148,14 +1146,6 @@ function makeAugmentedSchema( }, {}), }; - if (resolvers) { - generatedResolvers = wrapCustomResolvers({ - generatedResolvers, - nodeNames, - resolvers, - }); - } - unions.forEach((union) => { if (!generatedResolvers[union.name.value]) { // eslint-disable-next-line no-underscore-dangle 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/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/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; 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/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(); + } + }); + }); + }); +}); 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/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 } } diff --git a/yarn.lock b/yarn.lock index b8a73ce030..33b153f986 100644 --- a/yarn.lock +++ b/yarn.lock @@ -959,6 +959,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 @@ -1162,6 +1163,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" @@ -13169,6 +13198,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"