diff --git a/packages/graphql/src/schema-model/argument/Argument.ts b/packages/graphql/src/schema-model/argument/Argument.ts new file mode 100644 index 0000000000..1d10e459e5 --- /dev/null +++ b/packages/graphql/src/schema-model/argument/Argument.ts @@ -0,0 +1,48 @@ +/* + * 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 { ValueNode } from "graphql"; +import { parseValueNode } from "../parser/parse-value-node"; +import type { AttributeType } from "../attribute/AttributeType"; + +export class Argument { + public readonly name: string; + public readonly type: AttributeType; + public readonly defaultValue?: string; + public readonly description: string; + // Arguments can have annotations but we don't seem to use this feature + // public readonly annotations: Partial = {}; + + constructor({ + name, + type, + defaultValue, + description, + }: { + name: string; + type: AttributeType; + defaultValue?: ValueNode; + description?: string; + }) { + this.name = name; + this.type = type; + this.defaultValue = defaultValue ? parseValueNode(defaultValue) : undefined; + this.description = description || ""; + } +} diff --git a/packages/graphql/src/schema-model/argument/model-adapters/ArgumentAdapter.ts b/packages/graphql/src/schema-model/argument/model-adapters/ArgumentAdapter.ts new file mode 100644 index 0000000000..8541df7f17 --- /dev/null +++ b/packages/graphql/src/schema-model/argument/model-adapters/ArgumentAdapter.ts @@ -0,0 +1,238 @@ +/* + * 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 { AttributeType } from "../../attribute/AttributeType"; +import { + EnumType, + GraphQLBuiltInScalarType, + InterfaceType, + ListType, + Neo4jCartesianPointType, + Neo4jGraphQLNumberType, + Neo4jGraphQLSpatialType, + Neo4jGraphQLTemporalType, + Neo4jPointType, + ObjectType, + ScalarType, + UnionType, + UserScalarType, +} from "../../attribute/AttributeType"; +import type { Argument } from "../Argument"; + +// TODO: this file has a lot in common with AttributeAdapter +// if going to use this, design a solution to avoid this code duplication + +export class ArgumentAdapter { + public name: string; + public type: AttributeType; + public description: string; + public defaultValue?: string; + private assertionOptions: { + includeLists: boolean; + }; + constructor(argument: Argument) { + this.name = argument.name; + this.type = argument.type; + this.defaultValue = argument.defaultValue; + this.description = argument.description; + this.assertionOptions = { + includeLists: true, + }; + } + + /** + * Just a helper to get the wrapped type in case of a list, useful for the assertions + */ + private getTypeForAssertion(includeLists: boolean) { + if (includeLists) { + return this.isList() ? this.type.ofType : this.type; + } + return this.type; + } + + isBoolean(options = this.assertionOptions): boolean { + const type = this.getTypeForAssertion(options.includeLists); + return type instanceof ScalarType && type.name === GraphQLBuiltInScalarType.Boolean; + } + + isID(options = this.assertionOptions): boolean { + const type = this.getTypeForAssertion(options.includeLists); + return type instanceof ScalarType && type.name === GraphQLBuiltInScalarType.ID; + } + + isInt(options = this.assertionOptions): boolean { + const type = this.getTypeForAssertion(options.includeLists); + return type instanceof ScalarType && type.name === GraphQLBuiltInScalarType.Int; + } + + isFloat(options = this.assertionOptions): boolean { + const type = this.getTypeForAssertion(options.includeLists); + return type instanceof ScalarType && type.name === GraphQLBuiltInScalarType.Float; + } + + isString(options = this.assertionOptions): boolean { + const type = this.getTypeForAssertion(options.includeLists); + return type instanceof ScalarType && type.name === GraphQLBuiltInScalarType.String; + } + + isCartesianPoint(options = this.assertionOptions): boolean { + const type = this.getTypeForAssertion(options.includeLists); + return type instanceof Neo4jCartesianPointType; + } + + isPoint(options = this.assertionOptions): boolean { + const type = this.getTypeForAssertion(options.includeLists); + return type instanceof Neo4jPointType; + } + + isBigInt(options = this.assertionOptions): boolean { + const type = this.getTypeForAssertion(options.includeLists); + return type instanceof ScalarType && type.name === Neo4jGraphQLNumberType.BigInt; + } + + isDate(options = this.assertionOptions): boolean { + const type = this.getTypeForAssertion(options.includeLists); + return type instanceof ScalarType && type.name === Neo4jGraphQLTemporalType.Date; + } + + isDateTime(options = this.assertionOptions): boolean { + const type = this.getTypeForAssertion(options.includeLists); + return type instanceof ScalarType && type.name === Neo4jGraphQLTemporalType.DateTime; + } + + isLocalDateTime(options = this.assertionOptions): boolean { + const type = this.getTypeForAssertion(options.includeLists); + return type instanceof ScalarType && type.name === Neo4jGraphQLTemporalType.LocalDateTime; + } + + isTime(options = this.assertionOptions): boolean { + const type = this.getTypeForAssertion(options.includeLists); + return type instanceof ScalarType && type.name === Neo4jGraphQLTemporalType.Time; + } + + isLocalTime(options = this.assertionOptions): boolean { + const type = this.getTypeForAssertion(options.includeLists); + return (type.name as Neo4jGraphQLTemporalType) === Neo4jGraphQLTemporalType.LocalTime; + } + + isDuration(options = this.assertionOptions): boolean { + const type = this.getTypeForAssertion(options.includeLists); + return (type.name as Neo4jGraphQLTemporalType) === Neo4jGraphQLTemporalType.Duration; + } + + isObject(options = this.assertionOptions): boolean { + const type = this.getTypeForAssertion(options.includeLists); + return type instanceof ObjectType; + } + + isEnum(options = this.assertionOptions): boolean { + const type = this.getTypeForAssertion(options.includeLists); + return type instanceof EnumType; + } + + isInterface(options = this.assertionOptions): boolean { + const type = this.getTypeForAssertion(options.includeLists); + return type instanceof InterfaceType; + } + + isUnion(options = this.assertionOptions): boolean { + const type = this.getTypeForAssertion(options.includeLists); + return type instanceof UnionType; + } + + isList(): this is this & { type: ListType } { + return this.type instanceof ListType; + } + + isUserScalar(options = this.assertionOptions): boolean { + const type = this.getTypeForAssertion(options.includeLists); + return type instanceof UserScalarType; + } + + isTemporal(options = this.assertionOptions): boolean { + const type = this.getTypeForAssertion(options.includeLists); + return type.name in Neo4jGraphQLTemporalType; + } + + isListElementRequired(): boolean { + if (!(this.type instanceof ListType)) { + return false; + } + return this.type.ofType.isRequired; + } + + isRequired(): boolean { + return this.type.isRequired; + } + + /** + * + * Schema Generator Stuff + * + */ + isGraphQLBuiltInScalar(options = this.assertionOptions): boolean { + const type = this.getTypeForAssertion(options.includeLists); + return type.name in GraphQLBuiltInScalarType; + } + + isSpatial(options = this.assertionOptions): boolean { + const type = this.getTypeForAssertion(options.includeLists); + return type.name in Neo4jGraphQLSpatialType; + } + + isAbstract(options = this.assertionOptions): boolean { + return this.isInterface(options) || this.isUnion(options); + } + /** + * Returns true for both built-in and user-defined scalars + **/ + isScalar(options = this.assertionOptions): boolean { + return ( + this.isGraphQLBuiltInScalar(options) || + this.isTemporal(options) || + this.isBigInt(options) || + this.isUserScalar(options) + ); + } + + isNumeric(options = this.assertionOptions): boolean { + return this.isBigInt(options) || this.isFloat(options) || this.isInt(options); + } + + /** + * END of category assertions + */ + + /** + * + * Schema Generator Stuff + * + */ + + getTypePrettyName(): string { + if (this.isList()) { + return `[${this.getTypeName()}${this.isListElementRequired() ? "!" : ""}]${this.isRequired() ? "!" : ""}`; + } + return `${this.getTypeName()}${this.isRequired() ? "!" : ""}`; + } + + getTypeName(): string { + return this.isList() ? this.type.ofType.name : this.type.name; + } +} diff --git a/packages/graphql/src/schema-model/attribute/Attribute.test.ts b/packages/graphql/src/schema-model/attribute/Attribute.test.ts index e4b61aacad..7fbd72d2f6 100644 --- a/packages/graphql/src/schema-model/attribute/Attribute.test.ts +++ b/packages/graphql/src/schema-model/attribute/Attribute.test.ts @@ -26,6 +26,7 @@ describe("Attribute", () => { name: "test", annotations: [], type: new ScalarType(GraphQLBuiltInScalarType.String, true), + args: [], }); const clone = attribute.clone(); expect(attribute).toStrictEqual(clone); diff --git a/packages/graphql/src/schema-model/attribute/Attribute.ts b/packages/graphql/src/schema-model/attribute/Attribute.ts index 8bed16fc5c..56769484ec 100644 --- a/packages/graphql/src/schema-model/attribute/Attribute.ts +++ b/packages/graphql/src/schema-model/attribute/Attribute.ts @@ -19,6 +19,7 @@ import { Neo4jGraphQLSchemaValidationError } from "../../classes/Error"; import { annotationToKey, type Annotation, type Annotations } from "../annotation/Annotation"; +import type { Argument } from "../argument/Argument"; import type { AttributeType } from "./AttributeType"; export class Attribute { @@ -26,21 +27,29 @@ export class Attribute { public readonly annotations: Partial = {}; public readonly type: AttributeType; public readonly databaseName: string; + public readonly description: string; + public readonly args: Argument[]; constructor({ name, annotations = [], type, + args, databaseName, + description, }: { name: string; annotations: Annotation[]; type: AttributeType; + args: Argument[]; databaseName?: string; + description?: string; }) { this.name = name; this.type = type; + this.args = args; this.databaseName = databaseName ?? name; + this.description = description || ""; for (const annotation of annotations) { this.addAnnotation(annotation); @@ -52,7 +61,9 @@ export class Attribute { name: this.name, annotations: Object.values(this.annotations), type: this.type, + args: this.args, databaseName: this.databaseName, + description: this.description, }); } diff --git a/packages/graphql/src/schema-model/attribute/model-adapters/AttributeAdapter.test.ts b/packages/graphql/src/schema-model/attribute/model-adapters/AttributeAdapter.test.ts index 8c426edccf..87c52936ff 100644 --- a/packages/graphql/src/schema-model/attribute/model-adapters/AttributeAdapter.test.ts +++ b/packages/graphql/src/schema-model/attribute/model-adapters/AttributeAdapter.test.ts @@ -44,6 +44,7 @@ describe("Attribute", () => { name: "test", annotations: [], type: new ScalarType(GraphQLBuiltInScalarType.ID, true), + args: [], }) ); @@ -56,6 +57,7 @@ describe("Attribute", () => { name: "test", annotations: [], type: new ScalarType(GraphQLBuiltInScalarType.Boolean, true), + args: [], }) ); @@ -68,6 +70,7 @@ describe("Attribute", () => { name: "test", annotations: [], type: new ScalarType(GraphQLBuiltInScalarType.Int, true), + args: [], }) ); @@ -80,6 +83,7 @@ describe("Attribute", () => { name: "test", annotations: [], type: new ScalarType(GraphQLBuiltInScalarType.Float, true), + args: [], }) ); expect(attribute.isFloat()).toBe(true); @@ -91,6 +95,7 @@ describe("Attribute", () => { name: "test", annotations: [], type: new ScalarType(GraphQLBuiltInScalarType.String, true), + args: [], }) ); expect(attribute.isString()).toBe(true); @@ -102,6 +107,7 @@ describe("Attribute", () => { name: "test", annotations: [], type: new Neo4jCartesianPointType(true), + args: [], }) ); @@ -114,6 +120,7 @@ describe("Attribute", () => { name: "test", annotations: [], type: new Neo4jPointType(true), + args: [], }) ); @@ -126,6 +133,7 @@ describe("Attribute", () => { name: "test", annotations: [], type: new ScalarType(Neo4jGraphQLNumberType.BigInt, true), + args: [], }) ); @@ -138,6 +146,7 @@ describe("Attribute", () => { name: "test", annotations: [], type: new ScalarType(Neo4jGraphQLTemporalType.Date, true), + args: [], }) ); @@ -150,6 +159,7 @@ describe("Attribute", () => { name: "test", annotations: [], type: new ScalarType(Neo4jGraphQLTemporalType.DateTime, true), + args: [], }) ); @@ -162,6 +172,7 @@ describe("Attribute", () => { name: "test", annotations: [], type: new ScalarType(Neo4jGraphQLTemporalType.LocalDateTime, true), + args: [], }) ); @@ -174,6 +185,7 @@ describe("Attribute", () => { name: "test", annotations: [], type: new ScalarType(Neo4jGraphQLTemporalType.Time, true), + args: [], }) ); @@ -186,6 +198,7 @@ describe("Attribute", () => { name: "test", annotations: [], type: new ScalarType(Neo4jGraphQLTemporalType.LocalTime, true), + args: [], }) ); @@ -198,6 +211,7 @@ describe("Attribute", () => { name: "test", annotations: [], type: new ScalarType(Neo4jGraphQLTemporalType.Duration, true), + args: [], }) ); @@ -210,6 +224,7 @@ describe("Attribute", () => { name: "test", annotations: [], type: new ObjectType("testType", true), + args: [], }) ); @@ -222,6 +237,7 @@ describe("Attribute", () => { name: "test", annotations: [], type: new EnumType("testType", true), + args: [], }) ); @@ -234,6 +250,7 @@ describe("Attribute", () => { name: "test", annotations: [], type: new UserScalarType("testType", true), + args: [], }) ); @@ -246,6 +263,7 @@ describe("Attribute", () => { name: "test", annotations: [], type: new InterfaceType("Tool", true), + args: [], }) ); expect(attribute.isInterface()).toBe(true); @@ -257,6 +275,7 @@ describe("Attribute", () => { name: "test", annotations: [], type: new UnionType("Tool", true), + args: [], }) ); expect(attribute.isUnion()).toBe(true); @@ -271,6 +290,7 @@ describe("Attribute", () => { name: "test", annotations: [], type: new ListType(stringType, true), + args: [], }) ); @@ -285,6 +305,7 @@ describe("Attribute", () => { name: "test", annotations: [], type: stringType, + args: [], }) ); @@ -299,6 +320,7 @@ describe("Attribute", () => { name: "test", annotations: [], type: new ListType(stringType, true), + args: [], }) ); expect(attribute.isString({ includeLists: true })).toBe(true); @@ -313,6 +335,7 @@ describe("Attribute", () => { name: "test", annotations: [], type: new ListType(stringType, true), + args: [], }) ); expect(attribute.isInt({ includeLists: true })).toBe(false); @@ -328,6 +351,7 @@ describe("Attribute", () => { name: "test", annotations: [], type: new ScalarType(GraphQLBuiltInScalarType.String, true), + args: [], }) ); @@ -336,6 +360,7 @@ describe("Attribute", () => { name: "test", annotations: [], type: new ScalarType(Neo4jGraphQLNumberType.BigInt, true), + args: [], }) ); @@ -349,6 +374,7 @@ describe("Attribute", () => { name: "test", annotations: [], type: new Neo4jCartesianPointType(true), + args: [], }) ); const nonSpatial = new AttributeAdapter( @@ -356,6 +382,7 @@ describe("Attribute", () => { name: "test", annotations: [], type: new ScalarType(GraphQLBuiltInScalarType.String, true), + args: [], }) ); @@ -369,6 +396,7 @@ describe("Attribute", () => { name: "test", annotations: [], type: new ScalarType(Neo4jGraphQLTemporalType.Date, true), + args: [], }) ); @@ -377,6 +405,7 @@ describe("Attribute", () => { name: "test", annotations: [], type: new ScalarType(GraphQLBuiltInScalarType.String, true), + args: [], }) ); @@ -390,6 +419,7 @@ describe("Attribute", () => { name: "test", annotations: [], type: new UnionType("Tool", true), + args: [], }) ); @@ -398,6 +428,7 @@ describe("Attribute", () => { name: "test", annotations: [], type: new ScalarType(GraphQLBuiltInScalarType.String, true), + args: [], }) ); @@ -412,6 +443,7 @@ describe("Attribute", () => { name: "test", annotations: [], type: new ScalarType(GraphQLBuiltInScalarType.String, true), + args: [], }) ); @@ -420,6 +452,7 @@ describe("Attribute", () => { name: "test", annotations: [], type: new ScalarType(GraphQLBuiltInScalarType.String, false), + args: [], }) ); @@ -433,6 +466,7 @@ describe("Attribute", () => { name: "test", annotations: [], type: new ListType(new ScalarType(GraphQLBuiltInScalarType.String, true), true), + args: [], }) ); @@ -441,6 +475,7 @@ describe("Attribute", () => { name: "test", annotations: [], type: new ListType(new ScalarType(GraphQLBuiltInScalarType.String, true), false), + args: [], }) ); @@ -454,6 +489,7 @@ describe("Attribute", () => { name: "test", annotations: [], type: new ListType(new ScalarType(GraphQLBuiltInScalarType.String, true), false), + args: [], }) ); @@ -462,6 +498,7 @@ describe("Attribute", () => { name: "test", annotations: [], type: new ListType(new ScalarType(GraphQLBuiltInScalarType.String, false), true), + args: [], }) ); @@ -476,6 +513,7 @@ describe("Attribute", () => { name: "test", annotations: [new UniqueAnnotation({ constraintName: "test" })], type: new ScalarType(GraphQLBuiltInScalarType.ID, true), + args: [], }) ); expect(attribute.isUnique()).toBe(true); @@ -492,6 +530,7 @@ describe("Attribute", () => { }), ], type: new ScalarType(GraphQLBuiltInScalarType.ID, true), + args: [], }) ); expect(attribute.isCypher()).toBe(true); @@ -505,6 +544,7 @@ describe("Attribute", () => { name: "test", annotations: [], type: new ListType(new ScalarType(GraphQLBuiltInScalarType.String, true), false), + args: [], }) ); @@ -522,6 +562,7 @@ describe("Attribute", () => { name: "test", annotations: [], type: new ScalarType(GraphQLBuiltInScalarType.Int, true), + args: [], }) ); // TODO: test it with String as well. @@ -586,6 +627,7 @@ describe("Attribute", () => { name: "test", annotations: [], type: new ScalarType(GraphQLBuiltInScalarType.Int, true), + args: [], }) ); // TODO: test it with float as well. diff --git a/packages/graphql/src/schema-model/attribute/model-adapters/AttributeAdapter.ts b/packages/graphql/src/schema-model/attribute/model-adapters/AttributeAdapter.ts index b2462c16e0..ceb4467762 100644 --- a/packages/graphql/src/schema-model/attribute/model-adapters/AttributeAdapter.ts +++ b/packages/graphql/src/schema-model/attribute/model-adapters/AttributeAdapter.ts @@ -38,23 +38,28 @@ import { UnionType, ObjectType, } from "../AttributeType"; +import type { Argument } from "../../argument/Argument"; export class AttributeAdapter { private _listModel: ListAdapter | undefined; private _mathModel: MathAdapter | undefined; private _aggregationModel: AggregationAdapter | undefined; - public name: string; - public annotations: Partial; - public type: AttributeType; - public databaseName: string; + public readonly name: string; + public readonly annotations: Partial; + public readonly type: AttributeType; + public readonly args: Argument[]; + public readonly databaseName: string; + public readonly description: string; private assertionOptions: { includeLists: boolean; }; constructor(attribute: Attribute) { this.name = attribute.name; this.type = attribute.type; + this.args = attribute.args; this.annotations = attribute.annotations; this.databaseName = attribute.databaseName; + this.description = attribute.description; this.assertionOptions = { includeLists: true, }; diff --git a/packages/graphql/src/schema-model/entity/ConcreteEntity.ts b/packages/graphql/src/schema-model/entity/ConcreteEntity.ts index db32e6f77e..58de51e05e 100644 --- a/packages/graphql/src/schema-model/entity/ConcreteEntity.ts +++ b/packages/graphql/src/schema-model/entity/ConcreteEntity.ts @@ -28,6 +28,7 @@ import type { Entity } from "./Entity"; export class ConcreteEntity implements Entity { public readonly name: string; + public readonly description: string; public readonly labels: Set; public readonly attributes: Map = new Map(); public readonly relationships: Map = new Map(); @@ -35,6 +36,7 @@ export class ConcreteEntity implements Entity { constructor({ name, + description, labels, attributes = [], annotations = [], @@ -45,8 +47,10 @@ export class ConcreteEntity implements Entity { attributes?: Attribute[]; annotations?: Annotation[]; relationships?: Relationship[]; + description?: string; }) { this.name = name; + this.description = description || ""; this.labels = new Set(labels); for (const attribute of attributes) { this.addAttribute(attribute); diff --git a/packages/graphql/src/schema-model/entity/model-adapters/ConcreteEntityAdapter.test.ts b/packages/graphql/src/schema-model/entity/model-adapters/ConcreteEntityAdapter.test.ts index 3e8148259b..a2ce9f6fbb 100644 --- a/packages/graphql/src/schema-model/entity/model-adapters/ConcreteEntityAdapter.test.ts +++ b/packages/graphql/src/schema-model/entity/model-adapters/ConcreteEntityAdapter.test.ts @@ -36,12 +36,14 @@ describe("ConcreteEntityAdapter", () => { name: "id", annotations: [new UniqueAnnotation({ constraintName: "User_id_unique" })], type: new ScalarType(GraphQLBuiltInScalarType.ID, true), + args: [], }); const nameAttribute = new Attribute({ name: "name", annotations: [], type: new ScalarType(GraphQLBuiltInScalarType.String, true), + args: [], }); const closestUserAttribute = new Attribute({ @@ -53,6 +55,7 @@ describe("ConcreteEntityAdapter", () => { }), ], type: new ScalarType(GraphQLBuiltInScalarType.String, true), + args: [], }); const userEntity = new ConcreteEntity({ diff --git a/packages/graphql/src/schema-model/entity/model-adapters/ConcreteEntityAdapter.ts b/packages/graphql/src/schema-model/entity/model-adapters/ConcreteEntityAdapter.ts index 410a975686..bc4ca47d48 100644 --- a/packages/graphql/src/schema-model/entity/model-adapters/ConcreteEntityAdapter.ts +++ b/packages/graphql/src/schema-model/entity/model-adapters/ConcreteEntityAdapter.ts @@ -31,6 +31,7 @@ import type { UnionEntityAdapter } from "./UnionEntityAdapter"; export class ConcreteEntityAdapter { public readonly name: string; + public readonly description: string; public readonly labels: Set; public readonly attributes: Map = new Map(); public readonly relationships: Map = new Map(); @@ -51,6 +52,7 @@ export class ConcreteEntityAdapter { constructor(entity: ConcreteEntity) { this.name = entity.name; + this.description = entity.description; this.labels = entity.labels; this.annotations = entity.annotations; this.initAttributes(entity.attributes); diff --git a/packages/graphql/src/schema-model/generate-model.test.ts b/packages/graphql/src/schema-model/generate-model.test.ts index 5ff8262396..988c878fb6 100644 --- a/packages/graphql/src/schema-model/generate-model.test.ts +++ b/packages/graphql/src/schema-model/generate-model.test.ts @@ -34,8 +34,7 @@ import type { RelationshipAdapter } from "./relationship/model-adapters/Relation import type { ConcreteEntity } from "./entity/ConcreteEntity"; import { InterfaceEntity } from "./entity/InterfaceEntity"; import { UnionEntity } from "./entity/UnionEntity"; - -// TODO: interface implementing interface annotations inheritance +import { GraphQLBuiltInScalarType, ListType, ObjectType } from "./attribute/AttributeType"; describe("Schema model generation", () => { test("parses @authentication directive with no arguments", () => { @@ -741,6 +740,54 @@ describe("ComposeEntity Annotations & Attributes and Inheritance", () => { }); }); +describe("Arguments", () => { + test("attribute argument scalar", () => { + const typeDefs = gql` + type User { + id: ID! + name(something: Int): String! + } + `; + + const document = mergeTypeDefs(typeDefs); + const schemaModel = generateModel(document); + const userEntity = schemaModel.concreteEntities.find((e) => e.name === "User"); + expect(userEntity?.attributes.has("id")).toBeTrue(); + expect(userEntity?.attributes.has("name")).toBeTrue(); + const idAttribute = userEntity?.findAttribute("id"); + expect(idAttribute?.args).toHaveLength(0); + const nameAttribute = userEntity?.findAttribute("name"); + expect(nameAttribute?.args).toHaveLength(1); + expect(nameAttribute?.args[0]?.name).toBe("something"); + expect(nameAttribute?.args[0]?.type.name).toBe(GraphQLBuiltInScalarType.Int); + expect(nameAttribute?.args[0]?.type.isRequired).toBeFalse(); + }); + + test("attribute argument object", () => { + const typeDefs = gql` + type User { + id: ID! + favoritePet(from: [Animal]!): String! + } + type Animal { + sound: String + } + `; + + const document = mergeTypeDefs(typeDefs); + const schemaModel = generateModel(document); + const userEntity = schemaModel.concreteEntities.find((e) => e.name === "User"); + expect(userEntity?.attributes.has("id")).toBeTrue(); + expect(userEntity?.attributes.has("favoritePet")).toBeTrue(); + const idAttribute = userEntity?.findAttribute("id"); + expect(idAttribute?.args).toHaveLength(0); + const favoritePetAttribute = userEntity?.findAttribute("favoritePet"); + expect(favoritePetAttribute?.args).toHaveLength(1); + expect(favoritePetAttribute?.args[0]?.name).toBe("from"); + expect(favoritePetAttribute?.args[0]?.type).toEqual(new ListType(new ObjectType("Animal", false), true)); + }); +}); + describe("GraphQL adapters", () => { let schemaModel: Neo4jGraphQLSchemaModel; // entities diff --git a/packages/graphql/src/schema-model/parser/parse-attribute.ts b/packages/graphql/src/schema-model/parser/parse-attribute.ts index 490473248b..772fba3d79 100644 --- a/packages/graphql/src/schema-model/parser/parse-attribute.ts +++ b/packages/graphql/src/schema-model/parser/parse-attribute.ts @@ -17,7 +17,7 @@ * limitations under the License. */ -import type { DirectiveNode, FieldDefinitionNode, TypeNode } from "graphql"; +import type { DirectiveNode, FieldDefinitionNode, InputValueDefinitionNode, TypeNode } from "graphql"; import { Kind } from "graphql"; import type { AttributeType, Neo4jGraphQLScalarType } from "../attribute/AttributeType"; import { @@ -36,6 +36,7 @@ import { Neo4jCartesianPointType, } from "../attribute/AttributeType"; import { Attribute } from "../attribute/Attribute"; +import { Argument } from "../argument/Argument"; import { Field } from "../attribute/Field"; import type { DefinitionCollection } from "./definition-collection"; import { parseAnnotations } from "./parse-annotation"; @@ -43,6 +44,20 @@ import { aliasDirective } from "../../graphql/directives"; import { parseArguments } from "./parse-arguments"; import { findDirective } from "./utils"; +function parseAttributeArguments( + fieldArgs: readonly InputValueDefinitionNode[], + definitionCollection: DefinitionCollection +): Argument[] { + return fieldArgs.map((fieldArg) => { + return new Argument({ + name: fieldArg.name.value, + type: parseTypeNode(definitionCollection, fieldArg.type), + defaultValue: fieldArg.defaultValue, + description: fieldArg.description?.value || "", + }); + }); +} + export function parseAttribute( field: FieldDefinitionNode, inheritedField: FieldDefinitionNode[] | undefined, @@ -50,6 +65,7 @@ export function parseAttribute( ): Attribute | Field { const name = field.name.value; const type = parseTypeNode(definitionCollection, field.type); + const args = parseAttributeArguments(field.arguments || [], definitionCollection); const inheritedDirectives = inheritedField?.flatMap((f) => f.directives || []) || []; const annotations = parseAnnotations((field.directives || []).concat(inheritedDirectives)); const databaseName = getDatabaseName(field, inheritedField); @@ -57,7 +73,9 @@ export function parseAttribute( name, annotations, type, + args, databaseName, + description: field.description?.value || "", }); } diff --git a/packages/graphql/src/schema-model/relationship/model-adapters/RelationshipAdapter.test.ts b/packages/graphql/src/schema-model/relationship/model-adapters/RelationshipAdapter.test.ts index 6c11a8a7f9..617660ec23 100644 --- a/packages/graphql/src/schema-model/relationship/model-adapters/RelationshipAdapter.test.ts +++ b/packages/graphql/src/schema-model/relationship/model-adapters/RelationshipAdapter.test.ts @@ -34,24 +34,28 @@ describe("RelationshipAdapter", () => { name: "id", annotations: [new UniqueAnnotation({ constraintName: "User_id_unique" })], type: new ScalarType(GraphQLBuiltInScalarType.ID, true), + args: [], }); const userName = new Attribute({ name: "name", annotations: [], type: new ScalarType(GraphQLBuiltInScalarType.String, true), + args: [], }); const accountId = new Attribute({ name: "id", annotations: [new UniqueAnnotation({ constraintName: "User_id_unique" })], type: new ScalarType(GraphQLBuiltInScalarType.ID, true), + args: [], }); const accountUsername = new Attribute({ name: "username", annotations: [], type: new ScalarType(GraphQLBuiltInScalarType.String, true), + args: [], }); const userEntity = new ConcreteEntity({ @@ -70,6 +74,7 @@ describe("RelationshipAdapter", () => { name: "accountAlias", annotations: [], type: new ScalarType(GraphQLBuiltInScalarType.String, true), + args: [], }); relationship = new Relationship({