Compare commits

...

4 Commits

Author SHA1 Message Date
45a36cd17e Add ZGC 2023-04-25 01:26:47 +08:00
4e4301d18c Add Mongo 2023-04-23 22:04:03 +08:00
2d492f3263 Add nestjs-paginate and more relationship 2023-04-22 22:12:32 +08:00
7be9dd3e89 temp commit, not functioning 2023-04-20 23:18:31 +08:00
69 changed files with 2171 additions and 25 deletions

View File

@ -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
}

View File

@ -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.

View File

@ -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

View File

@ -28,8 +28,10 @@
"@nestjs/typeorm": "^9.0.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"mongo": "^0.1.0",
"mysql2": "^3.2.1",
"nest-winston": "^1.9.1",
"nestjs-paginate": "^8.1.3",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.2.0",
"typeorm": "^0.3.15",

View File

@ -13,8 +13,9 @@ 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 { ZgcModule } from './zgc/zgc.module';
// import { OrmMongoCompanyModule } from './ormmongocompany/ormmongocompany.module';
@Module({
imports: [
@ -22,22 +23,39 @@ import { DepartmentModule } from './department/department.module';
ConfigModule.forRoot(),
// For ORM-MySQL
TypeOrmModule.forRoot({
// ---- Mongo
// To set auth, run mongo shell: mongosh -u root -p example
// use boilerplate_nestjs_api_crud_company
// db.createUser( { user: "root", pwd: "example", roles: ["dbOwner"] })
// ---
// type: 'mongodb',
// host: 'localhost',
// port: 27017,
// database: 'boilerplate_nestjs_api_crud_company',
// username: 'root',
// password: 'example',
// ---- MYSQL:
type: 'mysql',
host: 'localhost',
port: 3306,
port: 23306,
username: 'root',
password: 'example',
database: 'example_nodejs_nest_crud',
database: 'boilerplate_nestjs_api_crud_company',
// ------
// entities: ['dist/modules/**/*.mysql.entity{.ts,.js}'],
autoLoadEntities: true,
// IMPORTANT: disable sync
synchronize: false,
// synchronize: true,
// synchronize: false,
synchronize: true,
logging: true,
}),
// Router resigration for module (2nd level) will be declared inside the module
// RouterModule.register([
// ]),
UsersModule,
StaffsModule,
DepartmentModule,
MySqlCompanyModule,
// OrmMongoCompanyModule,
ZgcModule,
],
controllers: [AppController],
providers: [

View File

@ -30,7 +30,8 @@ export class AllExceptionsFilter implements ExceptionFilter {
message = exception.message;
// trim the stack lines
if (exception.stack) {
stack = exception.stack.split('\n').slice(1, 3).join('\n');
// stack = exception.stack.split('\n').slice(1, 3).join('\n');
stack = exception.stack;
}
}
@ -55,15 +56,19 @@ export class AllExceptionsFilter implements ExceptionFilter {
this.logger.error(logMsg, stack, AllExceptionsFilter.name);
} else {
const requstBody =
Object.keys(body).length > 0 ? ` Body=${JSON.stringify(body)}` : '';
Object.keys(body).length > 0 ? `Body=${JSON.stringify(body)}` : '';
const rawHeadersStr =
Object.keys(rawHeaders).length > 0
? ` RawHeaders=${JSON.stringify(rawHeaders)}`
? `RawHeaders=${JSON.stringify(rawHeaders)}`
: '';
logMsg = `${message}\n${method.toLocaleUpperCase()} ${url}${requstBody}${rawHeadersStr}`;
this.logger.error(logMsg, stack, AllExceptionsFilter.name);
// this.logger.error(logMsg, stack, AllExceptionsFilter.name);
this.logger.error(message);
this.logger.debug(
`Request: ${method.toLocaleUpperCase()} ${url}\n${requstBody}\n${rawHeadersStr}\n${stack}`,
);
}
response.header('Content-Type', 'application/json; charset=utf-8');

View 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

View File

@ -0,0 +1,10 @@
# Contract 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

View File

@ -0,0 +1,77 @@
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
NotFoundException,
HttpCode,
BadRequestException,
} from '@nestjs/common';
import { ApiResponse } from '@nestjs/swagger';
import { ContractsService } from './contracts.service';
import { Contract } from './entities/contract.entity';
import { ContractCreateDto } from './dto/contract-create.dto';
import { ContractUpdateDto } from './dto/contract-update.dto';
import { EntityNotFoundError } from 'typeorm';
import { Paginate, PaginateQuery, Paginated } from 'nestjs-paginate';
@Controller()
export class ContractsController {
constructor(private readonly contractsService: ContractsService) {}
private handleEntityNotFoundError(id: number, e: Error) {
if (e instanceof EntityNotFoundError) {
throw new NotFoundException(`Not found: id=${id}`);
} else {
throw e;
}
}
@Get()
@ApiResponse({ type: [Contract] })
findAll(@Paginate() query: PaginateQuery): Promise<Paginated<Contract>> {
return this.contractsService.findAll(query);
}
@Get(':id')
@ApiResponse({ type: Contract })
async findOne(@Param('id') id: number): Promise<Contract | null> {
// NOTE: the + operator returns the numeric representation of the object.
return this.contractsService.findOne(+id).catch((err) => {
this.handleEntityNotFoundError(id, err);
return null;
});
}
@Post()
@ApiResponse({ type: [Contract] })
create(@Body() contractCreateDto: ContractCreateDto) {
return this.contractsService.create(contractCreateDto);
}
@Patch(':id')
// Status 204 because no content will be returned
@HttpCode(204)
async update(
@Param('id') id: number,
@Body() contractUpdateDto: ContractUpdateDto,
): Promise<void> {
if (Object.keys(contractUpdateDto).length === 0) {
throw new BadRequestException('Request body is empty');
}
await this.contractsService.update(+id, contractUpdateDto).catch((err) => {
this.handleEntityNotFoundError(id, err);
});
}
@Delete(':id')
@HttpCode(204)
async remove(@Param('id') id: number): Promise<void> {
await this.contractsService.remove(+id).catch((err) => {
this.handleEntityNotFoundError(id, err);
});
}
}

View File

@ -0,0 +1,15 @@
import { Logger, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ContractsService } from './contracts.service';
import { ContractsController } from './contracts.controller';
import { Contract } from './entities/contract.entity';
@Module({
imports: [TypeOrmModule.forFeature([Contract])],
controllers: [ContractsController],
providers: [Logger, ContractsService],
// 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 ContractsModule {}

View File

@ -0,0 +1,70 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { EntityNotFoundError, Repository } from 'typeorm';
import { Contract } from './entities/contract.entity';
import { ContractCreateDto } from './dto/contract-create.dto';
import { ContractUpdateDto } from './dto/contract-update.dto';
import {
FilterOperator,
FilterSuffix,
PaginateQuery,
Paginated,
paginate,
} from 'nestjs-paginate';
@Injectable()
export class ContractsService {
constructor(
private readonly logger: Logger,
@InjectRepository(Contract)
private contractsRepository: Repository<Contract>,
) {}
create(contractCreateDto: ContractCreateDto): Promise<Contract> {
// Use Repository.create() will copy the values to destination object.
const contract = this.contractsRepository.create(contractCreateDto);
return this.contractsRepository.save(contract);
}
findAll(query: PaginateQuery): Promise<Paginated<Contract>> {
return paginate(query, this.contractsRepository, {
relations: { staff: true },
sortableColumns: ['id', 'title', 'details', 'staff.name'],
select: ['id', 'title', 'details', 'staff.name'],
filterableColumns: {
title: [FilterOperator.EQ, FilterOperator.ILIKE, FilterSuffix.NOT],
// staff.name: [FilterOperator.EQ, FilterOperator.ILIKE, FilterSuffix.NOT],
},
});
}
findOne(id: number): Promise<Contract> {
return this.contractsRepository.findOneOrFail({ where: { id } });
}
async update(
id: number,
contractUpdateDto: ContractUpdateDto,
): Promise<void> {
await this.isExist(id);
await this.contractsRepository.update({ id }, contractUpdateDto);
}
async remove(id: number): Promise<void> {
await this.isExist(id);
await this.contractsRepository.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.contractsRepository.countBy({ id });
if (cnt > 0) {
return;
} else {
return Promise.reject(
new EntityNotFoundError(Contract, { where: { id } }),
);
}
}
}

View File

@ -0,0 +1,30 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString, IsNumber, IsOptional } from 'class-validator';
import { Staff } from 'src/mysqlcompany/staffs/entities/staff.entity';
export class ContractCreateDto {
// NOTE: Since the id is auto-inc, so no id for the creation
// id: number;
@ApiProperty({
description: 'Contract name',
})
@IsString()
@IsNotEmpty()
title: string;
@ApiProperty({
description: 'Contract details',
})
@IsString()
@IsOptional()
details: string;
@ApiProperty({
type: Staff,
name: 'departmentId',
description: 'Department ID',
required: false,
})
@IsOptional()
staff: Staff;
}

View File

@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { ContractCreateDto } from './contract-create.dto';
export class ContractUpdateDto extends PartialType(ContractCreateDto) {}

View File

@ -0,0 +1,52 @@
import { ApiProperty } from '@nestjs/swagger';
import { Staff } from 'src/mysqlcompany/staffs/entities/staff.entity';
import {
Column,
Entity,
JoinColumn,
OneToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
@Entity('contracts')
export class Contract {
@ApiProperty({
type: Number,
name: '_id',
description: 'id of contract, auto-incremented',
})
// https://github.com/ppetzold/nestjs-paginate/issues/518
// when use 'id', will cause duplicated column issue
@PrimaryGeneratedColumn('increment', { name: '_id' })
id: number;
@ApiProperty({
type: String,
description: 'Contract title',
})
@Column({
type: 'varchar',
length: 255,
})
title: string;
@ApiProperty({
type: String,
name: 'details',
description: 'Contract details',
required: false,
})
@Column({
type: 'text',
nullable: true,
})
details: string;
@ApiProperty({
name: 'staff',
description: 'The staff of this contract',
type: () => Staff,
})
@OneToOne(() => Staff)
staff: Staff;
}

View File

@ -0,0 +1,79 @@
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';
import { Paginate, PaginateQuery, Paginated } from 'nestjs-paginate';
@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(@Paginate() query: PaginateQuery): Promise<Paginated<Department>> {
return this.departmentService.findAll(query);
}
@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);
});
}
}

View File

@ -0,0 +1,66 @@
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';
import { PaginateQuery, Paginated, paginate } from 'nestjs-paginate';
@Injectable()
export class DepartmentService {
constructor(
private readonly logger: Logger,
@InjectRepository(Department)
private departmentsRepository: Repository<Department>,
) {}
create(departmentCreateDto: DepartmentCreateDto): Promise<Department> {
// Use Repository.create() will copy the values to destination object.
const department = this.departmentsRepository.create(departmentCreateDto);
return this.departmentsRepository.save(department);
}
findAll(query: PaginateQuery): Promise<Paginated<Department>> {
return paginate(query, this.departmentsRepository, {
relations: { staffs: true },
sortableColumns: ['id', 'name'],
// select: ['id', 'name', 'staffs.name'],
// searchableColumns: ['name', 'department.name'],
// filterableColumns: {
// 'home.pillows.color': [FilterOperator.EQ],
// },
});
// return this.departmentsRepository.find();
}
findOne(id: number): Promise<Department> {
return this.departmentsRepository.findOneOrFail({ where: { id } });
}
async update(
id: number,
departmentUpdateDto: DepartmentUpdateDto,
): Promise<void> {
await this.isExist(id);
await this.departmentsRepository.update({ id }, departmentUpdateDto);
}
async remove(id: number): Promise<void> {
await this.isExist(id);
await this.departmentsRepository.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.departmentsRepository.countBy({ id });
if (cnt > 0) {
return;
} else {
return Promise.reject(
new EntityNotFoundError(Department, { where: { id } }),
);
}
}
}

View File

@ -0,0 +1,43 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Staff } from 'src/mysqlcompany/staffs/entities/staff.entity';
import {
Column,
Entity,
JoinColumn,
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', { name: '_id' })
id: number;
@ApiProperty({
type: String,
name: 'name',
description: 'Department name',
required: true,
})
@Column({
type: 'varchar',
})
name: string;
@OneToMany(() => Staff, (staff) => staff.department)
@ApiProperty({
name: 'staffs',
description: 'The staffs under this department',
type: [Staff],
})
@ApiPropertyOptional()
// @JoinColumn({ name: 'id', referencedColumnName: 'department_id' })
staffs: Staff[];
}

View File

@ -0,0 +1,7 @@
import { Controller } from '@nestjs/common';
import { MySqlCompanyService } from './mysqlcompany.service';
@Controller()
export class MySqlCompanyController {
constructor(private readonly mySqlCompanyService: MySqlCompanyService) {}
}

View File

@ -0,0 +1,41 @@
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';
import { ContractsModule } from './contract/contracts.module';
@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,
},
{
path: 'contracts',
module: ContractsModule,
},
],
},
]),
DepartmentModule,
StaffsModule,
ContractsModule,
],
controllers: [MySqlCompanyController],
providers: [Logger, MySqlCompanyService],
})
export class MySqlCompanyModule {}

View File

@ -0,0 +1,6 @@
import { Injectable, Logger } from '@nestjs/common';
@Injectable()
export class MySqlCompanyService {
constructor(private readonly logger: Logger) {}
}

View File

@ -0,0 +1,76 @@
import { ApiProperty } from '@nestjs/swagger';
import { Contract } from 'src/mysqlcompany/contract/entities/contract.entity';
import { Department } from 'src/mysqlcompany/department/entities/department.entity';
import {
Column,
Entity,
JoinColumn,
JoinTable,
ManyToOne,
OneToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
@Entity('staffs')
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: 4,
nullable: true,
})
staffCode: number;
@ApiProperty({
name: 'department',
description: 'The Department that the staff is undered',
type: () => Department,
required: false,
})
@ManyToOne(() => Department, (department) => department.staffs, {
nullable: false,
eager: true,
})
// @JoinColumn({ name: 'department_id', referencedColumnName: 'id' })
@JoinColumn({ name: 'department_id' })
department: Department;
@ApiProperty({
name: 'contract',
description: 'The staff contract',
type: () => Contract,
})
@OneToOne(() => Contract, (contract) => contract.staff, {
nullable: false,
eager: true,
cascade: true,
})
@JoinColumn({ name: 'contract_id' })
contract: Contract;
}

View File

@ -0,0 +1,77 @@
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';
import { Paginate, PaginateQuery, Paginated } from 'nestjs-paginate';
@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(@Paginate() query: PaginateQuery): Promise<Paginated<Staff>> {
return this.staffsService.findAll(query);
}
@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);
});
}
}

View File

@ -0,0 +1,96 @@
import { Injectable, Logger } 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';
import {
FilterOperator,
FilterSuffix,
PaginateQuery,
Paginated,
paginate,
} from 'nestjs-paginate';
@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(query: PaginateQuery): Promise<Paginated<Staff>> {
return paginate(query, this.staffsRepository, {
// loadEagerRelations: true,
// relations: { department: true, contract: true },
relations: ['contract', 'department'],
// sortableColumns: ['id', 'name', 'staffCode', 'department.name'],
sortableColumns: ['id', 'name', 'department.name'],
select: [
'id',
'name',
'staffCode',
'department.id',
'department.name',
'contract.id',
'contract.title',
'contract.details',
],
// searchableColumns: ['name', 'department.name'],
// filterableColumns: {
// 'department.name': [
// FilterOperator.EQ,
// FilterOperator.ILIKE,
// FilterSuffix.NOT,
// ],
// },
});
// 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 } }));
}
}
}

View 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

View 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

View File

@ -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) {}

View 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 {}

View File

@ -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';

View 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;
}

View File

@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { DepartmentCreateDto } from './department-create.dto';
export class DepartmentUpdateDto extends PartialType(DepartmentCreateDto) {}

View File

@ -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,

View File

@ -0,0 +1,9 @@
import { Controller } from '@nestjs/common';
import { OrmMongoCompanyService } from './ormmongocompany.service';
@Controller()
export class OrmMongoCompanyController {
constructor(
private readonly ormMongoCompanyService: OrmMongoCompanyService,
) {}
}

View 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 {}

View File

@ -0,0 +1,6 @@
import { Injectable, Logger } from '@nestjs/common';
@Injectable()
export class OrmMongoCompanyService {
constructor(private readonly logger: Logger) {}
}

View 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

View 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;
}

View File

@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { StaffCreateDto } from './staff-create.dto';
export class StaffUpdateDto extends PartialType(StaffCreateDto) {}

View File

@ -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({

View File

@ -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) {}

View 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 {}

7
src/users/README.md Normal file
View 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.

View File

@ -0,0 +1,25 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNumber, IsOptional } from 'class-validator';
export class CategoryCreateDto {
// NOTE: Since the id is auto-inc, so no id for the creation
// id: number;
// ----------------------------------------
@ApiProperty({
type: String,
name: 'name',
description: 'Group name',
required: true,
})
name: string;
// ----------------------------------------
@ApiProperty({
type: Number,
name: 'displayOrder',
description: 'Display order',
required: false,
})
@IsNumber()
@IsOptional()
displayOrder: number | 0;
}

View File

@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CategoryCreateDto } from './category-create.dto';
export class CategoryUpdateDto extends PartialType(CategoryCreateDto) {}

View File

@ -0,0 +1,25 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNumber, IsOptional } from 'class-validator';
export class GroupCreateDto {
// NOTE: Since the id is auto-inc, so no id for the creation
// id: number;
// ----------------------------------------
@ApiProperty({
type: String,
name: 'name',
description: 'Group name',
required: true,
})
name: string;
// ----------------------------------------
@ApiProperty({
type: Number,
name: 'displayOrder',
description: 'Display order',
required: false,
})
@IsNumber()
@IsOptional()
displayOrder: number | 0;
}

View File

@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { GroupCreateDto } from './group-create.dto';
export class GroupUpdateDto extends PartialType(GroupCreateDto) {}

View File

@ -0,0 +1,25 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNumber, IsOptional } from 'class-validator';
export class ZoneCreateDto {
// NOTE: Since the id is auto-inc, so no id for the creation
// id: number;
// ----------------------------------------
@ApiProperty({
type: String,
name: 'name',
description: 'Zone name',
required: true,
})
name: string;
// ----------------------------------------
@ApiProperty({
type: Number,
name: 'displayOrder',
description: 'Display order',
required: false,
})
@IsNumber()
@IsOptional()
displayOrder: number | 0;
}

View File

@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { ZoneCreateDto } from './zone-create.dto';
export class ZoneUpdateDto extends PartialType(ZoneCreateDto) {}

View File

@ -0,0 +1,62 @@
import { ApiProperty } from '@nestjs/swagger';
import {
Column,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
import { Group } from './group.entity';
@Entity('categories')
export class Category {
// ----------------------------------------
@ApiProperty({
type: Number,
name: 'id',
description: 'id of category, auto-incremented',
required: true,
})
@PrimaryGeneratedColumn('increment')
id: number;
// ----------------------------------------
@ApiProperty({
type: String,
name: 'name',
description: 'Category name',
required: true,
})
@Column({
type: 'varchar',
})
name: string;
// ----------------------------------------
@ApiProperty({
type: Number,
name: 'displayOrder',
description: 'Display order',
required: false,
})
@Column({
type: 'tinyint',
default: 0,
})
displayOrder: number;
// ----------------------------------------
@ApiProperty({
name: 'group',
description: 'Parent node: group',
type: () => Group,
})
// @ApiPropertyOptional()
@ManyToOne(() => Group, (group) => group.categories, {
nullable: false,
eager: true,
})
// @JoinColumn({ name: 'group_id', referencedColumnName: 'id' })
@JoinColumn({ name: 'xref_group_id' })
group: Group;
}

View File

@ -0,0 +1,73 @@
import { ApiProperty } from '@nestjs/swagger';
import {
Column,
Entity,
JoinColumn,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
} from 'typeorm';
import { Zone } from './zone.entity';
import { Category } from './category.entity';
@Entity('groups')
export class Group {
// ----------------------------------------
@ApiProperty({
type: Number,
name: 'id',
description: 'id of group, auto-incremented',
required: true,
})
@PrimaryGeneratedColumn('increment')
id: number;
// ----------------------------------------
@ApiProperty({
type: String,
name: 'name',
description: 'Group name',
required: true,
})
@Column({
type: 'varchar',
})
name: string;
// ----------------------------------------
@ApiProperty({
type: Number,
name: 'displayOrder',
description: 'Display order',
required: false,
})
@Column({
type: 'tinyint',
default: 0,
})
displayOrder: number;
// ----------------------------------------
@ApiProperty({
name: 'zone',
description: 'Parent node: zone',
type: () => Zone,
})
// @ApiPropertyOptional()
@ManyToOne(() => Zone, (zone) => zone.groups, {
nullable: false,
eager: true,
})
@JoinColumn({ name: 'xref_zone_id' })
zone: Zone;
// ----------------------------------------
@ApiProperty({
name: 'categories',
description: 'Children: categories',
type: () => [Category],
})
// @ApiPropertyOptional()
@OneToMany(() => Category, (category) => category.group)
categories: Category[];
}

View File

@ -0,0 +1,51 @@
import { ApiProperty } from '@nestjs/swagger';
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
import { Group } from './group.entity';
@Entity('zones')
export class Zone {
// ----------------------------------------
@ApiProperty({
type: Number,
name: 'id',
description: 'id of zone, auto-incremented',
required: true,
})
@PrimaryGeneratedColumn('increment')
id: number;
// ----------------------------------------
@ApiProperty({
type: String,
name: 'name',
description: 'Zone name',
required: true,
})
@Column({
type: 'varchar',
})
name: string;
// ----------------------------------------
@ApiProperty({
type: Number,
name: 'displayOrder',
description: 'Display order',
required: false,
})
@Column({
type: 'tinyint',
default: 0,
})
displayOrder: number;
// ----------------------------------------
@ApiProperty({
name: 'groups',
description: 'Children: groups',
type: () => [Group],
})
// @ApiPropertyOptional()
@OneToMany(() => Group, (group) => group.zone)
groups: Group[];
}

181
src/zgc/zgc.controller.ts Normal file
View File

@ -0,0 +1,181 @@
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
NotFoundException,
HttpCode,
BadRequestException,
} from '@nestjs/common';
import { ApiResponse } from '@nestjs/swagger';
import { EntityNotFoundError } from 'typeorm';
import { Paginate, PaginateQuery, Paginated } from 'nestjs-paginate';
import { ZgcService } from './zgc.service';
import { ZoneCreateDto } from './dto/zone-create.dto';
import { ZoneUpdateDto } from './dto/zone-update.dto';
import { Zone } from './entities/zone.entity';
import { GroupCreateDto } from './dto/group-create.dto';
import { GroupUpdateDto } from './dto/group-update.dto';
import { Group } from './entities/group.entity';
import { CategoryCreateDto } from './dto/category-create.dto';
import { CategoryUpdateDto } from './dto/category-update.dto';
import { Category } from './entities/category.entity';
@Controller('zgc')
export class ZgcController {
constructor(private readonly service: ZgcService) {}
private handleEntityNotFoundError(id: number, e: Error) {
if (e instanceof EntityNotFoundError) {
throw new NotFoundException(`Not found: id=${id}`);
} else {
throw e;
}
}
// -------------------- Zone --------------------
// --
@Get('/zones')
@ApiResponse({ type: [Zone] })
findZones(@Paginate() query: PaginateQuery): Promise<Paginated<Zone>> {
return this.service.findZones(query);
}
@Get('/zones/:id')
@ApiResponse({ type: Zone })
async findZone(@Param('id') id: number): Promise<Zone | null> {
// NOTE: the + operator returns the numeric representation of the object.
return this.service.findZone(+id).catch((err) => {
this.handleEntityNotFoundError(id, err);
return null;
});
}
@Post('/zones')
@ApiResponse({ type: [Zone] })
createZone(@Body() zoneCreateDto: ZoneCreateDto) {
return this.service.createZone(zoneCreateDto);
}
@Patch('/zones/:id')
// Status 204 because no content will be returned
@HttpCode(204)
async updateZone(
@Param('id') id: number,
@Body() zoneUpdateDto: ZoneUpdateDto,
): Promise<void> {
if (Object.keys(zoneUpdateDto).length === 0) {
throw new BadRequestException('Request body is empty');
}
await this.service.updateZone(+id, zoneUpdateDto).catch((err) => {
this.handleEntityNotFoundError(id, err);
});
}
@Delete('/zones/:id')
@HttpCode(204)
async deleteZone(@Param('id') id: number): Promise<void> {
await this.service.deleteZone(+id).catch((err) => {
this.handleEntityNotFoundError(id, err);
});
}
// -------------------- Group --------------------
// --
@Get('/groups')
@ApiResponse({ type: [Group] })
findGroups(@Paginate() query: PaginateQuery): Promise<Paginated<Group>> {
return this.service.findGroups(query);
}
@Get('/groups/:id')
@ApiResponse({ type: Group })
async findGroup(@Param('id') id: number): Promise<Group | null> {
// NOTE: the + operator returns the numeric representation of the object.
return this.service.findGroup(+id).catch((err) => {
this.handleEntityNotFoundError(id, err);
return null;
});
}
@Post('/groups')
@ApiResponse({ type: [Group] })
createGroup(@Body() zoneCreateDto: GroupCreateDto) {
return this.service.createGroup(zoneCreateDto);
}
@Patch('/groups/:id')
// Status 204 because no content will be returned
@HttpCode(204)
async updateGroup(
@Param('id') id: number,
@Body() zoneUpdateDto: GroupUpdateDto,
): Promise<void> {
if (Object.keys(zoneUpdateDto).length === 0) {
throw new BadRequestException('Request body is empty');
}
await this.service.updateGroup(+id, zoneUpdateDto).catch((err) => {
this.handleEntityNotFoundError(id, err);
});
}
@Delete('/groups/:id')
@HttpCode(204)
async deleteGroup(@Param('id') id: number): Promise<void> {
await this.service.deleteGroup(+id).catch((err) => {
this.handleEntityNotFoundError(id, err);
});
}
// -------------------- Category --------------------
// --
@Get('/categories')
@ApiResponse({ type: [Category] })
findCategories(
@Paginate() query: PaginateQuery,
): Promise<Paginated<Category>> {
return this.service.findCategories(query);
}
@Get('/categories/:id')
@ApiResponse({ type: Category })
async findCategory(@Param('id') id: number): Promise<Category | null> {
// NOTE: the + operator returns the numeric representation of the object.
return this.service.findCategory(+id).catch((err) => {
this.handleEntityNotFoundError(id, err);
return null;
});
}
@Post('/categories')
@ApiResponse({ type: [Category] })
createCategory(@Body() categoryCreateDto: CategoryCreateDto) {
return this.service.createCategory(categoryCreateDto);
}
@Patch('/categories/:id')
// Status 204 because no content will be returned
@HttpCode(204)
async updateCategory(
@Param('id') id: number,
@Body() categoryUpdateDto: CategoryUpdateDto,
): Promise<void> {
if (Object.keys(categoryUpdateDto).length === 0) {
throw new BadRequestException('Request body is empty');
}
await this.service.updateCategory(+id, categoryUpdateDto).catch((err) => {
this.handleEntityNotFoundError(id, err);
});
}
@Delete('/categories/:id')
@HttpCode(204)
async deleteCategory(@Param('id') id: number): Promise<void> {
await this.service.deleteCategory(+id).catch((err) => {
this.handleEntityNotFoundError(id, err);
});
}
}

17
src/zgc/zgc.module.ts Normal file
View File

@ -0,0 +1,17 @@
import { Logger, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ZgcService } from './zgc.service';
import { ZgcController } from './zgc.controller';
import { Zone } from './entities/zone.entity';
import { Group } from './entities/group.entity';
import { Category } from './entities/category.entity';
@Module({
imports: [TypeOrmModule.forFeature([Zone, Group, Category])],
controllers: [ZgcController],
providers: [Logger, ZgcService],
// 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 ZgcModule {}

221
src/zgc/zgc.service.ts Normal file
View File

@ -0,0 +1,221 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { EntityNotFoundError, Repository } from 'typeorm';
import { PaginateQuery, Paginated, paginate } from 'nestjs-paginate';
import { ZoneCreateDto } from './dto/zone-create.dto';
import { ZoneUpdateDto } from './dto/zone-update.dto';
import { Zone } from './entities/zone.entity';
import { GroupCreateDto } from './dto/group-create.dto';
import { GroupUpdateDto } from './dto/group-update.dto';
import { Group } from './entities/group.entity';
import { Category } from './entities/category.entity';
import { CategoryCreateDto } from './dto/category-create.dto';
import { CategoryUpdateDto } from './dto/category-update.dto';
@Injectable()
export class ZgcService {
constructor(
private readonly logger: Logger,
@InjectRepository(Zone)
private zonesRepository: Repository<Zone>,
@InjectRepository(Group)
private groupsRepository: Repository<Group>,
@InjectRepository(Category)
private categoriesRepository: Repository<Category>,
) {}
// -------------------- Zone --------------------
// --
createZone(zoneCreateDto: ZoneCreateDto): Promise<Zone> {
// Use Repository.create() will copy the values to destination object.
const zone = this.zonesRepository.create(zoneCreateDto);
return this.zonesRepository.save(zone);
}
findZones(query: PaginateQuery): Promise<Paginated<Zone>> {
return paginate(query, this.zonesRepository, {
// loadEagerRelations: true,
// relations: { groups: true },
relations: ['groups'],
sortableColumns: ['id', 'name', 'displayOrder'],
defaultSortBy: [
['displayOrder', 'ASC'],
['id', 'ASC'],
['groups.displayOrder', 'ASC'],
['groups.id', 'ASC'],
],
select: [
'id',
'name',
'displayOrder',
'groups.id',
'groups.name',
'groups.displayOrder',
// 'groups.categories.id',
// 'groups.categories.name',
// 'groups.categories.displayOrder',
],
// searchableColumns: ['id', 'name'],
// filterableColumns: {
// 'id': [
// FilterOperator.EQ,
// FilterSuffix.NOT,
// ],
// 'name': [
// FilterOperator.EQ,
// FilterOperator.ILIKE,
// FilterSuffix.NOT,
// ],
// },
});
}
findZone(id: number): Promise<Zone> {
return this.zonesRepository.findOneOrFail({ where: { id } });
}
async updateZone(id: number, zoneUpdateDto: ZoneUpdateDto): Promise<void> {
await this.isZoneExist(id);
await this.zonesRepository.update({ id }, zoneUpdateDto);
}
async deleteZone(id: number): Promise<void> {
await this.isZoneExist(id);
await this.zonesRepository.delete(id);
}
// Helper function: Check if the entity exist.
// If entity does not exsit, return the Promise.reject()
private async isZoneExist(id: number): Promise<void> {
const cnt = await this.zonesRepository.countBy({ id });
if (cnt > 0) {
return;
} else {
return Promise.reject(new EntityNotFoundError(Zone, { where: { id } }));
}
}
// -------------------- Group --------------------
// --
createGroup(groupCreateDto: GroupCreateDto): Promise<Group> {
// Use Repository.create() will copy the values to destination object.
const group = this.groupsRepository.create(groupCreateDto);
return this.groupsRepository.save(group);
}
findGroups(query: PaginateQuery): Promise<Paginated<Group>> {
return paginate(query, this.groupsRepository, {
// loadEagerRelations: true,
// relations: { groups: true },
relations: ['zone', 'categories'],
sortableColumns: ['id', 'name', 'displayOrder'],
select: [
'id',
'name',
'displayOrder',
'zone.id',
'zone.name',
'categories.id',
'categories.name',
'categories.displayOrder',
],
// searchableColumns: ['id', 'name'],
// filterableColumns: {
// 'id': [
// FilterOperator.EQ,
// FilterSuffix.NOT,
// ],
// 'name': [
// FilterOperator.EQ,
// FilterOperator.ILIKE,
// FilterSuffix.NOT,
// ],
// },
});
}
findGroup(id: number): Promise<Group> {
return this.groupsRepository.findOneOrFail({ where: { id } });
}
async updateGroup(id: number, groupUpdateDto: GroupUpdateDto): Promise<void> {
await this.isGroupExist(id);
await this.groupsRepository.update({ id }, groupUpdateDto);
}
async deleteGroup(id: number): Promise<void> {
await this.isGroupExist(id);
await this.groupsRepository.delete(id);
}
// Helper function: Check if the entity exist.
// If entity does not exsit, return the Promise.reject()
private async isGroupExist(id: number): Promise<void> {
const cnt = await this.groupsRepository.countBy({ id });
if (cnt > 0) {
return;
} else {
return Promise.reject(new EntityNotFoundError(Group, { where: { id } }));
}
}
// -------------------- Category --------------------
// --
createCategory(cateogyCreateDto: CategoryCreateDto): Promise<Category> {
// Use Repository.create() will copy the values to destination object.
const cateogy = this.categoriesRepository.create(cateogyCreateDto);
return this.categoriesRepository.save(cateogy);
}
findCategories(query: PaginateQuery): Promise<Paginated<Category>> {
return paginate(query, this.categoriesRepository, {
// loadEagerRelations: true,
// relations: { groups: true },
relations: ['group'],
sortableColumns: ['id', 'name', 'displayOrder'],
select: ['id', 'name', 'displayOrder', 'group.id', 'group.name'],
searchableColumns: ['id', 'name'],
// filterableColumns: {
// 'id': [
// FilterOperator.EQ,
// FilterSuffix.NOT,
// ],
// 'name': [
// FilterOperator.EQ,
// FilterOperator.ILIKE,
// FilterSuffix.NOT,
// ],
// },
});
}
findCategory(id: number): Promise<Category> {
return this.categoriesRepository.findOneOrFail({ where: { id } });
}
async updateCategory(
id: number,
categoryUpdateDto: CategoryUpdateDto,
): Promise<void> {
await this.isCategoryExist(id);
await this.categoriesRepository.update({ id }, categoryUpdateDto);
}
async deleteCategory(id: number): Promise<void> {
await this.isCategoryExist(id);
await this.categoriesRepository.delete(id);
}
// Helper function: Check if the entity exist.
// If entity does not exsit, return the Promise.reject()
private async isCategoryExist(id: number): Promise<void> {
const cnt = await this.categoriesRepository.countBy({ id });
if (cnt > 0) {
return;
} else {
return Promise.reject(
new EntityNotFoundError(Category, { where: { id } }),
);
}
}
}

View File

@ -0,0 +1 @@
[]

View File

@ -0,0 +1,38 @@
[
{
"_id": "8a14d631-7522-4fcd-a3b8-9a68166e5496",
"colName": "Local NestJS App",
"created": "2023-04-20T12:42:42.547Z",
"sortNum": 10000,
"folders": []
},
{
"_id": "1d5b8a54-f8ba-480a-96bd-8faf7d7cb4ba",
"colName": "Local NestJS App - ZGC",
"created": "2023-04-24T16:41:46.695Z",
"sortNum": 20000,
"folders": [
{
"_id": "10f85be8-b759-4364-8949-5338d329703a",
"name": "Zone",
"containerId": "",
"created": "2023-04-24T16:41:57.395Z",
"sortNum": 10000
},
{
"_id": "b4b39ee3-0da2-4ec0-b2d4-ace00dd8280d",
"name": "Group",
"containerId": "",
"created": "2023-04-24T17:04:34.098Z",
"sortNum": 20000
},
{
"_id": "a16fc112-ae03-47e0-8eb2-eaf97bcbe36f",
"name": "Categories",
"containerId": "",
"created": "2023-04-24T17:04:47.131Z",
"sortNum": 30000
}
]
}
]

View 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": []
}
]

View File

@ -0,0 +1,260 @@
[
{
"_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/?limit=1&select=id,name,staffCode,department.name,contract.title",
"method": "GET",
"sortNum": 15000,
"created": "2023-04-20T13:01:50.431Z",
"modified": "2023-04-22T14:09:13.798Z",
"headers": [],
"params": [
{
"name": "limit",
"value": "1",
"isPath": false
},
{
"name": "select",
"value": "id,name,staffCode,department.name,contract.title",
"isPath": false
},
{
"name": "sortBy",
"value": "department.name:DESC",
"isDisabled": true,
"isPath": false
},
{
"name": "sortBy",
"value": "name:ASC",
"isDisabled": true,
"isPath": false
},
{
"name": "filter.department.name",
"value": "$ilike:Brown",
"isDisabled": true,
"isPath": false
}
],
"tests": []
},
{
"_id": "017f39a8-137d-4ab9-bb7e-f0dbef38c899",
"colId": "8a14d631-7522-4fcd-a3b8-9a68166e5496",
"containerId": "",
"name": "Get MySQL Comany Departments",
"url": "http://localhost:3000/mysql-company/departments/",
"method": "GET",
"sortNum": 17500,
"created": "2023-04-20T13:17:46.507Z",
"modified": "2023-04-22T08:37:35.984Z",
"headers": [],
"params": [],
"tests": []
},
{
"_id": "e4920ab9-8552-46d1-9431-a3aaaa187cce",
"colId": "8a14d631-7522-4fcd-a3b8-9a68166e5496",
"containerId": "",
"name": "Create MySQL Comany Staff",
"url": "http://localhost:3000/mysql-company/staffs/",
"method": "POST",
"sortNum": 16250,
"created": "2023-04-22T04:22:32.924Z",
"modified": "2023-04-22T13:45:24.200Z",
"headers": [
{
"name": "Content-Type",
"value": "application/json"
}
],
"params": [],
"body": {
"type": "json",
"raw": "{\n \"name\":\"{{#name}}\",\n \"department\":{ \"id\":2 },\n \"contract\": { \"title\":\"ZYX\" }\n}",
"form": []
},
"tests": []
},
{
"_id": "97997fc9-e49a-4a96-871b-6292c92c7567",
"colId": "8a14d631-7522-4fcd-a3b8-9a68166e5496",
"containerId": "",
"name": "Create MySQL Comany Department",
"url": "http://localhost:3000/mysql-company/departments/",
"method": "POST",
"sortNum": 16875,
"created": "2023-04-22T07:37:29.132Z",
"modified": "2023-04-22T07:38:11.328Z",
"headers": [
{
"name": "Content-Type",
"value": "application/json"
}
],
"params": [],
"body": {
"type": "json",
"raw": "{\n \"name\": \"Dept: {{#name}}\"\n}",
"form": []
},
"tests": []
},
{
"_id": "ee150c28-07bd-4792-b6a8-6551e3e6169b",
"colId": "1d5b8a54-f8ba-480a-96bd-8faf7d7cb4ba",
"containerId": "10f85be8-b759-4364-8949-5338d329703a",
"name": "Get Zones",
"url": "http://localhost:3000/zgc/zones/?limit=3",
"method": "GET",
"sortNum": 10000,
"created": "2023-04-24T16:42:10.562Z",
"modified": "2023-04-24T17:24:36.920Z",
"headers": [],
"params": [
{
"name": "limit",
"value": "3",
"isPath": false
}
],
"tests": []
},
{
"_id": "16119da3-4863-4650-86f5-7fac55269cea",
"colId": "1d5b8a54-f8ba-480a-96bd-8faf7d7cb4ba",
"containerId": "b4b39ee3-0da2-4ec0-b2d4-ace00dd8280d",
"name": "Get Groups",
"url": "http://localhost:3000/zgc/groups/",
"method": "GET",
"sortNum": 10000,
"created": "2023-04-24T16:44:34.640Z",
"modified": "2023-04-24T17:04:37.342Z",
"headers": [],
"params": [],
"tests": []
},
{
"_id": "27716e9b-e0a7-47d4-9fb6-ede300275c5c",
"colId": "1d5b8a54-f8ba-480a-96bd-8faf7d7cb4ba",
"containerId": "a16fc112-ae03-47e0-8eb2-eaf97bcbe36f",
"name": "Get Categories",
"url": "http://localhost:3000/zgc/categories/",
"method": "GET",
"sortNum": 10000,
"created": "2023-04-24T16:47:23.727Z",
"modified": "2023-04-24T17:04:50.733Z",
"headers": [],
"params": [],
"tests": []
},
{
"_id": "d9148936-364a-4e2d-aeb4-ab26adb10d63",
"colId": "1d5b8a54-f8ba-480a-96bd-8faf7d7cb4ba",
"containerId": "a16fc112-ae03-47e0-8eb2-eaf97bcbe36f",
"name": "Add Category",
"url": "http://localhost:3000/zgc/categories/",
"method": "POST",
"sortNum": 20000,
"created": "2023-04-24T17:04:59.718Z",
"modified": "2023-04-24T17:18:30.236Z",
"headers": [
{
"name": "Content-Type",
"value": "application/json"
}
],
"params": [],
"body": {
"type": "json",
"raw": "{\n \"name\": \"Cat {{#name}} - {{#number, 100, 999}}\",\n \"group\": {\n \"id\": 1\n }\n}",
"form": []
},
"tests": []
},
{
"_id": "55d7d81e-4779-4d78-aa71-dfa120c9d741",
"colId": "1d5b8a54-f8ba-480a-96bd-8faf7d7cb4ba",
"containerId": "10f85be8-b759-4364-8949-5338d329703a",
"name": "Add Zone",
"url": "http://localhost:3000/zgc/zones/",
"method": "POST",
"sortNum": 20000,
"created": "2023-04-24T17:12:09.466Z",
"modified": "2023-04-24T17:15:32.575Z",
"headers": [
{
"name": "Content-Type",
"value": "application/json"
}
],
"params": [],
"body": {
"type": "json",
"raw": "{\n \"name\": \"Zone {{#name}} - {{#number, 100, 999}}\"\n}",
"form": []
},
"tests": []
},
{
"_id": "594e6cf8-5eb6-49cb-b446-4848c36461be",
"colId": "1d5b8a54-f8ba-480a-96bd-8faf7d7cb4ba",
"containerId": "b4b39ee3-0da2-4ec0-b2d4-ace00dd8280d",
"name": "Add Group",
"url": "http://localhost:3000/zgc/groups/",
"method": "POST",
"sortNum": 20000,
"created": "2023-04-24T17:15:59.691Z",
"modified": "2023-04-24T17:24:11.934Z",
"headers": [
{
"name": "Content-Type",
"value": "application/json"
}
],
"params": [],
"body": {
"type": "json",
"raw": "{\n \"name\": \"Group {{#name}} - {{#number, 100, 999}}\",\n \"zone\": {\n \"id\": 7\n }\n}",
"form": []
},
"tests": []
}
]

View File

@ -1,4 +1,10 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts", "data"]
}
"exclude": [
"node_modules",
"test",
"dist",
"**/*spec.ts",
"data"
]
}

View File

@ -18,4 +18,4 @@
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false
}
}
}

110
yarn.lock
View File

@ -1091,6 +1091,19 @@
resolved "https://registry.npmjs.org/@types/validator/-/validator-13.7.14.tgz"
integrity sha512-J6OAed6rhN6zyqL9Of6ZMamhlsOEU/poBVvbHr/dKOYKTeuYYMlDkMv+b6UUV0o2i0tw73cgyv/97WTWaUl0/g==
"@types/webidl-conversions@*":
version "7.0.0"
resolved "https://registry.yarnpkg.com/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz#2b8e60e33906459219aa587e9d1a612ae994cfe7"
integrity sha512-xTE1E+YF4aWPJJeUzaZI5DRntlkY3+BCVJi0axFptnjGmAoWxkyREIh/XMrfxVLejwQxMCfDXdICo0VLxThrog==
"@types/whatwg-url@^8.2.1":
version "8.2.2"
resolved "https://registry.yarnpkg.com/@types/whatwg-url/-/whatwg-url-8.2.2.tgz#749d5b3873e845897ada99be4448041d4cc39e63"
integrity sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==
dependencies:
"@types/node" "*"
"@types/webidl-conversions" "*"
"@types/yargs-parser@*":
version "21.0.0"
resolved "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz"
@ -1655,6 +1668,11 @@ bser@2.1.1:
dependencies:
node-int64 "^0.4.0"
bson@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/bson/-/bson-5.2.0.tgz#c81d35dd30e2798203e5422a639780ea98dd25ba"
integrity sha512-HevkSpDbpUfsrHWmWiAsNavANKYIErV2ePXllp1bwq5CDreAaFVj6RVlZpJnxK4WWDCJ/5jMUpaY6G526q3Hjg==
buffer-from@^1.0.0:
version "1.1.2"
resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz"
@ -2932,6 +2950,11 @@ interpret@^1.0.0:
resolved "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz"
integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==
ip@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da"
integrity sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==
ipaddr.js@1.9.1:
version "1.9.1"
resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz"
@ -3675,6 +3698,11 @@ memfs@^3.4.1:
dependencies:
fs-monkey "^1.0.3"
memory-pager@^1.0.2:
version "1.5.0"
resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.5.0.tgz#d8751655d22d384682741c972f2c3d6dfa3e66b5"
integrity sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==
merge-descriptors@1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz"
@ -3773,6 +3801,32 @@ mkdirp@^2.1.3:
resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.6.tgz"
integrity sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==
mongo@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/mongo/-/mongo-0.1.0.tgz#c8af0f8df98d4894b772b37342987c3becf3718c"
integrity sha512-2MPq+GCNKhah0V/g/HIQI/S1h6Ycd87KPuXAITkeXWT6wncvABFxOaXdzCKlRvLSQRUkDimllrRrhoHUTD8usg==
dependencies:
mongodb "*"
mongodb-connection-string-url@^2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz#57901bf352372abdde812c81be47b75c6b2ec5cf"
integrity sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==
dependencies:
"@types/whatwg-url" "^8.2.1"
whatwg-url "^11.0.0"
mongodb@*:
version "5.3.0"
resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-5.3.0.tgz#9bef3ff35511a66fb7d9aafb7b06112787138db1"
integrity sha512-Wy/sbahguL8c3TXQWXmuBabiLD+iVmz+tOgQf+FwkCjhUIorqbAxRbbz00g4ZoN4sXIPwpAlTANMaGRjGGTikQ==
dependencies:
bson "^5.2.0"
mongodb-connection-string-url "^2.6.0"
socks "^2.7.1"
optionalDependencies:
saslprep "^1.0.3"
ms@2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz"
@ -3863,6 +3917,13 @@ nest-winston@^1.9.1:
dependencies:
fast-safe-stringify "^2.1.1"
nestjs-paginate@^8.1.3:
version "8.1.3"
resolved "https://registry.yarnpkg.com/nestjs-paginate/-/nestjs-paginate-8.1.3.tgz#89c6a4fe88e3b2c885c0ca1daa115e889981d4d0"
integrity sha512-dQ7VKfssee/FkIf4aI409x1LBkFV2KdKoVDVv6HIwQmVmSeF4n8mkOVLWNh2QSFjqe9IN15ezrrFGzuWvVooTQ==
dependencies:
lodash "^4.17.21"
node-abort-controller@^3.0.1:
version "3.1.1"
resolved "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz"
@ -4179,7 +4240,7 @@ pump@^3.0.0:
end-of-stream "^1.1.0"
once "^1.3.1"
punycode@^2.1.0:
punycode@^2.1.0, punycode@^2.1.1:
version "2.3.0"
resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz"
integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==
@ -4400,6 +4461,13 @@ safe-stable-stringify@^2.3.1:
resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
saslprep@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/saslprep/-/saslprep-1.0.3.tgz#4c02f946b56cf54297e347ba1093e7acac4cf226"
integrity sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==
dependencies:
sparse-bitfield "^3.0.3"
schema-utils@^3.1.0, schema-utils@^3.1.1:
version "3.1.1"
resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz"
@ -4527,6 +4595,19 @@ slash@^3.0.0:
resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz"
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
smart-buffer@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae"
integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==
socks@^2.7.1:
version "2.7.1"
resolved "https://registry.yarnpkg.com/socks/-/socks-2.7.1.tgz#d8e651247178fde79c0663043e07240196857d55"
integrity sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==
dependencies:
ip "^2.0.0"
smart-buffer "^4.2.0"
source-map-support@0.5.13:
version "0.5.13"
resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz"
@ -4558,6 +4639,13 @@ sourcemap-codec@^1.4.8:
resolved "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz"
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
sparse-bitfield@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz#ff4ae6e68656056ba4b3e792ab3334d38273ca11"
integrity sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==
dependencies:
memory-pager "^1.0.2"
sprintf-js@~1.0.2:
version "1.0.3"
resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz"
@ -4801,6 +4889,13 @@ toidentifier@1.0.1:
resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz"
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
tr46@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-3.0.0.tgz#555c4e297a950617e8eeddef633c87d4d9d6cbf9"
integrity sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==
dependencies:
punycode "^2.1.1"
tr46@~0.0.3:
version "0.0.3"
resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz"
@ -5066,6 +5161,11 @@ webidl-conversions@^3.0.0:
resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz"
integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
webidl-conversions@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a"
integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==
webpack-node-externals@3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz"
@ -5106,6 +5206,14 @@ webpack@5.76.2:
watchpack "^2.4.0"
webpack-sources "^3.2.3"
whatwg-url@^11.0.0:
version "11.0.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-11.0.0.tgz#0a849eebb5faf2119b901bb76fd795c2848d4018"
integrity sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==
dependencies:
tr46 "^3.0.0"
webidl-conversions "^7.0.0"
whatwg-url@^5.0.0:
version "5.0.0"
resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz"