From 441fd8bd285e3b482c3450a23cdcbde7dccec844 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Fri, 2 Aug 2024 11:09:20 +0100 Subject: [PATCH 1/7] Remove unused parameter typeNames from TopLevelCreateSchemaTypes --- .../schema-types/TopLevelEntitySchemaTypes.ts | 2 +- .../TopLevelCreateSchemaTypes.ts | 13 +------------ 2 files changed, 2 insertions(+), 13 deletions(-) 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..d31f4e47ee 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 @@ -35,27 +35,16 @@ import type { ConcreteEntity } from "../../../../schema-model/entity/ConcreteEnt 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"; 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 { From 35c510a259b9b269c774466b5b1a868fadeb3fbd Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Fri, 2 Aug 2024 14:34:05 +0100 Subject: [PATCH 2/7] initial implementation @default directive --- .../api-v6/schema-generation/SchemaBuilder.ts | 22 ++- .../TopLevelCreateSchemaTypes.ts | 38 ++-- .../api-v6/schema/directives/default.test.ts | 165 ++++++++++++++++++ 3 files changed, 205 insertions(+), 20 deletions(-) create mode 100644 packages/graphql/tests/api-v6/schema/directives/default.test.ts 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/mutation-schema-types/TopLevelCreateSchemaTypes.ts b/packages/graphql/src/api-v6/schema-generation/schema-types/mutation-schema-types/TopLevelCreateSchemaTypes.ts index d31f4e47ee..0affbe825d 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,7 +33,7 @@ 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 { InputFieldDefinition, SchemaBuilder, WrappedComposer } from "../../SchemaBuilder"; export class TopLevelCreateSchemaTypes { private entityTypeNames: TopLevelEntityTypeNames; @@ -68,37 +67,41 @@ 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 inputField = this.attributeToInputField(attribute.type, attribute); + const fieldDefinition: InputFieldDefinition = { + type: inputField, + defaultValue: attribute.annotations.default?.value, + }; if (inputField) { - return [attribute.name, inputField]; + return [attribute.name, fieldDefinition]; } }) ); return Object.fromEntries(inputFields); } - private attributeToInputField(type: AttributeType): any { + private attributeToInputField(type: AttributeType, attribute: Attribute): any { if (type instanceof ListType) { if (type.isRequired) { - return this.attributeToInputField(type.ofType).List.NonNull; + return this.attributeToInputField(type.ofType, attribute).List.NonNull; } - return this.attributeToInputField(type.ofType).List; + return this.attributeToInputField(type.ofType, attribute).List; } if (type instanceof ScalarType) { - return this.createBuiltInFieldInput(type); + return this.createBuiltInFieldInput(type, attribute); } if (type instanceof Neo4jTemporalType) { - return this.createTemporalFieldInput(type); + return this.createTemporalFieldInput(type, attribute); } if (type instanceof Neo4jSpatialType) { - return this.createSpatialFieldInput(type); + return this.createSpatialFieldInput(type, attribute); } } - private createBuiltInFieldInput(type: ScalarType): ScalarTypeComposer | NonNullComposer { + private createBuiltInFieldInput(type: ScalarType, attribute: Attribute): WrappedComposer { let builtInType: ScalarTypeComposer; switch (type.name) { case GraphQLBuiltInScalarType.Boolean: { @@ -136,8 +139,9 @@ export class TopLevelCreateSchemaTypes { } private createTemporalFieldInput( - type: Neo4jTemporalType - ): ScalarTypeComposer | NonNullComposer { + type: Neo4jTemporalType, + attribute: Attribute + ): WrappedComposer { let builtInType: ScalarTypeComposer; switch (type.name) { case Neo4jGraphQLTemporalType.Date: { @@ -174,7 +178,7 @@ export class TopLevelCreateSchemaTypes { return builtInType; } - private createSpatialFieldInput(type: Neo4jSpatialType): InputTypeComposer | NonNullComposer { + private createSpatialFieldInput(type: Neo4jSpatialType, attribute: Attribute): WrappedComposer { let builtInType: InputTypeComposer; switch (type.name) { case Neo4jGraphQLSpatialType.CartesianPoint: { 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..8bc59a047b --- /dev/null +++ b/packages/graphql/tests/api-v6/schema/directives/default.test.ts @@ -0,0 +1,165 @@ +/* + * 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", () => { + 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) + # isReleased: Boolean @default(value: true) + # playedCounter: BigInt @default(value: "100") + # duration: Duration @default(value: "PT190M") + # releasedDate: Date @default(value: "2021-01-01") + # releasedTime: Time @default(value: "00:00:00+0100") + # releasedDateTime: DateTime @default(value: "2021-01-01T00:00:00") + # releasedLocalTime: LocalTime @default(value: "00:00:00") + # releasedLocalDateTime: LocalDateTime @default(value: "2021-01-01T00:00:00+0100") + # premiereLocation: Point @default(value: { longitude: 1, latitude: 2 }) + # locations: [Point] @default(value: [{ longitude: 1, latitude: 2 }]) + # premiereGeoLocation: [CartesianPoint] @default(value: { x: 1, y: 2 }) + # geoLocations: [CartesianPoint] @default(value: [{ x: 1, y: 2 }]) + } + `; + 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 IDWhere { + AND: [IDWhere!] + NOT: IDWhere + OR: [IDWhere!] + contains: ID + endsWith: ID + equals: ID + in: [ID!] + startsWith: ID + } + + type Movie { + id: ID! + title: String + } + + type MovieConnection { + edges: [MovieEdge] + pageInfo: PageInfo + } + + input MovieConnectionSort { + node: MovieSort + } + + type MovieCreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + + input MovieCreateInput { + node: MovieCreateNode! + } + + input MovieCreateNode { + id: ID! = \\"id\\" + title: String = \\"title\\" + } + + 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 { + id: SortDirection + title: SortDirection + } + + input MovieWhere { + AND: [MovieWhere!] + NOT: MovieWhere + OR: [MovieWhere!] + id: IDWhere + title: StringWhere + } + + 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 invalid values"); + test.todo("@default directive with relationship properties"); + test.todo("@default directive with user defined scalars"); +}); From 663e8ca4c7afcabe795ab4bba3a6ad1068057073 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Fri, 2 Aug 2024 16:20:26 +0100 Subject: [PATCH 3/7] add @default test with create --- .../default/create-default.int.test.ts | 71 +++++ .../api-v6/schema/directives/default.test.ts | 247 +++++++++++++++++- 2 files changed, 304 insertions(+), 14 deletions(-) create mode 100644 packages/graphql/tests/api-v6/integration/directives/default/create-default.int.test.ts 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..57e28e07c5 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/directives/default/create-default.int.test.ts @@ -0,0 +1,71 @@ +/* + * 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 { UniqueType } from "../../../../utils/graphql-types"; +import { TestHelper } from "../../../../utils/tests-helper"; + +describe("Create with @default", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + title: String! + released: Int @default(value: 2001) + ratings: [Int!] @default(value: [1, 2, 3]) + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("should create two movies and project them", async () => { + const mutation = /* GraphQL */ ` + mutation { + ${Movie.operations.create}(input: [ + { node: { title: "The Matrix" } }, + { node: { title: "The Matrix 2"} } + ]) { + ${Movie.plural} { + title + released + ratings + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(mutation); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.operations.create]: { + [Movie.plural]: expect.toIncludeSameMembers([ + { title: "The Matrix", released: 2001, ratings: [1, 2, 3] }, + { title: "The Matrix 2", released: 2001, ratings: [1, 2, 3] }, + ]), + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/schema/directives/default.test.ts b/packages/graphql/tests/api-v6/schema/directives/default.test.ts index 8bc59a047b..6f208faa6d 100644 --- a/packages/graphql/tests/api-v6/schema/directives/default.test.ts +++ b/packages/graphql/tests/api-v6/schema/directives/default.test.ts @@ -28,20 +28,18 @@ describe("@default", () => { type Movie @node { id: ID! @default(value: "id") title: String @default(value: "title") - # year: Int @default(value: 2021) - # length: Float @default(value: 120.5) - # isReleased: Boolean @default(value: true) - # playedCounter: BigInt @default(value: "100") - # duration: Duration @default(value: "PT190M") - # releasedDate: Date @default(value: "2021-01-01") - # releasedTime: Time @default(value: "00:00:00+0100") - # releasedDateTime: DateTime @default(value: "2021-01-01T00:00:00") - # releasedLocalTime: LocalTime @default(value: "00:00:00") - # releasedLocalDateTime: LocalDateTime @default(value: "2021-01-01T00:00:00+0100") - # premiereLocation: Point @default(value: { longitude: 1, latitude: 2 }) - # locations: [Point] @default(value: [{ longitude: 1, latitude: 2 }]) - # premiereGeoLocation: [CartesianPoint] @default(value: { x: 1, y: 2 }) - # geoLocations: [CartesianPoint] @default(value: [{ x: 1, y: 2 }]) + year: Int @default(value: 2021) + length: Float @default(value: 120.5) + isReleased: Boolean @default(value: true) + playedCounter: BigInt @default(value: "100") + duration: Duration @default(value: "PT190M") + releasedDate: Date @default(value: "2021-01-01") + releasedTime: Time @default(value: "00:00:00") + releasedDateTime: DateTime @default(value: "2021-01-01T00:00:00") + releasedLocalTime: LocalTime @default(value: "00:00:00") + releasedLocalDateTime: LocalDateTime @default(value: "2015-07-04T19:32:24") + premiereLocation: Point @default(value: { longitude: 1, latitude: 2 }) + premiereGeoLocation: CartesianPoint @default(value: { x: 1, y: 2 }) } `; const neoSchema = new Neo4jGraphQL({ typeDefs }); @@ -54,6 +52,105 @@ describe("@default", () => { mutation: Mutation } + \\"\\"\\" + A BigInt value up to 64 bits in size, which can be a number or a string if used inline, or a string only if used as a variable. Always returned as a string. + \\"\\"\\" + scalar BigInt + + input BigIntWhere { + AND: [BigIntWhere!] + NOT: BigIntWhere + OR: [BigIntWhere!] + equals: BigInt + gt: BigInt + gte: BigInt + in: [BigInt!] + lt: BigInt + lte: BigInt + } + + input BooleanWhere { + AND: [BooleanWhere!] + NOT: BooleanWhere + OR: [BooleanWhere!] + equals: Boolean + } + + \\"\\"\\" + A point in a two- or three-dimensional Cartesian coordinate system or in a three-dimensional cylindrical coordinate system. For more information, see https://neo4j.com/docs/graphql/4/type-definitions/types/spatial/#cartesian-point + \\"\\"\\" + type CartesianPoint { + crs: String! + srid: Int! + x: Float! + y: Float! + z: Float + } + + \\"\\"\\"Input type for a cartesian point\\"\\"\\" + input CartesianPointInput { + x: Float! + y: Float! + z: Float + } + + \\"\\"\\"A date, represented as a 'yyyy-mm-dd' string\\"\\"\\" + scalar Date + + \\"\\"\\"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 DateWhere { + AND: [DateWhere!] + NOT: DateWhere + OR: [DateWhere!] + equals: Date + gt: Date + gte: Date + in: [Date!] + lt: Date + lte: Date + } + + \\"\\"\\"A duration, represented as an ISO 8601 duration string\\"\\"\\" + scalar Duration + + input DurationWhere { + AND: [DurationWhere!] + NOT: DurationWhere + OR: [DurationWhere!] + equals: Duration + gt: Duration + gte: Duration + in: [Duration!] + lt: Duration + lte: Duration + } + + 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 @@ -65,9 +162,65 @@ describe("@default", () => { startsWith: ID } + input IntWhere { + AND: [IntWhere!] + NOT: IntWhere + OR: [IntWhere!] + equals: Int + gt: Int + gte: Int + in: [Int!] + lt: Int + lte: Int + } + + \\"\\"\\"A local datetime, represented as 'YYYY-MM-DDTHH:MM:SS'\\"\\"\\" + scalar LocalDateTime + + input LocalDateTimeWhere { + AND: [LocalDateTimeWhere!] + NOT: LocalDateTimeWhere + OR: [LocalDateTimeWhere!] + equals: LocalDateTime + gt: LocalDateTime + gte: LocalDateTime + in: [LocalDateTime!] + lt: LocalDateTime + lte: LocalDateTime + } + + \\"\\"\\" + A local time, represented as a time string without timezone information + \\"\\"\\" + scalar LocalTime + + input LocalTimeWhere { + AND: [LocalTimeWhere!] + NOT: LocalTimeWhere + OR: [LocalTimeWhere!] + equals: LocalTime + gt: LocalTime + gte: LocalTime + in: [LocalTime!] + lt: LocalTime + lte: LocalTime + } + type Movie { + duration: Duration id: ID! + isReleased: Boolean + length: Float + playedCounter: BigInt + premiereGeoLocation: CartesianPoint + premiereLocation: Point + releasedDate: Date + releasedDateTime: DateTime + releasedLocalDateTime: LocalDateTime + releasedLocalTime: LocalTime + releasedTime: Time title: String + year: Int } type MovieConnection { @@ -89,8 +242,20 @@ describe("@default", () => { } input MovieCreateNode { + duration: Duration = \\"P0M0DT11400S\\" id: ID! = \\"id\\" + isReleased: Boolean = true + length: Float = 120.5 + playedCounter: BigInt = \\"100\\" + premiereGeoLocation: CartesianPointInput = {x: 1, y: 2} + premiereLocation: PointInput = {latitude: 2, longitude: 1} + releasedDate: Date = \\"2021-01-01\\" + releasedDateTime: DateTime = \\"2021-01-01T00:00:00.000Z\\" + releasedLocalDateTime: LocalDateTime = \\"2015-07-04T19:32:24\\" + releasedLocalTime: LocalTime = \\"00:00:00\\" + releasedTime: Time = \\"00:00:00Z\\" title: String = \\"title\\" + year: Int = 2021 } type MovieCreateResponse { @@ -115,16 +280,36 @@ describe("@default", () => { } input MovieSort { + duration: SortDirection id: SortDirection + isReleased: SortDirection + length: SortDirection + playedCounter: SortDirection + releasedDate: SortDirection + releasedDateTime: SortDirection + releasedLocalDateTime: SortDirection + releasedLocalTime: SortDirection + releasedTime: SortDirection title: SortDirection + year: SortDirection } input MovieWhere { AND: [MovieWhere!] NOT: MovieWhere OR: [MovieWhere!] + duration: DurationWhere id: IDWhere + isReleased: BooleanWhere + length: FloatWhere + playedCounter: BigIntWhere + releasedDate: DateWhere + releasedDateTime: DateTimeWhere + releasedLocalDateTime: LocalDateTimeWhere + releasedLocalTime: LocalTimeWhere + releasedTime: TimeWhere title: StringWhere + year: IntWhere } type Mutation { @@ -138,6 +323,24 @@ describe("@default", () => { startCursor: String } + \\"\\"\\" + A point in a coordinate system. For more information, see https://neo4j.com/docs/graphql/4/type-definitions/types/spatial/#point + \\"\\"\\" + type Point { + crs: String! + height: Float + latitude: Float! + longitude: Float! + srid: Int! + } + + \\"\\"\\"Input type for a point\\"\\"\\" + input PointInput { + height: Float + latitude: Float! + longitude: Float! + } + type Query { movies(where: MovieOperationWhere): MovieOperation } @@ -156,10 +359,26 @@ describe("@default", () => { equals: String in: [String!] startsWith: String + } + + \\"\\"\\"A time, represented as an RFC3339 time string\\"\\"\\" + scalar Time + + input TimeWhere { + AND: [TimeWhere!] + NOT: TimeWhere + OR: [TimeWhere!] + equals: Time + gt: Time + gte: Time + in: [Time!] + lt: Time + lte: Time }" `); }); test.todo("@default directive with invalid values"); + test.todo("@default directive with list properties"); test.todo("@default directive with relationship properties"); test.todo("@default directive with user defined scalars"); }); From 015cdf339e392dbbebbe760553c13bd13a076c0d Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Mon, 5 Aug 2024 17:21:14 +0100 Subject: [PATCH 4/7] add validation for @default, tests, and clean-up @default implementation --- .../TopLevelCreateSchemaTypes.ts | 23 +-- .../api-v6/validation/rules/valid-default.ts | 101 ++++++++++ .../api-v6/validation/validate-v6-document.ts | 2 + .../utils/same-type-argument-as-field.ts | 6 +- .../validation/validate-document.test.ts | 34 ---- .../default/create-default.int.test.ts | 30 ++- .../schema/directives/default-array.test.ts | 160 ++++++++++++++++ .../api-v6/schema/directives/default.test.ts | 181 +----------------- .../invalid-default-values.test.ts | 116 +++++++++++ 9 files changed, 413 insertions(+), 240 deletions(-) create mode 100644 packages/graphql/src/api-v6/validation/rules/valid-default.ts create mode 100644 packages/graphql/tests/api-v6/schema/directives/default-array.test.ts create mode 100644 packages/graphql/tests/api-v6/schema/invalid-schema/invalid-default-values.test.ts 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 0affbe825d..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 @@ -70,7 +70,7 @@ export class TopLevelCreateSchemaTypes { private getInputFields(attributes: Attribute[]): Record { const inputFields: Array<[string, InputFieldDefinition] | []> = filterTruthy( attributes.map((attribute) => { - const inputField = this.attributeToInputField(attribute.type, attribute); + const inputField = this.attributeToInputField(attribute.type); const fieldDefinition: InputFieldDefinition = { type: inputField, defaultValue: attribute.annotations.default?.value, @@ -83,25 +83,25 @@ export class TopLevelCreateSchemaTypes { return Object.fromEntries(inputFields); } - private attributeToInputField(type: AttributeType, attribute: Attribute): any { + private attributeToInputField(type: AttributeType): any { if (type instanceof ListType) { if (type.isRequired) { - return this.attributeToInputField(type.ofType, attribute).List.NonNull; + return this.attributeToInputField(type.ofType).List.NonNull; } - return this.attributeToInputField(type.ofType, attribute).List; + return this.attributeToInputField(type.ofType).List; } if (type instanceof ScalarType) { - return this.createBuiltInFieldInput(type, attribute); + return this.createBuiltInFieldInput(type); } if (type instanceof Neo4jTemporalType) { - return this.createTemporalFieldInput(type, attribute); + return this.createTemporalFieldInput(type); } if (type instanceof Neo4jSpatialType) { - return this.createSpatialFieldInput(type, attribute); + return this.createSpatialFieldInput(type); } } - private createBuiltInFieldInput(type: ScalarType, attribute: Attribute): WrappedComposer { + private createBuiltInFieldInput(type: ScalarType): WrappedComposer { let builtInType: ScalarTypeComposer; switch (type.name) { case GraphQLBuiltInScalarType.Boolean: { @@ -138,10 +138,7 @@ export class TopLevelCreateSchemaTypes { return builtInType; } - private createTemporalFieldInput( - type: Neo4jTemporalType, - attribute: Attribute - ): WrappedComposer { + private createTemporalFieldInput(type: Neo4jTemporalType): WrappedComposer { let builtInType: ScalarTypeComposer; switch (type.name) { case Neo4jGraphQLTemporalType.Date: { @@ -178,7 +175,7 @@ export class TopLevelCreateSchemaTypes { return builtInType; } - private createSpatialFieldInput(type: Neo4jSpatialType, attribute: Attribute): WrappedComposer { + 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..ced97d40fb --- /dev/null +++ b/packages/graphql/src/api-v6/validation/rules/valid-default.ts @@ -0,0 +1,101 @@ +/* + * 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 supported + // !isTypeABuiltInType(expectedType) && !userEnums.some((x) => x.name.value === expectedType) + 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..cbfd2b9a26 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` 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 index 57e28e07c5..5ff485814e 100644 --- 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 @@ -17,31 +17,30 @@ * limitations under the License. */ -import type { UniqueType } from "../../../../utils/graphql-types"; import { TestHelper } from "../../../../utils/tests-helper"; describe("Create with @default", () => { const testHelper = new TestHelper({ v6Api: true }); - let Movie: UniqueType; - beforeAll(async () => { - Movie = testHelper.createUniqueType("Movie"); + 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! - released: Int @default(value: 2001) - ratings: [Int!] @default(value: [1, 2, 3]) + testField: ${dataType} @default(value: ${typeof value === "string" ? `"${value}"` : value}) } `; await testHelper.initNeo4jGraphQL({ typeDefs }); - }); - - afterAll(async () => { - await testHelper.close(); - }); - test("should create two movies and project them", async () => { const mutation = /* GraphQL */ ` mutation { ${Movie.operations.create}(input: [ @@ -50,8 +49,7 @@ describe("Create with @default", () => { ]) { ${Movie.plural} { title - released - ratings + testField } } } @@ -62,8 +60,8 @@ describe("Create with @default", () => { expect(gqlResult.data).toEqual({ [Movie.operations.create]: { [Movie.plural]: expect.toIncludeSameMembers([ - { title: "The Matrix", released: 2001, ratings: [1, 2, 3] }, - { title: "The Matrix 2", released: 2001, ratings: [1, 2, 3] }, + { 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 index 6f208faa6d..6c09889c99 100644 --- a/packages/graphql/tests/api-v6/schema/directives/default.test.ts +++ b/packages/graphql/tests/api-v6/schema/directives/default.test.ts @@ -22,7 +22,7 @@ import { lexicographicSortSchema } from "graphql/utilities"; import { Neo4jGraphQL } from "../../../../src"; import { raiseOnInvalidSchema } from "../../../utils/raise-on-invalid-schema"; -describe("@default", () => { +describe("@default on fields", () => { test("@default should add a default value in mutation inputs", async () => { const typeDefs = /* GraphQL */ ` type Movie @node { @@ -30,16 +30,8 @@ describe("@default", () => { title: String @default(value: "title") year: Int @default(value: 2021) length: Float @default(value: 120.5) - isReleased: Boolean @default(value: true) - playedCounter: BigInt @default(value: "100") - duration: Duration @default(value: "PT190M") - releasedDate: Date @default(value: "2021-01-01") - releasedTime: Time @default(value: "00:00:00") + flag: Boolean @default(value: true) releasedDateTime: DateTime @default(value: "2021-01-01T00:00:00") - releasedLocalTime: LocalTime @default(value: "00:00:00") - releasedLocalDateTime: LocalDateTime @default(value: "2015-07-04T19:32:24") - premiereLocation: Point @default(value: { longitude: 1, latitude: 2 }) - premiereGeoLocation: CartesianPoint @default(value: { x: 1, y: 2 }) } `; const neoSchema = new Neo4jGraphQL({ typeDefs }); @@ -52,23 +44,6 @@ describe("@default", () => { mutation: Mutation } - \\"\\"\\" - A BigInt value up to 64 bits in size, which can be a number or a string if used inline, or a string only if used as a variable. Always returned as a string. - \\"\\"\\" - scalar BigInt - - input BigIntWhere { - AND: [BigIntWhere!] - NOT: BigIntWhere - OR: [BigIntWhere!] - equals: BigInt - gt: BigInt - gte: BigInt - in: [BigInt!] - lt: BigInt - lte: BigInt - } - input BooleanWhere { AND: [BooleanWhere!] NOT: BooleanWhere @@ -76,27 +51,6 @@ describe("@default", () => { equals: Boolean } - \\"\\"\\" - A point in a two- or three-dimensional Cartesian coordinate system or in a three-dimensional cylindrical coordinate system. For more information, see https://neo4j.com/docs/graphql/4/type-definitions/types/spatial/#cartesian-point - \\"\\"\\" - type CartesianPoint { - crs: String! - srid: Int! - x: Float! - y: Float! - z: Float - } - - \\"\\"\\"Input type for a cartesian point\\"\\"\\" - input CartesianPointInput { - x: Float! - y: Float! - z: Float - } - - \\"\\"\\"A date, represented as a 'yyyy-mm-dd' string\\"\\"\\" - scalar Date - \\"\\"\\"A date and time, represented as an ISO-8601 string\\"\\"\\" scalar DateTime @@ -112,33 +66,6 @@ describe("@default", () => { lte: DateTime } - input DateWhere { - AND: [DateWhere!] - NOT: DateWhere - OR: [DateWhere!] - equals: Date - gt: Date - gte: Date - in: [Date!] - lt: Date - lte: Date - } - - \\"\\"\\"A duration, represented as an ISO 8601 duration string\\"\\"\\" - scalar Duration - - input DurationWhere { - AND: [DurationWhere!] - NOT: DurationWhere - OR: [DurationWhere!] - equals: Duration - gt: Duration - gte: Duration - in: [Duration!] - lt: Duration - lte: Duration - } - input FloatWhere { AND: [FloatWhere!] NOT: FloatWhere @@ -174,51 +101,11 @@ describe("@default", () => { lte: Int } - \\"\\"\\"A local datetime, represented as 'YYYY-MM-DDTHH:MM:SS'\\"\\"\\" - scalar LocalDateTime - - input LocalDateTimeWhere { - AND: [LocalDateTimeWhere!] - NOT: LocalDateTimeWhere - OR: [LocalDateTimeWhere!] - equals: LocalDateTime - gt: LocalDateTime - gte: LocalDateTime - in: [LocalDateTime!] - lt: LocalDateTime - lte: LocalDateTime - } - - \\"\\"\\" - A local time, represented as a time string without timezone information - \\"\\"\\" - scalar LocalTime - - input LocalTimeWhere { - AND: [LocalTimeWhere!] - NOT: LocalTimeWhere - OR: [LocalTimeWhere!] - equals: LocalTime - gt: LocalTime - gte: LocalTime - in: [LocalTime!] - lt: LocalTime - lte: LocalTime - } - type Movie { - duration: Duration + flag: Boolean id: ID! - isReleased: Boolean length: Float - playedCounter: BigInt - premiereGeoLocation: CartesianPoint - premiereLocation: Point - releasedDate: Date releasedDateTime: DateTime - releasedLocalDateTime: LocalDateTime - releasedLocalTime: LocalTime - releasedTime: Time title: String year: Int } @@ -242,18 +129,10 @@ describe("@default", () => { } input MovieCreateNode { - duration: Duration = \\"P0M0DT11400S\\" + flag: Boolean = true id: ID! = \\"id\\" - isReleased: Boolean = true length: Float = 120.5 - playedCounter: BigInt = \\"100\\" - premiereGeoLocation: CartesianPointInput = {x: 1, y: 2} - premiereLocation: PointInput = {latitude: 2, longitude: 1} - releasedDate: Date = \\"2021-01-01\\" releasedDateTime: DateTime = \\"2021-01-01T00:00:00.000Z\\" - releasedLocalDateTime: LocalDateTime = \\"2015-07-04T19:32:24\\" - releasedLocalTime: LocalTime = \\"00:00:00\\" - releasedTime: Time = \\"00:00:00Z\\" title: String = \\"title\\" year: Int = 2021 } @@ -280,16 +159,10 @@ describe("@default", () => { } input MovieSort { - duration: SortDirection + flag: SortDirection id: SortDirection - isReleased: SortDirection length: SortDirection - playedCounter: SortDirection - releasedDate: SortDirection releasedDateTime: SortDirection - releasedLocalDateTime: SortDirection - releasedLocalTime: SortDirection - releasedTime: SortDirection title: SortDirection year: SortDirection } @@ -298,16 +171,10 @@ describe("@default", () => { AND: [MovieWhere!] NOT: MovieWhere OR: [MovieWhere!] - duration: DurationWhere + flag: BooleanWhere id: IDWhere - isReleased: BooleanWhere length: FloatWhere - playedCounter: BigIntWhere - releasedDate: DateWhere releasedDateTime: DateTimeWhere - releasedLocalDateTime: LocalDateTimeWhere - releasedLocalTime: LocalTimeWhere - releasedTime: TimeWhere title: StringWhere year: IntWhere } @@ -323,24 +190,6 @@ describe("@default", () => { startCursor: String } - \\"\\"\\" - A point in a coordinate system. For more information, see https://neo4j.com/docs/graphql/4/type-definitions/types/spatial/#point - \\"\\"\\" - type Point { - crs: String! - height: Float - latitude: Float! - longitude: Float! - srid: Int! - } - - \\"\\"\\"Input type for a point\\"\\"\\" - input PointInput { - height: Float - latitude: Float! - longitude: Float! - } - type Query { movies(where: MovieOperationWhere): MovieOperation } @@ -359,26 +208,10 @@ describe("@default", () => { equals: String in: [String!] startsWith: String - } - - \\"\\"\\"A time, represented as an RFC3339 time string\\"\\"\\" - scalar Time - - input TimeWhere { - AND: [TimeWhere!] - NOT: TimeWhere - OR: [TimeWhere!] - equals: Time - gt: Time - gte: Time - in: [Time!] - lt: Time - lte: Time }" `); }); - test.todo("@default directive with invalid values"); - test.todo("@default directive with list properties"); + 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-values.test.ts b/packages/graphql/tests/api-v6/schema/invalid-schema/invalid-default-values.test.ts new file mode 100644 index 0000000000..2cbd507f52 --- /dev/null +++ b/packages/graphql/tests/api-v6/schema/invalid-schema/invalid-default-values.test.ts @@ -0,0 +1,116 @@ +/* + * 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 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", + invalidValue: 1.2, + errorMsg: "@default.value on ID fields must be of type ID", + }, + { + dataType: "String", + invalidValue: 1.2, + errorMsg: "@default.value on String fields must be of type String", + }, + { + dataType: "Boolean", + invalidValue: 1.2, + errorMsg: "@default.value on Boolean fields must be of type Boolean", + }, + { dataType: "Int", invalidValue: 1.2, errorMsg: "@default.value on Int fields must be of type Int" }, + { + dataType: "Float", + invalidValue: "stuff", + errorMsg: "@default.value on Float fields must be of type Float", + }, + { dataType: "DateTime", invalidValue: "dummy", errorMsg: "@default.value is not a valid DateTime" }, + ] as const)( + "@default should fail with an invalid $dataType value", + async ({ dataType, invalidValue, errorMsg }) => { + const fn = async () => { + const typeDefs = /* GraphQL */ ` + type User @node { + name: ${dataType} @default(value: ${ + typeof invalidValue === "string" ? `"${invalidValue}"` : invalidValue + })} + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + }; + await expect(fn()).rejects.toEqual([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 typeDefs = /* GraphQL */ ` + type User @node { + name: ${dataType} @default(value: ${typeof value === "string" ? `"${value}"` : value})} + `; + 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"); +}); From 078d9bf96807dcf850eef2840136c456c6a7795f Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Mon, 5 Aug 2024 18:22:25 +0100 Subject: [PATCH 5/7] move @default tests --- .../validation/validate-document.test.ts | 459 ------------------ ...valid-default-usage-on-list-fields.test.ts | 130 +++++ ....test.ts => invalid-default-usage.test.ts} | 40 +- .../directives/default.int.test.ts | 66 --- .../graphql/tests/schema/issues/200.test.ts | 4 +- 5 files changed, 156 insertions(+), 543 deletions(-) create mode 100644 packages/graphql/tests/api-v6/schema/invalid-schema/invalid-default-usage-on-list-fields.test.ts rename packages/graphql/tests/api-v6/schema/invalid-schema/{invalid-default-values.test.ts => invalid-default-usage.test.ts} (75%) diff --git a/packages/graphql/src/schema/validation/validate-document.test.ts b/packages/graphql/src/schema/validation/validate-document.test.ts index cbfd2b9a26..f3b7a46781 100644 --- a/packages/graphql/src/schema/validation/validate-document.test.ts +++ b/packages/graphql/src/schema/validation/validate-document.test.ts @@ -964,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 { @@ -1180,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/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..e504d29a7e --- /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-values.test.ts b/packages/graphql/tests/api-v6/schema/invalid-schema/invalid-default-usage.test.ts similarity index 75% rename from packages/graphql/tests/api-v6/schema/invalid-schema/invalid-default-values.test.ts rename to packages/graphql/tests/api-v6/schema/invalid-schema/invalid-default-usage.test.ts index 2cbd507f52..ee27592c2f 100644 --- a/packages/graphql/tests/api-v6/schema/invalid-schema/invalid-default-values.test.ts +++ b/packages/graphql/tests/api-v6/schema/invalid-schema/invalid-default-usage.test.ts @@ -43,41 +43,44 @@ describe("invalid @default usage", () => { test.each([ { dataType: "ID", - invalidValue: 1.2, + value: 1.2, errorMsg: "@default.value on ID fields must be of type ID", }, { dataType: "String", - invalidValue: 1.2, + value: 1.2, errorMsg: "@default.value on String fields must be of type String", }, { dataType: "Boolean", - invalidValue: 1.2, + value: 1.2, errorMsg: "@default.value on Boolean fields must be of type Boolean", }, - { dataType: "Int", invalidValue: 1.2, errorMsg: "@default.value on Int fields must be of type Int" }, + { dataType: "Int", value: 1.2, errorMsg: "@default.value on Int fields must be of type Int" }, { dataType: "Float", - invalidValue: "stuff", + value: "stuff", errorMsg: "@default.value on Float fields must be of type Float", }, - { dataType: "DateTime", invalidValue: "dummy", errorMsg: "@default.value is not a valid DateTime" }, + { dataType: "DateTime", value: "dummy", errorMsg: "@default.value is not a valid DateTime" }, ] as const)( "@default should fail with an invalid $dataType value", - async ({ dataType, invalidValue, errorMsg }) => { + 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: ${ - typeof invalidValue === "string" ? `"${invalidValue}"` : invalidValue - })} - `; + 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)]); + await expect(fn()).rejects.toEqual([new GraphQLError(errorMsg), new GraphQLError(errorMsg)]); } ); @@ -99,10 +102,15 @@ describe("invalid @default usage", () => { { 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: ${typeof value === "string" ? `"${value}"` : value})} - `; + 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); 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 { From 98bb6c3ba9c0875c77552859da130ca36720c50a Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Tue, 6 Aug 2024 10:27:05 +0100 Subject: [PATCH 6/7] fix default for array tests --- .../src/api-v6/validation/rules/valid-default.ts | 3 +-- .../invalid-default-usage-on-list-fields.test.ts | 10 +++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/graphql/src/api-v6/validation/rules/valid-default.ts b/packages/graphql/src/api-v6/validation/rules/valid-default.ts index ced97d40fb..27e0f29453 100644 --- a/packages/graphql/src/api-v6/validation/rules/valid-default.ts +++ b/packages/graphql/src/api-v6/validation/rules/valid-default.ts @@ -65,8 +65,7 @@ export function ValidDefault(context: SDLValidationContext): ASTVisitor { ); } } else if (!isTypeABuiltInType(expectedType)) { - //TODO: Add check for user defined enums that are currently not supported - // !isTypeABuiltInType(expectedType) && !userEnums.some((x) => x.name.value === 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`, [] 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 index e504d29a7e..ae3d73a63b 100644 --- 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 @@ -43,28 +43,28 @@ describe("invalid @default usage on List fields", () => { test.each([ { dataType: "[ID]", - value: 1.2, + value: [1.2], errorMsg: "@default.value on ID list fields must be a list of ID values", }, { dataType: "[String]", - value: 1.2, + value: [1.2], errorMsg: "@default.value on String list fields must be a list of String values", }, { dataType: "[Boolean]", - value: 1.2, + 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", + value: ["stuff"], errorMsg: "@default.value on Float list fields must be a list of Float values", }, { dataType: "[DateTime]", - value: "dummy", + value: ["dummy"], errorMsg: "@default.value on DateTime list fields must be a list of DateTime values", }, ] as const)( From f08f22f5db39e638b1db9ba1a3da48e4c112a8ff Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Tue, 6 Aug 2024 13:27:54 +0100 Subject: [PATCH 7/7] Update packages/graphql/tests/api-v6/schema/invalid-schema/invalid-default-usage.test.ts Co-authored-by: Michael Webb <28074382+mjfwebb@users.noreply.github.com> --- .../api-v6/schema/invalid-schema/invalid-default-usage.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index ee27592c2f..56dd419d9f 100644 --- 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 @@ -22,7 +22,7 @@ import { Neo4jGraphQL } from "../../../../src"; import { raiseOnInvalidSchema } from "../../../utils/raise-on-invalid-schema"; describe("invalid @default usage", () => { - test("@default should fail without define a value", async () => { + test("@default should fail without a defined value", async () => { const fn = async () => { const typeDefs = /* GraphQL */ ` type User @node {