Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BC-6255 - POC: Board Object Composition using traits #4693

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 199 additions & 0 deletions apps/server/src/modules/board/poc/board-node.do.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import { boardNodeFactory } from './board-node.factory';
import { ROOT_PATH, joinPath } from './path-utils';

describe('BoardNode', () => {
describe('a new instance', () => {
const setup = () => {
const boardNode = boardNodeFactory.build();

return { boardNode };
};

it('should have no ancestors', () => {
const { boardNode } = setup();
expect(boardNode.ancestorIds).toHaveLength(0);
});

it('should have no parent', () => {
const { boardNode } = setup();
expect(boardNode.hasParent()).toBe(false);
expect(boardNode.parentId).not.toBeDefined();
});

it('should have level = 0', () => {
const { boardNode } = setup();
expect(boardNode.level).toBe(0);
});

it('should have root path', () => {
const { boardNode } = setup();
expect(boardNode.path).toBe(ROOT_PATH);
});
});

describe('when adding to a parent', () => {
const setup = () => {
const parent = boardNodeFactory.build();
const child = boardNodeFactory.build();

return { parent, child };
};

it('should update the ancestor list', () => {
const { parent, child } = setup();

child.addToParent(parent);

expect(child.ancestorIds).toEqual([parent.id]);
});

it('should update the children of the parent', () => {
const { parent, child } = setup();

child.addToParent(parent);

expect(parent.children).toEqual([child]);
});

it('should update the level of the child', () => {
const { parent, child } = setup();

child.addToParent(parent);

expect(child.level).toEqual(1);
});

it('should update the path', () => {
const { parent, child } = setup();

child.addToParent(parent);

expect(child.path).toEqual(joinPath(parent.path, parent.id));
});
});

describe('when adding a child', () => {
const setup = () => {
const parent = boardNodeFactory.build();
const child = boardNodeFactory.build();

return { parent, child };
};

it('should update the ancestor list', () => {
const { parent, child } = setup();

parent.addChild(child);

expect(child.ancestorIds).toEqual([parent.id]);
});

it('should update the children of the parent', () => {
const { parent, child } = setup();

parent.addChild(child);

expect(parent.children).toEqual([child]);
});

it('should update the level of the child', () => {
const { parent, child } = setup();

parent.addChild(child);

expect(child.level).toEqual(1);
});

it('should update the path of the child', () => {
const { parent, child } = setup();

parent.addChild(child);

expect(child.path).toEqual(joinPath(parent.path, parent.id));
});
});

describe('when removing from a parent', () => {
const setup = () => {
const parent = boardNodeFactory.build();
const child = boardNodeFactory.build();
child.addToParent(parent);

return { parent, child };
};

it('should update the ancestor list', () => {
const { parent, child } = setup();

child.removeFromParent(parent);

expect(child.ancestorIds).toEqual([]);
});

it('should update the children of the parent', () => {
const { parent, child } = setup();

child.removeFromParent(parent);

expect(parent.children).toEqual([]);
});

it('should update the level of the child', () => {
const { parent, child } = setup();

child.removeFromParent(parent);

expect(child.level).toEqual(0);
});

it('should update the path', () => {
const { parent, child } = setup();

child.removeFromParent(parent);

expect(child.path).toEqual(ROOT_PATH);
});
});

describe('when removing a child', () => {
const setup = () => {
const parent = boardNodeFactory.build();
const child = boardNodeFactory.build();
parent.addChild(child);

return { parent, child };
};

it('should update the ancestor list', () => {
const { parent, child } = setup();

parent.removeChild(child);

expect(child.ancestorIds).toEqual([]);
});

it('should update the children of the parent', () => {
const { parent, child } = setup();

parent.removeChild(child);

expect(parent.children).toEqual([]);
});

it('should update the level of the child', () => {
const { parent, child } = setup();

parent.removeChild(child);

expect(child.level).toEqual(0);
});

it('should update the path of the child', () => {
const { parent, child } = setup();

parent.removeChild(child);

expect(child.path).toEqual(ROOT_PATH);
});
});
});
87 changes: 87 additions & 0 deletions apps/server/src/modules/board/poc/board-node.do.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { DomainObject } from '@shared/domain/domain-object';
import { EntityId } from '@shared/domain/types';
import { ROOT_PATH, PATH_SEPARATOR, joinPath } from './path-utils';

export interface BoardNodeProps {
id: EntityId;
path: string;
level: number;
position: number;
// type!: BoardNodeType;
title?: string;
children: BoardNode[];
createdAt: Date;
updatedAt: Date;
}

export class BoardNode extends DomainObject<BoardNodeProps> {
get level(): number {
return this.ancestorIds.length;
}

get children(): readonly BoardNode[] {
// return this.props.children; // should we clone the array?
return [...this.props.children] as const;
}

get parentId(): EntityId | undefined {
const parentId = this.hasParent() ? this.ancestorIds[this.ancestorIds.length - 1] : undefined;
return parentId;
}

hasParent() {
return this.ancestorIds.length > 0;
}

get ancestorIds(): EntityId[] {
const parentIds = this.props.path.split(PATH_SEPARATOR).filter((id) => id !== '');
return parentIds;
}

get path(): string {
return this.props.path;
}

addToParent(parent: BoardNode, position?: { index1: string; index2: string }): void {
// TODO remove from potential previous parent (and its children)?
if (this.parentId !== parent.id) {
this.props.path = joinPath(parent.path, parent.id);
// TODO set index (position)
this.props.level = parent.level + 1;
}
if (!parent.children.includes(this)) {
parent.addChild(this, position);
}
}

removeFromParent(parent: BoardNode): void {
if (this.parentId === parent.id) {
this.props.path = ROOT_PATH;
// TODO set index (position)
this.props.level = 0;
}
if (parent.children.includes(this)) {
parent.removeChild(this);
}
}

addChild(child: BoardNode, position?: { index1: string; index2: string }): void {
if (!this.children.includes(child)) {
this.props.children.push(child);
// TODO sort children
}
if (child.parentId !== this.id) {
child.addToParent(this, position);
}
}

removeChild(child: BoardNode): void {
if (this.children.includes(child)) {
const index = this.children.indexOf(child);
this.props.children.splice(index, 1);
}
if (child.parentId === this.id) {
child.removeFromParent(this);
}
}
}
40 changes: 40 additions & 0 deletions apps/server/src/modules/board/poc/board-node.entity.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { MikroORM } from '@mikro-orm/mongodb';
import { BaseEntityWithTimestamps } from '@shared/domain/entity';
import { BoardNodeEntity } from './board-node.entity';

describe('entity', () => {
let orm: MikroORM;

beforeAll(async () => {
orm = await MikroORM.init({
entities: [BaseEntityWithTimestamps, BoardNodeEntity],
clientUrl: 'mongodb://localhost:27017/boardtest',
type: 'mongo',
validate: true,
allowGlobalContext: true,
});
});

beforeEach(async () => {
await orm.schema.clearDatabase();
});

afterAll(async () => {
await orm.schema.dropSchema();
await orm.close(true);
});

it('persists', async () => {
const entity = new BoardNodeEntity();

await orm.em.persistAndFlush(entity);
orm.em.clear();

expect(entity.id).toBeDefined();

const result = await orm.em.findOneOrFail(BoardNodeEntity, { id: entity.id });

expect(result).toBeDefined();
expect(result.id).toEqual(entity.id);
});
});
26 changes: 26 additions & 0 deletions apps/server/src/modules/board/poc/board-node.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { BaseEntityWithTimestamps } from '@shared/domain/entity';
import { Entity, Index, Property } from '@mikro-orm/core';
import { BoardNode, BoardNodeProps } from './board-node.do';

@Entity({ tableName: 'boardnodes' })
export class BoardNodeEntity extends BaseEntityWithTimestamps implements BoardNodeProps {
@Index()
@Property({ nullable: false })
path = ','; // TODO find better way to provide defaults!

@Property({ nullable: false })
level = 0;

@Property({ nullable: false })
position = 0;

// @Index()
// @Enum(() => BoardNodeType)
// type!: BoardNodeType;

@Property({ nullable: true })
title?: string;

@Property({ persist: false })
children: BoardNode[] = [];
}
18 changes: 18 additions & 0 deletions apps/server/src/modules/board/poc/board-node.factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/* istanbul ignore file */
import { BaseFactory } from '@shared/testing';
import { ObjectId } from 'bson';
import { BoardNode, BoardNodeProps } from './board-node.do';
import { ROOT_PATH } from './path-utils';

export const boardNodeFactory = BaseFactory.define<BoardNode, BoardNodeProps>(BoardNode, ({ sequence }) => {
return {
id: new ObjectId().toHexString(),
path: ROOT_PATH,
level: 0,
title: `board node #${sequence}`,
position: 0,
children: [],
createdAt: new Date(),
updatedAt: new Date(),
};
});
Loading
Loading