diff --git a/apps/backend/package.json b/apps/backend/package.json index 9cfb65c..c920418 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -24,6 +24,7 @@ "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", "@nestjs/jwt": "^11.0.1", + "@nestjs/mapped-types": "^2.1.0", "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^11.0.1", "@nestjs/typeorm": "^11.0.0", diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts index 37dbe62..0ff4254 100644 --- a/apps/backend/src/app.module.ts +++ b/apps/backend/src/app.module.ts @@ -5,6 +5,7 @@ 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'; @Module({ imports: [ @@ -27,6 +28,7 @@ import { AuthModule } from '@/auth/auth.module'; }), AuthModule, UsersModule, + ProjectsModule, ], controllers: [AppController], providers: [AppService], diff --git a/apps/backend/src/projects/dto/create-project.dto.ts b/apps/backend/src/projects/dto/create-project.dto.ts new file mode 100644 index 0000000..532051c --- /dev/null +++ b/apps/backend/src/projects/dto/create-project.dto.ts @@ -0,0 +1,12 @@ +import { IsNotEmpty, IsOptional, IsString, Matches } from 'class-validator'; + +export class CreateProjectDto { + @IsString() + @IsNotEmpty() + @Matches(/\S/, { message: 'name must contain non-whitespace characters' }) + name: string; + + @IsString() + @IsOptional() + description?: string; +} diff --git a/apps/backend/src/projects/dto/update-project.dto.ts b/apps/backend/src/projects/dto/update-project.dto.ts new file mode 100644 index 0000000..202f56d --- /dev/null +++ b/apps/backend/src/projects/dto/update-project.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateProjectDto } from '@/projects/dto/create-project.dto'; + +export class UpdateProjectDto extends PartialType(CreateProjectDto) {} diff --git a/apps/backend/src/projects/entities/project.entity.ts b/apps/backend/src/projects/entities/project.entity.ts new file mode 100644 index 0000000..643b006 --- /dev/null +++ b/apps/backend/src/projects/entities/project.entity.ts @@ -0,0 +1,35 @@ +import { User } from '@/users/user.entity'; +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; + +@Entity({ name: 'projects' }) +export class Project { + @PrimaryGeneratedColumn() + id: number; + + @Column() + name: string; + + @Column({ nullable: true }) + description: string; + + @Column({ name: 'user_id' }) + userId: number; + + @ManyToOne(() => User, (user) => user.projects, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'user_id' }) + user: User; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at' }) + updatedAt: Date; +} diff --git a/apps/backend/src/projects/projects.controller.spec.ts b/apps/backend/src/projects/projects.controller.spec.ts new file mode 100644 index 0000000..7ace097 --- /dev/null +++ b/apps/backend/src/projects/projects.controller.spec.ts @@ -0,0 +1,20 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ProjectsController } from '@/projects/projects.controller'; +import { ProjectsService } from '@/projects/projects.service'; + +describe('ProjectsController', () => { + let controller: ProjectsController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ProjectsController], + providers: [ProjectsService], + }).compile(); + + controller = module.get(ProjectsController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/apps/backend/src/projects/projects.controller.ts b/apps/backend/src/projects/projects.controller.ts new file mode 100644 index 0000000..082c6f7 --- /dev/null +++ b/apps/backend/src/projects/projects.controller.ts @@ -0,0 +1,59 @@ +import { + Controller, + Get, + Post, + Body, + Patch, + Param, + Delete, + UseGuards, + Request, +} from '@nestjs/common'; +import { ProjectsService } from '@/projects/projects.service'; +import { CreateProjectDto } from '@/projects/dto/create-project.dto'; +import { UpdateProjectDto } from '@/projects/dto/update-project.dto'; +import { JwtAuthGuard } from '@/auth/guards/jwt-auth.guard'; + +@UseGuards(JwtAuthGuard) +@Controller('projects') +export class ProjectsController { + constructor(private readonly projectsService: ProjectsService) {} + + @Post() + create( + @Body() createProjectDto: CreateProjectDto, + @Request() req: { user: { userId: number; email: string } }, + ) { + return this.projectsService.create(createProjectDto, req.user.userId); + } + + @Get() + findAll(@Request() req: { user: { userId: number; email: string } }) { + return this.projectsService.findAll(req.user.userId); + } + + @Get(':id') + findOne( + @Param('id') id: string, + @Request() req: { user: { userId: number; email: string } }, + ) { + return this.projectsService.findOne(+id, req.user.userId); + } + + @Patch(':id') + update( + @Param('id') id: string, + @Body() updateProjectDto: UpdateProjectDto, + @Request() req: { user: { userId: number; email: string } }, + ) { + return this.projectsService.update(+id, updateProjectDto, req.user.userId); + } + + @Delete(':id') + remove( + @Param('id') id: string, + @Request() req: { user: { userId: number; email: string } }, + ) { + return this.projectsService.remove(+id, req.user.userId); + } +} diff --git a/apps/backend/src/projects/projects.module.ts b/apps/backend/src/projects/projects.module.ts new file mode 100644 index 0000000..523be50 --- /dev/null +++ b/apps/backend/src/projects/projects.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { ProjectsService } from '@/projects/projects.service'; +import { ProjectsController } from '@/projects/projects.controller'; +import { Project } from '@/projects/entities/project.entity'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +@Module({ + imports: [TypeOrmModule.forFeature([Project])], + controllers: [ProjectsController], + providers: [ProjectsService], +}) +export class ProjectsModule {} diff --git a/apps/backend/src/projects/projects.service.spec.ts b/apps/backend/src/projects/projects.service.spec.ts new file mode 100644 index 0000000..d1314c2 --- /dev/null +++ b/apps/backend/src/projects/projects.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ProjectsService } from '@/projects/projects.service'; + +describe('ProjectsService', () => { + let service: ProjectsService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ProjectsService], + }).compile(); + + service = module.get(ProjectsService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/backend/src/projects/projects.service.ts b/apps/backend/src/projects/projects.service.ts new file mode 100644 index 0000000..bcd3d04 --- /dev/null +++ b/apps/backend/src/projects/projects.service.ts @@ -0,0 +1,61 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { CreateProjectDto } from '@/projects/dto/create-project.dto'; +import { UpdateProjectDto } from '@/projects/dto/update-project.dto'; +import { Repository } from 'typeorm'; +import { Project } from '@/projects/entities/project.entity'; +import { InjectRepository } from '@nestjs/typeorm'; + +@Injectable() +export class ProjectsService { + constructor( + @InjectRepository(Project) + private projectsRepository: Repository, + ) {} + + async create(createProjectDto: CreateProjectDto, userId: number) { + const project = this.projectsRepository.create({ + ...createProjectDto, + userId, + }); + + return await this.projectsRepository.save(project); + } + + async findAll(userId: number) { + return await this.projectsRepository.find({ + where: { userId }, + order: { createdAt: 'DESC' }, + }); + } + + async findOne(id: number, userId: number) { + const project = await this.projectsRepository.findOne({ + where: { id, userId }, + }); + + if (!project) { + throw new NotFoundException( + `Project with ID "${id}" not found or you don't have access.`, + ); + } + + return project; + } + + async update(id: number, updateProjectDto: UpdateProjectDto, userId: number) { + const project = await this.findOne(id, userId); + + const updatedProject = this.projectsRepository.merge( + project, + updateProjectDto, + ); + + return await this.projectsRepository.save(updatedProject); + } + + async remove(id: number, userId: number) { + const project = await this.findOne(id, userId); + + return await this.projectsRepository.remove(project); + } +} diff --git a/apps/backend/src/users/user.entity.ts b/apps/backend/src/users/user.entity.ts index ce98cd3..e4d5589 100644 --- a/apps/backend/src/users/user.entity.ts +++ b/apps/backend/src/users/user.entity.ts @@ -1,9 +1,11 @@ +import { Project } from '@/projects/entities/project.entity'; import { BeforeInsert, BeforeUpdate, Column, CreateDateColumn, Entity, + OneToMany, PrimaryGeneratedColumn, UpdateDateColumn, } from 'typeorm'; @@ -35,4 +37,7 @@ export class User { this.email = this.email.trim().toLowerCase(); } } + + @OneToMany(() => Project, (project) => project.user) + projects: Project[]; } diff --git a/package-lock.json b/package-lock.json index 0f09ec5..f7a0fd4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", "@nestjs/jwt": "^11.0.1", + "@nestjs/mapped-types": "^2.1.0", "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^11.0.1", "@nestjs/typeorm": "^11.0.0", @@ -5126,6 +5127,26 @@ "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0" } }, + "node_modules/@nestjs/mapped-types": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.1.0.tgz", + "integrity": "sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "class-transformer": "^0.4.0 || ^0.5.0", + "class-validator": "^0.13.0 || ^0.14.0", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, "node_modules/@nestjs/passport": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-11.0.5.tgz",