From 7fc284c2c0091abf8bbebf638061884ab9f38b3e Mon Sep 17 00:00:00 2001 From: badbuta Date: Mon, 17 Apr 2023 23:53:14 +0800 Subject: [PATCH] Add Department module for relationship --- src/app.module.ts | 7 +- src/department/README.md | 10 +++ src/department/department.controller.ts | 78 ++++++++++++++++++++ src/department/department.module.ts | 15 ++++ src/department/department.service.ts | 55 ++++++++++++++ src/department/dto/department-create.dto.ts | 16 ++++ src/department/dto/department-update.dto.ts | 4 + src/department/entities/department.entity.ts | 30 ++++++++ src/staffs/entities/staff.entity.ts | 16 +++- src/staffs/staffs.service.ts | 16 +++- src/users/dto/create-user.dto.ts | 2 +- src/users/entities/user.entity.ts | 4 +- 12 files changed, 244 insertions(+), 9 deletions(-) create mode 100644 src/department/README.md create mode 100644 src/department/department.controller.ts create mode 100644 src/department/department.module.ts create mode 100644 src/department/department.service.ts create mode 100644 src/department/dto/department-create.dto.ts create mode 100644 src/department/dto/department-update.dto.ts create mode 100644 src/department/entities/department.entity.ts diff --git a/src/app.module.ts b/src/app.module.ts index bdd791d..e5065ee 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -14,6 +14,7 @@ import { AllExceptionsFilter } from './logger/any-exception.filter'; import loggerMiddleware from './logger/logger.middleware'; import { TypeOrmModule } from '@nestjs/typeorm'; import { StaffsModule } from './staffs/staffs.module'; +import { DepartmentModule } from './department/department.module'; @Module({ imports: [ @@ -30,11 +31,13 @@ import { StaffsModule } from './staffs/staffs.module'; // entities: ['dist/modules/**/*.mysql.entity{.ts,.js}'], autoLoadEntities: true, // IMPORTANT: disable sync - // synchronize: false, - synchronize: true, + synchronize: false, + // synchronize: true, + logging: true, }), UsersModule, StaffsModule, + DepartmentModule, ], controllers: [AppController], providers: [ diff --git a/src/department/README.md b/src/department/README.md new file mode 100644 index 0000000..d57ae00 --- /dev/null +++ b/src/department/README.md @@ -0,0 +1,10 @@ +# Department Module + +- This module demonstrates using: + - CRUD + - TypeORM: MySQL + - TypeORM Repository + - No QueryBuilder + - TypeOrmModule.forFeature & TypeOrmModule.forRoot{ autoLoadEntities: true } +- The Entities + - DB and Web-API entities are shared \ No newline at end of file diff --git a/src/department/department.controller.ts b/src/department/department.controller.ts new file mode 100644 index 0000000..75ffe7f --- /dev/null +++ b/src/department/department.controller.ts @@ -0,0 +1,78 @@ +import { + Controller, + Get, + Post, + Body, + Patch, + Param, + Delete, + NotFoundException, + HttpCode, + BadRequestException, +} from '@nestjs/common'; +import { ApiResponse } from '@nestjs/swagger'; +import { DepartmentService } from './department.service'; +import { Department } from './entities/department.entity'; +import { DepartmentCreateDto } from './dto/department-create.dto'; +import { DepartmentUpdateDto } from './dto/department-update.dto'; +import { EntityNotFoundError } from 'typeorm'; + +@Controller('department') +export class DepartmentController { + constructor(private readonly departmentService: DepartmentService) {} + + private handleEntityNotFoundError(id: number, e: Error) { + if (e instanceof EntityNotFoundError) { + throw new NotFoundException(`Not found: id=${id}`); + } else { + throw e; + } + } + + @Get() + @ApiResponse({ type: [Department] }) + findAll(): Promise { + return this.departmentService.findAll(); + } + + @Get(':id') + @ApiResponse({ type: Department }) + async findOne(@Param('id') id: number): Promise { + // NOTE: the + operator returns the numeric representation of the object. + return this.departmentService.findOne(+id).catch((err) => { + this.handleEntityNotFoundError(id, err); + return null; + }); + } + + @Post() + @ApiResponse({ type: [Department] }) + create(@Body() departmentCreateDto: DepartmentCreateDto) { + return this.departmentService.create(departmentCreateDto); + } + + @Patch(':id') + // Status 204 because no content will be returned + @HttpCode(204) + async update( + @Param('id') id: number, + @Body() departmentUpdateDto: DepartmentUpdateDto, + ): Promise { + if (Object.keys(departmentUpdateDto).length === 0) { + throw new BadRequestException('Request body is empty'); + } + await this.departmentService + .update(+id, departmentUpdateDto) + .catch((err) => { + this.handleEntityNotFoundError(id, err); + }); + } + + @Delete(':id') + @HttpCode(204) + async remove(@Param('id') id: number): Promise { + await this.departmentService.remove(+id).catch((err) => { + this.handleEntityNotFoundError(id, err); + }); + } +} diff --git a/src/department/department.module.ts b/src/department/department.module.ts new file mode 100644 index 0000000..9251752 --- /dev/null +++ b/src/department/department.module.ts @@ -0,0 +1,15 @@ +import { Logger, Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { DepartmentService } from './department.service'; +import { DepartmentController } from './department.controller'; +import { Department } from './entities/department.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([Department])], + controllers: [DepartmentController], + providers: [Logger, DepartmentService], + // If you want to use the repository outside of the module + // which imports TypeOrmModule.forFeature, you'll need to re-export the providers generated by it. + // exports: [TypeOrmModule], +}) +export class DepartmentModule {} diff --git a/src/department/department.service.ts b/src/department/department.service.ts new file mode 100644 index 0000000..dcdb1c9 --- /dev/null +++ b/src/department/department.service.ts @@ -0,0 +1,55 @@ +import { Injectable, Logger, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { EntityNotFoundError, Repository } from 'typeorm'; +import { Department } from './entities/department.entity'; +import { DepartmentCreateDto } from './dto/department-create.dto'; +import { DepartmentUpdateDto } from './dto/department-update.dto'; + +@Injectable() +export class DepartmentService { + constructor( + private readonly logger: Logger, + @InjectRepository(Department) + private departmentRepository: Repository, + ) {} + + create(departmentCreateDto: DepartmentCreateDto): Promise { + // Use Repository.create() will copy the values to destination object. + const department = this.departmentRepository.create(departmentCreateDto); + return this.departmentRepository.save(department); + } + + findAll(): Promise { + return this.departmentRepository.find(); + } + + findOne(id: number): Promise { + return this.departmentRepository.findOneOrFail({ where: { id } }); + } + + async update( + id: number, + departmentUpdateDto: DepartmentUpdateDto, + ): Promise { + await this.isExist(id); + await this.departmentRepository.update({ id }, departmentUpdateDto); + } + + async remove(id: number): Promise { + await this.isExist(id); + await this.departmentRepository.delete(id); + } + + // Helper function: Check if the entity exist. + // If entity does not exsit, return the Promise.reject() + private async isExist(id: number): Promise { + const cnt = await this.departmentRepository.countBy({ id }); + if (cnt > 0) { + return; + } else { + return Promise.reject( + new EntityNotFoundError(Department, { where: { id } }), + ); + } + } +} diff --git a/src/department/dto/department-create.dto.ts b/src/department/dto/department-create.dto.ts new file mode 100644 index 0000000..605cbd2 --- /dev/null +++ b/src/department/dto/department-create.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; +export class DepartmentCreateDto { + // NOTE: Since the id is auto-inc, so no id for the creation + // id: number; + + @ApiProperty({ + type: String, + name: 'name', + description: 'Department name', + required: true, + }) + @IsString() + @IsNotEmpty() + name: string; +} diff --git a/src/department/dto/department-update.dto.ts b/src/department/dto/department-update.dto.ts new file mode 100644 index 0000000..5cb6092 --- /dev/null +++ b/src/department/dto/department-update.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { DepartmentCreateDto } from './department-create.dto'; + +export class DepartmentUpdateDto extends PartialType(DepartmentCreateDto) {} diff --git a/src/department/entities/department.entity.ts b/src/department/entities/department.entity.ts new file mode 100644 index 0000000..2194696 --- /dev/null +++ b/src/department/entities/department.entity.ts @@ -0,0 +1,30 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Staff } from 'src/staffs/entities/staff.entity'; +import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; + +// @Entity('departments') +@Entity() +export class Department { + @ApiProperty({ + type: Number, + name: 'id', + description: 'id of department, auto-incremented', + required: true, + }) + @PrimaryGeneratedColumn('increment') + id: number; + + @ApiProperty({ + type: String, + name: 'name', + description: 'Department name', + required: true, + }) + @Column({ + type: 'varchar', + }) + name: string; + + // @OneToMany((type) => Staff, (staff) => staff.departmentId) + // staffs: Staff[]; +} diff --git a/src/staffs/entities/staff.entity.ts b/src/staffs/entities/staff.entity.ts index 3b64a18..f9bd372 100644 --- a/src/staffs/entities/staff.entity.ts +++ b/src/staffs/entities/staff.entity.ts @@ -1,11 +1,19 @@ import { ApiProperty } from '@nestjs/swagger'; -import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; +import { Department } from 'src/department/entities/department.entity'; +import { + Column, + Entity, + JoinColumn, + JoinTable, + ManyToOne, + PrimaryGeneratedColumn, +} from 'typeorm'; // @Entity('staffs') @Entity() export class Staff { @ApiProperty({ - type: 'number', + type: Number, name: 'id', description: 'id of staff, auto-incremented', required: true, @@ -53,5 +61,7 @@ export class Staff { }) departmentId: number; - // TODO: JOIN TABLE CASE + @ManyToOne(() => Department) + @JoinColumn({ name: 'department_id' }) + department: Department; } diff --git a/src/staffs/staffs.service.ts b/src/staffs/staffs.service.ts index 06cf40b..f1ace5e 100644 --- a/src/staffs/staffs.service.ts +++ b/src/staffs/staffs.service.ts @@ -20,7 +20,21 @@ export class StaffsService { } findAll(): Promise { - return this.staffsRepository.find(); + return this.staffsRepository.find({ + relations: { + department: true, + }, + select: { + id: true, + name: true, + + departmentId: false, + department: { + id: false, + name: true, + }, + }, + }); } findOne(id: number): Promise { diff --git a/src/users/dto/create-user.dto.ts b/src/users/dto/create-user.dto.ts index 0db55d5..0d60dfd 100644 --- a/src/users/dto/create-user.dto.ts +++ b/src/users/dto/create-user.dto.ts @@ -10,7 +10,7 @@ import { export class CreateUserDto { // NOTE: Since the id is autoinc, so no id for user creation // @ApiProperty({ - // type: 'number', + // type: Number, // name: 'id', // description: 'id of user', // required: false, diff --git a/src/users/entities/user.entity.ts b/src/users/entities/user.entity.ts index a2e1fee..40095cb 100644 --- a/src/users/entities/user.entity.ts +++ b/src/users/entities/user.entity.ts @@ -11,7 +11,7 @@ import { UpdateUserDto } from '../dto/update-user.dto'; // export class User implements IUser { export class User { @ApiProperty({ - type: 'number', + type: Number, name: 'id', description: 'id of user', required: false, @@ -19,7 +19,7 @@ export class User { id: number; @ApiProperty({ - type: 'string', + type: String, name: 'name', description: 'name of user', required: false,