temp commit, not functioning
This commit is contained in:
parent
7fc284c2c0
commit
7be9dd3e89
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"typescript.format.enable": false,
|
"typescript.format.enable": false,
|
||||||
"javascript.format.enable": false,
|
"javascript.format.enable": false,
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"thunder-client.saveToWorkspace": true
|
||||||
}
|
}
|
72
README.md
72
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. docker-compose setup for local development
|
||||||
1. Minimal API implementation (version, healthcheck)
|
1. Minimal API implementation (version, healthcheck)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Local development using docker-compose and attach with VSCode
|
## Local development using docker-compose and attach with VSCode
|
||||||
|
|
||||||
### Preparation
|
### 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`
|
1. To use the VSCode debugger, click the debug and launch the `Debug Nest Framework`
|
||||||
(See `launch.json`) for details
|
(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
|
## Swagger
|
||||||
|
|
||||||
The Nest Swagger framework is added into this project. For details, please see [Nestjs OpenAPI](https://docs.nestjs.com/openapi/introduction) documentation.
|
The Nest Swagger framework is added into this project. For details, please see [Nestjs OpenAPI](https://docs.nestjs.com/openapi/introduction) documentation.
|
||||||
|
@ -17,7 +17,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
MARIADB_ROOT_PASSWORD: example
|
MARIADB_ROOT_PASSWORD: example
|
||||||
ports:
|
ports:
|
||||||
- "3306:3306"
|
- "23306:3306"
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/mysql:/var/lib/mysql
|
- ./data/mysql:/var/lib/mysql
|
||||||
# Without this, will get error when start up in WSL2
|
# Without this, will get error when start up in WSL2
|
||||||
|
@ -13,8 +13,8 @@ import { UsersModule } from './users/users.module';
|
|||||||
import { AllExceptionsFilter } from './logger/any-exception.filter';
|
import { AllExceptionsFilter } from './logger/any-exception.filter';
|
||||||
import loggerMiddleware from './logger/logger.middleware';
|
import loggerMiddleware from './logger/logger.middleware';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { StaffsModule } from './staffs/staffs.module';
|
import { MySqlCompanyModule } from './mysqlcompany/mysqlcompany.module';
|
||||||
import { DepartmentModule } from './department/department.module';
|
import { OrmMongoCompanyModule } from './ormmongocompany/ormmongocompany.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -24,10 +24,10 @@ import { DepartmentModule } from './department/department.module';
|
|||||||
TypeOrmModule.forRoot({
|
TypeOrmModule.forRoot({
|
||||||
type: 'mysql',
|
type: 'mysql',
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 3306,
|
port: 23306,
|
||||||
username: 'root',
|
username: 'root',
|
||||||
password: 'example',
|
password: 'example',
|
||||||
database: 'example_nodejs_nest_crud',
|
database: 'example_nodejs_nest_crud_company',
|
||||||
// entities: ['dist/modules/**/*.mysql.entity{.ts,.js}'],
|
// entities: ['dist/modules/**/*.mysql.entity{.ts,.js}'],
|
||||||
autoLoadEntities: true,
|
autoLoadEntities: true,
|
||||||
// IMPORTANT: disable sync
|
// IMPORTANT: disable sync
|
||||||
@ -35,9 +35,12 @@ import { DepartmentModule } from './department/department.module';
|
|||||||
// synchronize: true,
|
// synchronize: true,
|
||||||
logging: true,
|
logging: true,
|
||||||
}),
|
}),
|
||||||
|
// Router resigration for module (2nd level) will be declared inside the module
|
||||||
|
// RouterModule.register([
|
||||||
|
// ]),
|
||||||
UsersModule,
|
UsersModule,
|
||||||
StaffsModule,
|
MySqlCompanyModule,
|
||||||
DepartmentModule,
|
OrmMongoCompanyModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [
|
providers: [
|
||||||
|
7
src/mysqlcompany/README.md
Normal file
7
src/mysqlcompany/README.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# MysqlComany Module
|
||||||
|
|
||||||
|
MySQL-Comany module uses TypeORM + MySQL
|
||||||
|
|
||||||
|
This module demonstrates database relationships, including:
|
||||||
|
- One-to-many
|
||||||
|
- Many-to-many
|
@ -17,7 +17,7 @@ import { DepartmentCreateDto } from './dto/department-create.dto';
|
|||||||
import { DepartmentUpdateDto } from './dto/department-update.dto';
|
import { DepartmentUpdateDto } from './dto/department-update.dto';
|
||||||
import { EntityNotFoundError } from 'typeorm';
|
import { EntityNotFoundError } from 'typeorm';
|
||||||
|
|
||||||
@Controller('department')
|
@Controller()
|
||||||
export class DepartmentController {
|
export class DepartmentController {
|
||||||
constructor(private readonly departmentService: DepartmentService) {}
|
constructor(private readonly departmentService: DepartmentService) {}
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { EntityNotFoundError, Repository } from 'typeorm';
|
import { EntityNotFoundError, Repository } from 'typeorm';
|
||||||
import { Department } from './entities/department.entity';
|
import { Department } from './entities/department.entity';
|
@ -1,9 +1,9 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
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';
|
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
|
||||||
// @Entity('departments')
|
// @Entity('departments')
|
||||||
@Entity()
|
@Entity('departments')
|
||||||
export class Department {
|
export class Department {
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
type: Number,
|
type: Number,
|
7
src/mysqlcompany/mysqlcompany.controller.ts
Normal file
7
src/mysqlcompany/mysqlcompany.controller.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { Controller } from '@nestjs/common';
|
||||||
|
import { MySqlCompanyService } from './mysqlcompany.service';
|
||||||
|
|
||||||
|
@Controller()
|
||||||
|
export class MySqlCompanyController {
|
||||||
|
constructor(private readonly mySqlCompanyService: MySqlCompanyService) {}
|
||||||
|
}
|
35
src/mysqlcompany/mysqlcompany.module.ts
Normal file
35
src/mysqlcompany/mysqlcompany.module.ts
Normal file
@ -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 {}
|
6
src/mysqlcompany/mysqlcompany.service.ts
Normal file
6
src/mysqlcompany/mysqlcompany.service.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MySqlCompanyService {
|
||||||
|
constructor(private readonly logger: Logger) {}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Department } from 'src/department/entities/department.entity';
|
import { Department } from 'src/mysqlcompany/department/entities/department.entity';
|
||||||
import {
|
import {
|
||||||
Column,
|
Column,
|
||||||
Entity,
|
Entity,
|
||||||
@ -9,7 +9,7 @@ import {
|
|||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
|
||||||
// @Entity('staffs')
|
@Entity('staffs')
|
||||||
@Entity()
|
@Entity()
|
||||||
export class Staff {
|
export class Staff {
|
||||||
@ApiProperty({
|
@ApiProperty({
|
@ -17,7 +17,7 @@ import { StaffCreateDto } from './dto/staff-create.dto';
|
|||||||
import { StaffUpdateDto } from './dto/staff-update.dto';
|
import { StaffUpdateDto } from './dto/staff-update.dto';
|
||||||
import { EntityNotFoundError } from 'typeorm';
|
import { EntityNotFoundError } from 'typeorm';
|
||||||
|
|
||||||
@Controller('staffs')
|
@Controller()
|
||||||
export class StaffsController {
|
export class StaffsController {
|
||||||
constructor(private readonly staffsService: StaffsService) {}
|
constructor(private readonly staffsService: StaffsService) {}
|
||||||
|
|
7
src/ormmongocompany/README.md
Normal file
7
src/ormmongocompany/README.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# MysqlComany Module
|
||||||
|
|
||||||
|
MySQL-Comany module uses TypeORM + MySQL
|
||||||
|
|
||||||
|
This module demonstrates database relationships, including:
|
||||||
|
- One-to-many
|
||||||
|
- Many-to-many
|
10
src/ormmongocompany/department/README.md
Normal file
10
src/ormmongocompany/department/README.md
Normal file
@ -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
|
78
src/ormmongocompany/department/department.controller.ts
Normal file
78
src/ormmongocompany/department/department.controller.ts
Normal file
@ -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<Department[]> {
|
||||||
|
return this.departmentService.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@ApiResponse({ type: Department })
|
||||||
|
async findOne(@Param('id') id: number): Promise<Department | null> {
|
||||||
|
// 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<void> {
|
||||||
|
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<void> {
|
||||||
|
await this.departmentService.remove(+id).catch((err) => {
|
||||||
|
this.handleEntityNotFoundError(id, err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
15
src/ormmongocompany/department/department.module.ts
Normal file
15
src/ormmongocompany/department/department.module.ts
Normal file
@ -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 {}
|
55
src/ormmongocompany/department/department.service.ts
Normal file
55
src/ormmongocompany/department/department.service.ts
Normal file
@ -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<Department>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
create(departmentCreateDto: DepartmentCreateDto): Promise<Department> {
|
||||||
|
// Use Repository.create() will copy the values to destination object.
|
||||||
|
const department = this.departmentRepository.create(departmentCreateDto);
|
||||||
|
return this.departmentRepository.save(department);
|
||||||
|
}
|
||||||
|
|
||||||
|
findAll(): Promise<Department[]> {
|
||||||
|
return this.departmentRepository.find();
|
||||||
|
}
|
||||||
|
|
||||||
|
findOne(id: number): Promise<Department> {
|
||||||
|
return this.departmentRepository.findOneOrFail({ where: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(
|
||||||
|
id: number,
|
||||||
|
departmentUpdateDto: DepartmentUpdateDto,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.isExist(id);
|
||||||
|
await this.departmentRepository.update({ id }, departmentUpdateDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(id: number): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
const cnt = await this.departmentRepository.countBy({ id });
|
||||||
|
if (cnt > 0) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
return Promise.reject(
|
||||||
|
new EntityNotFoundError(Department, { where: { id } }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
src/ormmongocompany/department/dto/department-create.dto.ts
Normal file
16
src/ormmongocompany/department/dto/department-create.dto.ts
Normal file
@ -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;
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
import { PartialType } from '@nestjs/swagger';
|
||||||
|
import { DepartmentCreateDto } from './department-create.dto';
|
||||||
|
|
||||||
|
export class DepartmentUpdateDto extends PartialType(DepartmentCreateDto) {}
|
30
src/ormmongocompany/department/entities/department.entity.ts
Normal file
30
src/ormmongocompany/department/entities/department.entity.ts
Normal file
@ -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[];
|
||||||
|
}
|
9
src/ormmongocompany/ormmongocompany.controller.ts
Normal file
9
src/ormmongocompany/ormmongocompany.controller.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { Controller } from '@nestjs/common';
|
||||||
|
import { OrmMongoCompanyService } from './ormmongocompany.service';
|
||||||
|
|
||||||
|
@Controller()
|
||||||
|
export class OrmMongoCompanyController {
|
||||||
|
constructor(
|
||||||
|
private readonly ormMongoCompanyService: OrmMongoCompanyService,
|
||||||
|
) {}
|
||||||
|
}
|
35
src/ormmongocompany/ormmongocompany.module.ts
Normal file
35
src/ormmongocompany/ormmongocompany.module.ts
Normal file
@ -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 {}
|
6
src/ormmongocompany/ormmongocompany.service.ts
Normal file
6
src/ormmongocompany/ormmongocompany.service.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class OrmMongoCompanyService {
|
||||||
|
constructor(private readonly logger: Logger) {}
|
||||||
|
}
|
10
src/ormmongocompany/staffs/README.md
Normal file
10
src/ormmongocompany/staffs/README.md
Normal file
@ -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
|
36
src/ormmongocompany/staffs/dto/staff-create.dto.ts
Normal file
36
src/ormmongocompany/staffs/dto/staff-create.dto.ts
Normal file
@ -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;
|
||||||
|
}
|
4
src/ormmongocompany/staffs/dto/staff-update.dto.ts
Normal file
4
src/ormmongocompany/staffs/dto/staff-update.dto.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { PartialType } from '@nestjs/swagger';
|
||||||
|
import { StaffCreateDto } from './staff-create.dto';
|
||||||
|
|
||||||
|
export class StaffUpdateDto extends PartialType(StaffCreateDto) {}
|
67
src/ormmongocompany/staffs/entities/staff.entity.ts
Normal file
67
src/ormmongocompany/staffs/entities/staff.entity.ts
Normal file
@ -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;
|
||||||
|
}
|
76
src/ormmongocompany/staffs/staffs.controller.ts
Normal file
76
src/ormmongocompany/staffs/staffs.controller.ts
Normal file
@ -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<Staff[]> {
|
||||||
|
return this.staffsService.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@ApiResponse({ type: Staff })
|
||||||
|
async findOne(@Param('id') id: number): Promise<Staff | null> {
|
||||||
|
// 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<void> {
|
||||||
|
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<void> {
|
||||||
|
await this.staffsService.remove(+id).catch((err) => {
|
||||||
|
this.handleEntityNotFoundError(id, err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
15
src/ormmongocompany/staffs/staffs.module.ts
Normal file
15
src/ormmongocompany/staffs/staffs.module.ts
Normal file
@ -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 {}
|
64
src/ormmongocompany/staffs/staffs.service.ts
Normal file
64
src/ormmongocompany/staffs/staffs.service.ts
Normal file
@ -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<Staff>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
create(staffCreateDto: StaffCreateDto): Promise<Staff> {
|
||||||
|
// Use Repository.create() will copy the values to destination object.
|
||||||
|
const staff = this.staffsRepository.create(staffCreateDto);
|
||||||
|
return this.staffsRepository.save(staff);
|
||||||
|
}
|
||||||
|
|
||||||
|
findAll(): Promise<Staff[]> {
|
||||||
|
return this.staffsRepository.find({
|
||||||
|
relations: {
|
||||||
|
department: true,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
|
||||||
|
departmentId: false,
|
||||||
|
department: {
|
||||||
|
id: false,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
findOne(id: number): Promise<Staff> {
|
||||||
|
return this.staffsRepository.findOneOrFail({ where: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: number, staffUpdateDto: StaffUpdateDto): Promise<void> {
|
||||||
|
await this.isExist(id);
|
||||||
|
await this.staffsRepository.update({ id }, staffUpdateDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(id: number): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
const cnt = await this.staffsRepository.countBy({ id });
|
||||||
|
if (cnt > 0) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
return Promise.reject(new EntityNotFoundError(Staff, { where: { id } }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7
src/users/README.md
Normal file
7
src/users/README.md
Normal file
@ -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.
|
1
thunder-tests/thunderActivity.json
Normal file
1
thunder-tests/thunderActivity.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
[]
|
9
thunder-tests/thunderCollection.json
Normal file
9
thunder-tests/thunderCollection.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"_id": "8a14d631-7522-4fcd-a3b8-9a68166e5496",
|
||||||
|
"colName": "Local NestJS App",
|
||||||
|
"created": "2023-04-20T12:42:42.547Z",
|
||||||
|
"sortNum": 10000,
|
||||||
|
"folders": []
|
||||||
|
}
|
||||||
|
]
|
13
thunder-tests/thunderEnvironment.json
Normal file
13
thunder-tests/thunderEnvironment.json
Normal file
@ -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": []
|
||||||
|
}
|
||||||
|
]
|
63
thunder-tests/thunderclient.json
Normal file
63
thunder-tests/thunderclient.json
Normal file
@ -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": []
|
||||||
|
}
|
||||||
|
]
|
@ -1,4 +1,10 @@
|
|||||||
{
|
{
|
||||||
"extends": "./tsconfig.json",
|
"extends": "./tsconfig.json",
|
||||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts", "data"]
|
"exclude": [
|
||||||
}
|
"node_modules",
|
||||||
|
"test",
|
||||||
|
"dist",
|
||||||
|
"**/*spec.ts",
|
||||||
|
"data"
|
||||||
|
]
|
||||||
|
}
|
@ -18,4 +18,4 @@
|
|||||||
"forceConsistentCasingInFileNames": false,
|
"forceConsistentCasingInFileNames": false,
|
||||||
"noFallthroughCasesInSwitch": false
|
"noFallthroughCasesInSwitch": false
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user