Skip to content
6 changes: 5 additions & 1 deletion apps/backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { UsersModule } from '@/users/users.module';
import { AuthModule } from '@/auth/auth.module';
import { ProjectsModule } from './projects/projects.module';
import { ProjectsModule } from '@/projects/projects.module';
import { TasksModule } from '@/tasks/tasks.module';
import { BoardColumnsModule } from '@/board-columns/board-columns.module';

@Module({
imports: [
Expand All @@ -29,6 +31,8 @@ import { ProjectsModule } from './projects/projects.module';
AuthModule,
UsersModule,
ProjectsModule,
TasksModule,
BoardColumnsModule,
],
controllers: [AppController],
providers: [AppService],
Expand Down
20 changes: 20 additions & 0 deletions apps/backend/src/board-columns/board-columns.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Test, TestingModule } from '@nestjs/testing';
import { BoardColumnsController } from '@/board-columns/board-columns.controller';
import { BoardColumnsService } from '@/board-columns/board-columns.service';

describe('BoardColumnsController', () => {
let controller: BoardColumnsController;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [BoardColumnsController],
providers: [BoardColumnsService],
}).compile();
Comment thread
Zafar7645 marked this conversation as resolved.

controller = module.get<BoardColumnsController>(BoardColumnsController);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});
});
71 changes: 71 additions & 0 deletions apps/backend/src/board-columns/board-columns.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
UseGuards,
Request,
Query,
ParseIntPipe,
} from '@nestjs/common';
import { BoardColumnsService } from '@/board-columns/board-columns.service';
import { CreateBoardColumnDto } from '@/board-columns/dto/create-board-column.dto';
import { UpdateBoardColumnDto } from '@/board-columns/dto/update-board-column.dto';
import { JwtAuthGuard } from '@/auth/guards/jwt-auth.guard';

@UseGuards(JwtAuthGuard)
@Controller('board-columns')
export class BoardColumnsController {
constructor(private readonly boardColumnsService: BoardColumnsService) {}

@Post()
create(
@Body() createBoardColumnDto: CreateBoardColumnDto,
@Request() req: { user: { userId: number; email: string } },
) {
return this.boardColumnsService.create(
createBoardColumnDto,
req.user.userId,
);
}

@Get()
findAll(
@Query('projectId', ParseIntPipe) projectId: number,
@Request() req: { user: { userId: number; email: string } },
) {
return this.boardColumnsService.findAll(projectId, req.user.userId);
}

@Get(':id')
findOne(
@Param('id', ParseIntPipe) id: number,
@Request() req: { user: { userId: number; email: string } },
) {
return this.boardColumnsService.findOne(id, req.user.userId);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

@Patch(':id')
update(
@Param('id', ParseIntPipe) id: number,
@Body() updateBoardColumnDto: UpdateBoardColumnDto,
@Request() req: { user: { userId: number; email: string } },
) {
return this.boardColumnsService.update(
id,
updateBoardColumnDto,
req.user.userId,
);
}

@Delete(':id')
remove(
@Param('id', ParseIntPipe) id: number,
@Request() req: { user: { userId: number; email: string } },
) {
return this.boardColumnsService.remove(id, req.user.userId);
}
}
13 changes: 13 additions & 0 deletions apps/backend/src/board-columns/board-columns.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { BoardColumnsService } from '@/board-columns/board-columns.service';
import { BoardColumnsController } from '@/board-columns/board-columns.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BoardColumn } from '@/board-columns/entities/board-column.entity';
import { Project } from '@/projects/entities/project.entity';

@Module({
imports: [TypeOrmModule.forFeature([BoardColumn, Project])],
controllers: [BoardColumnsController],
providers: [BoardColumnsService],
})
export class BoardColumnsModule {}
18 changes: 18 additions & 0 deletions apps/backend/src/board-columns/board-columns.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { BoardColumnsService } from '@/board-columns/board-columns.service';

describe('BoardColumnsService', () => {
let service: BoardColumnsService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [BoardColumnsService],
}).compile();

service = module.get<BoardColumnsService>(BoardColumnsService);
Comment thread
Zafar7645 marked this conversation as resolved.
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
114 changes: 114 additions & 0 deletions apps/backend/src/board-columns/board-columns.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import {
ForbiddenException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { CreateBoardColumnDto } from '@/board-columns/dto/create-board-column.dto';
import { UpdateBoardColumnDto } from '@/board-columns/dto/update-board-column.dto';
import { Project } from '@/projects/entities/project.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { BoardColumn } from '@/board-columns/entities/board-column.entity';

@Injectable()
export class BoardColumnsService {
constructor(
@InjectRepository(BoardColumn)
private columnsRepository: Repository<BoardColumn>,
@InjectRepository(Project) private projectsRepository: Repository<Project>,
) {}

private async verifyProjectAccess(
projectId: number,
userId: number,
): Promise<void> {
const project = await this.projectsRepository.findOne({
where: { id: projectId },
});
if (!project) throw new NotFoundException('Project not found');
if (project.userId !== userId) {
throw new ForbiddenException(
'You do not have permission to modify this project board',
);
}
}

private async verifyColumnAccess(
columnId: number,
userId: number,
): Promise<BoardColumn> {
const column = await this.columnsRepository.findOne({
where: { id: columnId },
relations: ['project'],
});
if (!column) throw new NotFoundException('Column not found');
if (column.project.userId !== userId) {
throw new ForbiddenException(
'You do not have permission to modify this column',
);
}
return column;
}

async create(createDto: CreateBoardColumnDto, userId: number) {
return await this.columnsRepository.manager.transaction(
async (transactionalManager) => {
const project = await transactionalManager
.createQueryBuilder(Project, 'project')
.where('project.id = :id', { id: createDto.projectId })
.setLock('pessimistic_write')
.getOne();
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if (!project) {
throw new NotFoundException('Project not found');
}
if (project.userId !== userId) {
throw new ForbiddenException(
'You do not have permission to modify this project board',
);
}

let order = createDto.order;
if (order === undefined) {
const lastColumn = await transactionalManager.findOne(BoardColumn, {
where: { projectId: createDto.projectId },
order: { order: 'DESC' },
});
order = lastColumn ? lastColumn.order + 1 : 0;
}

const column = transactionalManager.create(BoardColumn, {
...createDto,
order,
});
return await transactionalManager.save(column);
},
);
}

async findAll(projectId: number, userId: number) {
await this.verifyProjectAccess(projectId, userId);
return await this.columnsRepository.find({
where: { projectId },
order: { order: 'ASC' },
});
}

async findOne(id: number, userId: number) {
return await this.verifyColumnAccess(id, userId);
}

async update(id: number, updateDto: UpdateBoardColumnDto, userId: number) {
const column = await this.verifyColumnAccess(id, userId);
if ('projectId' in updateDto) {
delete updateDto.projectId;
}
const updatedColumn = this.columnsRepository.merge(column, updateDto);
return await this.columnsRepository.save(updatedColumn);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

async remove(id: number, userId: number) {
const column = await this.verifyColumnAccess(id, userId);
return await this.columnsRepository.remove(column);
}
}
15 changes: 15 additions & 0 deletions apps/backend/src/board-columns/dto/create-board-column.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { IsNotEmpty, IsString, IsOptional, IsInt } from 'class-validator';

export class CreateBoardColumnDto {
@IsString()
@IsNotEmpty()
name: string;

@IsInt()
@IsOptional()
order?: number;

@IsInt()
@IsNotEmpty()
projectId: number;
}
6 changes: 6 additions & 0 deletions apps/backend/src/board-columns/dto/update-board-column.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { OmitType, PartialType } from '@nestjs/mapped-types';
import { CreateBoardColumnDto } from '@/board-columns/dto/create-board-column.dto';

export class UpdateBoardColumnDto extends PartialType(
OmitType(CreateBoardColumnDto, ['projectId'] as const),
) {}
42 changes: 42 additions & 0 deletions apps/backend/src/board-columns/entities/board-column.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Project } from '@/projects/entities/project.entity';
import { Task } from '@/tasks/entities/task.entity';
import {
Column,
CreateDateColumn,
UpdateDateColumn,
Entity,
ManyToOne,
JoinColumn,
OneToMany,
PrimaryGeneratedColumn,
} from 'typeorm';

@Entity({ name: 'board_columns' })
export class BoardColumn {
@PrimaryGeneratedColumn()
id: number;

@Column()
name: string;

@Column()
order: number;

@Column({ name: 'project_id' })
projectId: number;

@CreateDateColumn({ name: 'created_at' })
createdAt: Date;

@UpdateDateColumn({ name: 'updated_at' })
updatedAt: Date;

@ManyToOne(() => Project, (project) => project.boardColumns, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'project_id' })
project: Project;

@OneToMany(() => Task, (task) => task.column)
tasks: Task[];
}
7 changes: 6 additions & 1 deletion apps/backend/src/projects/entities/project.entity.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { BoardColumn } from '@/board-columns/entities/board-column.entity';
import { User } from '@/users/user.entity';
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
Expand All @@ -18,7 +20,7 @@ export class Project {
name: string;

@Column({ nullable: true })
description: string;
description: string | null;

@Column({ name: 'user_id' })
userId: number;
Expand All @@ -32,4 +34,7 @@ export class Project {

@UpdateDateColumn({ name: 'updated_at' })
updatedAt: Date;

@OneToMany(() => BoardColumn, (boardColumn) => boardColumn.project)
boardColumns: BoardColumn[];
}
13 changes: 7 additions & 6 deletions apps/backend/src/projects/projects.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
Delete,
UseGuards,
Request,
ParseIntPipe,
} from '@nestjs/common';
import { ProjectsService } from '@/projects/projects.service';
import { CreateProjectDto } from '@/projects/dto/create-project.dto';
Expand All @@ -34,26 +35,26 @@ export class ProjectsController {

@Get(':id')
findOne(
@Param('id') id: string,
@Param('id', ParseIntPipe) id: number,
@Request() req: { user: { userId: number; email: string } },
) {
return this.projectsService.findOne(+id, req.user.userId);
return this.projectsService.findOne(id, req.user.userId);
}

@Patch(':id')
update(
@Param('id') id: string,
@Param('id', ParseIntPipe) id: number,
@Body() updateProjectDto: UpdateProjectDto,
@Request() req: { user: { userId: number; email: string } },
) {
return this.projectsService.update(+id, updateProjectDto, req.user.userId);
return this.projectsService.update(id, updateProjectDto, req.user.userId);
}

@Delete(':id')
remove(
@Param('id') id: string,
@Param('id', ParseIntPipe) id: number,
@Request() req: { user: { userId: number; email: string } },
) {
return this.projectsService.remove(+id, req.user.userId);
return this.projectsService.remove(id, req.user.userId);
}
}
Loading
Loading