preface

In my last article, I used mongoDB database. Considering that many enterprises use mysql database, I changed the database to mysql and adjusted the functions of framework and CRUD. Such as adjusting directory structure, adding filters, interceptors, encapsulating paging, encapsulating return data structure, table design, and so on we do unified framework adjustments before the project.

Directory to adjust

Create new common (public code) and modules (business-specific code) respectively under the SRC directory

SRC ├─ App.Controll.spec.Ts ├─ app.Controll.ts ├─ app.module.Ts ├─ App.Service.Ts ├─ Common │ ├─ Common / / public dto and dto │ │ ├ ─ dto │ │ │ ├ ─ base. The dtos. The ts / / public class │ │ │ ├ ─ pagination. The dtos. The ts / / paging │ │ │ └ ─ result. The dtos. The ts/return to │ / results │ └ ─ the entity │ │ └ ─ base. The entity. The ts / / public entities │ ├ ─ config / / environment configuration │ ├ ─ exception / / exception encapsulation │ │ └ ─ error. Code. Ts / / exception class code │ ├─ ├─ ├─ ├─ garbage. Filters │ └ ─ validate. Pipe. Ts / / validation │ └ ─ utils/tools/encapsulation │ ├ ─ the convert. Utils. Ts │ ├ ─ cryptogram. Util. Ts │ ├ ─ page. Util. Ts │ └ ─ │ ├─ ├─ list.├. Exercises ├─ ├─ list.├. Exercises ├─ list.├ Update - user. Dto. Ts ├ ─ entities │ └ ─ user. The entity. The ts ├ ─ users. The controller. The ts ├ ─ users. The module. The ts └ ─ users. The service. The tsCopy the code

TypeORM integration

Connect to mysql database

For integration with SQL and NoSQL databases, Nest provides the @NestJS/Typeorm package. Nest uses TypeORM because it is TypeScript’s most mature object-relational mapper (ORM). Because it’s written in TypeScript, it integrates well with the Nest framework.

To get started, we first install the dependencies we need.

$ npm install --save @nestjs/typeorm typeorm mysql2
Copy the code

After the installation process is complete, we can import TypeOrmModule into AppModule.

app.module.ts

import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; @Module({ imports: [ TypeOrmModule.forRoot({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'root', database: 'nestjs', autoLoadEntities: true, // Use this configuration to import entities synchronize: true, }), ], }) export class AppModule {}Copy the code

Json is not supported because I have hot loading enabled locally. If you don’t have hot loading enabled, try the following methods:

Instead of passing the configuration object to forRoot(), we can create ormconfig.json.

{
  "type": "mysql",
  "host": "localhost",
  "port": 3306,
  "username": "root",
  "password": "root",
  "database": "test",
  "entities": ["dist/**/*.entity{.ts,.js}"],
  "synchronize": true
}
Copy the code

We can then call forRoot() without any options:

app.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [TypeOrmModule.forRoot()],
})
export class AppModule {}
Copy the code

Static global paths (e.g. Dist /**/*.entity{.ts,.js}) are not available for Webpack hot overloading.

Adding environment Configuration

Each project has a different environment configuration file, so when we switch environments and change some configurations, we only need to change the configuration file of each environment.

Under SRC, create directory config. Under config, create index.ts, env.development. Ts, env.production

Env.development. ts // Development environment configuration

Export default {// Basic service configuration SERVICE_CONFIG: {// Port port: 3000,}, // Database configuration DATABASE_CONFIG: {type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'root', database: 'nestjs', autoLoadEntities: true, synchronize: true, }, };Copy the code

Env.production. ts // Production environment configuration

Export default {// Basic service configuration SERVICE_CONFIG: {// Port port: 3000,}, // Database configuration DATABASE_CONFIG: {type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'root', database: 'nestjs_prod', autoLoadEntities: true, synchronize: true, }, };Copy the code

index.ts

import development from './env.development';
import production from './env.production';

const configs = {
  development,
  production,
};

const env = configs[process.env.NODE_ENV || 'development'];
export { env };
Copy the code

app.module.ts

import { Module } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ValidationPipe } from './common/pipe/validate.pipe';
import { UsersModule } from './modules/users/users.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { env } from './common/config';
@Module({
  imports: [TypeOrmModule.forRoot(env.DATABASE_CONFIG), UsersModule],
  controllers: [AppController],
  providers: [
    AppService,
    {
      provide: APP_PIPE,
      useClass: ValidationPipe,
    },
  ],
})
export class AppModule {}
Copy the code

package.json

NODE_ENV=production NODE_ENV=production NODE_ENV=production

“start:prod”: “NODE_ENV=production nest start –watch”

To encapsulate the CRUD

Table design and encapsulation of basic fields

When designing a database table, there are several common fields (primary key ID, creator, createTime, updater, updateTime, delFlag, update number version). So we wrap these public fields, and then the other Dtos and Entity inherit this class, respectively.

Tip:Database table names and fields are named in lowercase and separated by underscores. Generally, when we design data deletion, it is logical deletion

Create files SRC => Commcon =>common=>entity=>base.entity.ts SRC => Commcon =>common=> DTO => Base.dto.ts

base.entity.ts

import { Column, PrimaryGeneratedColumn, UpdateDateColumn, CreateDateColumn, VersionColumn, } from 'typeorm'; Export abstract class Base {// primary key id@primaryGeneratedColumn () ID: number; @createDatecolumn ({name: 'create_time'}) createTime: Date; @column () // Creator: string; @updateDatecolumn ({name: 'update_time'}) updateTime: Date; @column () // Update person updater: string; @column ({default: 0, select: false, name: 'del_flag',}) delFlag: number; @versioncolumn ({select: false,}) version: number; }Copy the code

Special column

There are several special column types that can be used:

  • @CreateDateColumnIs a special column that automatically inserts the date for the entity. You do not need to set this column, the value will be set automatically.
  • @UpdateDateColumnIs a special column for each invocation of the entity manager or repositorysave, automatically updates the entity date. You do not need to set this column, the value will be set automatically.
  • @VersionColumnIs a special column for each invocation of the entity manager or repositorysaveTo automatically grow the entity version (increment number). You do not need to set this column, the value will be set automatically.

base.dto.ts

import { ApiHideProperty } from '@nestjs/swagger'; Export class BaseDTO {/** * createTime * @example Date */ readonly createTime: Date; /** * creator */ creator: string; /** * updateTime * @date */ readonly updateTime: Date; /** * @string */ updater: string; @apihideProperty () delFlag: number; /** * @apihideProperty () version: number; }Copy the code

SRC =>commcon=>common=>dto=>pagination.dto.ts; SRC =>commcon=>common=>dto=>pagination.dto.ts

pagination.dto.ts

import { ApiProperty } from '@nestjs/swagger'; import { IsOptional, Matches } from 'class-validator'; import { regPositiveOrEmpty } from 'src/common/utils/regex.util'; Export class PaginationDTO {/** * 表 示 * @matches (regPositiveOrEmpty) {message: 'page cannot be less than 0'}) @apiProperty ({description: 'page'}) readonly page? : number; @isoptional () @matches (regPositiveOrEmpty, {message: 'pageSize cannot be less than 0'}) @APIProperty ({description: 'pageSize'}) Readonly pageSize? : number; /** * @pages: pages; /** * @example 100 */ total: number; // Data records: any; }Copy the code

There is some regular verification involved here, so we create a new file and default some regular expressions SRC =>common=>utils=>regex.util.ts

regex.util.ts`

/** * export const regPositive = /^[1-9]\d*$/; // export const regPositive = /^[1-9]\d*$/; / / non-zero positive integer or empty export const regPositiveOrEmpty = / \ s * | ^ \ [1-9] d * $/; Export const regMobileCN = /^1\d{10}$/g;Copy the code

Adjusting the business layer

Dao and Entity inherit from Base

create-user.dto.ts

import { ApiProperty } from '@nestjs/swagger'; import { BaseDTO } from 'src/common/common/dto/base.dto'; Export class CreateUserDto extends BaseDTO {@APIProperty ({description: 'userName ', example:' userName '}) userName: string; @apiproperty ({description: 'realName'}) realName: string; @apiProperty ({description: 'password'}) password: string; @apiProperty ({description: 'gender 0: male 1: female 2: confidential'}) gender: number; @apiProperty ({description: 'email'}) email: string; @APIProperty ({description: 'phone'}) mobile: string; @apiProperty ({description: 'department ID'}) deptId: string; @apiProperty ({description: 'status: 0 enabled 1 disabled'}) status: number; }Copy the code

user.entity.ts

import { Base } from 'src/common/common/entity/base.entity';
import { Entity, Column } from 'typeorm';

@Entity('user')
export class User extends Base {
  @Column({ name: 'user_name' })
  userName: string;

  @Column({ name: 'real_name' })
  realName: string;

  @Column()
  password: string;

  @Column()
  gender: number;

  @Column()
  email: string;

  @Column()
  mobile: string;

  @Column({ name: 'dept_id' })
  deptId: string;

  @Column({ default: 0 })
  status: number;
}
Copy the code

Adjust the service

users.service.ts

import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { getPagination } from 'src/common/utils/page.util'; import { Not, Repository } from 'typeorm'; import { CreateUserDto } from './dto/create-user.dto'; import { ListUserDto } from './dto/list-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; import { User } from './entities/user.entity'; import { sourceToTarget } from 'src/common/utils/convert.utils'; @Injectable() export class UsersService { constructor( @InjectRepository(User) private usersRepository: Repository<User>,) {} // Add async create(createUserDto: createUserDto): Promise<void> {// Since createUserTo. creator = 'admin'; createUserDto.updater = 'admin'; await this.usersRepository.save(createUserDto); Async findAll(params): Promise<ListUserDto> {const {page = 1, pageSize = 10} = Params; const getList = this.usersRepository .createQueryBuilder('user') .where({ delFlag: 0 }) .orderBy({ 'user.update_time': 'DESC', }) .skip((page - 1) * pageSize) .take(pageSize) .getManyAndCount(); const [list, total] = await getList; const pagination = getPagination(total, pageSize, page); return { records: list, ... pagination, }; } / / query information by id async findOne (id: string) : Promise < User > {return await this. UsersRepository. FindOne (id); Async findByName(userName: string, id: string): Promise<User> {const condition = {userName: userName }; if (id) { condition['id'] = Not(id); } return await this.usersRepository.findOne(condition); // update async update(updateUserDto: updateUserDto): Promise<void> { const user = sourceToTarget(updateUserDto, new UpdateUserDto()); await this.usersRepository.update(user.id, user); }}Copy the code

Temporarily write the basic method of adding, deleting, changing and checking, because it is logical deletion, so the implementation of the update operation can be. If you have some more statements based on database table operations that you want to learn more about, check out TypeORM’s official website description.

We’ll talk about paging in more detail. Since new and modified pages are separate Dtos, and the fields between them are different, we can also write a DTO for receiving parameters and returning data for paging.

list-user.dto.ts

import { ApiProperty, PartialType } from '@nestjs/swagger'; import { PaginationDTO } from 'src/common/common/dto/pagination.dto'; Export Class ListUserDto extends PartialType(PaginationDTO) {@APIProperty ({description: 'username ', required: false }) userName? : string; }Copy the code

It inherits PaginationDTO, and then in there you can customize some of the query parameters so that the data format that we’re going to return in pages is

{
    total: 0,
    page: 0,
    pageSize: 10,
    pages: 0,  
    records: []
}
Copy the code

SRC =>common=>utils=>index.util. Ts SRC =>common=>utils=>index

index.util.ts

* @param total * @param pageSize * @param page * @returns */ export const getPagination = (total: number, pageSize: number, page: number, ) => { const pages = Math.ceil(total / pageSize); return { total: Number(total), page: Number(page), pageSize: Number(pageSize), pages: Number(pages), }; };Copy the code

Adjust the controller

users.controller.ts

import { Controller, Get, Post, Body, Param, Delete, Query, Put, } from '@nestjs/common'; import { UsersService } from './users.service'; import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { ListUserDto } from './dto/list-user.dto'; import { Result } from 'src/common/common/dto/result.dto'; import { ErrorCode } from '.. /.. /common/exception/error.code'; @controller ('users') @apitags ('user ') export class UsersController {constructor(private readonly usersService: UsersService) {} @post () @apiOperation ({summary: 'New user info'}) async create(@body () createUserDto: CreateUserDto) { const user = this.usersService.findByName(createUserDto.userName, ''); If (user) {return new Result().error(new ErrorCode().internal_server_error, 'username already exists ',); } await this.usersService.create(createUserDto); return new Result().ok(); } @get () @apiOperation ({summary: 'Query user list'}) async findAll(@query () listUserDto: ListUserDto) { const userList = await this.usersService.findAll(listUserDto); return new Result<UpdateUserDto>().ok(userList); } @get (':id') @apiOperation ({summary: 'query user information'}) async findOne(@param ('id') id: string) { const user = await this.usersService.findOne(id); return new Result<UpdateUserDto>().ok(user); } @put (':id') @apiOperation ({summary: 'modify user information'}) async update(@body () updateUserDto: UpdateUserDto) { const user = this.usersService.findByName( updateUserDto.userName, updateUserDto.id + '', ); If (user) {return new Result().error(new ErrorCode().internal_server_error, 'username already exists ',); } await this.usersService.update(updateUserDto); return new Result().ok(); } @delete (':id') @apiOperation ({summary: 'Delete user information'}) async remove(@param ('id') id: string) { const user = await this.usersService.findOne(id); if (! User) {return new Result().error(new ErrorCode().internal_server_error, 'user does not exist ',); } user.delFlag = 1; await this.usersService.update(user); return new Result().ok(); }}Copy the code

Instead of using interceptors and filters to unify the format of the returned data, I prefer to do big things with interceptors and filters, global filtering and blocking, and have not yet studied the use of these two things, so I have encapsulated the return class here. src=>common=>common=>dto=>result.dto.ts

result.dto.ts

Export class Result<T> {// status code: number; // Request result message: string; // Data: T; ok(data = null, message = 'success') { this.code = 0; this.data = data; this.message = message; return this; } error(code = 1, message = 'error') { this.code = code; this.message = message; return this; }}Copy the code

Methods that encapsulate ok success and error failure. Data data and message may be transmitted on success, code and message may be returned on failure, and error return code class SRC =>common=> Exception =>error.code.ts is encapsulated

error.code.ts

/** * error code, consisting of 5 digits, the first two digits are module code, the last three digits are business code * example: 10001 (10 represents the system module, 001 represents the service code) */ export class ErrorCode {INTERNAL_SERVER_ERROR = 500; UNAUTHORIZED = 401; FORBIDDEN = 403; NOT_NULL = 10001; DB_RECORD_EXISTS = 10002; PARAMS_GET_ERROR = 10003; ACCOUNT_PASSWORD_ERROR = 10004; ACCOUNT_DISABLE = 10005; IDENTIFIER_NOT_NULL = 10006; CAPTCHA_ERROR = 10007; SUB_MENU_EXIST = 10008; PASSWORD_ERROR = 10009; ACCOUNT_NOT_EXIST = 10010; }Copy the code

summary

So far, mysql database connection and some basic encapsulation and done, run the program to try.

If you need to add some interceptors and filters, check out this section.

The interceptor

I only wrote part of the interceptor, if you need to go to the official nestJS website documentation

The response mapping

We already know that handle() returns an Observable. This stream contains the value returned from the route handler, so we can easily change it using the map() operator.

Response mapping does not work with library-specific response policies (direct use of @res () objects is prohibited).

Let’s create a TransformInterceptor that packages the response and assigns it to the data property.

transform.interceptor.ts

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; export interface Response<T> { data: T; } @Injectable() export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> { intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> { return next.handle().pipe(map(data => ({ data }))); }}Copy the code

main.ts

async function bootstrap() { ... app.useGlobalInterceptors(new TransformInterceptor()); . }Copy the code

The filter

Abnormal filter

While a basic (built-in) exception filter can handle many situations automatically for you, there are times when you might want to have complete control over the exception layer, for example, you might want to add logging or use a different JSON schema based on some dynamic factor. Exception filters are designed for this purpose. They allow you to control the precise flow of control and send the content of the response back to the client.

Let’s create an exception filter that catches exceptions as instances of the HttpException class and sets up custom response logic for them. To do this, we need to access the underlying platforms Request and Response. We will access the Request object to extract the original URL and include it in the log information. We’ll use the Response.json() method to directly control the sent Response using the Response object.

http-exception.filter.ts

import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common'; import { Request, Response } from 'express'; @Catch(HttpException) export class HttpExceptionFilter implements ExceptionFilter { catch(exception: HttpException, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse<Response>(); const request = ctx.getRequest<Request>(); const status = exception.getStatus(); response .status(status) .json({ statusCode: status, timestamp: new Date().toISOString(), path: request.url, }); }}Copy the code

All exception filters should implement the common ExceptionFilter

interface. It requires you to provide a catch(exception: T, host: ArgumentsHost) method with a valid signature. T indicates the type of exception.

The @catch () decorator binds the required metadata to the exception filter. It tells Nest that this particular filter is looking for HttpExceptions and not others. In practice, @catch () can pass multiple arguments, so you can set filters for multiple types of exceptions by separating them with commas.

main.ts

async function bootstrap() { ... app.useGlobalFilters(new HttpExceptionFilter()); . }Copy the code

conclusion

The above is the content of the framework optimization of this project. Although these are only some basic packaging, they are finally like a look. In the next chapter, I plan to do user login and other functions, combined with the background management front page built by VUE3 shelf, to do some practical business things.

Code address: gitee.com/wd_591/nest…

Reference for this article: juejin.cn/column/6992…