diff --git a/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts b/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts index 8615b735b3..51098e4f7e 100644 --- a/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts +++ b/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts @@ -31,6 +31,7 @@ import { SchemaComposer } from "graphql-compose"; import { SchemaBuilderTypes } from "./SchemaBuilderTypes"; export type TypeDefinition = string | WrappedComposer; +export type InputTypeDefinition = string | WrappedComposer; type ObjectOrInputTypeComposer = ObjectTypeComposer | InputTypeComposer; @@ -41,7 +42,7 @@ type ListOrNullComposer> | NonNullComposer>>; -type WrappedComposer = T | ListOrNullComposer; +export type WrappedComposer = T | ListOrNullComposer; export type GraphQLResolver = (...args) => any; @@ -53,6 +54,14 @@ export type FieldDefinition = { description?: string | null; }; +export type InputFieldDefinition = { + type: InputTypeDefinition; + args?: Record; + deprecationReason?: string | null; + description?: string | null; + defaultValue: any; +}; + export class SchemaBuilder { public readonly types: SchemaBuilderTypes; private composer: SchemaComposer; @@ -108,7 +117,6 @@ export class SchemaBuilder { if (description) { tc.setDescription(description); } - // This is used for global node, not sure if needed for other interfaces tc.setResolveType((obj) => { return obj.__resolveType; @@ -125,6 +133,7 @@ export class SchemaBuilder { | GraphQLInputType | GraphQLNonNull | WrappedComposer + | InputFieldDefinition >; description?: string; } @@ -142,7 +151,14 @@ export class SchemaBuilder { public createInputObjectType( name: string, - fields: Record>, + fields: Record< + string, + | EnumTypeComposer + | GraphQLInputType + | GraphQLNonNull + | WrappedComposer + | InputFieldDefinition + >, description?: string ): InputTypeComposer { return this.composer.createInputTC({ diff --git a/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts b/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts index 7a7a433237..186e3cf2b4 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts @@ -71,7 +71,7 @@ export class TopLevelEntitySchemaTypes { this.schemaBuilder = schemaBuilder; this.entityTypeNames = entity.typeNames; this.schemaTypes = schemaTypes; - this.createSchemaTypes = new TopLevelCreateSchemaTypes({ schemaBuilder, entity, schemaTypes }); + this.createSchemaTypes = new TopLevelCreateSchemaTypes({ schemaBuilder, entity }); } public addTopLevelQueryField( diff --git a/packages/graphql/src/api-v6/schema-generation/schema-types/mutation-schema-types/TopLevelCreateSchemaTypes.ts b/packages/graphql/src/api-v6/schema-generation/schema-types/mutation-schema-types/TopLevelCreateSchemaTypes.ts index 41f99984b2..e5efa704be 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/mutation-schema-types/TopLevelCreateSchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/mutation-schema-types/TopLevelCreateSchemaTypes.ts @@ -17,8 +17,7 @@ * limitations under the License. */ -import type { GraphQLScalarType } from "graphql"; -import type { InputTypeComposer, NonNullComposer, ScalarTypeComposer } from "graphql-compose"; +import type { InputTypeComposer, ScalarTypeComposer } from "graphql-compose"; import type { Attribute } from "../../../../schema-model/attribute/Attribute"; import type { AttributeType } from "../../../../schema-model/attribute/AttributeType"; import { @@ -34,28 +33,17 @@ import { import type { ConcreteEntity } from "../../../../schema-model/entity/ConcreteEntity"; import { filterTruthy } from "../../../../utils/utils"; import type { TopLevelEntityTypeNames } from "../../../schema-model/graphql-type-names/TopLevelEntityTypeNames"; -import type { SchemaBuilder } from "../../SchemaBuilder"; -import type { SchemaTypes } from "../SchemaTypes"; +import type { InputFieldDefinition, SchemaBuilder, WrappedComposer } from "../../SchemaBuilder"; export class TopLevelCreateSchemaTypes { private entityTypeNames: TopLevelEntityTypeNames; - private schemaTypes: SchemaTypes; private schemaBuilder: SchemaBuilder; private entity: ConcreteEntity; - constructor({ - entity, - schemaBuilder, - schemaTypes, - }: { - entity: ConcreteEntity; - schemaBuilder: SchemaBuilder; - schemaTypes: SchemaTypes; - }) { + constructor({ entity, schemaBuilder }: { entity: ConcreteEntity; schemaBuilder: SchemaBuilder }) { this.entity = entity; this.entityTypeNames = entity.typeNames; this.schemaBuilder = schemaBuilder; - this.schemaTypes = schemaTypes; } public get createInput(): InputTypeComposer { @@ -79,12 +67,16 @@ export class TopLevelCreateSchemaTypes { }); } - private getInputFields(attributes: Attribute[]): Record { - const inputFields: Array<[string, InputTypeComposer | GraphQLScalarType] | []> = filterTruthy( + private getInputFields(attributes: Attribute[]): Record { + const inputFields: Array<[string, InputFieldDefinition] | []> = filterTruthy( attributes.map((attribute) => { const inputField = this.attributeToInputField(attribute.type); + const fieldDefinition: InputFieldDefinition = { + type: inputField, + defaultValue: attribute.annotations.default?.value, + }; if (inputField) { - return [attribute.name, inputField]; + return [attribute.name, fieldDefinition]; } }) ); @@ -109,7 +101,7 @@ export class TopLevelCreateSchemaTypes { } } - private createBuiltInFieldInput(type: ScalarType): ScalarTypeComposer | NonNullComposer { + private createBuiltInFieldInput(type: ScalarType): WrappedComposer { let builtInType: ScalarTypeComposer; switch (type.name) { case GraphQLBuiltInScalarType.Boolean: { @@ -146,9 +138,7 @@ export class TopLevelCreateSchemaTypes { return builtInType; } - private createTemporalFieldInput( - type: Neo4jTemporalType - ): ScalarTypeComposer | NonNullComposer { + private createTemporalFieldInput(type: Neo4jTemporalType): WrappedComposer { let builtInType: ScalarTypeComposer; switch (type.name) { case Neo4jGraphQLTemporalType.Date: { @@ -185,7 +175,7 @@ export class TopLevelCreateSchemaTypes { return builtInType; } - private createSpatialFieldInput(type: Neo4jSpatialType): InputTypeComposer | NonNullComposer { + private createSpatialFieldInput(type: Neo4jSpatialType): WrappedComposer { let builtInType: InputTypeComposer; switch (type.name) { case Neo4jGraphQLSpatialType.CartesianPoint: { diff --git a/packages/graphql/src/api-v6/validation/rules/valid-default.ts b/packages/graphql/src/api-v6/validation/rules/valid-default.ts new file mode 100644 index 0000000000..27e0f29453 --- /dev/null +++ b/packages/graphql/src/api-v6/validation/rules/valid-default.ts @@ -0,0 +1,100 @@ +/* + * 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 type { ASTVisitor, FieldDefinitionNode, StringValueNode } from "graphql"; +import type { SDLValidationContext } from "graphql/validation/ValidationContext"; +import { isSpatial, isTemporal } from "../../../constants"; +import { defaultDirective } from "../../../graphql/directives"; +import { + GraphQLBuiltInScalarType, + Neo4jGraphQLNumberType, + Neo4jGraphQLSpatialType, + Neo4jGraphQLTemporalType, +} from "../../../schema-model/attribute/AttributeType"; +import { + assertValid, + createGraphQLError, + DocumentValidationError, +} from "../../../schema/validation/custom-rules/utils/document-validation-error"; +import { getPathToNode } from "../../../schema/validation/custom-rules/utils/path-parser"; +import { assertArgumentHasSameTypeAsField } from "../../../schema/validation/custom-rules/utils/same-type-argument-as-field"; +import { getInnerTypeName, isArrayType } from "../../../schema/validation/custom-rules/utils/utils"; + +export function ValidDefault(context: SDLValidationContext): ASTVisitor { + return { + FieldDefinition(fieldDefinitionNode: FieldDefinitionNode, _key, _parent, path, ancestors) { + const { directives } = fieldDefinitionNode; + if (!directives) { + return; + } + const defaultDirectiveNode = directives.find((directive) => directive.name.value === defaultDirective.name); + + if (!defaultDirectiveNode || !defaultDirectiveNode.arguments) { + return; + } + const defaultValue = defaultDirectiveNode.arguments.find((a) => a.name.value === "value"); + if (!defaultValue) { + return; + } + const expectedType = getInnerTypeName(fieldDefinitionNode.type); + const { isValid, errorMsg, errorPath } = assertValid(() => { + if (!isArrayType(fieldDefinitionNode)) { + if (isSpatial(expectedType)) { + throw new DocumentValidationError(`@default is not supported by Spatial types.`, ["value"]); + } else if (isTemporal(expectedType)) { + if (Number.isNaN(Date.parse((defaultValue?.value as StringValueNode).value))) { + throw new DocumentValidationError( + `@default.${defaultValue.name.value} is not a valid ${expectedType}`, + ["value"] + ); + } + } else if (!isTypeABuiltInType(expectedType)) { + //TODO: Add check for user defined enums that are currently not implemented + throw new DocumentValidationError( + `@default directive can only be used on Temporal types and types: Int | Float | String | Boolean | ID | Enum`, + [] + ); + } + } + assertArgumentHasSameTypeAsField({ + directiveName: "@default", + traversedDef: fieldDefinitionNode, + argument: defaultValue, + enums: [], + }); + }); + const [pathToNode] = getPathToNode(path, ancestors); + if (!isValid) { + context.reportError( + createGraphQLError({ + nodes: [fieldDefinitionNode], + path: [...pathToNode, fieldDefinitionNode.name.value, ...errorPath], + errorMsg, + }) + ); + } + }, + }; +} + +export function isTypeABuiltInType(expectedType: string): boolean { + return [GraphQLBuiltInScalarType, Neo4jGraphQLNumberType, Neo4jGraphQLSpatialType, Neo4jGraphQLTemporalType].some( + (enumValue) => enumValue[expectedType] === expectedType + ); +} diff --git a/packages/graphql/src/api-v6/validation/validate-v6-document.ts b/packages/graphql/src/api-v6/validation/validate-v6-document.ts index b3d1c0f0fe..d8c6981ca9 100644 --- a/packages/graphql/src/api-v6/validation/validate-v6-document.ts +++ b/packages/graphql/src/api-v6/validation/validate-v6-document.ts @@ -44,6 +44,7 @@ import { DirectiveCombinationValid } from "../../schema/validation/custom-rules/ import { WarnIfListOfListsFieldDefinition } from "../../schema/validation/custom-rules/warnings/list-of-lists"; import { validateSDL } from "../../schema/validation/validate-sdl"; import type { Neo4jFeaturesSettings } from "../../types"; +import { ValidDefault } from "./rules/valid-default"; import { ValidLimit } from "./rules/valid-limit"; import { ValidRelationship } from "./rules/valid-relationship"; @@ -66,6 +67,7 @@ function runNeo4jGraphQLValidationRules({ ...specifiedSDLRules, ValidRelationship, ValidLimit, + ValidDefault, DirectiveCombinationValid, ValidRelationshipProperties, ReservedTypeNames, diff --git a/packages/graphql/src/schema/validation/custom-rules/utils/same-type-argument-as-field.ts b/packages/graphql/src/schema/validation/custom-rules/utils/same-type-argument-as-field.ts index 97d547da09..b83f3a6932 100644 --- a/packages/graphql/src/schema/validation/custom-rules/utils/same-type-argument-as-field.ts +++ b/packages/graphql/src/schema/validation/custom-rules/utils/same-type-argument-as-field.ts @@ -16,11 +16,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import type { EnumTypeDefinitionNode, ArgumentNode, FieldDefinitionNode, ValueNode } from "graphql"; +import type { ArgumentNode, EnumTypeDefinitionNode, FieldDefinitionNode, ValueNode } from "graphql"; import { Kind } from "graphql"; -import { fromValueKind, getInnerTypeName, isArrayType } from "./utils"; import { isSpatial, isTemporal } from "../../../../constants"; import { DocumentValidationError } from "./document-validation-error"; +import { fromValueKind, getInnerTypeName, isArrayType } from "./utils"; export function assertArgumentHasSameTypeAsField({ directiveName, @@ -71,7 +71,7 @@ function doTypesMatch(expectedType: string, argumentValueType: ValueNode, enums: return true; } if (expectedType.toLowerCase() === "id") { - return !!(fromValueKind(argumentValueType, enums, expectedType)?.toLowerCase() === "string"); + return Boolean(fromValueKind(argumentValueType, enums, expectedType)?.toLowerCase() === "string"); } return fromValueKind(argumentValueType, enums, expectedType)?.toLowerCase() === expectedType.toLowerCase(); } diff --git a/packages/graphql/src/schema/validation/validate-document.test.ts b/packages/graphql/src/schema/validation/validate-document.test.ts index 27b5f67f0f..f3b7a46781 100644 --- a/packages/graphql/src/schema/validation/validate-document.test.ts +++ b/packages/graphql/src/schema/validation/validate-document.test.ts @@ -407,41 +407,7 @@ describe("validation 2.0", () => { expect(executeValidate).not.toThrow(); }); }); - describe("@default", () => { - test("@default property required", () => { - const doc = gql` - type User { - name: String @default - } - `; - // TODO: is "ScalarOrEnum" type exposed to the user? - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - expect(executeValidate).toThrow( - 'Directive "@default" argument "value" of type "ScalarOrEnum!" is required, but it was not provided.' - ); - }); - test("@default ok", () => { - const doc = gql` - type User { - name: String @default(value: "dummy") - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - expect(executeValidate).not.toThrow(); - }); - }); describe("@fulltext", () => { test("@fulltext property required", () => { const doc = gql` @@ -998,70 +964,6 @@ describe("validation 2.0", () => { describe("Directive Argument Value", () => { describe("@default", () => { - test("@default on datetime must be valid datetime", () => { - const doc = gql` - type User { - updatedAt: DateTime @default(value: "dummy") - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - const errors = getError(executeValidate); - - expect(errors).toHaveLength(1); - expect(errors[0]).not.toBeInstanceOf(NoErrorThrownError); - expect(errors[0]).toHaveProperty("message", "@default.value is not a valid DateTime"); - expect(errors[0]).toHaveProperty("path", ["User", "updatedAt", "@default", "value"]); - }); - - test("@default on datetime must be valid datetime extension", () => { - const doc = gql` - type User { - id: ID - } - extend type User { - updatedAt: DateTime @default(value: "dummy") - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - const errors = getError(executeValidate); - - expect(errors).toHaveLength(1); - expect(errors[0]).not.toBeInstanceOf(NoErrorThrownError); - expect(errors[0]).toHaveProperty("message", "@default.value is not a valid DateTime"); - expect(errors[0]).toHaveProperty("path", ["User", "updatedAt", "@default", "value"]); - }); - - test("@default on datetime must be valid datetime correct", () => { - const doc = gql` - type User { - updatedAt: DateTime @default(value: "2023-07-06T09:45:11.336Z") - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - expect(executeValidate).not.toThrow(); - }); - test("@default on enum must be enum", () => { const enumTypes = gql` enum Status { @@ -1214,401 +1116,6 @@ describe("validation 2.0", () => { expect(executeValidate).not.toThrow(); }); - test("@default on int must be int", () => { - const doc = gql` - type User { - age: Int @default(value: "dummy") - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - const errors = getError(executeValidate); - expect(errors).toHaveLength(1); - expect(errors[0]).not.toBeInstanceOf(NoErrorThrownError); - expect(errors[0]).toHaveProperty("message", "@default.value on Int fields must be of type Int"); - expect(errors[0]).toHaveProperty("path", ["User", "age", "@default", "value"]); - }); - - test("@default on int must be int correct", () => { - const doc = gql` - type User { - age: Int @default(value: 23) - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - expect(executeValidate).not.toThrow(); - }); - - test("@default on int list must be list of int values", () => { - const doc = gql` - type User { - ages: [Int] @default(value: ["dummy"]) - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - const errors = getError(executeValidate); - expect(errors).toHaveLength(1); - expect(errors[0]).not.toBeInstanceOf(NoErrorThrownError); - expect(errors[0]).toHaveProperty( - "message", - "@default.value on Int list fields must be a list of Int values" - ); - expect(errors[0]).toHaveProperty("path", ["User", "ages", "@default", "value"]); - }); - - test("@default on int list must be list of int values correct", () => { - const doc = gql` - type User { - ages: [Int] @default(value: [12]) - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - expect(executeValidate).not.toThrow(); - }); - - test("@default on float must be float", () => { - const doc = gql` - type User { - avg: Float @default(value: 2) - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - const errors = getError(executeValidate); - expect(errors).toHaveLength(1); - expect(errors[0]).not.toBeInstanceOf(NoErrorThrownError); - expect(errors[0]).toHaveProperty("message", "@default.value on Float fields must be of type Float"); - expect(errors[0]).toHaveProperty("path", ["User", "avg", "@default", "value"]); - }); - - test("@default on float must be float correct", () => { - const doc = gql` - type User { - avg: Float @default(value: 2.3) - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - expect(executeValidate).not.toThrow(); - }); - - test("@default on float list must be list of float values", () => { - const doc = gql` - type User { - avgs: [Float] @default(value: [1]) - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - const errors = getError(executeValidate); - expect(errors).toHaveLength(1); - expect(errors[0]).not.toBeInstanceOf(NoErrorThrownError); - expect(errors[0]).toHaveProperty( - "message", - "@default.value on Float list fields must be a list of Float values" - ); - expect(errors[0]).toHaveProperty("path", ["User", "avgs", "@default", "value"]); - }); - - test("@default on float list must be list of float values correct", () => { - const doc = gql` - type User { - avgs: [Float] @default(value: [1.2]) - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - expect(executeValidate).not.toThrow(); - }); - - test("@default on boolean must be boolean", () => { - const doc = gql` - type User { - registered: Boolean @default(value: 2) - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - const errors = getError(executeValidate); - expect(errors).toHaveLength(1); - expect(errors[0]).not.toBeInstanceOf(NoErrorThrownError); - expect(errors[0]).toHaveProperty("message", "@default.value on Boolean fields must be of type Boolean"); - expect(errors[0]).toHaveProperty("path", ["User", "registered", "@default", "value"]); - }); - - test("@default on boolean must be boolean correct", () => { - const doc = gql` - type User { - registered: Boolean @default(value: false) - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - expect(executeValidate).not.toThrow(); - }); - - test("@default on boolean list must be list of boolean values", () => { - const doc = gql` - type User { - statuses: [Boolean] @default(value: [2]) - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - const errors = getError(executeValidate); - expect(errors).toHaveLength(1); - expect(errors[0]).not.toBeInstanceOf(NoErrorThrownError); - expect(errors[0]).toHaveProperty( - "message", - "@default.value on Boolean list fields must be a list of Boolean values" - ); - expect(errors[0]).toHaveProperty("path", ["User", "statuses", "@default", "value"]); - }); - - test("@default on boolean list must be list of boolean values correct", () => { - const doc = gql` - type User { - statuses: [Boolean] @default(value: [true]) - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - expect(executeValidate).not.toThrow(); - }); - - test("@default on string must be string", () => { - const doc = gql` - type User { - name: String @default(value: 2) - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - const errors = getError(executeValidate); - expect(errors).toHaveLength(1); - expect(errors[0]).not.toBeInstanceOf(NoErrorThrownError); - expect(errors[0]).toHaveProperty("message", "@default.value on String fields must be of type String"); - expect(errors[0]).toHaveProperty("path", ["User", "name", "@default", "value"]); - }); - - test("@default on string must be string correct", () => { - const doc = gql` - type User { - registered: String @default(value: "Bob") - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - expect(executeValidate).not.toThrow(); - }); - - test("@default on string list must be list of string values", () => { - const doc = gql` - type User { - names: [String] @default(value: [2]) - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - const errors = getError(executeValidate); - expect(errors).toHaveLength(1); - expect(errors[0]).not.toBeInstanceOf(NoErrorThrownError); - expect(errors[0]).toHaveProperty( - "message", - "@default.value on String list fields must be a list of String values" - ); - expect(errors[0]).toHaveProperty("path", ["User", "names", "@default", "value"]); - }); - - test("@default on string list must be list of string values correct", () => { - const doc = gql` - type User { - names: [String] @default(value: ["Bob"]) - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - expect(executeValidate).not.toThrow(); - }); - - test("@default on ID must be ID", () => { - const doc = gql` - type User { - uid: ID @default(value: 2) - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - const errors = getError(executeValidate); - expect(errors).toHaveLength(1); - expect(errors[0]).not.toBeInstanceOf(NoErrorThrownError); - expect(errors[0]).toHaveProperty("message", "@default.value on ID fields must be of type ID"); - expect(errors[0]).toHaveProperty("path", ["User", "uid", "@default", "value"]); - }); - - test("@default on ID list must be list of ID values", () => { - const doc = gql` - type User { - ids: [ID] @default(value: [2]) - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - const errors = getError(executeValidate); - expect(errors).toHaveLength(1); - expect(errors[0]).not.toBeInstanceOf(NoErrorThrownError); - expect(errors[0]).toHaveProperty( - "message", - "@default.value on ID list fields must be a list of ID values" - ); - expect(errors[0]).toHaveProperty("path", ["User", "ids", "@default", "value"]); - }); - - test("@default on ID list must be list of ID values correct", () => { - const doc = gql` - type User { - ids: [ID] @default(value: ["123-223"]) - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - expect(executeValidate).not.toThrow(); - }); - - test("@default on ID must be ID correct", () => { - const doc = gql` - type User { - uid: ID @default(value: "234-432") - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - expect(executeValidate).not.toThrow(); - }); - test("@default not supported on Spatial types", () => { const doc = gql` type User { diff --git a/packages/graphql/tests/api-v6/integration/directives/default/create-default.int.test.ts b/packages/graphql/tests/api-v6/integration/directives/default/create-default.int.test.ts new file mode 100644 index 0000000000..5ff485814e --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/directives/default/create-default.int.test.ts @@ -0,0 +1,69 @@ +/* + * 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 { TestHelper } from "../../../../utils/tests-helper"; + +describe("Create with @default", () => { + const testHelper = new TestHelper({ v6Api: true }); + + afterEach(async () => { + await testHelper.close(); + }); + + test.each([ + { dataType: "Int", value: 1 }, + { dataType: "Float", value: 1.2 }, + { dataType: "DateTime", value: "2024-05-28T13:56:22.368Z" }, + ] as const)("should create two movies with a $dataType default value", async ({ dataType, value }) => { + const Movie = testHelper.createUniqueType("Movie"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + title: String! + testField: ${dataType} @default(value: ${typeof value === "string" ? `"${value}"` : value}) + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + const mutation = /* GraphQL */ ` + mutation { + ${Movie.operations.create}(input: [ + { node: { title: "The Matrix" } }, + { node: { title: "The Matrix 2"} } + ]) { + ${Movie.plural} { + title + testField + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(mutation); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.operations.create]: { + [Movie.plural]: expect.toIncludeSameMembers([ + { title: "The Matrix", testField: value }, + { title: "The Matrix 2", testField: value }, + ]), + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/schema/directives/default-array.test.ts b/packages/graphql/tests/api-v6/schema/directives/default-array.test.ts new file mode 100644 index 0000000000..5d1cdc80f0 --- /dev/null +++ b/packages/graphql/tests/api-v6/schema/directives/default-array.test.ts @@ -0,0 +1,160 @@ +/* + * 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 { printSchemaWithDirectives } from "@graphql-tools/utils"; +import { lexicographicSortSchema } from "graphql/utilities"; +import { Neo4jGraphQL } from "../../../../src"; +import { raiseOnInvalidSchema } from "../../../utils/raise-on-invalid-schema"; + +describe("@default on array fields", () => { + test("@default should add a default value in mutation inputs", async () => { + const typeDefs = /* GraphQL */ ` + type Movie @node { + id: [ID!]! @default(value: ["id"]) + title: [String!] @default(value: ["title"]) + year: [Int!] @default(value: [2021]) + length: [Float!] @default(value: [120.5]) + releasedDateTime: [DateTime!] @default(value: ["2021-01-01T00:00:00"]) + flags: [Boolean!] @default(value: [true, false]) + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(schema)); + expect(printedSchema).toMatchInlineSnapshot(` + "schema { + query: Query + mutation: Mutation + } + + input BooleanWhere { + AND: [BooleanWhere!] + NOT: BooleanWhere + OR: [BooleanWhere!] + equals: Boolean + } + + \\"\\"\\"A date and time, represented as an ISO-8601 string\\"\\"\\" + scalar DateTime + + input DateTimeListWhere { + equals: [DateTime!] + } + + input FloatListWhere { + equals: [Float!] + } + + input IDListWhere { + equals: [ID!] + } + + input IntListWhere { + equals: [Int!] + } + + type Movie { + flags: [Boolean!] + id: [ID!]! + length: [Float!] + releasedDateTime: [DateTime!] + title: [String!] + year: [Int!] + } + + type MovieConnection { + edges: [MovieEdge] + pageInfo: PageInfo + } + + type MovieCreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + + input MovieCreateInput { + node: MovieCreateNode! + } + + input MovieCreateNode { + flags: [Boolean!] = [true, false] + id: [ID!]! = [\\"id\\"] + length: [Float!] = [120.5] + releasedDateTime: [DateTime!] = [\\"2021-01-01T00:00:00.000Z\\"] + title: [String!] = [\\"title\\"] + year: [Int!] = [2021] + } + + type MovieCreateResponse { + info: MovieCreateInfo + movies: [Movie!]! + } + + type MovieEdge { + cursor: String + node: Movie + } + + type MovieOperation { + connection(after: String, first: Int): MovieConnection + } + + input MovieOperationWhere { + AND: [MovieOperationWhere!] + NOT: MovieOperationWhere + OR: [MovieOperationWhere!] + node: MovieWhere + } + + input MovieWhere { + AND: [MovieWhere!] + NOT: MovieWhere + OR: [MovieWhere!] + flags: BooleanWhere + id: IDListWhere + length: FloatListWhere + releasedDateTime: DateTimeListWhere + title: StringListWhere + year: IntListWhere + } + + type Mutation { + createMovies(input: [MovieCreateInput!]!): MovieCreateResponse + } + + type PageInfo { + endCursor: String + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String + } + + type Query { + movies(where: MovieOperationWhere): MovieOperation + } + + input StringListWhere { + equals: [String!] + }" + `); + }); + test.todo("@default directive with relationship properties"); + test.todo("@default directive with user defined scalars"); +}); diff --git a/packages/graphql/tests/api-v6/schema/directives/default.test.ts b/packages/graphql/tests/api-v6/schema/directives/default.test.ts new file mode 100644 index 0000000000..6c09889c99 --- /dev/null +++ b/packages/graphql/tests/api-v6/schema/directives/default.test.ts @@ -0,0 +1,217 @@ +/* + * 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 { printSchemaWithDirectives } from "@graphql-tools/utils"; +import { lexicographicSortSchema } from "graphql/utilities"; +import { Neo4jGraphQL } from "../../../../src"; +import { raiseOnInvalidSchema } from "../../../utils/raise-on-invalid-schema"; + +describe("@default on fields", () => { + test("@default should add a default value in mutation inputs", async () => { + const typeDefs = /* GraphQL */ ` + type Movie @node { + id: ID! @default(value: "id") + title: String @default(value: "title") + year: Int @default(value: 2021) + length: Float @default(value: 120.5) + flag: Boolean @default(value: true) + releasedDateTime: DateTime @default(value: "2021-01-01T00:00:00") + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(schema)); + expect(printedSchema).toMatchInlineSnapshot(` + "schema { + query: Query + mutation: Mutation + } + + input BooleanWhere { + AND: [BooleanWhere!] + NOT: BooleanWhere + OR: [BooleanWhere!] + equals: Boolean + } + + \\"\\"\\"A date and time, represented as an ISO-8601 string\\"\\"\\" + scalar DateTime + + input DateTimeWhere { + AND: [DateTimeWhere!] + NOT: DateTimeWhere + OR: [DateTimeWhere!] + equals: DateTime + gt: DateTime + gte: DateTime + in: [DateTime!] + lt: DateTime + lte: DateTime + } + + input FloatWhere { + AND: [FloatWhere!] + NOT: FloatWhere + OR: [FloatWhere!] + equals: Float + gt: Float + gte: Float + in: [Float!] + lt: Float + lte: Float + } + + input IDWhere { + AND: [IDWhere!] + NOT: IDWhere + OR: [IDWhere!] + contains: ID + endsWith: ID + equals: ID + in: [ID!] + startsWith: ID + } + + input IntWhere { + AND: [IntWhere!] + NOT: IntWhere + OR: [IntWhere!] + equals: Int + gt: Int + gte: Int + in: [Int!] + lt: Int + lte: Int + } + + type Movie { + flag: Boolean + id: ID! + length: Float + releasedDateTime: DateTime + title: String + year: Int + } + + type MovieConnection { + edges: [MovieEdge] + pageInfo: PageInfo + } + + input MovieConnectionSort { + node: MovieSort + } + + type MovieCreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + + input MovieCreateInput { + node: MovieCreateNode! + } + + input MovieCreateNode { + flag: Boolean = true + id: ID! = \\"id\\" + length: Float = 120.5 + releasedDateTime: DateTime = \\"2021-01-01T00:00:00.000Z\\" + title: String = \\"title\\" + year: Int = 2021 + } + + type MovieCreateResponse { + info: MovieCreateInfo + movies: [Movie!]! + } + + type MovieEdge { + cursor: String + node: Movie + } + + type MovieOperation { + connection(after: String, first: Int, sort: [MovieConnectionSort!]): MovieConnection + } + + input MovieOperationWhere { + AND: [MovieOperationWhere!] + NOT: MovieOperationWhere + OR: [MovieOperationWhere!] + node: MovieWhere + } + + input MovieSort { + flag: SortDirection + id: SortDirection + length: SortDirection + releasedDateTime: SortDirection + title: SortDirection + year: SortDirection + } + + input MovieWhere { + AND: [MovieWhere!] + NOT: MovieWhere + OR: [MovieWhere!] + flag: BooleanWhere + id: IDWhere + length: FloatWhere + releasedDateTime: DateTimeWhere + title: StringWhere + year: IntWhere + } + + type Mutation { + createMovies(input: [MovieCreateInput!]!): MovieCreateResponse + } + + type PageInfo { + endCursor: String + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String + } + + type Query { + movies(where: MovieOperationWhere): MovieOperation + } + + enum SortDirection { + ASC + DESC + } + + input StringWhere { + AND: [StringWhere!] + NOT: StringWhere + OR: [StringWhere!] + contains: String + endsWith: String + equals: String + in: [String!] + startsWith: String + }" + `); + }); + + test.todo("@default directive with relationship properties"); + test.todo("@default directive with user defined scalars"); +}); diff --git a/packages/graphql/tests/api-v6/schema/invalid-schema/invalid-default-usage-on-list-fields.test.ts b/packages/graphql/tests/api-v6/schema/invalid-schema/invalid-default-usage-on-list-fields.test.ts new file mode 100644 index 0000000000..ae3d73a63b --- /dev/null +++ b/packages/graphql/tests/api-v6/schema/invalid-schema/invalid-default-usage-on-list-fields.test.ts @@ -0,0 +1,130 @@ +/* + * 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 { GraphQLError } from "graphql"; +import { Neo4jGraphQL } from "../../../../src"; +import { raiseOnInvalidSchema } from "../../../utils/raise-on-invalid-schema"; + +describe("invalid @default usage on List fields", () => { + test("@default should fail without define a value", async () => { + const fn = async () => { + const typeDefs = /* GraphQL */ ` + type User @node { + name: [String] @default + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + }; + await expect(fn()).rejects.toEqual([ + new GraphQLError( + 'Directive "@default" argument "value" of type "ScalarOrEnum!" is required, but it was not provided.' + ), + ]); + }); + + test.each([ + { + dataType: "[ID]", + value: [1.2], + errorMsg: "@default.value on ID list fields must be a list of ID values", + }, + { + dataType: "[String]", + value: [1.2], + errorMsg: "@default.value on String list fields must be a list of String values", + }, + { + dataType: "[Boolean]", + value: [1.2], + errorMsg: "@default.value on Boolean list fields must be a list of Boolean values", + }, + { dataType: "[Int]", value: 1.2, errorMsg: "@default.value on Int list fields must be a list of Int values" }, + { + dataType: "[Float]", + value: ["stuff"], + errorMsg: "@default.value on Float list fields must be a list of Float values", + }, + { + dataType: "[DateTime]", + value: ["dummy"], + errorMsg: "@default.value on DateTime list fields must be a list of DateTime values", + }, + ] as const)( + "@default should fail with an invalid $dataType value", + async ({ dataType, value: value, errorMsg }) => { + const stringValue = typeof value === "string" ? `"${value}"` : value; + const fn = async () => { + const typeDefs = /* GraphQL */ ` + type User @node { + name: ${dataType} @default(value: ${stringValue}) + } + extend type User { + anotherField: ${dataType} @default(value: ${stringValue}) + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + }; + + await expect(fn()).rejects.toEqual([new GraphQLError(errorMsg), new GraphQLError(errorMsg)]); + } + ); + + test.each([ + { + dataType: "[ID]", + value: ["some-unique-id", "another-unique-id"], + }, + { + dataType: "[String]", + value: ["dummyValue", "anotherDummyValue"], + }, + { + dataType: "[Boolean]", + value: [false, true], + }, + { dataType: "[Int]", value: [1, 3] }, + { dataType: "[Float]", value: [1.2, 1.3] }, + { dataType: "[DateTime]", value: ["2021-01-01T00:00:00", "2022-01-01T00:00:00"] }, + ] as const)("@default should not fail with a valid $dataType value", async ({ dataType, value }) => { + const fn = async () => { + const stringValue = value.map((v) => (typeof v === "string" ? `"${v}"` : v)).join(", "); + const typeDefs = /* GraphQL */ ` + type User @node { + name: ${dataType} @default(value: [${stringValue}]) + } + extend type User { + anotherField: ${dataType} @default(value: [${stringValue}]) + } + `; + + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + }; + await expect(fn()).resolves.not.toThrow(); + }); + + test.todo("add tests for LocalDateTime, Date, Time, LocalTime, Duration when supported"); + test.todo("@default with custom enum"); + test.todo("@default with user defined scalar"); +}); diff --git a/packages/graphql/tests/api-v6/schema/invalid-schema/invalid-default-usage.test.ts b/packages/graphql/tests/api-v6/schema/invalid-schema/invalid-default-usage.test.ts new file mode 100644 index 0000000000..56dd419d9f --- /dev/null +++ b/packages/graphql/tests/api-v6/schema/invalid-schema/invalid-default-usage.test.ts @@ -0,0 +1,124 @@ +/* + * 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 { GraphQLError } from "graphql"; +import { Neo4jGraphQL } from "../../../../src"; +import { raiseOnInvalidSchema } from "../../../utils/raise-on-invalid-schema"; + +describe("invalid @default usage", () => { + test("@default should fail without a defined value", async () => { + const fn = async () => { + const typeDefs = /* GraphQL */ ` + type User @node { + name: String @default + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + }; + await expect(fn()).rejects.toEqual([ + new GraphQLError( + 'Directive "@default" argument "value" of type "ScalarOrEnum!" is required, but it was not provided.' + ), + ]); + }); + + test.each([ + { + dataType: "ID", + value: 1.2, + errorMsg: "@default.value on ID fields must be of type ID", + }, + { + dataType: "String", + value: 1.2, + errorMsg: "@default.value on String fields must be of type String", + }, + { + dataType: "Boolean", + value: 1.2, + errorMsg: "@default.value on Boolean fields must be of type Boolean", + }, + { dataType: "Int", value: 1.2, errorMsg: "@default.value on Int fields must be of type Int" }, + { + dataType: "Float", + value: "stuff", + errorMsg: "@default.value on Float fields must be of type Float", + }, + { dataType: "DateTime", value: "dummy", errorMsg: "@default.value is not a valid DateTime" }, + ] as const)( + "@default should fail with an invalid $dataType value", + async ({ dataType, value: value, errorMsg }) => { + const fn = async () => { + const stringValue = typeof value === "string" ? `"${value}"` : value; + const typeDefs = /* GraphQL */ ` + type User @node { + name: ${dataType} @default(value: ${stringValue}) + } + extend type User { + anotherField: ${dataType} @default(value: ${stringValue}) + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + }; + await expect(fn()).rejects.toEqual([new GraphQLError(errorMsg), new GraphQLError(errorMsg)]); + } + ); + + test.each([ + { + dataType: "ID", + value: "some-unique-id", + }, + { + dataType: "String", + value: "dummyValue", + }, + { + dataType: "Boolean", + value: false, + }, + { dataType: "Int", value: 1 }, + { dataType: "Float", value: 1.2 }, + { dataType: "DateTime", value: "2021-01-01T00:00:00" }, + ] as const)("@default should not fail with a valid $dataType value", async ({ dataType, value }) => { + const fn = async () => { + const stringValue = typeof value === "string" ? `"${value}"` : value; + const typeDefs = /* GraphQL */ ` + type User @node { + name: ${dataType} @default(value: ${stringValue}) + } + extend type User { + anotherField: ${dataType} @default(value: ${stringValue}) + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + }; + await expect(fn()).resolves.not.toThrow(); + }); + + test.todo("add tests for LocalDateTime, Date, Time, LocalTime, Duration when supported"); + test.todo("@default with custom enum"); + test.todo("@default with user defined scalar"); +}); diff --git a/packages/graphql/tests/integration/directives/default.int.test.ts b/packages/graphql/tests/integration/directives/default.int.test.ts index 46951cddc3..675b96b45f 100644 --- a/packages/graphql/tests/integration/directives/default.int.test.ts +++ b/packages/graphql/tests/integration/directives/default.int.test.ts @@ -27,72 +27,6 @@ describe("@default directive", () => { await testHelper.close(); }); - describe("with primitive fields", () => { - test("on non-primitive field should throw an error", async () => { - const typeDefs = ` - type User { - name: String! - location: Point! @default(value: "default") - } - `; - - const neoSchema = await testHelper.initNeo4jGraphQL({ - typeDefs, - }); - - await expect(neoSchema.getSchema()).rejects.toIncludeSameMembers([ - new GraphQLError("@default is not supported by Spatial types."), - ]); - }); - - test("with an argument with a type which doesn't match the field should throw an error", async () => { - const typeDefs = ` - type User { - name: String! @default(value: 2) - } - `; - - const neoSchema = await testHelper.initNeo4jGraphQL({ - typeDefs, - }); - - await expect(neoSchema.getSchema()).rejects.toIncludeSameMembers([ - new GraphQLError("@default.value on String fields must be of type String"), - ]); - }); - - test("on a DateTime with an invalid value should throw an error", async () => { - const typeDefs = ` - type User { - verifiedAt: DateTime! @default(value: "Not a date") - } - `; - - const neoSchema = await testHelper.initNeo4jGraphQL({ - typeDefs, - }); - - await expect(neoSchema.getSchema()).rejects.toIncludeSameMembers([ - new GraphQLError("@default.value is not a valid DateTime"), - ]); - }); - - test("on primitive field should not throw an error", async () => { - const typeDefs = ` - type User { - name: String! - location: String! @default(value: "somewhere") - } - `; - - const neoSchema = await testHelper.initNeo4jGraphQL({ - typeDefs, - }); - - await expect(neoSchema.getSchema()).resolves.not.toThrow(); - }); - }); - describe("with enum fields", () => { test("on enum field with incorrect value should throw an error", async () => { const typeDefs = ` diff --git a/packages/graphql/tests/schema/issues/200.test.ts b/packages/graphql/tests/schema/issues/200.test.ts index 9e834fc5b9..4e3a6f6fdb 100644 --- a/packages/graphql/tests/schema/issues/200.test.ts +++ b/packages/graphql/tests/schema/issues/200.test.ts @@ -18,11 +18,11 @@ */ import { printSchemaWithDirectives } from "@graphql-tools/utils"; -import { lexicographicSortSchema } from "graphql/utilities"; import { gql } from "graphql-tag"; +import { lexicographicSortSchema } from "graphql/utilities"; import { Neo4jGraphQL } from "../../../src"; -describe("200", () => { +describe("https://github.com/neo4j/graphql/issues/200", () => { test("Preserve schema array non null", async () => { const typeDefs = gql` type Category {