Skip to content

Commit ff06db0

Browse files
committed
feat(boards): implement board factory, repository, and controller for board management
1 parent 8fe4b93 commit ff06db0

14 files changed

Lines changed: 385 additions & 116 deletions

File tree

src/boards/application/boards.facade.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
GetBoardsQuery,
77
UpdateBoardUseCase,
88
} from './use-cases';
9+
import { CreateBoardDto, UpdateBoardDto } from './dtos';
10+
import type { BoardWithRelations } from '@core/boards/domain/entities';
911

1012
@Injectable()
1113
export class BoardsFacade {
@@ -17,23 +19,36 @@ export class BoardsFacade {
1719
private readonly getBoardsQ: GetBoardsQuery,
1820
) {}
1921

20-
public async create(projectId: string, userId: string, dto: any) {
22+
public async create(
23+
projectId: string,
24+
userId: string,
25+
dto: CreateBoardDto,
26+
): Promise<BoardWithRelations> {
2127
return this.createBoardUC.execute(projectId, userId, dto);
2228
}
2329

24-
public async update(id: string, projectId: string, userId: string, dto: any) {
30+
public async update(
31+
id: string,
32+
projectId: string,
33+
userId: string,
34+
dto: UpdateBoardDto,
35+
): Promise<BoardWithRelations | null> {
2536
return this.updateBoardUC.execute(id, projectId, userId, dto);
2637
}
2738

28-
public async delete(id: string, projectId: string, userId: string) {
39+
public async delete(id: string, projectId: string, userId: string): Promise<boolean> {
2940
return this.deleteBoardUC.execute(id, projectId, userId);
3041
}
3142

32-
public async getOne(id: string, projectId: string, userId: string) {
43+
public async getOne(
44+
id: string,
45+
projectId: string,
46+
userId: string,
47+
): Promise<BoardWithRelations | null> {
3348
return this.getBoardQ.execute(id, projectId, userId);
3449
}
3550

36-
public async getAll(projectId: string, userId: string) {
51+
public async getAll(projectId: string, userId: string): Promise<BoardWithRelations[]> {
3752
return this.getBoardsQ.execute(projectId, userId);
3853
}
3954
}
Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import { ApiBaseController, GetUserId } from '@shared/decorators';
22
import { BoardsFacade } from '@core/boards/application/boards.facade';
33
import { Body, Delete, Get, Param, Patch, Post } from '@nestjs/common';
4+
import { CreateBoardDto, UpdateBoardDto } from '@core/boards/application/dtos';
5+
import type { BoardWithRelations } from '@core/boards/domain/entities';
46

57
@ApiBaseController('projects/:projectId/boards', 'Boards', true)
68
export class BoardsController {
79
constructor(private readonly facade: BoardsFacade) {}
810

911
@Get()
10-
async findAll(@Param('projectId') projectId: string, @GetUserId() userId: string) {
12+
async findAll(
13+
@Param('projectId') projectId: string,
14+
@GetUserId() userId: string,
15+
): Promise<BoardWithRelations[]> {
1116
return this.facade.getAll(projectId, userId);
1217
}
1318

@@ -16,16 +21,16 @@ export class BoardsController {
1621
@Param('id') id: string,
1722
@Param('projectId') projectId: string,
1823
@GetUserId() userId: string,
19-
) {
24+
): Promise<BoardWithRelations | null> {
2025
return this.facade.getOne(id, projectId, userId);
2126
}
2227

2328
@Post()
2429
async create(
2530
@Param('projectId') projectId: string,
2631
@GetUserId() userId: string,
27-
@Body() dto: any,
28-
) {
32+
@Body() dto: CreateBoardDto,
33+
): Promise<BoardWithRelations> {
2934
return this.facade.create(projectId, userId, dto);
3035
}
3136

@@ -34,8 +39,8 @@ export class BoardsController {
3439
@Param('id') id: string,
3540
@Param('projectId') projectId: string,
3641
@GetUserId() userId: string,
37-
@Body() dto: any,
38-
) {
42+
@Body() dto: UpdateBoardDto,
43+
): Promise<BoardWithRelations | null> {
3944
return this.facade.update(id, projectId, userId, dto);
4045
}
4146

@@ -44,7 +49,7 @@ export class BoardsController {
4449
@Param('id') id: string,
4550
@Param('projectId') projectId: string,
4651
@GetUserId() userId: string,
47-
) {
52+
): Promise<boolean> {
4853
return this.facade.delete(id, projectId, userId);
4954
}
5055
}
Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,23 @@
1-
export class BoardsDto {}
1+
import { z } from 'zod/v4';
2+
import { createZodDto } from 'nestjs-zod';
3+
4+
export const CreateBoardSchema = z.object({
5+
name: z
6+
.string()
7+
.min(1, 'Название доски не может быть пустым')
8+
.max(100, 'Название доски не должно превышать 100 символов'),
9+
position: z.number().finite().optional(),
10+
settings: z.record(z.string(), z.unknown()).optional(),
11+
});
12+
13+
export class CreateBoardDto extends createZodDto(CreateBoardSchema) {}
14+
15+
export const UpdateBoardSchema = CreateBoardSchema.partial().refine(
16+
(data) => Object.keys(data).length > 0,
17+
{
18+
error: 'Необходимо передать хотя бы одно поле для обновления',
19+
abort: true,
20+
},
21+
);
22+
23+
export class UpdateBoardDto extends createZodDto(UpdateBoardSchema) {}
Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { Inject, Injectable } from '@nestjs/common';
22
import { IBoardsRepository } from '@core/boards/domain/repository';
3+
import { CreateBoardDto } from '@core/boards/application/dtos';
4+
import { BoardFactory } from '@core/boards/domain/factories/board.factory';
5+
import type { BoardWithRelations } from '@core/boards/domain/entities';
36

47
@Injectable()
58
export class CreateBoardUseCase {
@@ -8,7 +11,13 @@ export class CreateBoardUseCase {
811
private readonly boardsRepo: IBoardsRepository,
912
) {}
1013

11-
public async execute(_projectId: string, _userId: string, dto: any) {
12-
return this.boardsRepo.create(dto);
14+
public async execute(
15+
projectId: string,
16+
userId: string,
17+
dto: CreateBoardDto,
18+
): Promise<BoardWithRelations> {
19+
const { board, columns, views } = BoardFactory.createBoard(projectId, userId, dto);
20+
21+
return this.boardsRepo.create(board, columns, views);
1322
}
1423
}

src/boards/application/use-cases/delete-board.use-case.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export class DeleteBoardUseCase {
88
private readonly boardsRepo: IBoardsRepository,
99
) {}
1010

11-
public async execute(id: string, _projectId: string, _userId: string) {
11+
public async execute(id: string, _projectId: string, _userId: string): Promise<boolean> {
1212
return this.boardsRepo.remove(id);
1313
}
1414
}
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Inject, Injectable } from '@nestjs/common';
22
import { IBoardsRepository } from '@core/boards/domain/repository';
3+
import type { BoardWithRelations } from '@core/boards/domain/entities';
34

45
@Injectable()
56
export class GetBoardQuery {
@@ -8,7 +9,11 @@ export class GetBoardQuery {
89
private readonly boardsRepo: IBoardsRepository,
910
) {}
1011

11-
public async execute(id: string, _projectId: string, _userId: string) {
12+
public async execute(
13+
id: string,
14+
_projectId: string,
15+
_userId: string,
16+
): Promise<BoardWithRelations | null> {
1217
return await this.boardsRepo.findById(id);
1318
}
1419
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Inject, Injectable } from '@nestjs/common';
22
import { IBoardsRepository } from '@core/boards/domain/repository';
3+
import type { BoardWithRelations } from '@core/boards/domain/entities';
34

45
@Injectable()
56
export class GetBoardsQuery {
@@ -8,7 +9,7 @@ export class GetBoardsQuery {
89
private readonly boardsRepo: IBoardsRepository,
910
) {}
1011

11-
public async execute(projectId: string, _userId: string) {
12+
public async execute(projectId: string, _userId: string): Promise<BoardWithRelations[]> {
1213
return this.boardsRepo.findAll(projectId);
1314
}
1415
}
Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { Inject, Injectable } from '@nestjs/common';
22
import { IBoardsRepository } from '@core/boards/domain/repository';
3+
import { UpdateBoardDto } from '@core/boards/application/dtos';
4+
import type { BoardWithRelations } from '@core/boards/domain/entities';
35

46
@Injectable()
57
export class UpdateBoardUseCase {
@@ -8,7 +10,12 @@ export class UpdateBoardUseCase {
810
private readonly boardsRepo: IBoardsRepository,
911
) {}
1012

11-
public async execute(id: string, _projectId: string, _userId: string, dto: any) {
13+
public async execute(
14+
id: string,
15+
_projectId: string,
16+
_userId: string,
17+
dto: UpdateBoardDto,
18+
): Promise<BoardWithRelations | null> {
1219
return await this.boardsRepo.update(id, dto);
1320
}
1421
}
Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
11
import type { InferInsertModel, InferSelectModel } from 'drizzle-orm';
2-
import { boards } from '@core/boards/infrastructure/persistence/models/boards.model';
2+
import {
3+
boards,
4+
boardViews,
5+
boardTypeEnum,
6+
boardColumns,
7+
} from '@core/boards/infrastructure/persistence/models/boards.model';
38

4-
export enum BoardType {
5-
Kanban = 'kanban',
6-
Calendar = 'calendar',
7-
Matrix = 'matrix',
8-
}
9+
export type BoardType = (typeof boardTypeEnum.enumValues)[number];
910

1011
export type Board = InferSelectModel<typeof boards>;
1112
export type NewBoard = InferInsertModel<typeof boards>;
13+
14+
export type BoardColumn = InferSelectModel<typeof boardColumns>;
15+
export type NewBoardColumn = InferInsertModel<typeof boardColumns>;
16+
17+
export type BoardView = InferSelectModel<typeof boardViews>;
18+
export type NewBoardView = InferInsertModel<typeof boardViews>;
19+
20+
export type BoardWithRelations = Board & {
21+
boardColumns: BoardColumn[];
22+
boardViews: BoardView[];
23+
};
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { BoardType, NewBoard, NewBoardColumn, NewBoardView } from '@core/boards/domain/entities';
2+
import { createId } from '@paralleldrive/cuid2';
3+
import { CreateBoardDto } from '@core/boards/application/dtos';
4+
5+
export class BoardFactory {
6+
static createView(props: {
7+
boardId: string;
8+
type: BoardType;
9+
name?: string;
10+
position: number;
11+
settings?: Record<string, unknown>;
12+
}): NewBoardView {
13+
return {
14+
id: createId(),
15+
boardId: props.boardId,
16+
type: props.type,
17+
name: props.name ?? this.getDefaultViewName(props.type),
18+
position: props.position,
19+
settings: props.settings ?? this.getDefaultSettings(props.type),
20+
};
21+
}
22+
23+
static createBoard(
24+
projectId: string,
25+
ownerId: string,
26+
dto: CreateBoardDto,
27+
): { board: NewBoard; columns: NewBoardColumn[]; views: NewBoardView[] } {
28+
const boardId = createId();
29+
const boardPosition = dto.position ?? Date.now();
30+
const board: NewBoard = {
31+
id: boardId,
32+
name: dto.name,
33+
projectId,
34+
ownerId,
35+
position: boardPosition,
36+
settings: dto.settings ?? {},
37+
};
38+
39+
const defaultViewTypes: BoardType[] = ['kanban', 'calendar', 'gantt_matrix'];
40+
41+
const views = defaultViewTypes.map((type, index) =>
42+
this.createView({
43+
boardId,
44+
type,
45+
position: (index + 1) * 1000,
46+
}),
47+
);
48+
49+
const columns = [
50+
{ id: createId(), boardId, name: 'К выполнению', position: 1000, color: '#64748b' },
51+
{ id: createId(), boardId, name: 'В работе', position: 2000, color: '#3b82f6' },
52+
{ id: createId(), boardId, name: 'Готово', position: 3000, color: '#22c55e' },
53+
];
54+
55+
return { board, columns, views };
56+
}
57+
58+
private static getDefaultViewName(type: BoardType): string {
59+
const names: Record<BoardType, string> = {
60+
kanban: 'Доска',
61+
calendar: 'Календарь',
62+
gantt_matrix: 'Гант',
63+
};
64+
return names[type];
65+
}
66+
67+
private static getDefaultSettings(type: BoardType): Record<string, unknown> {
68+
switch (type) {
69+
case 'kanban':
70+
return { mock: 'kanban_mock_setting' };
71+
case 'calendar':
72+
return { mock: 'calendar_mock_setting' };
73+
case 'gantt_matrix':
74+
return { mock: 'gantt_mock_setting' };
75+
default:
76+
const exhaustiveCheck: never = type;
77+
return exhaustiveCheck;
78+
}
79+
}
80+
}

0 commit comments

Comments
 (0)