Add nestjs-paginate and more relationship

This commit is contained in:
BadBUTA 2023-04-22 22:12:32 +08:00
parent 7be9dd3e89
commit 2d492f3263
19 changed files with 484 additions and 63 deletions

View File

@ -30,6 +30,7 @@
"class-validator": "^0.14.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

@ -14,7 +14,7 @@ import { AllExceptionsFilter } from './logger/any-exception.filter';
import loggerMiddleware from './logger/logger.middleware';
import { TypeOrmModule } from '@nestjs/typeorm';
import { MySqlCompanyModule } from './mysqlcompany/mysqlcompany.module';
import { OrmMongoCompanyModule } from './ormmongocompany/ormmongocompany.module';
// import { OrmMongoCompanyModule } from './ormmongocompany/ormmongocompany.module';
@Module({
imports: [
@ -27,12 +27,12 @@ import { OrmMongoCompanyModule } from './ormmongocompany/ormmongocompany.module'
port: 23306,
username: 'root',
password: 'example',
database: 'example_nodejs_nest_crud_company',
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
@ -40,7 +40,7 @@ import { OrmMongoCompanyModule } from './ormmongocompany/ormmongocompany.module'
// ]),
UsersModule,
MySqlCompanyModule,
OrmMongoCompanyModule,
// OrmMongoCompanyModule,
],
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,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

@ -16,6 +16,7 @@ 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 {
@ -31,8 +32,8 @@ export class DepartmentController {
@Get()
@ApiResponse({ type: [Department] })
findAll(): Promise<Department[]> {
return this.departmentService.findAll();
findAll(@Paginate() query: PaginateQuery): Promise<Paginated<Department>> {
return this.departmentService.findAll(query);
}
@Get(':id')

View File

@ -4,27 +4,38 @@ 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 departmentRepository: Repository<Department>,
private departmentsRepository: 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);
const department = this.departmentsRepository.create(departmentCreateDto);
return this.departmentsRepository.save(department);
}
findAll(): Promise<Department[]> {
return this.departmentRepository.find();
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.departmentRepository.findOneOrFail({ where: { id } });
return this.departmentsRepository.findOneOrFail({ where: { id } });
}
async update(
@ -32,18 +43,18 @@ export class DepartmentService {
departmentUpdateDto: DepartmentUpdateDto,
): Promise<void> {
await this.isExist(id);
await this.departmentRepository.update({ id }, departmentUpdateDto);
await this.departmentsRepository.update({ id }, departmentUpdateDto);
}
async remove(id: number): Promise<void> {
await this.isExist(id);
await this.departmentRepository.delete(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.departmentRepository.countBy({ id });
const cnt = await this.departmentsRepository.countBy({ id });
if (cnt > 0) {
return;
} else {

View File

@ -1,17 +1,23 @@
import { ApiProperty } from '@nestjs/swagger';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Staff } from 'src/mysqlcompany/staffs/entities/staff.entity';
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
import {
Column,
Entity,
JoinColumn,
OneToMany,
PrimaryGeneratedColumn,
} from 'typeorm';
// @Entity('departments')
@Entity('departments')
export class Department {
@ApiProperty({
type: Number,
name: 'id',
name: '_id',
description: 'id of department, auto-incremented',
required: true,
})
@PrimaryGeneratedColumn('increment')
@PrimaryGeneratedColumn('increment', { name: '_id' })
id: number;
@ApiProperty({
@ -25,6 +31,13 @@ export class Department {
})
name: string;
// @OneToMany((type) => Staff, (staff) => staff.departmentId)
// staffs: Staff[];
@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

@ -4,6 +4,7 @@ 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: [
@ -23,11 +24,16 @@ import { RouterModule } from '@nestjs/core';
path: 'staffs',
module: StaffsModule,
},
{
path: 'contracts',
module: ContractsModule,
},
],
},
]),
DepartmentModule,
StaffsModule,
ContractsModule,
],
controllers: [MySqlCompanyController],
providers: [Logger, MySqlCompanyService],

View File

@ -1,4 +1,5 @@
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,
@ -6,11 +7,11 @@ import {
JoinColumn,
JoinTable,
ManyToOne,
OneToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
@Entity('staffs')
@Entity()
export class Staff {
@ApiProperty({
type: Number,
@ -41,27 +42,35 @@ export class Staff {
@Column({
name: 'staff_code',
type: 'varchar',
length: 10,
length: 4,
nullable: true,
})
staffCode: number;
@ApiProperty({
type: Number,
name: 'departmentId',
description: 'Department ID',
name: 'department',
description: 'The Department that the staff is undered',
type: () => Department,
required: false,
})
@Column({
// Match existing column naming convention
name: 'department_id',
type: 'tinyint',
unsigned: true,
nullable: true,
@ManyToOne(() => Department, (department) => department.staffs, {
nullable: false,
eager: true,
})
departmentId: number;
@ManyToOne(() => Department)
// @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

@ -16,6 +16,7 @@ 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 {
@ -31,8 +32,8 @@ export class StaffsController {
@Get()
@ApiResponse({ type: [Staff] })
findAll(): Promise<Staff[]> {
return this.staffsService.findAll();
findAll(@Paginate() query: PaginateQuery): Promise<Paginated<Staff>> {
return this.staffsService.findAll(query);
}
@Get(':id')

View File

@ -1,9 +1,16 @@
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
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 {
@ -19,22 +26,47 @@ export class StaffsService {
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,
},
},
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> {

View File

@ -37,13 +37,42 @@
"colId": "8a14d631-7522-4fcd-a3b8-9a68166e5496",
"containerId": "",
"name": "Get MySQL Comany Staffs",
"url": "http://localhost:3000/mysql-company/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-20T13:17:36.471Z",
"modified": "2023-04-22T14:09:13.798Z",
"headers": [],
"params": [],
"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": []
},
{
@ -51,13 +80,61 @@
"colId": "8a14d631-7522-4fcd-a3b8-9a68166e5496",
"containerId": "",
"name": "Get MySQL Comany Departments",
"url": "http://localhost:3000/ormmongo-company/staffs",
"url": "http://localhost:3000/mysql-company/departments/",
"method": "GET",
"sortNum": 17500,
"created": "2023-04-20T13:17:46.507Z",
"modified": "2023-04-20T15:00:49.647Z",
"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": []
}
]

View File

@ -3863,6 +3863,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"