Skip to content

Commit

Permalink
Merge pull request #1975 from DISSINET/1288-check-circularity-in-asym…
Browse files Browse the repository at this point in the history
…etric-relations

1288 check circularity in asymetric relations
  • Loading branch information
jancimertel authored Mar 16, 2024
2 parents 053fe30 + b96fb06 commit 53555d8
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 1 deletion.
56 changes: 56 additions & 0 deletions packages/server/src/models/relation/path.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import "ts-jest";
import Superclass from "./superclass";
import Path from "./path";
import { RelationEnums } from "@shared/enums";
import { IRelationModel } from "./relation";
import Synonym from "./synonym";

describe("test Path for superclasses", () => {
let pathInstance = new Path(RelationEnums.Type.Superclass);
let entries: IRelationModel[] = [];

beforeAll(async () => {
const ab = new Superclass({ entityIds: ["A", "B"] })
const bc = new Superclass({ entityIds: ["B", "C"] });
const cd = new Superclass({ entityIds: ["C", "D"] });
entries = [ab, bc, cd];
})

test("test Path.build", async () => {
pathInstance.build(entries);
expect(Object.keys(pathInstance.trees)).toHaveLength(entries.length);
})

test("test existing path from A->D", () => {
expect(pathInstance.pathExists("A", "D")).toBeTruthy();
})

test("test existing path D -> A", () => {
expect(pathInstance.pathExists("D", "A")).toBeFalsy();
})
});

describe("test Path for synonyms & superclass", () => {
let pathInstance = new Path(RelationEnums.Type.Synonym);
let entries: IRelationModel[] = [];

beforeAll(async () => {
const ab = new Synonym({ entityIds: ["A", "B"] })
const bc = new Synonym({ entityIds: ["B", "C"] });
const cd = new Superclass({ entityIds: ["C", "D"] });
entries = [ab, bc, cd];
})

test("test Path.build", async () => {
pathInstance.build(entries);
expect(Object.keys(pathInstance.trees)).toHaveLength(2); // only two synonyms
})

test("test existing path from A->D", () => {
expect(pathInstance.pathExists("A", "D")).toBeFalsy();
})

test("test existing path D -> A", () => {
expect(pathInstance.pathExists("D", "A")).toBeFalsy();
})
});
59 changes: 59 additions & 0 deletions packages/server/src/models/relation/path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { RelationEnums } from "@shared/enums";
import { IRelationModel } from "./relation";

interface PathTree {
mainId: string;
ids: string[];
}

export default class Path {
type: RelationEnums.Type;
trees: Record<string, PathTree>

constructor(type: RelationEnums.Type) {
this.type = type;
this.trees = {};
}


async build(entries: IRelationModel[]) {
this.trees = {};
for (const entry of entries.filter(e => e.type === this.type)) {
this.trees[entry.entityIds[0]] = {
mainId: entry.entityIds[0],
ids: entry.entityIds,
}
}
}

/**
* Tests if there exists path from entity A -> B through relations
* @param a
* @param b
*/
pathExists(a: string, b: string): boolean {
if (!this.trees[a]) {
return false;
}

let start = [this.trees[a]]
while (start.length) {
const nextStart = [];
for (const subtree of start) {
for (const entityId of subtree.ids) {
if (entityId === b) {
return true;
}

// entry from 'entityId' -> X must exists to be added to nextStat
if (entityId !== subtree.mainId && this.trees[entityId]) {
nextStart.push(this.trees[entityId]);
}
}
}
start = nextStart;
}

return false;
}
}
21 changes: 20 additions & 1 deletion packages/server/src/models/relation/relation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import { r as rethink, Connection, WriteResult } from "rethinkdb-ts";
import { IEntity, Relation as RelationTypes } from "@shared/types";
import { DbEnums, EntityEnums, RelationEnums, UserEnums } from "@shared/enums";
import { EnumValidators } from "@shared/enums";
import { InternalServerError, ModelNotValidError } from "@shared/types/errors";
import { InternalServerError, ModelNotValidError, RelationAsymetricalPathExist } from "@shared/types/errors";
import User from "@models/user/user";
import { IRequest } from "../../custom_typings/request";
import { nonenumerable } from "@common/decorators";
import Entity from "@models/entity/entity";
import Path from "./path";

export interface IRelationModel extends RelationTypes.IRelation, IDbModel {
beforeSave(request: IRequest): Promise<void>;
Expand Down Expand Up @@ -178,6 +179,15 @@ export default class Relation implements IRelationModel {
* @param request
*/
async beforeSave(request: IRequest): Promise<void> {
if (RelationTypes.RelationRules[this.type]?.asymmetrical) {
const pathHelper = new Path(this.type);
await pathHelper.build(await Relation.getByType(request.db.connection, this.type))

if (pathHelper.pathExists(this.entityIds[1], this.entityIds[0])) {
throw new RelationAsymetricalPathExist();
}
}

if (!this.entities || this.entities.length !== this.entityIds.length) {
this.entities = await Entity.findEntitiesByIds(
request.db.connection,
Expand Down Expand Up @@ -403,6 +413,15 @@ export default class Relation implements IRelationModel {
return items;
}

static async getByType<T extends RelationTypes.IRelation>(db: Connection, relType: RelationEnums.Type): Promise<T[]> {
const items: T[] = await rethink
.table(Relation.table)
.filter({ type: relType })
.run(db)

return items;
}

/**
* searches for relations with specific entity id and returns both relation ids and connected entity ids
* @param request IRequest
Expand Down
18 changes: 18 additions & 0 deletions packages/shared/types/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,22 @@ class RelationDoesNotExist extends CustomError {
}
}


/**
* RelationAsymetricalPathExist will be thrown when attempting to add asymetrical relation while there could already be path from A -> B
*/
class RelationAsymetricalPathExist extends CustomError {
public static code = 400;
public static title = "Asymetrical constraint check failed";
public static message = "Relation cannot be created";

static forId(id: string): RelationAsymetricalPathExist {
return new RelationAsymetricalPathExist(
RelationAsymetricalPathExist.message.replace("$1", id)
);
}
}

/**
* DocumentDoesNotExist will be thrown when attempting to retrieve document by id, which does not exist
*/
Expand Down Expand Up @@ -461,6 +477,7 @@ const allErrors: Record<string, any> = {
StatementInvalidMove,
EmailError,
RelationDoesNotExist,
RelationAsymetricalPathExist,
DocumentDoesNotExist,
NetworkError,
UnsafePasswordError,
Expand Down Expand Up @@ -509,6 +526,7 @@ export {
StatementInvalidMove,
EmailError,
RelationDoesNotExist,
RelationAsymetricalPathExist,
DocumentDoesNotExist,
NetworkError,
UnsafePasswordError,
Expand Down

0 comments on commit 53555d8

Please sign in to comment.