Skip to content

Commit

Permalink
add domain object and persistence
Browse files Browse the repository at this point in the history
  • Loading branch information
uidp committed Jan 14, 2024
1 parent 3bf7056 commit fb8bca5
Show file tree
Hide file tree
Showing 9 changed files with 615 additions and 0 deletions.
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 { INITIAL_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 initial path', () => {
const { boardNode } = setup();
expect(boardNode.path).toBe(INITIAL_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(INITIAL_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(INITIAL_PATH);
});
});
});
86 changes: 86 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,86 @@
import { DomainObject } from '@shared/domain/domain-object';
import { EntityId } from '@shared/domain/types';
import { INITIAL_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 {
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 = INITIAL_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 { INITIAL_PATH } from './path-utils';

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

0 comments on commit fb8bca5

Please sign in to comment.