From c5a0e339f598d94bbd770dfa35b007e904d56025 Mon Sep 17 00:00:00 2001 From: a-alle Date: Fri, 18 Aug 2023 17:08:15 +0100 Subject: [PATCH 1/9] implement interfaces and unions with CompositEntity distinction --- .../schema-model/Neo4jGraphQLSchemaModel.ts | 6 +- .../src/schema-model/attribute/Attribute.ts | 2 +- .../schema-model/attribute/AttributeType.ts | 5 +- .../schema-model/entity/CompositeEntity.ts | 16 +- .../src/schema-model/entity/ConcreteEntity.ts | 13 +- .../schema-model/entity/InterfaceEntity.ts | 110 ++++++++++++ ...mpositeEntityAdapter.ts => UnionEntity.ts} | 12 +- .../model-adapters/InterfaceEntityAdapter.ts | 64 +++++++ .../model-adapters/UnionEntityAdapter.ts | 40 +++++ .../src/schema-model/generate-model.test.ts | 165 +++++++++++++++++- .../src/schema-model/generate-model.ts | 131 ++++++++++++-- .../schema-model/relationship/Relationship.ts | 5 +- .../model-adapters/RelationshipAdapter.ts | 28 ++- .../translate-cypher-directive-projection.ts | 3 +- .../translate/translate-top-level-cypher.ts | 5 +- 15 files changed, 550 insertions(+), 55 deletions(-) create mode 100644 packages/graphql/src/schema-model/entity/InterfaceEntity.ts rename packages/graphql/src/schema-model/entity/{model-adapters/CompositeEntityAdapter.ts => UnionEntity.ts} (69%) create mode 100644 packages/graphql/src/schema-model/entity/model-adapters/InterfaceEntityAdapter.ts create mode 100644 packages/graphql/src/schema-model/entity/model-adapters/UnionEntityAdapter.ts diff --git a/packages/graphql/src/schema-model/Neo4jGraphQLSchemaModel.ts b/packages/graphql/src/schema-model/Neo4jGraphQLSchemaModel.ts index 0ead6e0e93..377b8c3dd3 100644 --- a/packages/graphql/src/schema-model/Neo4jGraphQLSchemaModel.ts +++ b/packages/graphql/src/schema-model/Neo4jGraphQLSchemaModel.ts @@ -21,10 +21,12 @@ import { Neo4jGraphQLSchemaValidationError } from "../classes"; import type { Operation } from "./Operation"; import type { Annotations, Annotation } from "./annotation/Annotation"; import { annotationToKey } from "./annotation/Annotation"; -import { CompositeEntity } from "./entity/CompositeEntity"; +import type { CompositeEntity } from "./entity/CompositeEntity"; import { ConcreteEntity } from "./entity/ConcreteEntity"; import type { Entity } from "./entity/Entity"; import { ConcreteEntityAdapter } from "./entity/model-adapters/ConcreteEntityAdapter"; +import { InterfaceEntity } from "./entity/InterfaceEntity"; +import { UnionEntity } from "./entity/UnionEntity"; export type Operations = { Query?: Operation; @@ -86,7 +88,7 @@ export class Neo4jGraphQLSchemaModel { } public isCompositeEntity(entity?: Entity): entity is CompositeEntity { - return entity instanceof CompositeEntity; + return entity instanceof InterfaceEntity || entity instanceof UnionEntity; } private addAnnotation(annotation: Annotation): void { diff --git a/packages/graphql/src/schema-model/attribute/Attribute.ts b/packages/graphql/src/schema-model/attribute/Attribute.ts index 8bed16fc5c..385c7089ba 100644 --- a/packages/graphql/src/schema-model/attribute/Attribute.ts +++ b/packages/graphql/src/schema-model/attribute/Attribute.ts @@ -56,7 +56,7 @@ export class Attribute { }); } - private addAnnotation(annotation: Annotation): void { + public addAnnotation(annotation: Annotation): void { const annotationKey = annotationToKey(annotation); if (this.annotations[annotationKey]) { throw new Neo4jGraphQLSchemaValidationError(`Annotation ${annotationKey} already exists in ${this.name}`); diff --git a/packages/graphql/src/schema-model/attribute/AttributeType.ts b/packages/graphql/src/schema-model/attribute/AttributeType.ts index c2852935d7..cd40f24ab5 100644 --- a/packages/graphql/src/schema-model/attribute/AttributeType.ts +++ b/packages/graphql/src/schema-model/attribute/AttributeType.ts @@ -69,7 +69,7 @@ export class Neo4jCartesianPointType { export class Neo4jPointType { public readonly name: string; public readonly isRequired: boolean; - constructor( isRequired: boolean) { + constructor(isRequired: boolean) { this.name = Neo4jGraphQLSpatialType.Point; this.isRequired = isRequired; } @@ -88,13 +88,14 @@ export class ObjectType { public readonly name: string; public readonly isRequired: boolean; // TODO: add fields - + constructor(name: string, isRequired: boolean) { this.name = name; this.isRequired = isRequired; } } +// TODO: consider replacing this with a isList field on the other classes export class ListType { public readonly name: string; public readonly ofType: Exclude; diff --git a/packages/graphql/src/schema-model/entity/CompositeEntity.ts b/packages/graphql/src/schema-model/entity/CompositeEntity.ts index b600b7abe0..1e240e8067 100644 --- a/packages/graphql/src/schema-model/entity/CompositeEntity.ts +++ b/packages/graphql/src/schema-model/entity/CompositeEntity.ts @@ -18,17 +18,9 @@ */ import type { ConcreteEntity } from "./ConcreteEntity"; -import type { Entity } from "./Entity"; -/** Entity for abstract GraphQL types, Interface and Union */ -export class CompositeEntity implements Entity { - public readonly name: string; - public concreteEntities: ConcreteEntity[]; - // TODO: add type interface or union, and for interface add fields - // TODO: add annotations - - constructor({ name, concreteEntities }: { name: string; concreteEntities: ConcreteEntity[] }) { - this.name = name; - this.concreteEntities = concreteEntities; - } +/** models the concept of an Abstract Type */ +export interface CompositeEntity { + readonly name: string; + concreteEntities: ConcreteEntity[]; } diff --git a/packages/graphql/src/schema-model/entity/ConcreteEntity.ts b/packages/graphql/src/schema-model/entity/ConcreteEntity.ts index c4e25c61bb..ae1c985603 100644 --- a/packages/graphql/src/schema-model/entity/ConcreteEntity.ts +++ b/packages/graphql/src/schema-model/entity/ConcreteEntity.ts @@ -71,7 +71,18 @@ export class ConcreteEntity implements Entity { this.attributes.set(attribute.name, attribute); } - private addAnnotation(annotation: Annotation): void { + private deleteAttribute(attributeName: string): void { + if (this.attributes.has(attributeName)) { + this.attributes.delete(attributeName); + } + } + + public attributeToRelationship(relationship: Relationship): void { + this.addRelationship(relationship); + this.deleteAttribute(relationship.name); + } + + public addAnnotation(annotation: Annotation): void { const annotationKey = annotationToKey(annotation); const existingAnnotation = this.annotations[annotationKey]; diff --git a/packages/graphql/src/schema-model/entity/InterfaceEntity.ts b/packages/graphql/src/schema-model/entity/InterfaceEntity.ts new file mode 100644 index 0000000000..76f6a48c55 --- /dev/null +++ b/packages/graphql/src/schema-model/entity/InterfaceEntity.ts @@ -0,0 +1,110 @@ +/* + * 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 { Neo4jGraphQLSchemaValidationError } from "../../classes"; +import type { Annotation, Annotations } from "../annotation/Annotation"; +import { annotationToKey } from "../annotation/Annotation"; +import type { Attribute } from "../attribute/Attribute"; +import type { Relationship } from "../relationship/Relationship"; +import type { CompositeEntity } from "./CompositeEntity"; +import type { ConcreteEntity } from "./ConcreteEntity"; + +export class InterfaceEntity implements CompositeEntity { + public readonly name: string; + public readonly concreteEntities: ConcreteEntity[]; + public readonly attributes: Map = new Map(); + public readonly relationships: Map = new Map(); + public readonly annotations: Partial = {}; + + constructor({ + name, + concreteEntities, + attributes = [], + annotations = [], + relationships = [], + }: { + name: string; + concreteEntities: ConcreteEntity[]; + attributes?: Attribute[]; + annotations?: Annotation[]; + relationships?: Relationship[]; + }) { + this.name = name; + this.concreteEntities = concreteEntities; + for (const attribute of attributes) { + this.addAttribute(attribute); + } + + for (const annotation of annotations) { + this.addAnnotation(annotation); + } + + for (const relationship of relationships) { + this.addRelationship(relationship); + } + } + + private addAttribute(attribute: Attribute): void { + if (this.attributes.has(attribute.name)) { + throw new Neo4jGraphQLSchemaValidationError(`Attribute ${attribute.name} already exists in ${this.name}`); + } + this.attributes.set(attribute.name, attribute); + } + + private addAnnotation(annotation: Annotation): void { + const annotationKey = annotationToKey(annotation); + const existingAnnotation = this.annotations[annotationKey]; + + if (existingAnnotation) { + throw new Neo4jGraphQLSchemaValidationError(`Annotation ${annotationKey} already exists in ${this.name}`); + } + + // We cast to any because we aren't narrowing the Annotation type here. + // There's no reason to narrow either, since we care more about performance. + this.annotations[annotationKey] = annotation as any; + } + + private deleteAttribute(attributeName: string): void { + if (this.attributes.has(attributeName)) { + this.attributes.delete(attributeName); + } + } + + public attributeToRelationship(relationship: Relationship): void { + this.addRelationship(relationship); + this.deleteAttribute(relationship.name); + } + + public addRelationship(relationship: Relationship): void { + if (this.relationships.has(relationship.name)) { + throw new Neo4jGraphQLSchemaValidationError( + `Attribute ${relationship.name} already exists in ${this.name}` + ); + } + this.relationships.set(relationship.name, relationship); + } + + public findAttribute(name: string): Attribute | undefined { + return this.attributes.get(name); + } + + public findRelationship(name: string): Relationship | undefined { + return this.relationships.get(name); + } +} diff --git a/packages/graphql/src/schema-model/entity/model-adapters/CompositeEntityAdapter.ts b/packages/graphql/src/schema-model/entity/UnionEntity.ts similarity index 69% rename from packages/graphql/src/schema-model/entity/model-adapters/CompositeEntityAdapter.ts rename to packages/graphql/src/schema-model/entity/UnionEntity.ts index a61d88b0bc..d1a1f62dc3 100644 --- a/packages/graphql/src/schema-model/entity/model-adapters/CompositeEntityAdapter.ts +++ b/packages/graphql/src/schema-model/entity/UnionEntity.ts @@ -17,16 +17,14 @@ * limitations under the License. */ -import type { ConcreteEntityAdapter } from "./ConcreteEntityAdapter"; +import type { ConcreteEntity } from "./ConcreteEntity"; +import type { CompositeEntity } from "./CompositeEntity"; -// As the composite entity is not yet implemented, this is a placeholder -export class CompositeEntityAdapter { +export class UnionEntity implements CompositeEntity { public readonly name: string; - public concreteEntities: ConcreteEntityAdapter[]; - // TODO: add type interface or union, and for interface add fields - // TODO: add annotations + public concreteEntities: ConcreteEntity[]; - constructor({ name, concreteEntities }: { name: string; concreteEntities: ConcreteEntityAdapter[] }) { + constructor({ name, concreteEntities }: { name: string; concreteEntities: ConcreteEntity[] }) { this.name = name; this.concreteEntities = concreteEntities; } diff --git a/packages/graphql/src/schema-model/entity/model-adapters/InterfaceEntityAdapter.ts b/packages/graphql/src/schema-model/entity/model-adapters/InterfaceEntityAdapter.ts new file mode 100644 index 0000000000..1fa622748f --- /dev/null +++ b/packages/graphql/src/schema-model/entity/model-adapters/InterfaceEntityAdapter.ts @@ -0,0 +1,64 @@ +/* + * 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 { Annotations } from "../../annotation/Annotation"; +import type { Attribute } from "../../attribute/Attribute"; +import { AttributeAdapter } from "../../attribute/model-adapters/AttributeAdapter"; +import { RelationshipAdapter } from "../../relationship/model-adapters/RelationshipAdapter"; +import type { Relationship } from "../../relationship/Relationship"; +import type { ConcreteEntity } from "../ConcreteEntity"; +import type { InterfaceEntity } from "../InterfaceEntity"; +import { ConcreteEntityAdapter } from "./ConcreteEntityAdapter"; + +export class InterfaceEntityAdapter { + public readonly name: string; + public concreteEntities: ConcreteEntityAdapter[]; + public readonly attributes: Map = new Map(); + public readonly relationships: Map = new Map(); + public readonly annotations: Partial; + + constructor(entity: InterfaceEntity) { + this.name = entity.name; + this.concreteEntities = []; + this.annotations = entity.annotations; + this.initAttributes(entity.attributes); + this.initRelationships(entity.relationships); + this.initConcreteEntities(entity.concreteEntities); + } + + private initConcreteEntities(entities: ConcreteEntity[]) { + for (const entity of entities) { + const entityAdapter = new ConcreteEntityAdapter(entity); + this.concreteEntities.push(entityAdapter); + } + } + + private initAttributes(attributes: Map) { + for (const [attributeName, attribute] of attributes.entries()) { + const attributeAdapter = new AttributeAdapter(attribute); + this.attributes.set(attributeName, attributeAdapter); + } + } + + private initRelationships(relationships: Map) { + for (const [relationshipName, relationship] of relationships.entries()) { + this.relationships.set(relationshipName, new RelationshipAdapter(relationship, this)); + } + } +} diff --git a/packages/graphql/src/schema-model/entity/model-adapters/UnionEntityAdapter.ts b/packages/graphql/src/schema-model/entity/model-adapters/UnionEntityAdapter.ts new file mode 100644 index 0000000000..013c1c0dd8 --- /dev/null +++ b/packages/graphql/src/schema-model/entity/model-adapters/UnionEntityAdapter.ts @@ -0,0 +1,40 @@ +/* + * 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 { ConcreteEntity } from "../ConcreteEntity"; +import type { UnionEntity } from "../UnionEntity"; +import { ConcreteEntityAdapter } from "./ConcreteEntityAdapter"; + +export class UnionEntityAdapter { + public readonly name: string; + public concreteEntities: ConcreteEntityAdapter[]; + + constructor(entity: UnionEntity) { + this.name = entity.name; + this.concreteEntities = []; + this.initConcreteEntities(entity.concreteEntities); + } + + private initConcreteEntities(entities: ConcreteEntity[]) { + for (const entity of entities) { + const entityAdapter = new ConcreteEntityAdapter(entity); + this.concreteEntities.push(entityAdapter); + } + } +} diff --git a/packages/graphql/src/schema-model/generate-model.test.ts b/packages/graphql/src/schema-model/generate-model.test.ts index 3998a18730..03296b0a14 100644 --- a/packages/graphql/src/schema-model/generate-model.test.ts +++ b/packages/graphql/src/schema-model/generate-model.test.ts @@ -32,6 +32,10 @@ import type { AttributeAdapter } from "./attribute/model-adapters/AttributeAdapt import type { ConcreteEntityAdapter } from "./entity/model-adapters/ConcreteEntityAdapter"; import type { RelationshipAdapter } from "./relationship/model-adapters/RelationshipAdapter"; import type { ConcreteEntity } from "./entity/ConcreteEntity"; +import { InterfaceEntity } from "./entity/InterfaceEntity"; +import { UnionEntity } from "./entity/UnionEntity"; + +// TODO: interface implementing interface annotations inheritance describe("Schema model generation", () => { test("parses @authentication directive with no arguments", () => { @@ -291,6 +295,13 @@ describe("ComposeEntity generation", () => { expect(humanEntities?.concreteEntities).toHaveLength(1); // User }); + test("composite entities has correct type", () => { + const toolEntities = schemaModel.compositeEntities.find((e) => e.name === "Tool"); + expect(toolEntities).toBeInstanceOf(UnionEntity); + const humanEntities = schemaModel.compositeEntities.find((e) => e.name === "Human"); + expect(humanEntities).toBeInstanceOf(InterfaceEntity); + }); + test("concrete entity has correct attributes", () => { const userEntity = schemaModel.concreteEntities.find((e) => e.name === "User"); expect(userEntity?.attributes.has("id")).toBeTrue(); @@ -318,13 +329,23 @@ describe("Relationship", () => { union Show = Movie | TvShow - type Movie { + type Actor { + name: String + } + + interface Production { + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: OUT) + } + + type Movie implements Production { name: String! + actors: [Actor!]! } - type TvShow { + type TvShow implements Production { name: String! episodes: Int + actors: [Actor!]! @relationship(type: "STARED_IN", direction: OUT) } type Account { @@ -359,9 +380,63 @@ describe("Relationship", () => { expect(accounts?.target.name).toBe("Account"); expect(accounts?.attributes.has("creationTime")).toBeTrue(); }); + + test("composite interface entity has correct relationship", () => { + const productionEntity = schemaModel.compositeEntities.find((e) => e.name === "Production") as InterfaceEntity; + const actors = productionEntity?.relationships.get("actors"); + expect(actors).toBeDefined(); + expect(actors?.type).toBe("ACTED_IN"); + expect(actors?.direction).toBe("OUT"); + expect(actors?.queryDirection).toBe("DEFAULT_DIRECTED"); + expect(actors?.nestedOperations).toEqual([ + "CREATE", + "UPDATE", + "DELETE", + "CONNECT", + "DISCONNECT", + "CONNECT_OR_CREATE", + ]); + expect(actors?.target.name).toBe("Actor"); + }); + + test("concrete entity has inherited relationship", () => { + const movieEntity = schemaModel.concreteEntities.find((e) => e.name === "Movie"); + const actors = movieEntity?.relationships.get("actors"); + expect(actors).toBeDefined(); + expect(actors?.type).toBe("ACTED_IN"); + expect(actors?.direction).toBe("OUT"); + expect(actors?.queryDirection).toBe("DEFAULT_DIRECTED"); + expect(actors?.nestedOperations).toEqual([ + "CREATE", + "UPDATE", + "DELETE", + "CONNECT", + "DISCONNECT", + "CONNECT_OR_CREATE", + ]); + expect(actors?.target.name).toBe("Actor"); + }); + + test("concrete entity has overwritten the inherited relationship", () => { + const showEntity = schemaModel.concreteEntities.find((e) => e.name === "TvShow"); + const actors = showEntity?.relationships.get("actors"); + expect(actors).toBeDefined(); + expect(actors?.type).toBe("STARED_IN"); + expect(actors?.direction).toBe("OUT"); + expect(actors?.queryDirection).toBe("DEFAULT_DIRECTED"); + expect(actors?.nestedOperations).toEqual([ + "CREATE", + "UPDATE", + "DELETE", + "CONNECT", + "DISCONNECT", + "CONNECT_OR_CREATE", + ]); + expect(actors?.target.name).toBe("Actor"); + }); }); -describe("Annotations & Attributes", () => { +describe("ConcreteEntity Annotations & Attributes", () => { let schemaModel: Neo4jGraphQLSchemaModel; let userEntity: ConcreteEntity; let accountEntity: ConcreteEntity; @@ -447,6 +522,90 @@ describe("Annotations & Attributes", () => { }); }); +describe("ComposeEntity Annotations & Attributes", () => { + let schemaModel: Neo4jGraphQLSchemaModel; + let movieEntity: ConcreteEntity; + let showEntity: ConcreteEntity; + let productionEntity: InterfaceEntity; + + beforeAll(() => { + const typeDefs = gql` + interface Production { + year: Int @populatedBy(callback: "thisCallback", operations: [CREATE]) + defaultName: String! @default(value: "AwesomeProduction") + aliasedProp: String! @alias(property: "dbName") + } + + type Movie implements Production { + name: String! + year: Int + defaultName: String! + aliasedProp: String! @alias(property: "movieDbName") + } + + type TvShow implements Production { + name: String! + episodes: Int + year: Int @populatedBy(callback: "thisOtherCallback", operations: [CREATE]) + aliasedProp: String! + } + + extend type TvShow { + defaultName: String! @default(value: "AwesomeShow") + } + `; + + const document = mergeTypeDefs(typeDefs); + schemaModel = generateModel(document); + movieEntity = schemaModel.concreteEntities.find((e) => e.name === "Movie") as ConcreteEntity; + showEntity = schemaModel.concreteEntities.find((e) => e.name === "TvShow") as ConcreteEntity; + productionEntity = schemaModel.compositeEntities.find((e) => e.name === "Production") as InterfaceEntity; + }); + + test("attributes should be generated with the correct annotations", () => { + const productionYear = productionEntity?.attributes.get("year"); + expect(productionYear?.annotations[AnnotationsKey.populatedBy]).toBeDefined(); + expect(productionYear?.annotations[AnnotationsKey.populatedBy]?.callback).toBe("thisCallback"); + expect(productionYear?.annotations[AnnotationsKey.populatedBy]?.operations).toStrictEqual(["CREATE"]); + + const movieYear = movieEntity?.attributes.get("year"); + expect(movieYear?.annotations[AnnotationsKey.populatedBy]).toBeDefined(); + expect(movieYear?.annotations[AnnotationsKey.populatedBy]?.callback).toBe("thisCallback"); + expect(movieYear?.annotations[AnnotationsKey.populatedBy]?.operations).toStrictEqual(["CREATE"]); + + const showYear = showEntity?.attributes.get("year"); + expect(showYear?.annotations[AnnotationsKey.populatedBy]).toBeDefined(); + expect(showYear?.annotations[AnnotationsKey.populatedBy]?.callback).toBe("thisOtherCallback"); + expect(showYear?.annotations[AnnotationsKey.populatedBy]?.operations).toStrictEqual(["CREATE"]); + + const productionDefaultName = productionEntity?.attributes.get("defaultName"); + expect(productionDefaultName).toBeDefined(); + expect(productionDefaultName?.annotations[AnnotationsKey.default]).toBeDefined(); + expect(productionDefaultName?.annotations[AnnotationsKey.default]?.value).toBe("AwesomeProduction"); + + const movieDefaultName = movieEntity?.attributes.get("defaultName"); + expect(movieDefaultName).toBeDefined(); + expect(movieDefaultName?.annotations[AnnotationsKey.default]).toBeDefined(); + expect(movieDefaultName?.annotations[AnnotationsKey.default]?.value).toBe("AwesomeProduction"); + + const showDefaultName = showEntity?.attributes.get("defaultName"); + expect(showDefaultName).toBeDefined(); + expect(showDefaultName?.annotations[AnnotationsKey.default]).toBeDefined(); + expect(showDefaultName?.annotations[AnnotationsKey.default]?.value).toBe("AwesomeShow"); + + // TODO: fix databaseName + // const productionAliasedProp = productionEntity?.attributes.get("aliasedProp"); + // const movieAliasedProp = movieEntity?.attributes.get("aliasedProp"); + // const showAliasedProp = showEntity?.attributes.get("aliasedProp"); + // expect(productionAliasedProp?.databaseName).toBeDefined(); + // expect(productionAliasedProp?.databaseName).toBe("dbName"); + // expect(movieAliasedProp?.databaseName).toBeDefined(); + // expect(movieAliasedProp?.databaseName).toBe("movieDbName"); + // expect(showAliasedProp?.databaseName).toBeDefined(); + // expect(showAliasedProp?.databaseName).toBe("dbName"); + }); +}); + describe("GraphQL adapters", () => { let schemaModel: Neo4jGraphQLSchemaModel; // entities diff --git a/packages/graphql/src/schema-model/generate-model.ts b/packages/graphql/src/schema-model/generate-model.ts index ef52cfd5ce..1f1c29029e 100644 --- a/packages/graphql/src/schema-model/generate-model.ts +++ b/packages/graphql/src/schema-model/generate-model.ts @@ -16,15 +16,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import type { DirectiveNode, DocumentNode, FieldDefinitionNode, ObjectTypeDefinitionNode } from "graphql"; +import type { + DirectiveNode, + DocumentNode, + FieldDefinitionNode, + InterfaceTypeDefinitionNode, + ObjectTypeDefinitionNode, +} from "graphql"; import { Neo4jGraphQLSchemaValidationError } from "../classes"; import getFieldTypeMeta from "../schema/get-field-type-meta"; import { filterTruthy } from "../utils/utils"; import { Neo4jGraphQLSchemaModel } from "./Neo4jGraphQLSchemaModel"; import type { Operations } from "./Neo4jGraphQLSchemaModel"; -import type { Annotation } from "./annotation/Annotation"; +import type { Annotation, Annotations } from "./annotation/Annotation"; import type { Attribute } from "./attribute/Attribute"; -import { CompositeEntity } from "./entity/CompositeEntity"; +import type { CompositeEntity } from "./entity/CompositeEntity"; import { ConcreteEntity } from "./entity/ConcreteEntity"; import { findDirective } from "./parser/utils"; import { parseArguments } from "./parser/parse-arguments"; @@ -37,6 +43,8 @@ import { parseAttribute, parseField } from "./parser/parse-attribute"; import { nodeDirective, relationshipDirective } from "../graphql/directives"; import { parseKeyAnnotation } from "./parser/annotations-parser/key-annotation"; import { parseAnnotations } from "./parser/parse-annotation"; +import { InterfaceEntity } from "./entity/InterfaceEntity"; +import { UnionEntity } from "./entity/UnionEntity"; export function generateModel(document: DocumentNode): Neo4jGraphQLSchemaModel { const definitionCollection: DefinitionCollection = getDefinitionCollection(document); @@ -63,11 +71,21 @@ export function generateModel(document: DocumentNode): Neo4jGraphQLSchemaModel { const interfaceEntities = Array.from(definitionCollection.interfaceToImplementingTypeNamesMap.entries()).map( ([name, concreteEntities]) => { - return generateCompositeEntity(name, concreteEntities, concreteEntitiesMap); + const interfaceNode = definitionCollection.interfaceTypes.get(name); + if (!interfaceNode) { + throw new Error(`Cannot find interface ${name}`); + } + return generateInterfaceEntity( + name, + interfaceNode, + concreteEntities, + concreteEntitiesMap, + definitionCollection + ); } ); const unionEntities = Array.from(definitionCollection.unionTypes).map(([unionName, unionDefinition]) => { - return generateCompositeEntity( + return generateUnionEntity( unionName, unionDefinition.types?.map((t) => t.name.value) || [], concreteEntitiesMap @@ -83,9 +101,57 @@ export function generateModel(document: DocumentNode): Neo4jGraphQLSchemaModel { annotations, }); definitionCollection.nodes.forEach((def) => hydrateRelationships(def, schema, definitionCollection)); + definitionCollection.interfaceTypes.forEach((def) => hydrateRelationships(def, schema, definitionCollection)); + interfaceEntities.forEach((interfaceEntity) => hydrateConcreteEntitiesWithInheritedAnnotations(interfaceEntity)); + // TODO: interface implements interface inheritance hydrate + // TODO: refactor flow?? + return schema; } +function hydrateConcreteEntitiesWithInheritedAnnotations(interfaceEntity: InterfaceEntity) { + const interfaceRelationships = interfaceEntity.relationships; + const interfaceAttributes = interfaceEntity.attributes; + for (const implementingEntity of interfaceEntity.concreteEntities) { + // overwrite entity + // mergeAnnotations(interfaceEntity.annotations, implementingEntity); // only `@exclude` + mergeRelationships(interfaceRelationships, implementingEntity); + mergeAttributes(interfaceAttributes, implementingEntity); + } +} + +function mergeAnnotations(interfaceAnnotations: Partial, entity: ConcreteEntity | Attribute): void { + const mergerConflictResolutionStrategy = function (interfaceAnnotation: string): boolean { + return !entity.annotations[interfaceAnnotation]; + }; + for (const annotation in interfaceAnnotations) { + if (mergerConflictResolutionStrategy(annotation)) { + entity.addAnnotation(interfaceAnnotations[annotation]); + } + } +} + +function mergeRelationships(interfaceRelationships: Map, entity: ConcreteEntity): void { + const mergerConflictResolutionStrategy = function (interfaceRelationship: string): boolean { + return !entity.relationships.get(interfaceRelationship); + }; + for (const [relationshipName, relationship] of interfaceRelationships.entries()) { + if (mergerConflictResolutionStrategy(relationshipName)) { + entity.attributeToRelationship(relationship); + } + } +} + +function mergeAttributes(interfaceAttributes: Map, entity: ConcreteEntity): void { + for (const [attributeName, attribute] of interfaceAttributes.entries()) { + const entityAttribute = entity.findAttribute(attributeName); + if (entityAttribute) { + // TODO: change databaseName if alias annotation + mergeAnnotations(attribute.annotations, entityAttribute); + } + } +} + function hydrateInterfacesToTypeNamesMap(definitionCollection: DefinitionCollection) { return definitionCollection.nodes.forEach((node) => { if (!node.interfaces) { @@ -109,6 +175,40 @@ function hydrateInterfacesToTypeNamesMap(definitionCollection: DefinitionCollect }); } +function generateUnionEntity( + entityDefinitionName: string, + entityImplementingTypeNames: string[], + concreteEntities: Map +): UnionEntity { + const unionEntity = generateCompositeEntity(entityDefinitionName, entityImplementingTypeNames, concreteEntities); + return new UnionEntity(unionEntity); +} + +function generateInterfaceEntity( + entityDefinitionName: string, + definition: InterfaceTypeDefinitionNode, + entityImplementingTypeNames: string[], + concreteEntities: Map, + definitionCollection: DefinitionCollection +): InterfaceEntity { + const interfaceEntity = generateCompositeEntity( + entityDefinitionName, + entityImplementingTypeNames, + concreteEntities + ); + const fields = (definition.fields || []).map((fieldDefinition) => + parseAttribute(fieldDefinition, definitionCollection) + ); + + const annotations = createEntityAnnotations(definition.directives || []); + + return new InterfaceEntity({ + ...interfaceEntity, + attributes: filterTruthy(fields) as Attribute[], + annotations, + }); +} + function generateCompositeEntity( entityDefinitionName: string, entityImplementingTypeNames: string[], @@ -130,36 +230,41 @@ function generateCompositeEntity( ); } */ // TODO: add annotations - return new CompositeEntity({ + return { name: entityDefinitionName, concreteEntities: compositeFields, - }); + }; } function hydrateRelationships( - definition: ObjectTypeDefinitionNode, + definition: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, schema: Neo4jGraphQLSchemaModel, definitionCollection: DefinitionCollection ): void { const name = definition.name.value; const entity = schema.getEntity(name); - if (!schema.isConcreteEntity(entity)) { - throw new Error(`Cannot add relationship to non-concrete entity ${name}`); + if (!entity) { + throw new Error(`Cannot find entity ${name}`); + } + if (entity instanceof UnionEntity) { + throw new Error(`Cannot add relationship to union entity ${name}`); } + // TODO: fix ts + const entityWithRelationships: ConcreteEntity | InterfaceEntity = entity as ConcreteEntity | InterfaceEntity; const relationshipFields = (definition.fields || []).map((fieldDefinition) => { - return generateRelationshipField(fieldDefinition, schema, entity, definitionCollection); + return generateRelationshipField(fieldDefinition, schema, entityWithRelationships, definitionCollection); }); for (const relationship of filterTruthy(relationshipFields)) { - entity.addRelationship(relationship); + entityWithRelationships.attributeToRelationship(relationship); } } function generateRelationshipField( field: FieldDefinitionNode, schema: Neo4jGraphQLSchemaModel, - source: ConcreteEntity, + source: ConcreteEntity | InterfaceEntity, definitionCollection: DefinitionCollection ): Relationship | undefined { const fieldTypeMeta = getFieldTypeMeta(field.type); diff --git a/packages/graphql/src/schema-model/relationship/Relationship.ts b/packages/graphql/src/schema-model/relationship/Relationship.ts index fbc9df3982..e0f90a9bd6 100644 --- a/packages/graphql/src/schema-model/relationship/Relationship.ts +++ b/packages/graphql/src/schema-model/relationship/Relationship.ts @@ -20,7 +20,6 @@ import { Neo4jGraphQLSchemaValidationError } from "../../classes"; import { upperFirst } from "../../utils/upper-first"; import type { Attribute } from "../attribute/Attribute"; -import type { ConcreteEntity } from "../entity/ConcreteEntity"; import type { Entity } from "../entity/Entity"; export type RelationshipDirection = "IN" | "OUT"; @@ -31,7 +30,7 @@ export class Relationship { public readonly name: string; // name of the relationship field, e.g. friends public readonly type: string; // name of the relationship type, e.g. "IS_FRIENDS_WITH" public readonly attributes: Map = new Map(); - public readonly source: ConcreteEntity; + public readonly source: Entity; public readonly target: Entity; public readonly direction: RelationshipDirection; public readonly isList: boolean; @@ -65,7 +64,7 @@ export class Relationship { name: string; type: string; attributes?: Attribute[]; - source: ConcreteEntity; + source: Entity; target: Entity; direction: RelationshipDirection; isList: boolean; diff --git a/packages/graphql/src/schema-model/relationship/model-adapters/RelationshipAdapter.ts b/packages/graphql/src/schema-model/relationship/model-adapters/RelationshipAdapter.ts index 65ed42f4b5..343a4e77e5 100644 --- a/packages/graphql/src/schema-model/relationship/model-adapters/RelationshipAdapter.ts +++ b/packages/graphql/src/schema-model/relationship/model-adapters/RelationshipAdapter.ts @@ -24,13 +24,16 @@ import type { NestedOperation, QueryDirection, Relationship, RelationshipDirecti import { AttributeAdapter } from "../../attribute/model-adapters/AttributeAdapter"; import type { Attribute } from "../../attribute/Attribute"; import { ConcreteEntity } from "../../entity/ConcreteEntity"; -import { CompositeEntity } from "../../entity/CompositeEntity"; +import { InterfaceEntity } from "../../entity/InterfaceEntity"; +import { UnionEntity } from "../../entity/UnionEntity"; +import { UnionEntityAdapter } from "../../entity/model-adapters/UnionEntityAdapter"; +import { InterfaceEntityAdapter } from "../../entity/model-adapters/InterfaceEntityAdapter"; export class RelationshipAdapter { public readonly name: string; public readonly type: string; public readonly attributes: Map = new Map(); - public readonly source: ConcreteEntityAdapter; + public readonly source: ConcreteEntityAdapter | InterfaceEntityAdapter | UnionEntityAdapter; private rawEntity: Entity; private _target: Entity | undefined; public readonly direction: RelationshipDirection; @@ -58,7 +61,10 @@ export class RelationshipAdapter { )}${nestedFieldStr}${aggregationStr}Selection`; } - constructor(relationship: Relationship, sourceAdapter?: ConcreteEntityAdapter) { + constructor( + relationship: Relationship, + sourceAdapter?: ConcreteEntityAdapter | InterfaceEntityAdapter | UnionEntityAdapter + ) { const { name, type, @@ -76,7 +82,15 @@ export class RelationshipAdapter { if (sourceAdapter) { this.source = sourceAdapter; } else { - this.source = new ConcreteEntityAdapter(source); + if (source instanceof ConcreteEntity) { + this.source = new ConcreteEntityAdapter(source); + } else if (source instanceof InterfaceEntity) { + this.source = new InterfaceEntityAdapter(source); + } else if (source instanceof UnionEntity) { + this.source = new UnionEntityAdapter(source); + } else { + throw new Error("relationship source must be an Entity"); + } } this.direction = direction; this.isList = isList; @@ -135,8 +149,10 @@ export class RelationshipAdapter { if (!this._target) { if (this.rawEntity instanceof ConcreteEntity) { this._target = new ConcreteEntityAdapter(this.rawEntity); - } else if (this.rawEntity instanceof CompositeEntity) { - this._target = new CompositeEntity(this.rawEntity); + } else if (this.rawEntity instanceof InterfaceEntity) { + this._target = new InterfaceEntityAdapter(this.rawEntity); + } else if (this.rawEntity instanceof UnionEntity) { + this._target = new UnionEntityAdapter(this.rawEntity); } else { throw new Error("invalid target entity type"); } diff --git a/packages/graphql/src/translate/projection/subquery/translate-cypher-directive-projection.ts b/packages/graphql/src/translate/projection/subquery/translate-cypher-directive-projection.ts index f332571a63..80016b56d3 100644 --- a/packages/graphql/src/translate/projection/subquery/translate-cypher-directive-projection.ts +++ b/packages/graphql/src/translate/projection/subquery/translate-cypher-directive-projection.ts @@ -22,7 +22,6 @@ import type { Node } from "../../../classes"; import type { GraphQLSortArg, CypherField, CypherFieldReferenceMap } from "../../../types"; import Cypher from "@neo4j/cypher-builder"; import createProjectionAndParams from "../../create-projection-and-params"; -import { CompositeEntity } from "../../../schema-model/entity/CompositeEntity"; import { compileCypher } from "../../../utils/compile-cypher"; import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphql-translation-context"; @@ -93,7 +92,7 @@ export function translateCypherDirectiveProjection({ res.params = { ...res.params, ...p }; subqueries.push(...nestedSubqueriesBeforeSort, ...nestedSubqueries); predicates.push(...nestedPredicates); - } else if (entity instanceof CompositeEntity) { + } else if (context.schemaModel.isCompositeEntity(entity)) { const unionProjections: Array<{ predicate: Cypher.Predicate; projection: Cypher.Expr }> = []; const labelsSubPredicates: Cypher.Predicate[] = []; diff --git a/packages/graphql/src/translate/translate-top-level-cypher.ts b/packages/graphql/src/translate/translate-top-level-cypher.ts index c0da5d9221..da63963023 100644 --- a/packages/graphql/src/translate/translate-top-level-cypher.ts +++ b/packages/graphql/src/translate/translate-top-level-cypher.ts @@ -21,7 +21,6 @@ import createProjectionAndParams from "./create-projection-and-params"; import type { CypherField } from "../types"; import { AUTH_FORBIDDEN_ERROR, AUTHORIZATION_UNAUTHENTICATED } from "../constants"; import Cypher from "@neo4j/cypher-builder"; -import { CompositeEntity } from "../schema-model/entity/CompositeEntity"; import { Neo4jGraphQLError } from "../classes"; import { filterByValues } from "./authorization/utils/filter-by-values"; import { compileCypher } from "../utils/compile-cypher"; @@ -105,7 +104,7 @@ export function translateTopLevelCypher({ const entity = context.schemaModel.entities.get(field.typeMeta.name); - if (entity instanceof CompositeEntity) { + if (context.schemaModel.isCompositeEntity(entity)) { const headStrs: Cypher.Clause[] = []; const referencedNodes = entity.concreteEntities @@ -228,7 +227,7 @@ export function translateTopLevelCypher({ if (field.isScalar || field.isEnum) { cypherStrs.push(`RETURN this`); - } else if (entity instanceof CompositeEntity) { + } else if (context.schemaModel.isCompositeEntity(entity)) { cypherStrs.push(`RETURN head( ${projectionStr.getCypher(env)} ) AS this`); } else { cypherStrs.push(`RETURN this ${projectionStr.getCypher(env)} AS this`); From 5ffd6b4b7ccb4ccef887f1da2fde99589467222a Mon Sep 17 00:00:00 2001 From: a-alle Date: Tue, 22 Aug 2023 18:06:41 +0100 Subject: [PATCH 2/9] leaves todo --- packages/graphql/src/schema-model/generate-model.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/graphql/src/schema-model/generate-model.ts b/packages/graphql/src/schema-model/generate-model.ts index 1f1c29029e..abdd44b50e 100644 --- a/packages/graphql/src/schema-model/generate-model.ts +++ b/packages/graphql/src/schema-model/generate-model.ts @@ -105,6 +105,7 @@ export function generateModel(document: DocumentNode): Neo4jGraphQLSchemaModel { interfaceEntities.forEach((interfaceEntity) => hydrateConcreteEntitiesWithInheritedAnnotations(interfaceEntity)); // TODO: interface implements interface inheritance hydrate // TODO: refactor flow?? + // TODO: add tests for interfaces and relationshipProperties interface annotations return schema; } From 08f487a551c965b44d0bb34cee00d18d31f87d76 Mon Sep 17 00:00:00 2001 From: a-alle Date: Fri, 1 Sep 2023 13:13:56 +0100 Subject: [PATCH 3/9] remove need for attributeToRelationship --- .../src/schema-model/generate-model.ts | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/packages/graphql/src/schema-model/generate-model.ts b/packages/graphql/src/schema-model/generate-model.ts index abdd44b50e..8e269b20dd 100644 --- a/packages/graphql/src/schema-model/generate-model.ts +++ b/packages/graphql/src/schema-model/generate-model.ts @@ -138,7 +138,8 @@ function mergeRelationships(interfaceRelationships: Map, e }; for (const [relationshipName, relationship] of interfaceRelationships.entries()) { if (mergerConflictResolutionStrategy(relationshipName)) { - entity.attributeToRelationship(relationship); + // entity.attributeToRelationship(relationship); + entity.addRelationship(relationship); } } } @@ -230,7 +231,6 @@ function generateCompositeEntity( `Composite entity ${entityDefinitionName} has no concrete entities` ); } */ - // TODO: add annotations return { name: entityDefinitionName, concreteEntities: compositeFields, @@ -258,7 +258,8 @@ function hydrateRelationships( }); for (const relationship of filterTruthy(relationshipFields)) { - entityWithRelationships.attributeToRelationship(relationship); + // entityWithRelationships.attributeToRelationship(relationship); + entityWithRelationships.addRelationship(relationship); } } @@ -312,9 +313,22 @@ function generateConcreteEntity( definition: ObjectTypeDefinitionNode, definitionCollection: DefinitionCollection ): ConcreteEntity { - const fields = (definition.fields || []).map((fieldDefinition) => - parseAttribute(fieldDefinition, definitionCollection) - ); + const inheritedFields = definition.interfaces?.flatMap((interfaceNamedNode) => { + const interfaceName = interfaceNamedNode.name.value; + return definitionCollection.interfaceTypes.get(interfaceName)?.fields || []; + }); + const fields = (definition.fields || []).map((fieldDefinition) => { + const isRelationshipAttribute = findDirective(fieldDefinition.directives, relationshipDirective.name); + const isInheritedRelationshipAttribute = inheritedFields?.some( + (inheritedField) => + inheritedField.name.value === fieldDefinition.name.value && + findDirective(inheritedField.directives, relationshipDirective.name) + ); + if (isRelationshipAttribute || isInheritedRelationshipAttribute) { + return; + } + return parseAttribute(fieldDefinition, definitionCollection); + }); const annotations = createEntityAnnotations(definition.directives || []); From 9cc34410d6485f53798001954cebd981cac71051 Mon Sep 17 00:00:00 2001 From: a-alle Date: Fri, 1 Sep 2023 16:34:30 +0100 Subject: [PATCH 4/9] move inheritance to schemaModel construction time rather than after-the-fact --- .../src/schema-model/attribute/Attribute.ts | 2 +- .../src/schema-model/entity/ConcreteEntity.ts | 13 +- .../schema-model/entity/InterfaceEntity.ts | 11 - .../src/schema-model/generate-model.ts | 194 ++++++++++++------ .../schema-model/parser/parse-annotation.ts | 121 ++++++----- .../schema-model/parser/parse-attribute.ts | 25 ++- 6 files changed, 222 insertions(+), 144 deletions(-) diff --git a/packages/graphql/src/schema-model/attribute/Attribute.ts b/packages/graphql/src/schema-model/attribute/Attribute.ts index 385c7089ba..8bed16fc5c 100644 --- a/packages/graphql/src/schema-model/attribute/Attribute.ts +++ b/packages/graphql/src/schema-model/attribute/Attribute.ts @@ -56,7 +56,7 @@ export class Attribute { }); } - public addAnnotation(annotation: Annotation): void { + private addAnnotation(annotation: Annotation): void { const annotationKey = annotationToKey(annotation); if (this.annotations[annotationKey]) { throw new Neo4jGraphQLSchemaValidationError(`Annotation ${annotationKey} already exists in ${this.name}`); diff --git a/packages/graphql/src/schema-model/entity/ConcreteEntity.ts b/packages/graphql/src/schema-model/entity/ConcreteEntity.ts index ae1c985603..c4e25c61bb 100644 --- a/packages/graphql/src/schema-model/entity/ConcreteEntity.ts +++ b/packages/graphql/src/schema-model/entity/ConcreteEntity.ts @@ -71,18 +71,7 @@ export class ConcreteEntity implements Entity { this.attributes.set(attribute.name, attribute); } - private deleteAttribute(attributeName: string): void { - if (this.attributes.has(attributeName)) { - this.attributes.delete(attributeName); - } - } - - public attributeToRelationship(relationship: Relationship): void { - this.addRelationship(relationship); - this.deleteAttribute(relationship.name); - } - - public addAnnotation(annotation: Annotation): void { + private addAnnotation(annotation: Annotation): void { const annotationKey = annotationToKey(annotation); const existingAnnotation = this.annotations[annotationKey]; diff --git a/packages/graphql/src/schema-model/entity/InterfaceEntity.ts b/packages/graphql/src/schema-model/entity/InterfaceEntity.ts index 76f6a48c55..45cc73f497 100644 --- a/packages/graphql/src/schema-model/entity/InterfaceEntity.ts +++ b/packages/graphql/src/schema-model/entity/InterfaceEntity.ts @@ -80,17 +80,6 @@ export class InterfaceEntity implements CompositeEntity { this.annotations[annotationKey] = annotation as any; } - private deleteAttribute(attributeName: string): void { - if (this.attributes.has(attributeName)) { - this.attributes.delete(attributeName); - } - } - - public attributeToRelationship(relationship: Relationship): void { - this.addRelationship(relationship); - this.deleteAttribute(relationship.name); - } - public addRelationship(relationship: Relationship): void { if (this.relationships.has(relationship.name)) { throw new Neo4jGraphQLSchemaValidationError( diff --git a/packages/graphql/src/schema-model/generate-model.ts b/packages/graphql/src/schema-model/generate-model.ts index 8e269b20dd..3dbe59a25c 100644 --- a/packages/graphql/src/schema-model/generate-model.ts +++ b/packages/graphql/src/schema-model/generate-model.ts @@ -28,7 +28,7 @@ import getFieldTypeMeta from "../schema/get-field-type-meta"; import { filterTruthy } from "../utils/utils"; import { Neo4jGraphQLSchemaModel } from "./Neo4jGraphQLSchemaModel"; import type { Operations } from "./Neo4jGraphQLSchemaModel"; -import type { Annotation, Annotations } from "./annotation/Annotation"; +import type { Annotation } from "./annotation/Annotation"; import type { Attribute } from "./attribute/Attribute"; import type { CompositeEntity } from "./entity/CompositeEntity"; import { ConcreteEntity } from "./entity/ConcreteEntity"; @@ -46,6 +46,76 @@ import { parseAnnotations } from "./parser/parse-annotation"; import { InterfaceEntity } from "./entity/InterfaceEntity"; import { UnionEntity } from "./entity/UnionEntity"; +/* +export function generateModel2(document: DocumentNode): Neo4jGraphQLSchemaModel { + const definitionCollection: DefinitionCollection = getDefinitionCollection(document); + + const operations: Operations = definitionCollection.operations.reduce((acc, definition): Operations => { + acc[definition.name.value] = generateOperation(definition); + return acc; + }, {}); + + // hydrate interface to typeNames map + hydrateInterfacesToTypeNamesMap(definitionCollection); + + // === 1. add composites w/o concreteEntities connection + const unionEntities = Array.from(definitionCollection.unionTypes).map(([unionName, unionDefinition]) => { + return generateUnionEntity( + unionName, + unionDefinition.types?.map((t) => t.name.value) || [], + concreteEntitiesMap + ); + }); + const interfaceEntities = Array.from(definitionCollection.interfaceToImplementingTypeNamesMap.entries()).map( + ([name, concreteEntities]) => { + const interfaceNode = definitionCollection.interfaceTypes.get(name); + if (!interfaceNode) { + throw new Error(`Cannot find interface ${name}`); + } + return generateInterfaceEntity( + name, + interfaceNode, + concreteEntities, + concreteEntitiesMap, + definitionCollection + ); + } + ); + + // === 2. add concretes with attributes, annotations and relationships already resolved (with inherited) + const concreteEntities = Array.from(definitionCollection.nodes.values()).map((node) => + generateConcreteEntity(node, definitionCollection) + ); + + // === 3. update composites with links to concrete + // TODO + + // TODO: still need this? + const concreteEntitiesMap = concreteEntities.reduce((acc, entity) => { + if (acc.has(entity.name)) { + throw new Neo4jGraphQLSchemaValidationError(`Duplicate node ${entity.name}`); + } + acc.set(entity.name, entity); + return acc; + }, new Map()); + + // === 4. create schema model with everything + const annotations = createSchemaModelAnnotations(definitionCollection.schemaDirectives); + const schema = new Neo4jGraphQLSchemaModel({ + compositeEntities: [...unionEntities, ...interfaceEntities], + concreteEntities, + operations, + annotations, + }); + + definitionCollection.nodes.forEach((def) => hydrateRelationships(def, schema, definitionCollection)); + definitionCollection.interfaceTypes.forEach((def) => hydrateRelationships(def, schema, definitionCollection)); + + return schema; +} +*/ +// =============================================================== + export function generateModel(document: DocumentNode): Neo4jGraphQLSchemaModel { const definitionCollection: DefinitionCollection = getDefinitionCollection(document); @@ -102,58 +172,13 @@ export function generateModel(document: DocumentNode): Neo4jGraphQLSchemaModel { }); definitionCollection.nodes.forEach((def) => hydrateRelationships(def, schema, definitionCollection)); definitionCollection.interfaceTypes.forEach((def) => hydrateRelationships(def, schema, definitionCollection)); - interfaceEntities.forEach((interfaceEntity) => hydrateConcreteEntitiesWithInheritedAnnotations(interfaceEntity)); - // TODO: interface implements interface inheritance hydrate + // TODO: test - interface implements interface inheritance hydrate // TODO: refactor flow?? // TODO: add tests for interfaces and relationshipProperties interface annotations return schema; } -function hydrateConcreteEntitiesWithInheritedAnnotations(interfaceEntity: InterfaceEntity) { - const interfaceRelationships = interfaceEntity.relationships; - const interfaceAttributes = interfaceEntity.attributes; - for (const implementingEntity of interfaceEntity.concreteEntities) { - // overwrite entity - // mergeAnnotations(interfaceEntity.annotations, implementingEntity); // only `@exclude` - mergeRelationships(interfaceRelationships, implementingEntity); - mergeAttributes(interfaceAttributes, implementingEntity); - } -} - -function mergeAnnotations(interfaceAnnotations: Partial, entity: ConcreteEntity | Attribute): void { - const mergerConflictResolutionStrategy = function (interfaceAnnotation: string): boolean { - return !entity.annotations[interfaceAnnotation]; - }; - for (const annotation in interfaceAnnotations) { - if (mergerConflictResolutionStrategy(annotation)) { - entity.addAnnotation(interfaceAnnotations[annotation]); - } - } -} - -function mergeRelationships(interfaceRelationships: Map, entity: ConcreteEntity): void { - const mergerConflictResolutionStrategy = function (interfaceRelationship: string): boolean { - return !entity.relationships.get(interfaceRelationship); - }; - for (const [relationshipName, relationship] of interfaceRelationships.entries()) { - if (mergerConflictResolutionStrategy(relationshipName)) { - // entity.attributeToRelationship(relationship); - entity.addRelationship(relationship); - } - } -} - -function mergeAttributes(interfaceAttributes: Map, entity: ConcreteEntity): void { - for (const [attributeName, attribute] of interfaceAttributes.entries()) { - const entityAttribute = entity.findAttribute(attributeName); - if (entityAttribute) { - // TODO: change databaseName if alias annotation - mergeAnnotations(attribute.annotations, entityAttribute); - } - } -} - function hydrateInterfacesToTypeNamesMap(definitionCollection: DefinitionCollection) { return definitionCollection.nodes.forEach((node) => { if (!node.interfaces) { @@ -198,10 +223,26 @@ function generateInterfaceEntity( entityImplementingTypeNames, concreteEntities ); - const fields = (definition.fields || []).map((fieldDefinition) => - parseAttribute(fieldDefinition, definitionCollection) - ); + const inheritedFields = + definition.interfaces?.flatMap((interfaceNamedNode) => { + const interfaceName = interfaceNamedNode.name.value; + return definitionCollection.interfaceTypes.get(interfaceName)?.fields || []; + }) || []; + const fields = (definition.fields || []).map((fieldDefinition) => { + const inheritedField = inheritedFields?.filter( + (inheritedField) => inheritedField.name.value === fieldDefinition.name.value + ); + const isRelationshipAttribute = findDirective(fieldDefinition.directives, relationshipDirective.name); + const isInheritedRelationshipAttribute = inheritedField?.some((inheritedField) => + findDirective(inheritedField.directives, relationshipDirective.name) + ); + if (isRelationshipAttribute || isInheritedRelationshipAttribute) { + return; + } + return parseAttribute(fieldDefinition, inheritedField, definitionCollection); + }); + // TODO: inherited annotations? const annotations = createEntityAnnotations(definition.directives || []); return new InterfaceEntity({ @@ -253,11 +294,32 @@ function hydrateRelationships( } // TODO: fix ts const entityWithRelationships: ConcreteEntity | InterfaceEntity = entity as ConcreteEntity | InterfaceEntity; - const relationshipFields = (definition.fields || []).map((fieldDefinition) => { - return generateRelationshipField(fieldDefinition, schema, entityWithRelationships, definitionCollection); - }); + const inheritedFields = + definition.interfaces?.flatMap((interfaceNamedNode) => { + const interfaceName = interfaceNamedNode.name.value; + return definitionCollection.interfaceTypes.get(interfaceName)?.fields || []; + }) || []; + // TODO: directives on definition have priority over interfaces + const mergedFields = (definition.fields || []).concat(inheritedFields); + const relationshipFieldsMap = new Map(); + for (const fieldDefinition of mergedFields) { + // TODO: takes the first one + // multiple interfaces can have this annotation - must constrain this flexibility by design + if (relationshipFieldsMap.has(fieldDefinition.name.value)) { + continue; + } + const relationshipField = generateRelationshipField( + fieldDefinition, + schema, + entityWithRelationships, + definitionCollection + ); + if (relationshipField) { + relationshipFieldsMap.set(fieldDefinition.name.value, relationshipField); + } + } - for (const relationship of filterTruthy(relationshipFields)) { + for (const relationship of relationshipFieldsMap.values()) { // entityWithRelationships.attributeToRelationship(relationship); entityWithRelationships.addRelationship(relationship); } @@ -291,7 +353,17 @@ function generateRelationshipField( ); } - const fields = (propertyInterface.fields || []).map((field) => parseAttribute(field, definitionCollection)); + const inheritedFields = + propertyInterface.interfaces?.flatMap((interfaceNamedNode) => { + const interfaceName = interfaceNamedNode.name.value; + return definitionCollection.interfaceTypes.get(interfaceName)?.fields || []; + }) || []; + const fields = (propertyInterface.fields || []).map((fieldDefinition) => { + const inheritedField = inheritedFields?.filter( + (inheritedField) => inheritedField.name.value === fieldDefinition.name.value + ); + return parseAttribute(fieldDefinition, inheritedField, definitionCollection); + }); attributes = filterTruthy(fields) as Attribute[]; } @@ -318,21 +390,21 @@ function generateConcreteEntity( return definitionCollection.interfaceTypes.get(interfaceName)?.fields || []; }); const fields = (definition.fields || []).map((fieldDefinition) => { + const inheritedField = inheritedFields?.filter( + (inheritedField) => inheritedField.name.value === fieldDefinition.name.value + ); const isRelationshipAttribute = findDirective(fieldDefinition.directives, relationshipDirective.name); - const isInheritedRelationshipAttribute = inheritedFields?.some( - (inheritedField) => - inheritedField.name.value === fieldDefinition.name.value && - findDirective(inheritedField.directives, relationshipDirective.name) + const isInheritedRelationshipAttribute = inheritedField?.some((inheritedField) => + findDirective(inheritedField.directives, relationshipDirective.name) ); if (isRelationshipAttribute || isInheritedRelationshipAttribute) { return; } - return parseAttribute(fieldDefinition, definitionCollection); + return parseAttribute(fieldDefinition, inheritedField, definitionCollection); }); const annotations = createEntityAnnotations(definition.directives || []); - // TODO: add annotations inherited from interface return new ConcreteEntity({ name: definition.name.value, labels: getLabels(definition), diff --git a/packages/graphql/src/schema-model/parser/parse-annotation.ts b/packages/graphql/src/schema-model/parser/parse-annotation.ts index 47be28ccef..f96cc89a95 100644 --- a/packages/graphql/src/schema-model/parser/parse-annotation.ts +++ b/packages/graphql/src/schema-model/parser/parse-annotation.ts @@ -40,64 +40,75 @@ import { parseJWTPayloadAnnotation } from "./annotations-parser/jwt-payload-anno import { parseAuthorizationAnnotation } from "./annotations-parser/authorization-annotation"; import { parseAuthenticationAnnotation } from "./annotations-parser/authentication-annotation"; import { parseSubscriptionsAuthorizationAnnotation } from "./annotations-parser/subscriptions-authorization-annotation"; -import { filterTruthy } from "../../utils/utils"; import type { Annotation } from "../annotation/Annotation"; import { AnnotationsKey } from "../annotation/Annotation"; import { IDAnnotation } from "../annotation/IDAnnotation"; export function parseAnnotations(directives: readonly DirectiveNode[]): Annotation[] { - return filterTruthy( - directives.map((directive) => { - switch (directive.name.value) { - case AnnotationsKey.authentication: - return parseAuthenticationAnnotation(directive); - case AnnotationsKey.authorization: - return parseAuthorizationAnnotation(directive); - case AnnotationsKey.coalesce: - return parseCoalesceAnnotation(directive); - case AnnotationsKey.customResolver: - return parseCustomResolverAnnotation(directive); - case AnnotationsKey.cypher: - return parseCypherAnnotation(directive); - case AnnotationsKey.default: - return parseDefaultAnnotation(directive); - case AnnotationsKey.filterable: - return parseFilterableAnnotation(directive); - case AnnotationsKey.fulltext: - return parseFullTextAnnotation(directive); - case AnnotationsKey.id: - return new IDAnnotation(); - case AnnotationsKey.jwtClaim: - return parseJWTClaimAnnotation(directive); - case AnnotationsKey.jwtPayload: - return parseJWTPayloadAnnotation(directive); - case AnnotationsKey.mutation: - return parseMutationAnnotation(directive); - case AnnotationsKey.plural: - return parsePluralAnnotation(directive); - case AnnotationsKey.populatedBy: - return parsePopulatedByAnnotation(directive); - case AnnotationsKey.private: - return parsePrivateAnnotation(directive); - case AnnotationsKey.query: - return parseQueryAnnotation(directive); - case AnnotationsKey.limit: - return parseLimitAnnotation(directive); - case AnnotationsKey.selectable: - return parseSelectableAnnotation(directive); - case AnnotationsKey.settable: - return parseSettableAnnotation(directive); - case AnnotationsKey.subscription: - return parseSubscriptionAnnotation(directive); - case AnnotationsKey.subscriptionsAuthorization: - return parseSubscriptionsAuthorizationAnnotation(directive); - case AnnotationsKey.timestamp: - return parseTimestampAnnotation(directive); - case AnnotationsKey.unique: - return parseUniqueAnnotation(directive); - default: - return undefined; - } - }) - ); + const annotations = directives.reduce((directivesMap, directive) => { + if (directivesMap.has(directive.name.value)) { + // TODO: takes the first one + // multiple interfaces can have this annotation - must constrain this flexibility by design + return directivesMap; + } + const annotation = parseDirective(directive); + if (annotation) { + directivesMap.set(directive.name.value, annotation); + } + return directivesMap; + }, new Map()); + return Array.from(annotations.values()); +} + +function parseDirective(directive: DirectiveNode): Annotation | undefined { + switch (directive.name.value) { + case AnnotationsKey.authentication: + return parseAuthenticationAnnotation(directive); + case AnnotationsKey.authorization: + return parseAuthorizationAnnotation(directive); + case AnnotationsKey.coalesce: + return parseCoalesceAnnotation(directive); + case AnnotationsKey.customResolver: + return parseCustomResolverAnnotation(directive); + case AnnotationsKey.cypher: + return parseCypherAnnotation(directive); + case AnnotationsKey.default: + return parseDefaultAnnotation(directive); + case AnnotationsKey.filterable: + return parseFilterableAnnotation(directive); + case AnnotationsKey.fulltext: + return parseFullTextAnnotation(directive); + case AnnotationsKey.id: + return new IDAnnotation(); + case AnnotationsKey.jwtClaim: + return parseJWTClaimAnnotation(directive); + case AnnotationsKey.jwtPayload: + return parseJWTPayloadAnnotation(directive); + case AnnotationsKey.mutation: + return parseMutationAnnotation(directive); + case AnnotationsKey.plural: + return parsePluralAnnotation(directive); + case AnnotationsKey.populatedBy: + return parsePopulatedByAnnotation(directive); + case AnnotationsKey.private: + return parsePrivateAnnotation(directive); + case AnnotationsKey.query: + return parseQueryAnnotation(directive); + case AnnotationsKey.limit: + return parseLimitAnnotation(directive); + case AnnotationsKey.selectable: + return parseSelectableAnnotation(directive); + case AnnotationsKey.settable: + return parseSettableAnnotation(directive); + case AnnotationsKey.subscription: + return parseSubscriptionAnnotation(directive); + case AnnotationsKey.subscriptionsAuthorization: + return parseSubscriptionsAuthorizationAnnotation(directive); + case AnnotationsKey.timestamp: + return parseTimestampAnnotation(directive); + case AnnotationsKey.unique: + return parseUniqueAnnotation(directive); + default: + return undefined; + } } diff --git a/packages/graphql/src/schema-model/parser/parse-attribute.ts b/packages/graphql/src/schema-model/parser/parse-attribute.ts index 7112405bce..c440a3fbe6 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 { FieldDefinitionNode, TypeNode } from "graphql"; +import type { DirectiveNode, FieldDefinitionNode, TypeNode } from "graphql"; import { Kind } from "graphql"; import type { AttributeType, Neo4jGraphQLScalarType } from "../attribute/AttributeType"; import { @@ -45,12 +45,14 @@ import { findDirective } from "./utils"; export function parseAttribute( field: FieldDefinitionNode, + inheritedField: FieldDefinitionNode[] | undefined, definitionCollection: DefinitionCollection ): Attribute | Field { const name = field.name.value; const type = parseTypeNode(definitionCollection, field.type); - const annotations = parseAnnotations(field.directives || []); - const databaseName = getDatabaseName(field); + const inheritedDirectives = inheritedField?.flatMap((f) => f.directives || []) || []; + const annotations = parseAnnotations((field.directives || []).concat(inheritedDirectives)); + const databaseName = getDatabaseName(field, inheritedField); return new Attribute({ name, annotations, @@ -59,12 +61,27 @@ export function parseAttribute( }); } -function getDatabaseName(fieldDefinitionNode: FieldDefinitionNode): string | undefined { +function getDatabaseName( + fieldDefinitionNode: FieldDefinitionNode, + inheritedFields: FieldDefinitionNode[] | undefined +): string | undefined { const aliasUsage = findDirective(fieldDefinitionNode.directives, aliasDirective.name); if (aliasUsage) { const { property } = parseArguments(aliasDirective, aliasUsage) as { property: string }; return property; } + const inheritedAliasUsage = inheritedFields?.reduce((aliasUsage, field) => { + // TODO: takes the first one + // multiple interfaces can have this annotation - must constrain this flexibility by design + if (!aliasUsage) { + aliasUsage = findDirective(field.directives, aliasDirective.name); + } + return aliasUsage; + }, undefined); + if (inheritedAliasUsage) { + const { property } = parseArguments(aliasDirective, inheritedAliasUsage) as { property: string }; + return property; + } } // we may want to remove Fields from the schema model From 717d80eab3b7419c6c675054e55b15276af84526 Mon Sep 17 00:00:00 2001 From: a-alle Date: Fri, 1 Sep 2023 16:36:40 +0100 Subject: [PATCH 5/9] uncomment databaseName test --- .../src/schema-model/generate-model.test.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/graphql/src/schema-model/generate-model.test.ts b/packages/graphql/src/schema-model/generate-model.test.ts index 03296b0a14..f4e16829cf 100644 --- a/packages/graphql/src/schema-model/generate-model.test.ts +++ b/packages/graphql/src/schema-model/generate-model.test.ts @@ -593,16 +593,15 @@ describe("ComposeEntity Annotations & Attributes", () => { expect(showDefaultName?.annotations[AnnotationsKey.default]).toBeDefined(); expect(showDefaultName?.annotations[AnnotationsKey.default]?.value).toBe("AwesomeShow"); - // TODO: fix databaseName - // const productionAliasedProp = productionEntity?.attributes.get("aliasedProp"); - // const movieAliasedProp = movieEntity?.attributes.get("aliasedProp"); - // const showAliasedProp = showEntity?.attributes.get("aliasedProp"); - // expect(productionAliasedProp?.databaseName).toBeDefined(); - // expect(productionAliasedProp?.databaseName).toBe("dbName"); - // expect(movieAliasedProp?.databaseName).toBeDefined(); - // expect(movieAliasedProp?.databaseName).toBe("movieDbName"); - // expect(showAliasedProp?.databaseName).toBeDefined(); - // expect(showAliasedProp?.databaseName).toBe("dbName"); + const productionAliasedProp = productionEntity?.attributes.get("aliasedProp"); + const movieAliasedProp = movieEntity?.attributes.get("aliasedProp"); + const showAliasedProp = showEntity?.attributes.get("aliasedProp"); + expect(productionAliasedProp?.databaseName).toBeDefined(); + expect(productionAliasedProp?.databaseName).toBe("dbName"); + expect(movieAliasedProp?.databaseName).toBeDefined(); + expect(movieAliasedProp?.databaseName).toBe("movieDbName"); + expect(showAliasedProp?.databaseName).toBeDefined(); + expect(showAliasedProp?.databaseName).toBe("dbName"); }); }); From e8437fea98e0c95e5a1a4e1dd937b2323779acd5 Mon Sep 17 00:00:00 2001 From: a-alle Date: Fri, 1 Sep 2023 16:54:09 +0100 Subject: [PATCH 6/9] inverts knowledge of isComposite/isConcrete from schema model to entity itself --- packages/graphql/src/classes/Subgraph.ts | 2 +- .../src/schema-model/Neo4jGraphQLSchemaModel.ts | 12 +----------- .../src/schema-model/entity/CompositeEntity.ts | 3 ++- .../src/schema-model/entity/ConcreteEntity.ts | 7 +++++++ packages/graphql/src/schema-model/entity/Entity.ts | 5 +++++ .../src/schema-model/entity/InterfaceEntity.ts | 7 +++++++ .../graphql/src/schema-model/entity/UnionEntity.ts | 6 ++++++ .../entity/model-adapters/ConcreteEntityAdapter.ts | 14 ++++++-------- .../graphql/src/schema-model/generate-model.ts | 3 +-- .../model-adapters/RelationshipAdapter.ts | 4 ++-- .../authentication/selection-set-parser.ts | 4 ++-- .../translate-cypher-directive-projection.ts | 2 +- .../src/translate/translate-top-level-cypher.ts | 4 ++-- 13 files changed, 43 insertions(+), 30 deletions(-) diff --git a/packages/graphql/src/classes/Subgraph.ts b/packages/graphql/src/classes/Subgraph.ts index ffb88ed7ef..b8097cfedd 100644 --- a/packages/graphql/src/classes/Subgraph.ts +++ b/packages/graphql/src/classes/Subgraph.ts @@ -114,7 +114,7 @@ export class Subgraph { if (def.kind === Kind.OBJECT_TYPE_DEFINITION) { const entity = schemaModel.getEntity(def.name.value); - if (schemaModel.isConcreteEntity(entity)) { + if (entity?.isConcreteEntity()) { const keyAnnotation = entity.annotations.key; // If there is a @key directive with `resolvable` set to false, then do not add __resolveReference diff --git a/packages/graphql/src/schema-model/Neo4jGraphQLSchemaModel.ts b/packages/graphql/src/schema-model/Neo4jGraphQLSchemaModel.ts index 377b8c3dd3..981ffb8170 100644 --- a/packages/graphql/src/schema-model/Neo4jGraphQLSchemaModel.ts +++ b/packages/graphql/src/schema-model/Neo4jGraphQLSchemaModel.ts @@ -22,11 +22,9 @@ import type { Operation } from "./Operation"; import type { Annotations, Annotation } from "./annotation/Annotation"; import { annotationToKey } from "./annotation/Annotation"; import type { CompositeEntity } from "./entity/CompositeEntity"; -import { ConcreteEntity } from "./entity/ConcreteEntity"; +import type { ConcreteEntity } from "./entity/ConcreteEntity"; import type { Entity } from "./entity/Entity"; import { ConcreteEntityAdapter } from "./entity/model-adapters/ConcreteEntityAdapter"; -import { InterfaceEntity } from "./entity/InterfaceEntity"; -import { UnionEntity } from "./entity/UnionEntity"; export type Operations = { Query?: Operation; @@ -83,14 +81,6 @@ export class Neo4jGraphQLSchemaModel { return this.concreteEntities.filter((entity) => entity.name === name && entity.matchLabels(labels)); } - public isConcreteEntity(entity?: Entity): entity is ConcreteEntity { - return entity instanceof ConcreteEntity; - } - - public isCompositeEntity(entity?: Entity): entity is CompositeEntity { - return entity instanceof InterfaceEntity || entity instanceof UnionEntity; - } - private addAnnotation(annotation: Annotation): void { const annotationKey = annotationToKey(annotation); const existingAnnotation = this.annotations[annotationKey]; diff --git a/packages/graphql/src/schema-model/entity/CompositeEntity.ts b/packages/graphql/src/schema-model/entity/CompositeEntity.ts index 1e240e8067..b82091a1c7 100644 --- a/packages/graphql/src/schema-model/entity/CompositeEntity.ts +++ b/packages/graphql/src/schema-model/entity/CompositeEntity.ts @@ -18,9 +18,10 @@ */ import type { ConcreteEntity } from "./ConcreteEntity"; +import type { Entity } from "./Entity"; /** models the concept of an Abstract Type */ -export interface CompositeEntity { +export interface CompositeEntity extends Entity { readonly name: string; concreteEntities: ConcreteEntity[]; } diff --git a/packages/graphql/src/schema-model/entity/ConcreteEntity.ts b/packages/graphql/src/schema-model/entity/ConcreteEntity.ts index c4e25c61bb..db32e6f77e 100644 --- a/packages/graphql/src/schema-model/entity/ConcreteEntity.ts +++ b/packages/graphql/src/schema-model/entity/ConcreteEntity.ts @@ -23,6 +23,7 @@ import type { Annotation, Annotations } from "../annotation/Annotation"; import { annotationToKey } from "../annotation/Annotation"; import type { Attribute } from "../attribute/Attribute"; import type { Relationship } from "../relationship/Relationship"; +import type { CompositeEntity } from "./CompositeEntity"; import type { Entity } from "./Entity"; export class ConcreteEntity implements Entity { @@ -59,6 +60,12 @@ export class ConcreteEntity implements Entity { this.addRelationship(relationship); } } + isConcreteEntity(): this is ConcreteEntity { + return true; + } + isCompositeEntity(): this is CompositeEntity { + return false; + } public matchLabels(labels: string[]) { return setsAreEqual(new Set(labels), this.labels); diff --git a/packages/graphql/src/schema-model/entity/Entity.ts b/packages/graphql/src/schema-model/entity/Entity.ts index ba59a90a21..c856bc5148 100644 --- a/packages/graphql/src/schema-model/entity/Entity.ts +++ b/packages/graphql/src/schema-model/entity/Entity.ts @@ -17,10 +17,15 @@ * limitations under the License. */ +import type { CompositeEntity } from "./CompositeEntity"; +import type { ConcreteEntity } from "./ConcreteEntity"; export interface Entity { readonly name: string; + isConcreteEntity(): this is ConcreteEntity; + isCompositeEntity(): this is CompositeEntity; + // attributes // relationships // annotations diff --git a/packages/graphql/src/schema-model/entity/InterfaceEntity.ts b/packages/graphql/src/schema-model/entity/InterfaceEntity.ts index 45cc73f497..f748d4f3d1 100644 --- a/packages/graphql/src/schema-model/entity/InterfaceEntity.ts +++ b/packages/graphql/src/schema-model/entity/InterfaceEntity.ts @@ -60,6 +60,13 @@ export class InterfaceEntity implements CompositeEntity { } } + isConcreteEntity(): this is ConcreteEntity { + return false; + } + isCompositeEntity(): this is CompositeEntity { + return true; + } + private addAttribute(attribute: Attribute): void { if (this.attributes.has(attribute.name)) { throw new Neo4jGraphQLSchemaValidationError(`Attribute ${attribute.name} already exists in ${this.name}`); diff --git a/packages/graphql/src/schema-model/entity/UnionEntity.ts b/packages/graphql/src/schema-model/entity/UnionEntity.ts index d1a1f62dc3..07fbbe5d02 100644 --- a/packages/graphql/src/schema-model/entity/UnionEntity.ts +++ b/packages/graphql/src/schema-model/entity/UnionEntity.ts @@ -28,4 +28,10 @@ export class UnionEntity implements CompositeEntity { this.name = name; this.concreteEntities = concreteEntities; } + isConcreteEntity(): this is ConcreteEntity { + return false; + } + isCompositeEntity(): this is CompositeEntity { + return true; + } } 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 def5dbcdde..410a975686 100644 --- a/packages/graphql/src/schema-model/entity/model-adapters/ConcreteEntityAdapter.ts +++ b/packages/graphql/src/schema-model/entity/model-adapters/ConcreteEntityAdapter.ts @@ -20,13 +20,14 @@ import { AttributeAdapter } from "../../attribute/model-adapters/AttributeAdapter"; import type { Relationship } from "../../relationship/Relationship"; import { getFromMap } from "../../utils/get-from-map"; -import type { Entity } from "../Entity"; import { singular, plural } from "../../utils/string-manipulation"; import type { ConcreteEntity } from "../ConcreteEntity"; import type { Attribute } from "../../attribute/Attribute"; import { RelationshipAdapter } from "../../relationship/model-adapters/RelationshipAdapter"; import type { Annotations } from "../../annotation/Annotation"; import { ConcreteEntityOperations } from "./ConcreteEntityOperations"; +import type { InterfaceEntityAdapter } from "./InterfaceEntityAdapter"; +import type { UnionEntityAdapter } from "./UnionEntityAdapter"; export class ConcreteEntityAdapter { public readonly name: string; @@ -40,7 +41,7 @@ export class ConcreteEntityAdapter { private uniqueFieldsKeys: string[] = []; private constrainableFieldsKeys: string[] = []; - private _relatedEntities: Entity[] | undefined; + private _relatedEntities: (ConcreteEntityAdapter | InterfaceEntityAdapter | UnionEntityAdapter)[] | undefined; private _singular: string | undefined; private _plural: string | undefined; @@ -75,10 +76,7 @@ export class ConcreteEntityAdapter { private initRelationships(relationships: Map) { for (const [relationshipName, relationship] of relationships.entries()) { - this.relationships.set( - relationshipName, - new RelationshipAdapter(relationship, this) - ); + this.relationships.set(relationshipName, new RelationshipAdapter(relationship, this)); } } @@ -94,7 +92,7 @@ export class ConcreteEntityAdapter { return this.constrainableFieldsKeys.map((key) => getFromMap(this.attributes, key)); } - public get relatedEntities(): Entity[] { + public get relatedEntities(): (ConcreteEntityAdapter | InterfaceEntityAdapter | UnionEntityAdapter)[] { if (!this._relatedEntities) { this._relatedEntities = [...this.relationships.values()].map((relationship) => relationship.target); } @@ -107,7 +105,7 @@ export class ConcreteEntityAdapter { } public getMainLabel(): string { - return this.getLabels()[0] as string; + return this.getLabels()[0] as string; } public get singular(): string { diff --git a/packages/graphql/src/schema-model/generate-model.ts b/packages/graphql/src/schema-model/generate-model.ts index 3dbe59a25c..e6b863ffb1 100644 --- a/packages/graphql/src/schema-model/generate-model.ts +++ b/packages/graphql/src/schema-model/generate-model.ts @@ -30,7 +30,6 @@ import { Neo4jGraphQLSchemaModel } from "./Neo4jGraphQLSchemaModel"; import type { Operations } from "./Neo4jGraphQLSchemaModel"; import type { Annotation } from "./annotation/Annotation"; import type { Attribute } from "./attribute/Attribute"; -import type { CompositeEntity } from "./entity/CompositeEntity"; import { ConcreteEntity } from "./entity/ConcreteEntity"; import { findDirective } from "./parser/utils"; import { parseArguments } from "./parser/parse-arguments"; @@ -256,7 +255,7 @@ function generateCompositeEntity( entityDefinitionName: string, entityImplementingTypeNames: string[], concreteEntities: Map -): CompositeEntity { +): { name: string; concreteEntities: ConcreteEntity[] } { const compositeFields = entityImplementingTypeNames.map((type) => { const concreteEntity = concreteEntities.get(type); if (!concreteEntity) { diff --git a/packages/graphql/src/schema-model/relationship/model-adapters/RelationshipAdapter.ts b/packages/graphql/src/schema-model/relationship/model-adapters/RelationshipAdapter.ts index 343a4e77e5..5fc2877e6e 100644 --- a/packages/graphql/src/schema-model/relationship/model-adapters/RelationshipAdapter.ts +++ b/packages/graphql/src/schema-model/relationship/model-adapters/RelationshipAdapter.ts @@ -35,7 +35,7 @@ export class RelationshipAdapter { public readonly attributes: Map = new Map(); public readonly source: ConcreteEntityAdapter | InterfaceEntityAdapter | UnionEntityAdapter; private rawEntity: Entity; - private _target: Entity | undefined; + private _target: ConcreteEntityAdapter | InterfaceEntityAdapter | UnionEntityAdapter | undefined; public readonly direction: RelationshipDirection; public readonly queryDirection: QueryDirection; public readonly nestedOperations: NestedOperation[]; @@ -145,7 +145,7 @@ export class RelationshipAdapter { } // construct the target entity only when requested - get target(): Entity { + get target(): ConcreteEntityAdapter | InterfaceEntityAdapter | UnionEntityAdapter { if (!this._target) { if (this.rawEntity instanceof ConcreteEntity) { this._target = new ConcreteEntityAdapter(this.rawEntity); diff --git a/packages/graphql/src/schema/resolvers/subscriptions/authentication/selection-set-parser.ts b/packages/graphql/src/schema/resolvers/subscriptions/authentication/selection-set-parser.ts index aad93a2d58..9ff529c98a 100644 --- a/packages/graphql/src/schema/resolvers/subscriptions/authentication/selection-set-parser.ts +++ b/packages/graphql/src/schema/resolvers/subscriptions/authentication/selection-set-parser.ts @@ -109,10 +109,10 @@ function getTargetEntities({ if (!relationshipTarget) { return; } - if (context.schemaModel.isConcreteEntity(relationshipTarget)) { + if (relationshipTarget.isConcreteEntity()) { return [relationshipTarget]; } - if (context.schemaModel.isCompositeEntity(relationshipTarget)) { + if (relationshipTarget.isCompositeEntity()) { return relationshipTarget.concreteEntities; } } diff --git a/packages/graphql/src/translate/projection/subquery/translate-cypher-directive-projection.ts b/packages/graphql/src/translate/projection/subquery/translate-cypher-directive-projection.ts index 80016b56d3..4023a9efbf 100644 --- a/packages/graphql/src/translate/projection/subquery/translate-cypher-directive-projection.ts +++ b/packages/graphql/src/translate/projection/subquery/translate-cypher-directive-projection.ts @@ -92,7 +92,7 @@ export function translateCypherDirectiveProjection({ res.params = { ...res.params, ...p }; subqueries.push(...nestedSubqueriesBeforeSort, ...nestedSubqueries); predicates.push(...nestedPredicates); - } else if (context.schemaModel.isCompositeEntity(entity)) { + } else if (entity?.isCompositeEntity()) { const unionProjections: Array<{ predicate: Cypher.Predicate; projection: Cypher.Expr }> = []; const labelsSubPredicates: Cypher.Predicate[] = []; diff --git a/packages/graphql/src/translate/translate-top-level-cypher.ts b/packages/graphql/src/translate/translate-top-level-cypher.ts index da63963023..b6adbcfe36 100644 --- a/packages/graphql/src/translate/translate-top-level-cypher.ts +++ b/packages/graphql/src/translate/translate-top-level-cypher.ts @@ -104,7 +104,7 @@ export function translateTopLevelCypher({ const entity = context.schemaModel.entities.get(field.typeMeta.name); - if (context.schemaModel.isCompositeEntity(entity)) { + if (entity?.isCompositeEntity()) { const headStrs: Cypher.Clause[] = []; const referencedNodes = entity.concreteEntities @@ -227,7 +227,7 @@ export function translateTopLevelCypher({ if (field.isScalar || field.isEnum) { cypherStrs.push(`RETURN this`); - } else if (context.schemaModel.isCompositeEntity(entity)) { + } else if (entity?.isCompositeEntity()) { cypherStrs.push(`RETURN head( ${projectionStr.getCypher(env)} ) AS this`); } else { cypherStrs.push(`RETURN this ${projectionStr.getCypher(env)} AS this`); From dc4de7145bce1cc928cbb462efc7703198c1bf41 Mon Sep 17 00:00:00 2001 From: a-alle Date: Fri, 1 Sep 2023 17:03:02 +0100 Subject: [PATCH 7/9] inherit annotations on interfaces implementing interfaces --- packages/graphql/src/schema-model/generate-model.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/graphql/src/schema-model/generate-model.ts b/packages/graphql/src/schema-model/generate-model.ts index e6b863ffb1..a3663360a8 100644 --- a/packages/graphql/src/schema-model/generate-model.ts +++ b/packages/graphql/src/schema-model/generate-model.ts @@ -241,8 +241,13 @@ function generateInterfaceEntity( return parseAttribute(fieldDefinition, inheritedField, definitionCollection); }); - // TODO: inherited annotations? - const annotations = createEntityAnnotations(definition.directives || []); + const inheritedDirectives = + definition.interfaces?.flatMap((interfaceNamedNode) => { + const interfaceName = interfaceNamedNode.name.value; + return definitionCollection.interfaceTypes.get(interfaceName)?.directives || []; + }) || []; + const mergedDirectives = (definition.directives || []).concat(inheritedDirectives); + const annotations = createEntityAnnotations(mergedDirectives); return new InterfaceEntity({ ...interfaceEntity, @@ -319,7 +324,6 @@ function hydrateRelationships( } for (const relationship of relationshipFieldsMap.values()) { - // entityWithRelationships.attributeToRelationship(relationship); entityWithRelationships.addRelationship(relationship); } } @@ -426,6 +430,8 @@ function getLabels(entityDefinition: ObjectTypeDefinitionNode): string[] { function createEntityAnnotations(directives: readonly DirectiveNode[]): Annotation[] { const entityAnnotations: Annotation[] = []; + // TODO: I think this is done already with the map change and we do not have repeatable directives + // We only ever want to create one annotation even when an entity contains several key directives const keyDirectives = directives.filter((directive) => directive.name.value === "key"); if (keyDirectives) { From c225ffc1c399da8b2ec817752722ea238e0a73cd Mon Sep 17 00:00:00 2001 From: a-alle Date: Mon, 4 Sep 2023 17:27:47 +0100 Subject: [PATCH 8/9] adds tests for inheritance --- .../src/schema-model/generate-model.test.ts | 164 ++++++++++++++++-- .../src/schema-model/generate-model.ts | 73 -------- 2 files changed, 150 insertions(+), 87 deletions(-) diff --git a/packages/graphql/src/schema-model/generate-model.test.ts b/packages/graphql/src/schema-model/generate-model.test.ts index f4e16829cf..5ff8262396 100644 --- a/packages/graphql/src/schema-model/generate-model.test.ts +++ b/packages/graphql/src/schema-model/generate-model.test.ts @@ -323,6 +323,18 @@ describe("Relationship", () => { favoriteShow: [Show!]! @relationship(type: "FAVORITE_SHOW", direction: OUT) } + interface Person { + favoriteActors: [Actor!]! @relationship(type: "FAVORITE_ACTOR", direction: OUT) + } + + interface Human implements Person { + favoriteActors: [Actor!]! @relationship(type: "LIKES", direction: OUT) + } + + interface Worker implements Person { + favoriteActors: [Actor!]! + } + interface hasAccount @relationshipProperties { creationTime: DateTime! } @@ -353,8 +365,9 @@ describe("Relationship", () => { username: String! } - extend type User { + extend type User implements Worker & Human & Person { password: String! @authorization(filter: [{ where: { node: { id: { equals: "$jwt.sub" } } } }]) + favoriteActors: [Actor!]! } `; @@ -434,6 +447,60 @@ describe("Relationship", () => { ]); expect(actors?.target.name).toBe("Actor"); }); + + test("composite entity has overwritten the inherited relationship", () => { + const humanEntity = schemaModel.compositeEntities.find((e) => e.name === "Human") as InterfaceEntity; + const actors = humanEntity?.relationships.get("favoriteActors"); + expect(actors).toBeDefined(); + expect(actors?.type).toBe("LIKES"); + expect(actors?.direction).toBe("OUT"); + expect(actors?.queryDirection).toBe("DEFAULT_DIRECTED"); + expect(actors?.nestedOperations).toEqual([ + "CREATE", + "UPDATE", + "DELETE", + "CONNECT", + "DISCONNECT", + "CONNECT_OR_CREATE", + ]); + expect(actors?.target.name).toBe("Actor"); + }); + + test("composite entity has inherited relationship", () => { + const workerEntity = schemaModel.compositeEntities.find((e) => e.name === "Worker") as InterfaceEntity; + const actors = workerEntity?.relationships.get("favoriteActors"); + expect(actors).toBeDefined(); + expect(actors?.type).toBe("FAVORITE_ACTOR"); + expect(actors?.direction).toBe("OUT"); + expect(actors?.queryDirection).toBe("DEFAULT_DIRECTED"); + expect(actors?.nestedOperations).toEqual([ + "CREATE", + "UPDATE", + "DELETE", + "CONNECT", + "DISCONNECT", + "CONNECT_OR_CREATE", + ]); + expect(actors?.target.name).toBe("Actor"); + }); + + test("concrete entity has inherited relationship of first implemented interface with defined relationship", () => { + const userEntity = schemaModel.concreteEntities.find((e) => e.name === "User"); + const actors = userEntity?.relationships.get("favoriteActors"); + expect(actors).toBeDefined(); + expect(actors?.type).toBe("LIKES"); + expect(actors?.direction).toBe("OUT"); + expect(actors?.queryDirection).toBe("DEFAULT_DIRECTED"); + expect(actors?.nestedOperations).toEqual([ + "CREATE", + "UPDATE", + "DELETE", + "CONNECT", + "DISCONNECT", + "CONNECT_OR_CREATE", + ]); + expect(actors?.target.name).toBe("Actor"); + }); }); describe("ConcreteEntity Annotations & Attributes", () => { @@ -522,13 +589,8 @@ describe("ConcreteEntity Annotations & Attributes", () => { }); }); -describe("ComposeEntity Annotations & Attributes", () => { - let schemaModel: Neo4jGraphQLSchemaModel; - let movieEntity: ConcreteEntity; - let showEntity: ConcreteEntity; - let productionEntity: InterfaceEntity; - - beforeAll(() => { +describe("ComposeEntity Annotations & Attributes and Inheritance", () => { + test("concrete entity inherits from composite entity", () => { const typeDefs = gql` interface Production { year: Int @populatedBy(callback: "thisCallback", operations: [CREATE]) @@ -556,13 +618,11 @@ describe("ComposeEntity Annotations & Attributes", () => { `; const document = mergeTypeDefs(typeDefs); - schemaModel = generateModel(document); - movieEntity = schemaModel.concreteEntities.find((e) => e.name === "Movie") as ConcreteEntity; - showEntity = schemaModel.concreteEntities.find((e) => e.name === "TvShow") as ConcreteEntity; - productionEntity = schemaModel.compositeEntities.find((e) => e.name === "Production") as InterfaceEntity; - }); + const schemaModel = generateModel(document); + const movieEntity = schemaModel.concreteEntities.find((e) => e.name === "Movie") as ConcreteEntity; + const showEntity = schemaModel.concreteEntities.find((e) => e.name === "TvShow") as ConcreteEntity; + const productionEntity = schemaModel.compositeEntities.find((e) => e.name === "Production") as InterfaceEntity; - test("attributes should be generated with the correct annotations", () => { const productionYear = productionEntity?.attributes.get("year"); expect(productionYear?.annotations[AnnotationsKey.populatedBy]).toBeDefined(); expect(productionYear?.annotations[AnnotationsKey.populatedBy]?.callback).toBe("thisCallback"); @@ -603,6 +663,82 @@ describe("ComposeEntity Annotations & Attributes", () => { expect(showAliasedProp?.databaseName).toBeDefined(); expect(showAliasedProp?.databaseName).toBe("dbName"); }); + + test("composite entity inherits from composite entity", () => { + const typeDefs = gql` + interface Production { + year: Int @populatedBy(callback: "thisCallback", operations: [CREATE]) + defaultName: String! @default(value: "AwesomeProduction") + aliasedProp: String! @alias(property: "dbName") + } + + interface TvProduction implements Production { + name: String! + year: Int + defaultName: String! + aliasedProp: String! @alias(property: "movieDbName") + } + + type TvShow implements TvProduction & Production { + name: String! + episodes: Int + year: Int @populatedBy(callback: "thisOtherCallback", operations: [CREATE]) + aliasedProp: String! + } + + extend type TvShow { + defaultName: String! @default(value: "AwesomeShow") + } + `; + + const document = mergeTypeDefs(typeDefs); + const schemaModel = generateModel(document); + const tvProductionEntity = schemaModel.compositeEntities.find( + (e) => e.name === "TvProduction" + ) as InterfaceEntity; + const showEntity = schemaModel.concreteEntities.find((e) => e.name === "TvShow") as ConcreteEntity; + const productionEntity = schemaModel.compositeEntities.find((e) => e.name === "Production") as InterfaceEntity; + + const productionYear = productionEntity?.attributes.get("year"); + expect(productionYear?.annotations[AnnotationsKey.populatedBy]).toBeDefined(); + expect(productionYear?.annotations[AnnotationsKey.populatedBy]?.callback).toBe("thisCallback"); + expect(productionYear?.annotations[AnnotationsKey.populatedBy]?.operations).toStrictEqual(["CREATE"]); + + const tvProductionYear = tvProductionEntity?.attributes.get("year"); + expect(tvProductionYear?.annotations[AnnotationsKey.populatedBy]).toBeDefined(); + expect(tvProductionYear?.annotations[AnnotationsKey.populatedBy]?.callback).toBe("thisCallback"); + expect(tvProductionYear?.annotations[AnnotationsKey.populatedBy]?.operations).toStrictEqual(["CREATE"]); + + const showYear = showEntity?.attributes.get("year"); + expect(showYear?.annotations[AnnotationsKey.populatedBy]).toBeDefined(); + expect(showYear?.annotations[AnnotationsKey.populatedBy]?.callback).toBe("thisOtherCallback"); + expect(showYear?.annotations[AnnotationsKey.populatedBy]?.operations).toStrictEqual(["CREATE"]); + + const productionDefaultName = productionEntity?.attributes.get("defaultName"); + expect(productionDefaultName).toBeDefined(); + expect(productionDefaultName?.annotations[AnnotationsKey.default]).toBeDefined(); + expect(productionDefaultName?.annotations[AnnotationsKey.default]?.value).toBe("AwesomeProduction"); + + const tvProductionDefaultName = tvProductionEntity?.attributes.get("defaultName"); + expect(tvProductionDefaultName).toBeDefined(); + expect(tvProductionDefaultName?.annotations[AnnotationsKey.default]).toBeDefined(); + expect(tvProductionDefaultName?.annotations[AnnotationsKey.default]?.value).toBe("AwesomeProduction"); + + const showDefaultName = showEntity?.attributes.get("defaultName"); + expect(showDefaultName).toBeDefined(); + expect(showDefaultName?.annotations[AnnotationsKey.default]).toBeDefined(); + expect(showDefaultName?.annotations[AnnotationsKey.default]?.value).toBe("AwesomeShow"); + + const productionAliasedProp = productionEntity?.attributes.get("aliasedProp"); + const tvProductionAliasedProp = tvProductionEntity?.attributes.get("aliasedProp"); + const showAliasedProp = showEntity?.attributes.get("aliasedProp"); + expect(productionAliasedProp?.databaseName).toBeDefined(); + expect(productionAliasedProp?.databaseName).toBe("dbName"); + expect(tvProductionAliasedProp?.databaseName).toBeDefined(); + expect(tvProductionAliasedProp?.databaseName).toBe("movieDbName"); + expect(showAliasedProp?.databaseName).toBeDefined(); + expect(showAliasedProp?.databaseName).toBe("movieDbName"); // first one listed in the implements list decides + }); }); describe("GraphQL adapters", () => { diff --git a/packages/graphql/src/schema-model/generate-model.ts b/packages/graphql/src/schema-model/generate-model.ts index a3663360a8..ff0bb860c7 100644 --- a/packages/graphql/src/schema-model/generate-model.ts +++ b/packages/graphql/src/schema-model/generate-model.ts @@ -45,76 +45,6 @@ import { parseAnnotations } from "./parser/parse-annotation"; import { InterfaceEntity } from "./entity/InterfaceEntity"; import { UnionEntity } from "./entity/UnionEntity"; -/* -export function generateModel2(document: DocumentNode): Neo4jGraphQLSchemaModel { - const definitionCollection: DefinitionCollection = getDefinitionCollection(document); - - const operations: Operations = definitionCollection.operations.reduce((acc, definition): Operations => { - acc[definition.name.value] = generateOperation(definition); - return acc; - }, {}); - - // hydrate interface to typeNames map - hydrateInterfacesToTypeNamesMap(definitionCollection); - - // === 1. add composites w/o concreteEntities connection - const unionEntities = Array.from(definitionCollection.unionTypes).map(([unionName, unionDefinition]) => { - return generateUnionEntity( - unionName, - unionDefinition.types?.map((t) => t.name.value) || [], - concreteEntitiesMap - ); - }); - const interfaceEntities = Array.from(definitionCollection.interfaceToImplementingTypeNamesMap.entries()).map( - ([name, concreteEntities]) => { - const interfaceNode = definitionCollection.interfaceTypes.get(name); - if (!interfaceNode) { - throw new Error(`Cannot find interface ${name}`); - } - return generateInterfaceEntity( - name, - interfaceNode, - concreteEntities, - concreteEntitiesMap, - definitionCollection - ); - } - ); - - // === 2. add concretes with attributes, annotations and relationships already resolved (with inherited) - const concreteEntities = Array.from(definitionCollection.nodes.values()).map((node) => - generateConcreteEntity(node, definitionCollection) - ); - - // === 3. update composites with links to concrete - // TODO - - // TODO: still need this? - const concreteEntitiesMap = concreteEntities.reduce((acc, entity) => { - if (acc.has(entity.name)) { - throw new Neo4jGraphQLSchemaValidationError(`Duplicate node ${entity.name}`); - } - acc.set(entity.name, entity); - return acc; - }, new Map()); - - // === 4. create schema model with everything - const annotations = createSchemaModelAnnotations(definitionCollection.schemaDirectives); - const schema = new Neo4jGraphQLSchemaModel({ - compositeEntities: [...unionEntities, ...interfaceEntities], - concreteEntities, - operations, - annotations, - }); - - definitionCollection.nodes.forEach((def) => hydrateRelationships(def, schema, definitionCollection)); - definitionCollection.interfaceTypes.forEach((def) => hydrateRelationships(def, schema, definitionCollection)); - - return schema; -} -*/ -// =============================================================== - export function generateModel(document: DocumentNode): Neo4jGraphQLSchemaModel { const definitionCollection: DefinitionCollection = getDefinitionCollection(document); @@ -171,9 +101,6 @@ export function generateModel(document: DocumentNode): Neo4jGraphQLSchemaModel { }); definitionCollection.nodes.forEach((def) => hydrateRelationships(def, schema, definitionCollection)); definitionCollection.interfaceTypes.forEach((def) => hydrateRelationships(def, schema, definitionCollection)); - // TODO: test - interface implements interface inheritance hydrate - // TODO: refactor flow?? - // TODO: add tests for interfaces and relationshipProperties interface annotations return schema; } From d456b8481493186933ab21a13f464434a3db0228 Mon Sep 17 00:00:00 2001 From: a-alle Date: Tue, 5 Sep 2023 14:24:48 +0100 Subject: [PATCH 9/9] fix subscriptions authentication tests --- .../authorization/authentication.int.test.ts | 46 ++++--------------- 1 file changed, 8 insertions(+), 38 deletions(-) diff --git a/packages/graphql/tests/e2e/subscriptions/authorization/authentication.int.test.ts b/packages/graphql/tests/e2e/subscriptions/authorization/authentication.int.test.ts index 490497bb37..7290c621dd 100644 --- a/packages/graphql/tests/e2e/subscriptions/authorization/authentication.int.test.ts +++ b/packages/graphql/tests/e2e/subscriptions/authorization/authentication.int.test.ts @@ -3598,7 +3598,7 @@ describe("Subscription authentication", () => { expect(wsClient.events).toEqual([]); expect(wsClient.errors).toEqual([expect.objectContaining({ message: "Unauthenticated" })]); }); - test("unauthenticated subscription sends events if interface field queried on unauthenticated implementing type - create_relationship", async () => { + test("unauthenticated subscription does not send events if interface field queried on unauthenticated implementing type - create_relationship", async () => { wsClient = new WebSocketTestClient(server.wsPath); await wsClient.subscribe(` subscription SubscriptionMovie { @@ -3658,25 +3658,10 @@ describe("Subscription authentication", () => { await wsClient.waitForEvents(1); expect(result.body.errors).toBeUndefined(); - expect(wsClient.events).toIncludeSameMembers([ - { - [typeMovie.operations.subscribe.relationship_created]: { - event: "CREATE_RELATIONSHIP", - relationshipFieldName: "reviewers", - createdRelationship: { - reviewers: { - score: 10, - node: { - name: "Bob", - reputation: 10, - }, - }, - }, - }, - }, - ]); - expect(wsClient.errors).toEqual([]); + expect(wsClient.events).toEqual([]); + expect(wsClient.errors).toEqual([expect.objectContaining({ message: "Unauthenticated" })]); }); + test("unauthenticated subscription sends events if no authenticated field queried - create_relationship", async () => { wsClient = new WebSocketTestClient(server.wsPath); await wsClient.subscribe(` @@ -4180,7 +4165,7 @@ describe("Subscription authentication", () => { expect(wsClient.events).toEqual([]); expect(wsClient.errors).toEqual([expect.objectContaining({ message: "Unauthenticated" })]); }); - test("unauthenticated subscription sends events if interface field queried on unauthenticated implementing type - delete_relationship", async () => { + test("unauthenticated subscription does not send events if interface field queried on unauthenticated implementing type - delete_relationship", async () => { await supertest(server.path) .post("") .send({ @@ -4269,24 +4254,9 @@ describe("Subscription authentication", () => { expect(result.body.errors).toBeUndefined(); await wsClient.waitForEvents(1); - expect(wsClient.events).toIncludeSameMembers([ - { - [typeMovie.operations.subscribe.relationship_deleted]: { - event: "DELETE_RELATIONSHIP", - relationshipFieldName: "reviewers", - deletedRelationship: { - reviewers: { - score: 10, - node: { - name: "Bob", - reputation: 10, - }, - }, - }, - }, - }, - ]); - expect(wsClient.errors).toEqual([]); + expect(result.body.errors).toBeUndefined(); + expect(wsClient.events).toEqual([]); + expect(wsClient.errors).toEqual([expect.objectContaining({ message: "Unauthenticated" })]); }); test("unauthenticated subscription sends events if no authenticated field queried - delete_relationship", async () => { await supertest(server.path)