-
Notifications
You must be signed in to change notification settings - Fork 1
feat(tasks): [#8] Implement task CRUD API endpoints #22
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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(); | ||
|
|
||
| controller = module.get<BoardColumnsController>(BoardColumnsController); | ||
| }); | ||
|
|
||
| it('should be defined', () => { | ||
| expect(controller).toBeDefined(); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| 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) { | ||
| return this.boardColumnsService.findOne(id); | ||
| } | ||
|
Comment on lines
+43
to
+46
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add user context to This handler should pass Suggested change `@Get`(':id')
- findOne(`@Param`('id', ParseIntPipe) id: number) {
- return this.boardColumnsService.findOne(id);
+ findOne(
+ `@Param`('id', ParseIntPipe) id: number,
+ `@Request`() req: { user: { userId: number; email: string } },
+ ) {
+ return this.boardColumnsService.findOne(id, req.user.userId);
}🤖 Prompt for AI Agents
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: Length of output: 1996
// board-columns.controller.ts — still unfixed
findOne(`@Param`('id', ParseIntPipe) id: number) {
return this.boardColumnsService.findOne(id);
}
// board-columns.service.ts — still a stub
findOne(id: number) {
return `This action returns a #${id} boardColumn`;
}Here's what both files need: Controller ( `@Get`(':id')
findOne(
`@Param`('id', ParseIntPipe) id: number,
`@Request`() req: { user: { userId: number; email: string } },
) {
return this.boardColumnsService.findOne(id, req.user.userId);
}Service ( async findOne(id: number, userId: number): Promise<BoardColumn> {
return this.verifyColumnAccess(id, userId);
}
🧠 Learnings used |
||
|
|
||
| @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); | ||
| } | ||
| } | ||
| 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 {} |
| 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 on lines
+7
to
+12
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Test will fail: Missing repository mocks for injected dependencies.
🐛 Proposed fix to add repository mocks import { Test, TestingModule } from '@nestjs/testing';
import { BoardColumnsService } from '@/board-columns/board-columns.service';
+import { getRepositoryToken } from '@nestjs/typeorm';
+import { BoardColumn } from '@/board-columns/entities/board-column.entity';
+import { Project } from '@/projects/entities/project.entity';
describe('BoardColumnsService', () => {
let service: BoardColumnsService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
- providers: [BoardColumnsService],
+ providers: [
+ BoardColumnsService,
+ {
+ provide: getRepositoryToken(BoardColumn),
+ useValue: {
+ findOne: jest.fn(),
+ find: jest.fn(),
+ create: jest.fn(),
+ save: jest.fn(),
+ merge: jest.fn(),
+ remove: jest.fn(),
+ },
+ },
+ {
+ provide: getRepositoryToken(Project),
+ useValue: {
+ findOne: jest.fn(),
+ },
+ },
+ ],
}).compile();📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All tests will be handled in a separate issue for tests. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
🧠 Learnings used |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('should be defined', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(service).toBeDefined(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| 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) { | ||
| await this.verifyProjectAccess(createDto.projectId, userId); | ||
|
|
||
| let order = createDto.order; | ||
| if (order === undefined) { | ||
| const lastColumn = await this.columnsRepository.findOne({ | ||
| where: { projectId: createDto.projectId }, | ||
| order: { order: 'DESC' }, | ||
| }); | ||
| order = lastColumn ? lastColumn.order + 1 : 0; | ||
| } | ||
|
Comment on lines
+56
to
+63
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Order assignment is race-prone under concurrent creates. Two concurrent requests can read the same last order and insert duplicate positions. Consider a transaction + lock (or a unique 🤖 Prompt for AI Agents |
||
|
|
||
| const column = this.columnsRepository.create({ ...createDto, order }); | ||
| return await this.columnsRepository.save(column); | ||
| } | ||
|
|
||
| async findAll(projectId: number, userId: number) { | ||
| await this.verifyProjectAccess(projectId, userId); | ||
| return await this.columnsRepository.find({ | ||
| where: { projectId }, | ||
| order: { order: 'ASC' }, | ||
| }); | ||
| } | ||
|
|
||
| findOne(id: number) { | ||
| return `This action returns a #${id} boardColumn`; | ||
| } | ||
|
Comment on lines
+77
to
+79
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This currently returns a string instead of the actual column and does not enforce ownership. It should use the same access check path as update/delete. Suggested change- findOne(id: number) {
- return `This action returns a #${id} boardColumn`;
+ async findOne(id: number, userId: number) {
+ return await this.verifyColumnAccess(id, userId);
}Downstream impact: 🤖 Prompt for AI Agents |
||
|
|
||
| async update(id: number, updateDto: UpdateBoardColumnDto, userId: number) { | ||
| const column = await this.verifyColumnAccess(id, userId); | ||
| const updatedColumn = this.columnsRepository.merge(column, updateDto); | ||
| return await this.columnsRepository.save(updatedColumn); | ||
| } | ||
|
|
||
| async remove(id: number, userId: number) { | ||
| const column = await this.verifyColumnAccess(id, userId); | ||
| return await this.columnsRepository.remove(column); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import { IsNotEmpty, IsNumber, IsString, IsOptional } from 'class-validator'; | ||
|
|
||
| export class CreateBoardColumnDto { | ||
| @IsString() | ||
| @IsNotEmpty() | ||
| name: string; | ||
|
|
||
| @IsNumber() | ||
| @IsOptional() | ||
| order?: number; | ||
|
|
||
| @IsNumber() | ||
| @IsNotEmpty() | ||
| projectId: number; | ||
|
Comment on lines
+8
to
+14
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Verify current validators and integer schema expectations
rg -n -C2 'class CreateBoardColumnDto|@IsNumber|@IsInt|order|projectId' apps/backend/src/board-columns/dto/create-board-column.dto.ts
rg -n -C2 'CREATE TABLE board_columns|project_id|\"order\"| INT' database/init.sqlRepository: Zafar7645/syncup Length of output: 533 🏁 Script executed: #!/bin/bash
# Search for the board_columns table schema definition
rg -n 'CREATE TABLE board_columns' database/init.sql -A 20Repository: Zafar7645/syncup Length of output: 799 🏁 Script executed: #!/bin/bash
# Check for global validation pipes or transformers
rg -n 'ValidationPipe|ClassSerializerInterceptor|TransformOptions|transform:' apps/backend/src/main.ts apps/backend/src/app.module.ts --max-count=20
# Also check if there's a common DTO base class or validation config
fd -t f name '*validation*' 'apps/backend/src/' | head -20Repository: Zafar7645/syncup Length of output: 315 🏁 Script executed: #!/bin/bash
# Read the full ValidationPipe configuration
cat -n apps/backend/src/main.ts | head -20
# Check if CreateBoardColumnDto uses any `@Type` or `@Transform` decorators
rg -n '@Type|@Transform' apps/backend/src/board-columns/dto/create-board-column.dto.tsRepository: Zafar7645/syncup Length of output: 818 🏁 Script executed: #!/bin/bash
# Check if `@Min` constraints make sense
# order: 0 is typical starting value for ordering; projectId: 1 is typical for auto-increment IDs
# Search for other DTOs with similar number validation patterns
rg -n '@IsNumber\(\)' apps/backend/src --type ts | head -30Repository: Zafar7645/syncup Length of output: 342 Replace
🔧 Proposed fix-import { IsNotEmpty, IsNumber, IsString, IsOptional } from 'class-validator';
+import { IsInt, IsNotEmpty, IsOptional, IsString, Min } from 'class-validator';
@@
- `@IsNumber`()
+ `@IsInt`()
+ `@Min`(0)
`@IsOptional`()
order?: number;
@@
- `@IsNumber`()
+ `@IsInt`()
+ `@Min`(1)
`@IsNotEmpty`()
projectId: number;🤖 Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| import { PartialType } from '@nestjs/mapped-types'; | ||
| import { CreateBoardColumnDto } from '@/board-columns/dto/create-board-column.dto'; | ||
|
|
||
| export class UpdateBoardColumnDto extends PartialType(CreateBoardColumnDto) {} |
| 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[]; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| import { IsNotEmpty, IsOptional, IsString, IsNumber } from 'class-validator'; | ||
|
|
||
| export class CreateTaskDto { | ||
| @IsString() | ||
| @IsNotEmpty() | ||
| title: string; | ||
|
|
||
| @IsString() | ||
| @IsOptional() | ||
| description?: string; | ||
|
|
||
| @IsNumber() | ||
| @IsOptional() | ||
| order?: number; | ||
|
|
||
| @IsNumber() | ||
| @IsNotEmpty() | ||
| columnId: number; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| import { PartialType } from '@nestjs/mapped-types'; | ||
| import { CreateTaskDto } from '@/tasks/dto/create-task.dto'; | ||
|
|
||
| export class UpdateTaskDto extends PartialType(CreateTaskDto) {} |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: Zafar7645/syncup
Length of output: 1143
Mock
BoardColumnsServicein this controller unit test.BoardColumnsServicehas constructor dependencies on two TypeORM repositories (BoardColumnandProject). Registering the concrete service as a provider will cause module compilation to fail due to missing repository providers. Use auseValuemock to keep this unit test isolated from the database layer.🔧 Proposed fix
beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [BoardColumnsController], - providers: [BoardColumnsService], + providers: [ + { + provide: BoardColumnsService, + useValue: { + create: jest.fn(), + findAll: jest.fn(), + findOne: jest.fn(), + update: jest.fn(), + remove: jest.fn(), + }, + }, + ], }).compile();📝 Committable suggestion
🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All tests will be handled in a separate issue for tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Zafar7645, understood! Would you like me to open a follow-up GitHub issue to track the test improvements (mockingBoardColumnsServiceandTasksServicein their respective controller specs, and any other test coverage work) so it doesn't get lost?🧠 Learnings used