Skip to content

Commit 4767211

Browse files
maksberegovoisoorq
authored andcommitted
refactor(api): consolidate fixes for areas, invitations, and state management (#108)
* refactor(area): fix area update * refactor(area): fix area creating and update response * refactor(area): fix deleting area * refactor(invitations): fix getting invitations query * refactor(state): rename `orderIndex` to `position` * Feat/casl (#101) * feat(authorization): implement role-based access control system * feat(teams): add ability-based access control for invitations, roles, and team management * refactor(teams): rename TEAMS to TEAM * refactor(teams): update imports
1 parent 80f046e commit 4767211

8 files changed

Lines changed: 118 additions & 73 deletions

File tree

src/area/application/controllers/area/swagger.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@ import {
1010
import { ZOD_RESPONSE_TOKEN } from '@shared/interceptors';
1111
import { ActionResponse } from '@shared/schemas';
1212

13-
import { CreateAreaDto, UpdateAreaDto, AreaResponse, AreasResponse } from '../../dtos';
13+
import {
14+
CreateAreaDto,
15+
UpdateAreaDto,
16+
AreaResponse,
17+
AreasResponse,
18+
CreateAreaResponse,
19+
} from '../../dtos';
1420

1521
export const CreateAreaSwagger = () =>
1622
applyDecorators(
@@ -42,14 +48,14 @@ export const CreateAreaSwagger = () =>
4248
ApiResponse({
4349
status: 201,
4450
description: 'Область успешно создана',
45-
type: ActionResponse.Output,
51+
type: CreateAreaResponse.Output,
4652
}),
4753
ApiValidationError(),
4854
ApiUnauthorized(),
4955
ApiForbidden('Нет прав для создания области в этом проекте'),
5056
ApiConflict('Область с таким названием или slug уже существует'),
5157

52-
SetMetadata(ZOD_RESPONSE_TOKEN, ActionResponse),
58+
SetMetadata(ZOD_RESPONSE_TOKEN, CreateAreaResponse),
5359
);
5460

5561
export const FindAllAreasSwagger = () =>

src/area/application/dtos/area.dto.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { DEFAULT_VIEWS } from '@core/area/domain/entities';
2+
import { ActionResponseSchema } from '@shared/schemas';
23
import { createZodDto } from 'nestjs-zod';
34
import { z } from 'zod/v4';
45

@@ -65,6 +66,7 @@ export const AreaSchema = z.object({
6566
maxTasksLimit: z
6667
.number()
6768
.int('Лимит задач должен быть целым числом')
69+
.max(100000, 'Лимит задач не может превышать 100 000')
6870
.positive('Лимит задач должен быть положительным числом')
6971
.nullable()
7072
.optional()
@@ -117,6 +119,12 @@ export const CreateAreaSchema = AreaSchema.omit({
117119
})
118120
.describe('Схема для создания новой области');
119121

122+
export const CreateAreaResponseSchema = ActionResponseSchema.extend({
123+
slug: z.string(
124+
'URL-дружественный идентификатор (например: "development", "contract-approval")',
125+
),
126+
});
127+
120128
export const UpdateAreaSchema = CreateAreaSchema.partial()
121129
.refine((data) => Object.keys(data).length > 0, {
122130
error: 'Необходимо передать хотя бы одно поле для обновления',
@@ -126,6 +134,7 @@ export const UpdateAreaSchema = CreateAreaSchema.partial()
126134

127135
export const AreasSchema = z.array(AreaSchema);
128136

137+
export class CreateAreaResponse extends createZodDto(CreateAreaResponseSchema) {}
129138
export class AreaResponse extends createZodDto(AreaSchema) {}
130139
export class AreasResponse extends createZodDto(AreasSchema) {}
131140
export class CreateAreaDto extends createZodDto(CreateAreaSchema) {}

src/area/application/dtos/state.dto.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export const StateSchema = z.object({
4747
.nullable()
4848
.optional()
4949
.describe('Emoji или иконка для визуального обозначения (например: "📋", "🚀", "✅")'),
50-
orderIndex: z
50+
position: z
5151
.number()
5252
.int('Порядковый номер должен быть целым числом')
5353
.min(0, 'Порядковый номер не может быть отрицательным')
@@ -60,6 +60,7 @@ export const StateSchema = z.object({
6060
maxTasksLimit: z
6161
.number()
6262
.int('Лимит задач должен быть целым числом')
63+
.max(100000, 'Лимит задач должен быть целым числом')
6364
.positive('Лимит задач должен быть положительным числом')
6465
.nullable()
6566
.optional()

src/area/application/use-cases/areas/create.use-case.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export class CreateAreaUseCase {
4040
);
4141
}
4242

43-
const existingArea = await this.areaRepo.findBySlug(project.id, currentSlug);
43+
const existingArea = await this.areaRepo.findBySlug(currentSlug, project.id);
4444

4545
if (existingArea) {
4646
throw new BaseException(

src/area/application/use-cases/areas/delete.use-case.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export class DeleteAreaUseCase {
1919
'owner',
2020
]);
2121

22-
const area = await this.areaRepo.findBySlug(project.id, key);
22+
const area = await this.areaRepo.findBySlug(key, project.id);
2323

2424
if (!area) {
2525
throw new BaseException(

src/area/application/use-cases/areas/update.use-case.ts

Lines changed: 87 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import slugify from 'slugify';
77

88
import { UpdateAreaDto } from '../../dtos';
99

10-
import type { NewArea } from '../../../domain/entities';
10+
import type { Area } from '../../../domain/entities';
1111

1212
@Injectable()
1313
export class UpdateAreaUseCase {
@@ -24,7 +24,7 @@ export class UpdateAreaUseCase {
2424
'owner',
2525
]);
2626

27-
const area = await this.areaRepo.findBySlug(project.id, key);
27+
const area = await this.areaRepo.findBySlug(key, project.id);
2828

2929
if (!area) {
3030
throw new BaseException(
@@ -36,75 +36,13 @@ export class UpdateAreaUseCase {
3636
);
3737
}
3838

39-
const updateData: Partial<NewArea> = {
40-
updatedAt: new Date().toISOString(),
41-
...(dto.title && dto.title !== area.title && { title: dto.title.trim() }),
42-
...(dto.description &&
43-
dto.description !== area.description && {
44-
description: dto.description?.trim() || null,
45-
}),
46-
...(dto.descriptionHtml &&
47-
dto.descriptionHtml !== area.descriptionHtml && {
48-
descriptionHtml: dto.descriptionHtml?.trim() || null,
49-
}),
50-
...(dto.color && dto.color !== area.color && { color: dto.color || null }),
51-
...(dto.icon && dto.icon !== area.icon && { icon: dto.icon || null }),
52-
...(dto.defaultView &&
53-
dto.defaultView !== area.defaultView && { defaultView: dto.defaultView }),
54-
...(dto.position &&
55-
dto.position !== area.position &&
56-
dto.position >= 0 && { position: dto.position }),
57-
...(dto.maxTasksLimit &&
58-
dto.maxTasksLimit !== area.maxTasksLimit &&
59-
dto.maxTasksLimit > 0 && { maxTasksLimit: dto.maxTasksLimit }),
60-
...(dto.isLocked && dto.isLocked !== area.isLocked && { isLocked: dto.isLocked }),
61-
};
62-
63-
let hasChanges = false;
39+
const updateData = this.buildUpdateData(area, dto);
6440

6541
if (dto.slug && dto.slug !== area.slug) {
66-
let newSlug = dto.slug;
67-
68-
if (newSlug) {
69-
newSlug = slugify(newSlug, {
70-
lower: true,
71-
strict: true,
72-
trim: true,
73-
});
74-
75-
if (!newSlug) {
76-
throw new BaseException(
77-
{
78-
code: AreaErrorCodes.SLUG_INVALID,
79-
message: AreaErrorMessages[AreaErrorCodes.SLUG_INVALID],
80-
},
81-
HttpStatus.BAD_REQUEST,
82-
);
83-
}
84-
85-
const existingArea = await this.areaRepo.findBySlug(project.id, newSlug);
86-
if (existingArea && existingArea.id !== area.id) {
87-
throw new BaseException(
88-
{
89-
code: AreaErrorCodes.SLUG_DUPLICATE,
90-
message: AreaErrorMessages[AreaErrorCodes.SLUG_DUPLICATE],
91-
},
92-
HttpStatus.CONFLICT,
93-
);
94-
}
95-
96-
updateData.slug = newSlug;
97-
} else {
98-
updateData.slug = slugify(updateData.title || area.title, {
99-
lower: true,
100-
strict: true,
101-
trim: true,
102-
});
103-
}
104-
hasChanges = true;
42+
await this.updateSlug(project.id, area, dto.slug, updateData);
10543
}
10644

107-
if (!hasChanges) {
45+
if (Object.keys(updateData).length === 0) {
10846
return {
10947
success: true,
11048
message: 'Нет изменений для обновления',
@@ -131,4 +69,86 @@ export class UpdateAreaUseCase {
13169
);
13270
}
13371
}
72+
73+
private buildUpdateData(area: Area, dto: UpdateAreaDto): Partial<Area> {
74+
const updateData: Partial<Area> = {};
75+
76+
if (dto.title !== undefined && dto.title !== area.title) {
77+
updateData.title = dto.title.trim();
78+
}
79+
80+
if (dto.description !== undefined && dto.description !== area.description) {
81+
updateData.description = dto.description?.trim() ?? null;
82+
}
83+
84+
if (dto.descriptionHtml !== undefined && dto.descriptionHtml !== area.descriptionHtml) {
85+
updateData.descriptionHtml = dto.descriptionHtml?.trim() ?? null;
86+
}
87+
88+
if (dto.color !== undefined && dto.color !== area.color) {
89+
updateData.color = dto.color ?? null;
90+
}
91+
92+
if (dto.icon !== undefined && dto.icon !== area.icon) {
93+
updateData.icon = dto.icon ?? null;
94+
}
95+
96+
if (dto.defaultView !== undefined && dto.defaultView !== area.defaultView) {
97+
updateData.defaultView = dto.defaultView;
98+
}
99+
100+
if (dto.position !== undefined && dto.position !== area.position && dto.position >= 0) {
101+
updateData.position = dto.position;
102+
}
103+
104+
if (
105+
dto.maxTasksLimit &&
106+
dto.maxTasksLimit !== area.maxTasksLimit &&
107+
dto.maxTasksLimit > 0
108+
) {
109+
updateData.maxTasksLimit = dto.maxTasksLimit;
110+
}
111+
112+
if (dto.isLocked !== undefined && dto.isLocked !== area.isLocked) {
113+
updateData.isLocked = dto.isLocked;
114+
}
115+
116+
return updateData;
117+
}
118+
119+
private async updateSlug(
120+
projectId: string,
121+
area: Area,
122+
slug: string,
123+
updateData: Partial<Area>,
124+
): Promise<void> {
125+
const newSlug = slugify(slug, {
126+
lower: true,
127+
strict: true,
128+
trim: true,
129+
});
130+
131+
if (!newSlug) {
132+
throw new BaseException(
133+
{
134+
code: AreaErrorCodes.SLUG_INVALID,
135+
message: AreaErrorMessages[AreaErrorCodes.SLUG_INVALID],
136+
},
137+
HttpStatus.BAD_REQUEST,
138+
);
139+
}
140+
141+
const existingArea = await this.areaRepo.findBySlug(projectId, slug);
142+
if (existingArea && existingArea.id !== area.id) {
143+
throw new BaseException(
144+
{
145+
code: AreaErrorCodes.SLUG_DUPLICATE,
146+
message: AreaErrorMessages[AreaErrorCodes.SLUG_DUPLICATE],
147+
},
148+
HttpStatus.CONFLICT,
149+
);
150+
}
151+
152+
updateData.slug = slug;
153+
}
134154
}

src/area/application/use-cases/states/reorder.use-case.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ export class ReorderStateUseCase {
1515
) {}
1616

1717
async execute(slug: string, _dto: ReordersStatesDto, userId: string) {
18+
throw new BaseException(
19+
{
20+
code: 'NOT_IMPLEMENTED',
21+
message: 'Функция в разработке',
22+
},
23+
HttpStatus.NOT_IMPLEMENTED,
24+
);
25+
1826
try {
1927
const area = await this.getAreaQ.execute({ key: slug }, userId);
2028

src/area/application/use-cases/states/update.use-case.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export class UpdateStateUseCase {
1616

1717
async execute(slug: string, stateId: string, dto: UpdateStateDto, userId: string) {
1818
try {
19+
//TODO: 500 for unique constraint
1920
const area = await this.getAreaQ.execute({ key: slug }, userId);
2021

2122
const state = await this.stateRepo.findOne(area.id, stateId);

0 commit comments

Comments
 (0)