diff --git a/libs/common/src/constants/controller-route.ts b/libs/common/src/constants/controller-route.ts index eeacd0f..2aff604 100644 --- a/libs/common/src/constants/controller-route.ts +++ b/libs/common/src/constants/controller-route.ts @@ -1,4 +1,29 @@ export class ControllerRoute { + static PROJECT = class { + public static readonly ROUTE = 'projects'; + static ACTIONS = class { + public static readonly CREATE_PROJECT_SUMMARY = 'Create a new project'; + public static readonly CREATE_PROJECT_DESCRIPTION = + 'This endpoint allows you to create a new project by providing the required project details.'; + + public static readonly GET_PROJECT_SUMMARY = 'Retrieve project details'; + public static readonly GET_PROJECT_DESCRIPTION = + 'This endpoint retrieves the details of a project by its unique identifier (UUID).'; + + public static readonly UPDATE_PROJECT_SUMMARY = 'Update project details'; + public static readonly UPDATE_PROJECT_DESCRIPTION = + 'This endpoint updates the details of an existing project using its unique identifier (UUID).'; + + public static readonly LIST_PROJECTS_SUMMARY = 'List all projects'; + public static readonly LIST_PROJECTS_DESCRIPTION = + 'This endpoint retrieves a list of all existing projects, including their details.'; + + public static readonly DELETE_PROJECT_SUMMARY = 'Delete a project'; + public static readonly DELETE_PROJECT_DESCRIPTION = + 'This endpoint deletes an existing project by its unique identifier (UUID).'; + }; + }; + static REGION = class { public static readonly ROUTE = 'region'; static ACTIONS = class { diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index 8b4acf4..34e4bb6 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -27,7 +27,7 @@ import { DeviceStatusLogEntity } from '../modules/device-status-log/entities'; import { SceneEntity, SceneIconEntity } from '../modules/scene/entities'; import { SceneDeviceEntity } from '../modules/scene-device/entities'; import { SpaceProductEntity } from '../modules/space/entities/space-product.entity'; - +import { ProjectEntity } from '../modules/project/entities'; @Module({ imports: [ TypeOrmModule.forRootAsync({ @@ -42,6 +42,7 @@ import { SpaceProductEntity } from '../modules/space/entities/space-product.enti password: configService.get('DB_PASSWORD'), database: configService.get('DB_NAME'), entities: [ + ProjectEntity, UserEntity, UserSessionEntity, UserOtpEntity, diff --git a/libs/common/src/modules/project/dtos/index.ts b/libs/common/src/modules/project/dtos/index.ts new file mode 100644 index 0000000..9d264cb --- /dev/null +++ b/libs/common/src/modules/project/dtos/index.ts @@ -0,0 +1 @@ +export * from './project.dto'; diff --git a/libs/common/src/modules/project/dtos/project.dto.ts b/libs/common/src/modules/project/dtos/project.dto.ts new file mode 100644 index 0000000..ef08e55 --- /dev/null +++ b/libs/common/src/modules/project/dtos/project.dto.ts @@ -0,0 +1,15 @@ +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; + +export class ProjectDto { + @IsString() + @IsNotEmpty() + uuid: string; + + @IsString() + @IsNotEmpty() + name: string; + + @IsString() + @IsOptional() + public description?: string; +} diff --git a/libs/common/src/modules/project/entities/index.ts b/libs/common/src/modules/project/entities/index.ts new file mode 100644 index 0000000..bd8d161 --- /dev/null +++ b/libs/common/src/modules/project/entities/index.ts @@ -0,0 +1 @@ +export * from './project.entity'; diff --git a/libs/common/src/modules/project/entities/project.entity.ts b/libs/common/src/modules/project/entities/project.entity.ts new file mode 100644 index 0000000..6c18b6c --- /dev/null +++ b/libs/common/src/modules/project/entities/project.entity.ts @@ -0,0 +1,27 @@ +import { Entity, Column, Unique } from 'typeorm'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { ProjectDto } from '../dtos'; + +@Entity({ name: 'project' }) +@Unique(['name']) +export class ProjectEntity extends AbstractEntity { + @Column({ + type: 'uuid', + default: () => 'gen_random_uuid()', + nullable: false, + }) + public uuid: string; + + @Column({ + nullable: false, + }) + public name: string; + + @Column({ length: 255, nullable: true }) + description: string; + + constructor(partial: Partial) { + super(); + Object.assign(this, partial); + } +} diff --git a/libs/common/src/modules/project/index.ts b/libs/common/src/modules/project/index.ts new file mode 100644 index 0000000..cacdb88 --- /dev/null +++ b/libs/common/src/modules/project/index.ts @@ -0,0 +1 @@ +export * from './project.repository.module'; diff --git a/libs/common/src/modules/project/project.repository.module.ts b/libs/common/src/modules/project/project.repository.module.ts new file mode 100644 index 0000000..0b744f8 --- /dev/null +++ b/libs/common/src/modules/project/project.repository.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ProjectEntity } from './entities'; + +@Module({ + providers: [], + exports: [], + controllers: [], + imports: [TypeOrmModule.forFeature([ProjectEntity])], +}) +export class ProjectEntityModule {} diff --git a/libs/common/src/modules/project/repositiories/index.ts b/libs/common/src/modules/project/repositiories/index.ts new file mode 100644 index 0000000..3e35c16 --- /dev/null +++ b/libs/common/src/modules/project/repositiories/index.ts @@ -0,0 +1 @@ +export * from './project.repository'; diff --git a/libs/common/src/modules/project/repositiories/project.repository.ts b/libs/common/src/modules/project/repositiories/project.repository.ts new file mode 100644 index 0000000..b4602e7 --- /dev/null +++ b/libs/common/src/modules/project/repositiories/project.repository.ts @@ -0,0 +1,10 @@ +import { DataSource, Repository } from 'typeorm'; +import { ProjectEntity } from '../entities'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class ProjectRepository extends Repository { + constructor(private dataSource: DataSource) { + super(ProjectEntity, dataSource.createEntityManager()); + } +} diff --git a/libs/common/src/util/buildTypeORMIncludeQuery.ts b/libs/common/src/util/buildTypeORMIncludeQuery.ts index c2d401e..029bd67 100644 --- a/libs/common/src/util/buildTypeORMIncludeQuery.ts +++ b/libs/common/src/util/buildTypeORMIncludeQuery.ts @@ -16,6 +16,9 @@ const mappingInclude: { [key: string]: any } = { subspace: { subspace: true, }, + project: { + project: true, + }, }; export function buildTypeORMIncludeQuery( diff --git a/src/app.module.ts b/src/app.module.ts index 609147c..bb5986b 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -22,6 +22,7 @@ import { VisitorPasswordModule } from './vistor-password/visitor-password.module import { ScheduleModule } from './schedule/schedule.module'; import { SpaceModule } from './space/space.module'; import { ProductModule } from './product'; +import { ProjectModule } from './project'; @Module({ imports: [ ConfigModule.forRoot({ @@ -48,6 +49,7 @@ import { ProductModule } from './product'; VisitorPasswordModule, ScheduleModule, ProductModule, + ProjectModule, ], providers: [ { diff --git a/src/project/controllers/index.ts b/src/project/controllers/index.ts new file mode 100644 index 0000000..2b606fd --- /dev/null +++ b/src/project/controllers/index.ts @@ -0,0 +1 @@ +export * from './project.controller'; diff --git a/src/project/controllers/project.controller.ts b/src/project/controllers/project.controller.ts new file mode 100644 index 0000000..a39ced1 --- /dev/null +++ b/src/project/controllers/project.controller.ts @@ -0,0 +1,89 @@ +import { ControllerRoute } from '@app/common/constants/controller-route'; +import { + Body, + Controller, + Delete, + Get, + Param, + Post, + Put, + Query, + UseGuards, +} from '@nestjs/common'; +import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { ProjectService } from '../services'; +import { CreateProjectDto, GetProjectParam } from '../dto'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; +import { PaginationRequestGetListDto } from '@app/common/dto/pagination.request.dto'; + +@ApiTags('Project Module') +@Controller({ + version: '1', + path: ControllerRoute.PROJECT.ROUTE, +}) +export class ProjectController { + constructor(private readonly projectService: ProjectService) {} + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @ApiOperation({ + summary: ControllerRoute.PROJECT.ACTIONS.CREATE_PROJECT_SUMMARY, + description: ControllerRoute.PROJECT.ACTIONS.CREATE_PROJECT_DESCRIPTION, + }) + @Post() + async createProject(@Body() dto: CreateProjectDto): Promise { + return this.projectService.createProject(dto); + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @ApiOperation({ + summary: ControllerRoute.PROJECT.ACTIONS.UPDATE_PROJECT_SUMMARY, + description: ControllerRoute.PROJECT.ACTIONS.UPDATE_PROJECT_DESCRIPTION, + }) + @Put(':projectUuid') + async updateProject( + @Param() params: GetProjectParam, + @Body() dto: CreateProjectDto, + ): Promise { + return this.projectService.updateProject(params.projectUuid, dto); + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @ApiOperation({ + summary: ControllerRoute.PROJECT.ACTIONS.DELETE_PROJECT_SUMMARY, + description: ControllerRoute.PROJECT.ACTIONS.DELETE_PROJECT_DESCRIPTION, + }) + @Delete(':projectUuid') + async deleteProject( + @Param() params: GetProjectParam, + ): Promise { + return this.projectService.deleteProject(params.projectUuid); + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @ApiOperation({ + summary: ControllerRoute.PROJECT.ACTIONS.LIST_PROJECTS_SUMMARY, + description: ControllerRoute.PROJECT.ACTIONS.LIST_PROJECTS_DESCRIPTION, + }) + @Get() + async list( + @Query() query: PaginationRequestGetListDto, + ): Promise { + return this.projectService.listProjects(query); + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @ApiOperation({ + summary: ControllerRoute.PROJECT.ACTIONS.GET_PROJECT_SUMMARY, + description: ControllerRoute.PROJECT.ACTIONS.GET_PROJECT_DESCRIPTION, + }) + @Get(':projectUuid') + async findOne(@Param() params: GetProjectParam): Promise { + return this.projectService.getProject(params.projectUuid); + } +} diff --git a/src/project/dto/create-project.dto.ts b/src/project/dto/create-project.dto.ts new file mode 100644 index 0000000..9637856 --- /dev/null +++ b/src/project/dto/create-project.dto.ts @@ -0,0 +1,21 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, IsNotEmpty, IsOptional } from 'class-validator'; + +export class CreateProjectDto { + @ApiProperty({ + example: 'Project 1', + description: 'Name of the project', + }) + @IsString() + @IsNotEmpty() + name: string; + + @ApiProperty({ + example: 'Optional description of the project', + description: 'Description of the project', + required: false, + }) + @IsString() + @IsOptional() + description?: string; +} diff --git a/src/project/dto/get-project.param.ts b/src/project/dto/get-project.param.ts new file mode 100644 index 0000000..2ec5825 --- /dev/null +++ b/src/project/dto/get-project.param.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsUUID } from 'class-validator'; + +export class GetProjectParam { + @ApiProperty({ + description: 'UUID of the Project', + example: 'd290f1ee-6c54-4b01-90e6-d701748f0851', + }) + @IsUUID() + projectUuid: string; +} diff --git a/src/project/dto/index.ts b/src/project/dto/index.ts new file mode 100644 index 0000000..568bd4f --- /dev/null +++ b/src/project/dto/index.ts @@ -0,0 +1,2 @@ +export * from './create-project.dto'; +export * from './get-project.param'; diff --git a/src/project/index.ts b/src/project/index.ts new file mode 100644 index 0000000..c3c63b7 --- /dev/null +++ b/src/project/index.ts @@ -0,0 +1 @@ +export * from './project.module'; diff --git a/src/project/project.module.ts b/src/project/project.module.ts new file mode 100644 index 0000000..9d82ba9 --- /dev/null +++ b/src/project/project.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { ProjectController } from './controllers'; +import { ProjectService } from './services'; +import { ProjectRepository } from '@app/common/modules/project/repositiories'; + +@Module({ + imports: [], + controllers: [ProjectController], + providers: [ProjectService, ProjectRepository], + exports: [ProjectService], +}) +export class ProjectModule {} diff --git a/src/project/services/index.ts b/src/project/services/index.ts new file mode 100644 index 0000000..82e8c13 --- /dev/null +++ b/src/project/services/index.ts @@ -0,0 +1 @@ +export * from './project.service'; diff --git a/src/project/services/project.service.ts b/src/project/services/project.service.ts new file mode 100644 index 0000000..63ea252 --- /dev/null +++ b/src/project/services/project.service.ts @@ -0,0 +1,185 @@ +import { ProjectRepository } from '@app/common/modules/project/repositiories'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { CreateProjectDto } from '../dto'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; +import { ProjectEntity } from '@app/common/modules/project/entities'; +import { + TypeORMCustomModel, + TypeORMCustomModelFindAllQuery, +} from '@app/common/models/typeOrmCustom.model'; +import { ProjectDto } from '@app/common/modules/project/dtos'; +import { PageResponse } from '@app/common/dto/pagination.response.dto'; + +@Injectable() +export class ProjectService { + constructor(private readonly projectRepository: ProjectRepository) {} + + async createProject( + createProjectDto: CreateProjectDto, + ): Promise { + const { name } = createProjectDto; + + try { + // Check if the project name already exists + const isNameExist = await this.validate(name); + if (isNameExist) { + throw new HttpException( + `Project with name ${name} already exists`, + HttpStatus.CONFLICT, + ); + } + + const newProject = this.projectRepository.create(createProjectDto); + const savedProject = await this.projectRepository.save(newProject); + + return new SuccessResponseDto({ + message: `Project with ID ${savedProject.uuid} successfully created`, + data: savedProject, + statusCode: HttpStatus.CREATED, + }); + } catch (error) { + if (error instanceof HttpException) { + throw error; + } + + throw new HttpException( + 'An error occurred while creating the project. Please try again later.', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async deleteProject(uuid: string): Promise { + try { + // Find the project by UUID + const project = await this.findOne(uuid); + + if (!project) { + throw new HttpException( + `Project with ID ${uuid} not found`, + HttpStatus.NOT_FOUND, + ); + } + + // Delete the project + await this.projectRepository.delete({ uuid }); + + return new SuccessResponseDto({ + message: `Project with ID ${uuid} successfully deleted`, + statusCode: HttpStatus.OK, + }); + } catch (error) { + if (error instanceof HttpException) { + throw error; + } + + throw new HttpException( + `An error occurred while deleting the project ${uuid}`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async listProjects( + pageable: Partial, + ): Promise { + try { + pageable.modelName = 'project'; + const customModel = TypeORMCustomModel(this.projectRepository); + + const { baseResponseDto, paginationResponseDto } = + await customModel.findAll(pageable); + return new PageResponse( + baseResponseDto, + paginationResponseDto, + ); + } catch (error) { + throw new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + async getProject(uuid: string): Promise { + try { + // Find the project by UUID + const project = await this.findOne(uuid); + + if (!project) { + throw new HttpException( + `Project with ID ${uuid} not found`, + HttpStatus.NOT_FOUND, + ); + } + + return new SuccessResponseDto({ + message: `Project with ID ${uuid} retrieved successfully`, + data: project, + statusCode: HttpStatus.OK, + }); + } catch (error) { + if (error instanceof HttpException) { + throw error; + } + + throw new HttpException( + `An error occurred while retrieving the project with id ${uuid}`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async updateProject( + uuid: string, + updateProjectDto: CreateProjectDto, + ): Promise { + try { + // Find the project by UUID + const project = await this.findOne(uuid); + + if (!project) { + throw new HttpException( + `Project with ID ${uuid} not found`, + HttpStatus.NOT_FOUND, + ); + } + + if (updateProjectDto.name && updateProjectDto.name !== project.name) { + const isNameExist = await this.validate(updateProjectDto.name); + if (isNameExist) { + throw new HttpException( + `Project with name ${updateProjectDto.name} already exists`, + HttpStatus.CONFLICT, + ); + } + } + + // Update the project details + Object.assign(project, updateProjectDto); + const updatedProject = await this.projectRepository.save(project); + + return new SuccessResponseDto({ + message: `Project with ID ${uuid} successfully updated`, + data: updatedProject, + statusCode: HttpStatus.OK, + }); + } catch (error) { + if (error instanceof HttpException) { + throw error; + } + + throw new HttpException( + `An error occurred while updating the project with ID ${uuid}`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async findOne(uuid: string): Promise { + const project = await this.projectRepository.findOne({ where: { uuid } }); + return project; + } + + async validate(name: string): Promise { + return await this.projectRepository.exists({ where: { name } }); + } +}