From 7be9dd3e89daf114472e9d1879df7dc51f0cad63 Mon Sep 17 00:00:00 2001 From: badbuta Date: Thu, 20 Apr 2023 23:18:31 +0800 Subject: [PATCH] temp commit, not functioning --- .vscode/settings.json | 3 +- README.md | 72 +++++++++++++++++ docker-compose.db.yml | 2 +- src/app.module.ts | 15 ++-- src/mysqlcompany/README.md | 7 ++ src/{ => mysqlcompany}/department/README.md | 0 .../department/department.controller.ts | 2 +- .../department/department.module.ts | 0 .../department/department.service.ts | 2 +- .../department/dto/department-create.dto.ts | 0 .../department/dto/department-update.dto.ts | 0 .../department/entities/department.entity.ts | 4 +- src/mysqlcompany/mysqlcompany.controller.ts | 7 ++ src/mysqlcompany/mysqlcompany.module.ts | 35 +++++++++ src/mysqlcompany/mysqlcompany.service.ts | 6 ++ src/{ => mysqlcompany}/staffs/README.md | 0 .../staffs/dto/staff-create.dto.ts | 0 .../staffs/dto/staff-update.dto.ts | 0 .../staffs/entities/staff.entity.ts | 4 +- .../staffs/staffs.controller.ts | 2 +- .../staffs/staffs.module.ts | 0 .../staffs/staffs.service.ts | 0 src/ormmongocompany/README.md | 7 ++ src/ormmongocompany/department/README.md | 10 +++ .../department/department.controller.ts | 78 +++++++++++++++++++ .../department/department.module.ts | 15 ++++ .../department/department.service.ts | 55 +++++++++++++ .../department/dto/department-create.dto.ts | 16 ++++ .../department/dto/department-update.dto.ts | 4 + .../department/entities/department.entity.ts | 30 +++++++ .../ormmongocompany.controller.ts | 9 +++ src/ormmongocompany/ormmongocompany.module.ts | 35 +++++++++ .../ormmongocompany.service.ts | 6 ++ src/ormmongocompany/staffs/README.md | 10 +++ .../staffs/dto/staff-create.dto.ts | 36 +++++++++ .../staffs/dto/staff-update.dto.ts | 4 + .../staffs/entities/staff.entity.ts | 67 ++++++++++++++++ .../staffs/staffs.controller.ts | 76 ++++++++++++++++++ src/ormmongocompany/staffs/staffs.module.ts | 15 ++++ src/ormmongocompany/staffs/staffs.service.ts | 64 +++++++++++++++ src/users/README.md | 7 ++ thunder-tests/thunderActivity.json | 1 + thunder-tests/thunderCollection.json | 9 +++ thunder-tests/thunderEnvironment.json | 13 ++++ thunder-tests/thunderclient.json | 63 +++++++++++++++ tsconfig.build.json | 10 ++- tsconfig.json | 2 +- 47 files changed, 785 insertions(+), 18 deletions(-) create mode 100644 src/mysqlcompany/README.md rename src/{ => mysqlcompany}/department/README.md (100%) rename src/{ => mysqlcompany}/department/department.controller.ts (98%) rename src/{ => mysqlcompany}/department/department.module.ts (100%) rename src/{ => mysqlcompany}/department/department.service.ts (95%) rename src/{ => mysqlcompany}/department/dto/department-create.dto.ts (100%) rename src/{ => mysqlcompany}/department/dto/department-update.dto.ts (100%) rename src/{ => mysqlcompany}/department/entities/department.entity.ts (86%) create mode 100644 src/mysqlcompany/mysqlcompany.controller.ts create mode 100644 src/mysqlcompany/mysqlcompany.module.ts create mode 100644 src/mysqlcompany/mysqlcompany.service.ts rename src/{ => mysqlcompany}/staffs/README.md (100%) rename src/{ => mysqlcompany}/staffs/dto/staff-create.dto.ts (100%) rename src/{ => mysqlcompany}/staffs/dto/staff-update.dto.ts (100%) rename src/{ => mysqlcompany}/staffs/entities/staff.entity.ts (91%) rename src/{ => mysqlcompany}/staffs/staffs.controller.ts (98%) rename src/{ => mysqlcompany}/staffs/staffs.module.ts (100%) rename src/{ => mysqlcompany}/staffs/staffs.service.ts (100%) create mode 100644 src/ormmongocompany/README.md create mode 100644 src/ormmongocompany/department/README.md create mode 100644 src/ormmongocompany/department/department.controller.ts create mode 100644 src/ormmongocompany/department/department.module.ts create mode 100644 src/ormmongocompany/department/department.service.ts create mode 100644 src/ormmongocompany/department/dto/department-create.dto.ts create mode 100644 src/ormmongocompany/department/dto/department-update.dto.ts create mode 100644 src/ormmongocompany/department/entities/department.entity.ts create mode 100644 src/ormmongocompany/ormmongocompany.controller.ts create mode 100644 src/ormmongocompany/ormmongocompany.module.ts create mode 100644 src/ormmongocompany/ormmongocompany.service.ts create mode 100644 src/ormmongocompany/staffs/README.md create mode 100644 src/ormmongocompany/staffs/dto/staff-create.dto.ts create mode 100644 src/ormmongocompany/staffs/dto/staff-update.dto.ts create mode 100644 src/ormmongocompany/staffs/entities/staff.entity.ts create mode 100644 src/ormmongocompany/staffs/staffs.controller.ts create mode 100644 src/ormmongocompany/staffs/staffs.module.ts create mode 100644 src/ormmongocompany/staffs/staffs.service.ts create mode 100644 src/users/README.md create mode 100644 thunder-tests/thunderActivity.json create mode 100644 thunder-tests/thunderCollection.json create mode 100644 thunder-tests/thunderEnvironment.json create mode 100644 thunder-tests/thunderclient.json diff --git a/.vscode/settings.json b/.vscode/settings.json index 994f292..231a33d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "typescript.format.enable": false, "javascript.format.enable": false, - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "esbenp.prettier-vscode", + "thunder-client.saveToWorkspace": true } \ No newline at end of file diff --git a/README.md b/README.md index a246025..a68ba41 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ This project is based on the [Basic Docker Project with Git CI](https://git.pric 1. docker-compose setup for local development 1. Minimal API implementation (version, healthcheck) +--- + ## Local development using docker-compose and attach with VSCode ### Preparation @@ -47,6 +49,76 @@ This project is based on the [Basic Docker Project with Git CI](https://git.pric 1. To use the VSCode debugger, click the debug and launch the `Debug Nest Framework` (See `launch.json`) for details +--- + +## Databases + +This project requires connecting to the database (MySQL, MongoDB). There are docker-compose files contain the database service. + +### Standalone DB service + +If you try this project with the local Node.js environment, you can use standalone database service which provided by the docker-compose.db.yml. + +It comes with these service: + +1. MySQL/MariaDB server +1. Adminer - MySQL Web GUI Client +1. MonogoDB server +1. Monogo Express Web GUI Client + +Use the following command to manage the docker services/stack: + +```bash +# To start the service as daemon +docker-compose -f docker-compose.db.yml up -d + +# To stop and down the service (and network resources) +docker-compose -f docker-compose.db.yml down +``` + +#### Adminer + +Adminer is the Web GUI for MySQL/Mariadb. Once the docker-compose.db.yml is started, you may access it by: + +`http://localhost:33306` + +#### Mongo Express + +Mongo Express is the Web GUI for MongoDB. Once the docker-compose.db.yml is started, you may access it by: + +`http://localhost:32017` + +### Troubleshooting + +#### Port binding issue + +In WSL2 environment, you may get the following error sometime: + +> Error response from daemon: Ports are not available: exposing port TCP 0.0.0.0:3306 -> 0.0.0.0:0: listen tcp 0.0.0.0:3306: bind: An attempt was made to access a socket in a way forbidden by its access permissions. + +First of all, you can check if there is stopped container that occupied the port, you can use the prune command to clean up the container: + +```bash +docker container prune +``` + +__NOTE:__ +>In this project, we change the exposed ports to high-port, not low-port like 3306. + +Sometime, Windows firewall will block the port/port-range. It can be checked and add a rule via Powershell + +```powershell +# Check if 3306 is excluded: +netsh int ipv4 show excludedportrange protocol=tcp + +# Add excluded range/port: (Need to run-as admin) +netsh int ipv4 add excludedportrange protocol=tcp startport=3306 numberofports=1 +# + +``` + +--- + ## Swagger The Nest Swagger framework is added into this project. For details, please see [Nestjs OpenAPI](https://docs.nestjs.com/openapi/introduction) documentation. diff --git a/docker-compose.db.yml b/docker-compose.db.yml index cba7aad..d79d291 100644 --- a/docker-compose.db.yml +++ b/docker-compose.db.yml @@ -17,7 +17,7 @@ services: environment: MARIADB_ROOT_PASSWORD: example ports: - - "3306:3306" + - "23306:3306" volumes: - ./data/mysql:/var/lib/mysql # Without this, will get error when start up in WSL2 diff --git a/src/app.module.ts b/src/app.module.ts index e5065ee..cfe15b3 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -13,8 +13,8 @@ import { UsersModule } from './users/users.module'; 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'; +import { MySqlCompanyModule } from './mysqlcompany/mysqlcompany.module'; +import { OrmMongoCompanyModule } from './ormmongocompany/ormmongocompany.module'; @Module({ imports: [ @@ -24,10 +24,10 @@ import { DepartmentModule } from './department/department.module'; TypeOrmModule.forRoot({ type: 'mysql', host: 'localhost', - port: 3306, + port: 23306, username: 'root', password: 'example', - database: 'example_nodejs_nest_crud', + database: 'example_nodejs_nest_crud_company', // entities: ['dist/modules/**/*.mysql.entity{.ts,.js}'], autoLoadEntities: true, // IMPORTANT: disable sync @@ -35,9 +35,12 @@ import { DepartmentModule } from './department/department.module'; // synchronize: true, logging: true, }), + // Router resigration for module (2nd level) will be declared inside the module + // RouterModule.register([ + // ]), UsersModule, - StaffsModule, - DepartmentModule, + MySqlCompanyModule, + OrmMongoCompanyModule, ], controllers: [AppController], providers: [ diff --git a/src/mysqlcompany/README.md b/src/mysqlcompany/README.md new file mode 100644 index 0000000..1f0f1c3 --- /dev/null +++ b/src/mysqlcompany/README.md @@ -0,0 +1,7 @@ +# MysqlComany Module + +MySQL-Comany module uses TypeORM + MySQL + +This module demonstrates database relationships, including: +- One-to-many +- Many-to-many diff --git a/src/department/README.md b/src/mysqlcompany/department/README.md similarity index 100% rename from src/department/README.md rename to src/mysqlcompany/department/README.md diff --git a/src/department/department.controller.ts b/src/mysqlcompany/department/department.controller.ts similarity index 98% rename from src/department/department.controller.ts rename to src/mysqlcompany/department/department.controller.ts index 75ffe7f..7ca3074 100644 --- a/src/department/department.controller.ts +++ b/src/mysqlcompany/department/department.controller.ts @@ -17,7 +17,7 @@ import { DepartmentCreateDto } from './dto/department-create.dto'; import { DepartmentUpdateDto } from './dto/department-update.dto'; import { EntityNotFoundError } from 'typeorm'; -@Controller('department') +@Controller() export class DepartmentController { constructor(private readonly departmentService: DepartmentService) {} diff --git a/src/department/department.module.ts b/src/mysqlcompany/department/department.module.ts similarity index 100% rename from src/department/department.module.ts rename to src/mysqlcompany/department/department.module.ts diff --git a/src/department/department.service.ts b/src/mysqlcompany/department/department.service.ts similarity index 95% rename from src/department/department.service.ts rename to src/mysqlcompany/department/department.service.ts index dcdb1c9..24b77e4 100644 --- a/src/department/department.service.ts +++ b/src/mysqlcompany/department/department.service.ts @@ -1,4 +1,4 @@ -import { Injectable, Logger, NotFoundException } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { EntityNotFoundError, Repository } from 'typeorm'; import { Department } from './entities/department.entity'; diff --git a/src/department/dto/department-create.dto.ts b/src/mysqlcompany/department/dto/department-create.dto.ts similarity index 100% rename from src/department/dto/department-create.dto.ts rename to src/mysqlcompany/department/dto/department-create.dto.ts diff --git a/src/department/dto/department-update.dto.ts b/src/mysqlcompany/department/dto/department-update.dto.ts similarity index 100% rename from src/department/dto/department-update.dto.ts rename to src/mysqlcompany/department/dto/department-update.dto.ts diff --git a/src/department/entities/department.entity.ts b/src/mysqlcompany/department/entities/department.entity.ts similarity index 86% rename from src/department/entities/department.entity.ts rename to src/mysqlcompany/department/entities/department.entity.ts index 2194696..f21ab53 100644 --- a/src/department/entities/department.entity.ts +++ b/src/mysqlcompany/department/entities/department.entity.ts @@ -1,9 +1,9 @@ import { ApiProperty } from '@nestjs/swagger'; -import { Staff } from 'src/staffs/entities/staff.entity'; +import { Staff } from 'src/mysqlcompany/staffs/entities/staff.entity'; import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; // @Entity('departments') -@Entity() +@Entity('departments') export class Department { @ApiProperty({ type: Number, diff --git a/src/mysqlcompany/mysqlcompany.controller.ts b/src/mysqlcompany/mysqlcompany.controller.ts new file mode 100644 index 0000000..ce1485f --- /dev/null +++ b/src/mysqlcompany/mysqlcompany.controller.ts @@ -0,0 +1,7 @@ +import { Controller } from '@nestjs/common'; +import { MySqlCompanyService } from './mysqlcompany.service'; + +@Controller() +export class MySqlCompanyController { + constructor(private readonly mySqlCompanyService: MySqlCompanyService) {} +} diff --git a/src/mysqlcompany/mysqlcompany.module.ts b/src/mysqlcompany/mysqlcompany.module.ts new file mode 100644 index 0000000..d8234d8 --- /dev/null +++ b/src/mysqlcompany/mysqlcompany.module.ts @@ -0,0 +1,35 @@ +import { Logger, Module } from '@nestjs/common'; +import { MySqlCompanyService } from './mysqlcompany.service'; +import { MySqlCompanyController } from './mysqlcompany.controller'; +import { DepartmentModule } from './department/department.module'; +import { StaffsModule } from './staffs/staffs.module'; +import { RouterModule } from '@nestjs/core'; + +@Module({ + imports: [ + // The router register is commonly placed in app.module + // However, I suggest to put registation in the 2nd level module only + // for easy management (i.e. not for root and children level) + RouterModule.register([ + { + path: 'mysql-company', + module: MySqlCompanyModule, + children: [ + { + path: 'departments', + module: DepartmentModule, + }, + { + path: 'staffs', + module: StaffsModule, + }, + ], + }, + ]), + DepartmentModule, + StaffsModule, + ], + controllers: [MySqlCompanyController], + providers: [Logger, MySqlCompanyService], +}) +export class MySqlCompanyModule {} diff --git a/src/mysqlcompany/mysqlcompany.service.ts b/src/mysqlcompany/mysqlcompany.service.ts new file mode 100644 index 0000000..29f1bbd --- /dev/null +++ b/src/mysqlcompany/mysqlcompany.service.ts @@ -0,0 +1,6 @@ +import { Injectable, Logger } from '@nestjs/common'; + +@Injectable() +export class MySqlCompanyService { + constructor(private readonly logger: Logger) {} +} diff --git a/src/staffs/README.md b/src/mysqlcompany/staffs/README.md similarity index 100% rename from src/staffs/README.md rename to src/mysqlcompany/staffs/README.md diff --git a/src/staffs/dto/staff-create.dto.ts b/src/mysqlcompany/staffs/dto/staff-create.dto.ts similarity index 100% rename from src/staffs/dto/staff-create.dto.ts rename to src/mysqlcompany/staffs/dto/staff-create.dto.ts diff --git a/src/staffs/dto/staff-update.dto.ts b/src/mysqlcompany/staffs/dto/staff-update.dto.ts similarity index 100% rename from src/staffs/dto/staff-update.dto.ts rename to src/mysqlcompany/staffs/dto/staff-update.dto.ts diff --git a/src/staffs/entities/staff.entity.ts b/src/mysqlcompany/staffs/entities/staff.entity.ts similarity index 91% rename from src/staffs/entities/staff.entity.ts rename to src/mysqlcompany/staffs/entities/staff.entity.ts index f9bd372..e235615 100644 --- a/src/staffs/entities/staff.entity.ts +++ b/src/mysqlcompany/staffs/entities/staff.entity.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { Department } from 'src/department/entities/department.entity'; +import { Department } from 'src/mysqlcompany/department/entities/department.entity'; import { Column, Entity, @@ -9,7 +9,7 @@ import { PrimaryGeneratedColumn, } from 'typeorm'; -// @Entity('staffs') +@Entity('staffs') @Entity() export class Staff { @ApiProperty({ diff --git a/src/staffs/staffs.controller.ts b/src/mysqlcompany/staffs/staffs.controller.ts similarity index 98% rename from src/staffs/staffs.controller.ts rename to src/mysqlcompany/staffs/staffs.controller.ts index bf9e8f1..63038dc 100644 --- a/src/staffs/staffs.controller.ts +++ b/src/mysqlcompany/staffs/staffs.controller.ts @@ -17,7 +17,7 @@ import { StaffCreateDto } from './dto/staff-create.dto'; import { StaffUpdateDto } from './dto/staff-update.dto'; import { EntityNotFoundError } from 'typeorm'; -@Controller('staffs') +@Controller() export class StaffsController { constructor(private readonly staffsService: StaffsService) {} diff --git a/src/staffs/staffs.module.ts b/src/mysqlcompany/staffs/staffs.module.ts similarity index 100% rename from src/staffs/staffs.module.ts rename to src/mysqlcompany/staffs/staffs.module.ts diff --git a/src/staffs/staffs.service.ts b/src/mysqlcompany/staffs/staffs.service.ts similarity index 100% rename from src/staffs/staffs.service.ts rename to src/mysqlcompany/staffs/staffs.service.ts diff --git a/src/ormmongocompany/README.md b/src/ormmongocompany/README.md new file mode 100644 index 0000000..1f0f1c3 --- /dev/null +++ b/src/ormmongocompany/README.md @@ -0,0 +1,7 @@ +# MysqlComany Module + +MySQL-Comany module uses TypeORM + MySQL + +This module demonstrates database relationships, including: +- One-to-many +- Many-to-many diff --git a/src/ormmongocompany/department/README.md b/src/ormmongocompany/department/README.md new file mode 100644 index 0000000..d57ae00 --- /dev/null +++ b/src/ormmongocompany/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/ormmongocompany/department/department.controller.ts b/src/ormmongocompany/department/department.controller.ts new file mode 100644 index 0000000..7ca3074 --- /dev/null +++ b/src/ormmongocompany/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() +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/ormmongocompany/department/department.module.ts b/src/ormmongocompany/department/department.module.ts new file mode 100644 index 0000000..9251752 --- /dev/null +++ b/src/ormmongocompany/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/ormmongocompany/department/department.service.ts b/src/ormmongocompany/department/department.service.ts new file mode 100644 index 0000000..24b77e4 --- /dev/null +++ b/src/ormmongocompany/department/department.service.ts @@ -0,0 +1,55 @@ +import { Injectable, Logger } 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/ormmongocompany/department/dto/department-create.dto.ts b/src/ormmongocompany/department/dto/department-create.dto.ts new file mode 100644 index 0000000..605cbd2 --- /dev/null +++ b/src/ormmongocompany/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/ormmongocompany/department/dto/department-update.dto.ts b/src/ormmongocompany/department/dto/department-update.dto.ts new file mode 100644 index 0000000..5cb6092 --- /dev/null +++ b/src/ormmongocompany/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/ormmongocompany/department/entities/department.entity.ts b/src/ormmongocompany/department/entities/department.entity.ts new file mode 100644 index 0000000..f21ab53 --- /dev/null +++ b/src/ormmongocompany/department/entities/department.entity.ts @@ -0,0 +1,30 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Staff } from 'src/mysqlcompany/staffs/entities/staff.entity'; +import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; + +// @Entity('departments') +@Entity('departments') +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/ormmongocompany/ormmongocompany.controller.ts b/src/ormmongocompany/ormmongocompany.controller.ts new file mode 100644 index 0000000..fccbfaf --- /dev/null +++ b/src/ormmongocompany/ormmongocompany.controller.ts @@ -0,0 +1,9 @@ +import { Controller } from '@nestjs/common'; +import { OrmMongoCompanyService } from './ormmongocompany.service'; + +@Controller() +export class OrmMongoCompanyController { + constructor( + private readonly ormMongoCompanyService: OrmMongoCompanyService, + ) {} +} diff --git a/src/ormmongocompany/ormmongocompany.module.ts b/src/ormmongocompany/ormmongocompany.module.ts new file mode 100644 index 0000000..9133bfd --- /dev/null +++ b/src/ormmongocompany/ormmongocompany.module.ts @@ -0,0 +1,35 @@ +import { Logger, Module } from '@nestjs/common'; +import { OrmMongoCompanyService } from './ormmongocompany.service'; +import { OrmMongoCompanyController } from './ormmongocompany.controller'; +import { DepartmentModule } from './department/department.module'; +import { StaffsModule } from './staffs/staffs.module'; +import { RouterModule } from '@nestjs/core'; + +@Module({ + imports: [ + // The router register is commonly placed in app.module + // However, I suggest to put registation in the 2nd level module only + // for easy management (i.e. not for root and children level) + RouterModule.register([ + { + path: 'ormmongo-company', + module: OrmMongoCompanyModule, + children: [ + { + path: 'departments', + module: DepartmentModule, + }, + { + path: 'staffs', + module: StaffsModule, + }, + ], + }, + ]), + DepartmentModule, + StaffsModule, + ], + controllers: [OrmMongoCompanyController], + providers: [Logger, OrmMongoCompanyService], +}) +export class OrmMongoCompanyModule {} diff --git a/src/ormmongocompany/ormmongocompany.service.ts b/src/ormmongocompany/ormmongocompany.service.ts new file mode 100644 index 0000000..54ff069 --- /dev/null +++ b/src/ormmongocompany/ormmongocompany.service.ts @@ -0,0 +1,6 @@ +import { Injectable, Logger } from '@nestjs/common'; + +@Injectable() +export class OrmMongoCompanyService { + constructor(private readonly logger: Logger) {} +} diff --git a/src/ormmongocompany/staffs/README.md b/src/ormmongocompany/staffs/README.md new file mode 100644 index 0000000..1a77713 --- /dev/null +++ b/src/ormmongocompany/staffs/README.md @@ -0,0 +1,10 @@ +# Staffs 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/ormmongocompany/staffs/dto/staff-create.dto.ts b/src/ormmongocompany/staffs/dto/staff-create.dto.ts new file mode 100644 index 0000000..a677a26 --- /dev/null +++ b/src/ormmongocompany/staffs/dto/staff-create.dto.ts @@ -0,0 +1,36 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString, IsNumber, IsOptional } from 'class-validator'; +export class StaffCreateDto { + // NOTE: Since the id is auto-inc, so no id for the creation + // id: number; + + @ApiProperty({ + type: String, + name: 'name', + description: 'Staff name', + required: true, + }) + @IsString() + @IsNotEmpty() + name: string; + + @ApiProperty({ + type: Number, + name: 'staffCode', + description: 'Staff code/ID', + required: false, + }) + @IsNumber() + @IsOptional() + staffCode: number; + + @ApiProperty({ + type: Number, + name: 'departmentId', + description: 'Department ID', + required: false, + }) + @IsNumber() + @IsOptional() + departmentId: number; +} diff --git a/src/ormmongocompany/staffs/dto/staff-update.dto.ts b/src/ormmongocompany/staffs/dto/staff-update.dto.ts new file mode 100644 index 0000000..8b9efc8 --- /dev/null +++ b/src/ormmongocompany/staffs/dto/staff-update.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { StaffCreateDto } from './staff-create.dto'; + +export class StaffUpdateDto extends PartialType(StaffCreateDto) {} diff --git a/src/ormmongocompany/staffs/entities/staff.entity.ts b/src/ormmongocompany/staffs/entities/staff.entity.ts new file mode 100644 index 0000000..e235615 --- /dev/null +++ b/src/ormmongocompany/staffs/entities/staff.entity.ts @@ -0,0 +1,67 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Department } from 'src/mysqlcompany/department/entities/department.entity'; +import { + Column, + Entity, + JoinColumn, + JoinTable, + ManyToOne, + PrimaryGeneratedColumn, +} from 'typeorm'; + +@Entity('staffs') +@Entity() +export class Staff { + @ApiProperty({ + type: Number, + name: 'id', + description: 'id of staff, auto-incremented', + required: true, + }) + @PrimaryGeneratedColumn('increment') + id: number; + + @ApiProperty({ + type: String, + name: 'name', + description: 'Staff name', + required: true, + }) + @Column({ + type: 'varchar', + }) + name: string; + + @ApiProperty({ + type: Number, + name: 'staffCode', + description: 'Staff code/ID', + required: false, + }) + @Column({ + name: 'staff_code', + type: 'varchar', + length: 10, + nullable: true, + }) + staffCode: number; + + @ApiProperty({ + type: Number, + name: 'departmentId', + description: 'Department ID', + required: false, + }) + @Column({ + // Match existing column naming convention + name: 'department_id', + type: 'tinyint', + unsigned: true, + nullable: true, + }) + departmentId: number; + + @ManyToOne(() => Department) + @JoinColumn({ name: 'department_id' }) + department: Department; +} diff --git a/src/ormmongocompany/staffs/staffs.controller.ts b/src/ormmongocompany/staffs/staffs.controller.ts new file mode 100644 index 0000000..63038dc --- /dev/null +++ b/src/ormmongocompany/staffs/staffs.controller.ts @@ -0,0 +1,76 @@ +import { + Controller, + Get, + Post, + Body, + Patch, + Param, + Delete, + NotFoundException, + HttpCode, + BadRequestException, +} from '@nestjs/common'; +import { ApiResponse } from '@nestjs/swagger'; +import { StaffsService } from './staffs.service'; +import { Staff } from './entities/staff.entity'; +import { StaffCreateDto } from './dto/staff-create.dto'; +import { StaffUpdateDto } from './dto/staff-update.dto'; +import { EntityNotFoundError } from 'typeorm'; + +@Controller() +export class StaffsController { + constructor(private readonly staffsService: StaffsService) {} + + private handleEntityNotFoundError(id: number, e: Error) { + if (e instanceof EntityNotFoundError) { + throw new NotFoundException(`Not found: id=${id}`); + } else { + throw e; + } + } + + @Get() + @ApiResponse({ type: [Staff] }) + findAll(): Promise { + return this.staffsService.findAll(); + } + + @Get(':id') + @ApiResponse({ type: Staff }) + async findOne(@Param('id') id: number): Promise { + // NOTE: the + operator returns the numeric representation of the object. + return this.staffsService.findOne(+id).catch((err) => { + this.handleEntityNotFoundError(id, err); + return null; + }); + } + + @Post() + @ApiResponse({ type: [Staff] }) + create(@Body() staffCreateDto: StaffCreateDto) { + return this.staffsService.create(staffCreateDto); + } + + @Patch(':id') + // Status 204 because no content will be returned + @HttpCode(204) + async update( + @Param('id') id: number, + @Body() staffUpdateDto: StaffUpdateDto, + ): Promise { + if (Object.keys(staffUpdateDto).length === 0) { + throw new BadRequestException('Request body is empty'); + } + await this.staffsService.update(+id, staffUpdateDto).catch((err) => { + this.handleEntityNotFoundError(id, err); + }); + } + + @Delete(':id') + @HttpCode(204) + async remove(@Param('id') id: number): Promise { + await this.staffsService.remove(+id).catch((err) => { + this.handleEntityNotFoundError(id, err); + }); + } +} diff --git a/src/ormmongocompany/staffs/staffs.module.ts b/src/ormmongocompany/staffs/staffs.module.ts new file mode 100644 index 0000000..c563e4d --- /dev/null +++ b/src/ormmongocompany/staffs/staffs.module.ts @@ -0,0 +1,15 @@ +import { Logger, Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { StaffsService } from './staffs.service'; +import { StaffsController } from './staffs.controller'; +import { Staff } from './entities/staff.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([Staff])], + controllers: [StaffsController], + providers: [Logger, StaffsService], + // 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 StaffsModule {} diff --git a/src/ormmongocompany/staffs/staffs.service.ts b/src/ormmongocompany/staffs/staffs.service.ts new file mode 100644 index 0000000..f1ace5e --- /dev/null +++ b/src/ormmongocompany/staffs/staffs.service.ts @@ -0,0 +1,64 @@ +import { Injectable, Logger, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { EntityNotFoundError, Repository } from 'typeorm'; +import { Staff } from './entities/staff.entity'; +import { StaffCreateDto } from './dto/staff-create.dto'; +import { StaffUpdateDto } from './dto/staff-update.dto'; + +@Injectable() +export class StaffsService { + constructor( + private readonly logger: Logger, + @InjectRepository(Staff) + private staffsRepository: Repository, + ) {} + + create(staffCreateDto: StaffCreateDto): Promise { + // Use Repository.create() will copy the values to destination object. + const staff = this.staffsRepository.create(staffCreateDto); + return this.staffsRepository.save(staff); + } + + findAll(): Promise { + return this.staffsRepository.find({ + relations: { + department: true, + }, + select: { + id: true, + name: true, + + departmentId: false, + department: { + id: false, + name: true, + }, + }, + }); + } + + findOne(id: number): Promise { + return this.staffsRepository.findOneOrFail({ where: { id } }); + } + + async update(id: number, staffUpdateDto: StaffUpdateDto): Promise { + await this.isExist(id); + await this.staffsRepository.update({ id }, staffUpdateDto); + } + + async remove(id: number): Promise { + await this.isExist(id); + await this.staffsRepository.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.staffsRepository.countBy({ id }); + if (cnt > 0) { + return; + } else { + return Promise.reject(new EntityNotFoundError(Staff, { where: { id } })); + } + } +} diff --git a/src/users/README.md b/src/users/README.md new file mode 100644 index 0000000..8e83a5c --- /dev/null +++ b/src/users/README.md @@ -0,0 +1,7 @@ +# User Module + +User module use Repository class to handle the 'database' representation. + +The Respository class uses a map object (in-memory) as a database. It is not persistence storage. + +I am trying to apply the Repository pattern concept here and abstract the data layer out of the business logic. \ No newline at end of file diff --git a/thunder-tests/thunderActivity.json b/thunder-tests/thunderActivity.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/thunder-tests/thunderActivity.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/thunder-tests/thunderCollection.json b/thunder-tests/thunderCollection.json new file mode 100644 index 0000000..66e711b --- /dev/null +++ b/thunder-tests/thunderCollection.json @@ -0,0 +1,9 @@ +[ + { + "_id": "8a14d631-7522-4fcd-a3b8-9a68166e5496", + "colName": "Local NestJS App", + "created": "2023-04-20T12:42:42.547Z", + "sortNum": 10000, + "folders": [] + } +] \ No newline at end of file diff --git a/thunder-tests/thunderEnvironment.json b/thunder-tests/thunderEnvironment.json new file mode 100644 index 0000000..8879d1b --- /dev/null +++ b/thunder-tests/thunderEnvironment.json @@ -0,0 +1,13 @@ +[ + { + "_id": "af943170-fabe-4f13-9725-3bd52e585aa5", + "name": "(Local Env)", + "default": false, + "global": true, + "local": true, + "sortNum": -1, + "created": "2023-04-20T12:43:21.880Z", + "modified": "2023-04-20T12:43:21.880Z", + "data": [] + } +] \ No newline at end of file diff --git a/thunder-tests/thunderclient.json b/thunder-tests/thunderclient.json new file mode 100644 index 0000000..9932580 --- /dev/null +++ b/thunder-tests/thunderclient.json @@ -0,0 +1,63 @@ +[ + { + "_id": "901d7d10-2ba0-481d-9230-182df284741e", + "colId": "8a14d631-7522-4fcd-a3b8-9a68166e5496", + "containerId": "", + "name": "Get Users", + "url": "http://localhost:3000/users", + "method": "GET", + "sortNum": 10000, + "created": "2023-04-20T12:42:42.550Z", + "modified": "2023-04-20T13:17:30.029Z", + "headers": [], + "params": [], + "tests": [] + }, + { + "_id": "db11f3a6-418b-43ae-9a1d-f2f667b5d0e6", + "colId": "8a14d631-7522-4fcd-a3b8-9a68166e5496", + "containerId": "", + "name": "Add User", + "url": "http://localhost:3000/users/", + "method": "POST", + "sortNum": 12500, + "created": "2023-04-20T12:47:21.164Z", + "modified": "2023-04-20T13:18:12.522Z", + "headers": [], + "params": [], + "body": { + "type": "json", + "raw": "{\n \"name\": \"{{#name}} - {{#number, 100, 999}}\"\n}", + "form": [] + }, + "tests": [] + }, + { + "_id": "858c3b4a-186d-4338-9dab-4eb00a450808", + "colId": "8a14d631-7522-4fcd-a3b8-9a68166e5496", + "containerId": "", + "name": "Get MySQL Comany Staffs", + "url": "http://localhost:3000/mysql-company/staffs/", + "method": "GET", + "sortNum": 15000, + "created": "2023-04-20T13:01:50.431Z", + "modified": "2023-04-20T13:17:36.471Z", + "headers": [], + "params": [], + "tests": [] + }, + { + "_id": "017f39a8-137d-4ab9-bb7e-f0dbef38c899", + "colId": "8a14d631-7522-4fcd-a3b8-9a68166e5496", + "containerId": "", + "name": "Get MySQL Comany Departments", + "url": "http://localhost:3000/ormmongo-company/staffs", + "method": "GET", + "sortNum": 17500, + "created": "2023-04-20T13:17:46.507Z", + "modified": "2023-04-20T15:00:49.647Z", + "headers": [], + "params": [], + "tests": [] + } +] \ No newline at end of file diff --git a/tsconfig.build.json b/tsconfig.build.json index f4cc41d..9d157b5 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,4 +1,10 @@ { "extends": "./tsconfig.json", - "exclude": ["node_modules", "test", "dist", "**/*spec.ts", "data"] -} + "exclude": [ + "node_modules", + "test", + "dist", + "**/*spec.ts", + "data" + ] +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index adb614c..12cebf4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,4 +18,4 @@ "forceConsistentCasingInFileNames": false, "noFallthroughCasesInSwitch": false } -} +} \ No newline at end of file