Skip to content

Commit

Permalink
Merge pull request neo4j#3889 from mjfwebb/schema-model-abstract-types-2
Browse files Browse the repository at this point in the history
Implement interfaces and unions
  • Loading branch information
a-alle authored Sep 5, 2023
2 parents 6d43410 + b1ef39b commit 5818d76
Show file tree
Hide file tree
Showing 21 changed files with 819 additions and 180 deletions.
2 changes: 1 addition & 1 deletion packages/graphql/src/classes/Subgraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 2 additions & 10 deletions packages/graphql/src/schema-model/Neo4jGraphQLSchemaModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ 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 { ConcreteEntity } from "./entity/ConcreteEntity";
import type { CompositeEntity } from "./entity/CompositeEntity";
import type { ConcreteEntity } from "./entity/ConcreteEntity";
import type { Entity } from "./entity/Entity";
import { ConcreteEntityAdapter } from "./entity/model-adapters/ConcreteEntityAdapter";

Expand Down Expand Up @@ -81,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 CompositeEntity;
}

private addAnnotation(annotation: Annotation): void {
const annotationKey = annotationToKey(annotation);
const existingAnnotation = this.annotations[annotationKey];
Expand Down
5 changes: 3 additions & 2 deletions packages/graphql/src/schema-model/attribute/AttributeType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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<AttributeType, ListType>;
Expand Down
15 changes: 4 additions & 11 deletions packages/graphql/src/schema-model/entity/CompositeEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,8 @@
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 extends Entity {
readonly name: string;
concreteEntities: ConcreteEntity[];
}
7 changes: 7 additions & 0 deletions packages/graphql/src/schema-model/entity/ConcreteEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions packages/graphql/src/schema-model/entity/Entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
106 changes: 106 additions & 0 deletions packages/graphql/src/schema-model/entity/InterfaceEntity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* 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<string, Attribute> = new Map();
public readonly relationships: Map<string, Relationship> = new Map();
public readonly annotations: Partial<Annotations> = {};

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);
}
}

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}`);
}
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;
}

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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,21 @@
* 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;
}
isConcreteEntity(): this is ConcreteEntity {
return false;
}
isCompositeEntity(): this is CompositeEntity {
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -75,10 +76,7 @@ export class ConcreteEntityAdapter {

private initRelationships(relationships: Map<string, Relationship>) {
for (const [relationshipName, relationship] of relationships.entries()) {
this.relationships.set(
relationshipName,
new RelationshipAdapter(relationship, this)
);
this.relationships.set(relationshipName, new RelationshipAdapter(relationship, this));
}
}

Expand All @@ -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);
}
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string, AttributeAdapter> = new Map();
public readonly relationships: Map<string, RelationshipAdapter> = new Map();
public readonly annotations: Partial<Annotations>;

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<string, Attribute>) {
for (const [attributeName, attribute] of attributes.entries()) {
const attributeAdapter = new AttributeAdapter(attribute);
this.attributes.set(attributeName, attributeAdapter);
}
}

private initRelationships(relationships: Map<string, Relationship>) {
for (const [relationshipName, relationship] of relationships.entries()) {
this.relationships.set(relationshipName, new RelationshipAdapter(relationship, this));
}
}
}
Loading

0 comments on commit 5818d76

Please sign in to comment.