NestJs 快速入门

  npm i -g @nestjs/cli(以后更新用npm update @nestjs/cli -g),nest new car-report创建car-report项目。src目录下的main.ts,它监听3000端口,服务器都会监听一个端口,因此它是整个项目的入口文件。main.ts import了AppModule,这个类比较特殊,它什么都不做,只是用@Module装饰。@Module接收一个对象,有三个属性,imports、controllers和providers。controllers提供(注册)控制器,控制器来源于MVC架构,接受请求,返回响应。来到AppController类,它装饰@Controller,getHello方法装饰了@Get。但它是怎么接受请求,返回响应的?就是怎么就定义了路由呢? 一个路由通常包含三部分,请求方法(如GET、POST)、请求的URL和处理函数(当匹配的请求到达时会被执行)。@Get、@Post等装饰器定义请求方法,并且装饰到类方法上,类方法就成了路由处理函数,类方法的参数接受请求参数,方法体返回响应。@Controller()和@Get()都能接收参数,请求的URL就是它们参数的拼接。如果都没有参数,那就是根URL。所从在Nest.js中,处理请求(定义路由),就是写一个@Controller() 装饰的类,用@Get,@Post参数定义URL,然后装饰类方法上,类方法来处理具体请求,最后把这个类注册到@Module的controllers属性中。

import 'reflect-metadata';
/* Reflect.defineMetadata 和 Reflect.getMetadata 分别用于设置和获取某个类的元数据,如果最后传入了属性名,还可以单独为某个属性设置元数据。*/
function Controller(prefix: string) {
    // constructor 类的构造函数
    return function(constructor: Function) {
        Reflect.defineMetadata('prefix', prefix, constructor)
    }
}

function Get(path: string) {
    //  target 是 the prototype of the class,property是类的方法名
    return function(target, propertyKey) {
        Reflect.defineMetadata('route', {method: 'GET', path}, target, propertyKey)
    }
}

function Post(path: string) {
    return function(target, propertyKey) {
        Reflect.defineMetadata('route', {method: 'POST', path}, target, propertyKey)
    }
}

@Controller('/useers')
class UserController {
    @Get('/')
    getAllUser() {}
    
    @Post('/')
    createUser() {}
}

function handleRequest(controllerClass: any, requestMethod: string, requestPath: string) {
    const prefix = Reflect.getMetadata('prefix', controllerClass);
    const controllerInstance = new controllerClass();

    Object.getOwnPropertyNames(controllerClass.prototype).forEach(methodName => {
        // 定义的Metadata  {method: 'GET', path}
        const route = Reflect.getMetadata('route', controllerClass.prototype, methodName)

        if(route && route.method === requestMethod && (prefix + route.path) === requestPath) {
            controllerInstance[methodName]()
        }
    })
}

// 处理请求
handleRequest(UserController, 'GET', '/users')

  provider只是一个用Injectable() 装饰的纯TypeScript类,这个类也称之为服务,这意味着这个类的实例可以注入到其他类中,这也是getHello方法中,直接调用了this.appService的原因,appService实例对象被注入到构造函数中。A对象中方法依赖B对象,它不是直接创建B对象,而是用参数声明它依赖B对象,使用A对象的方法前,要先把B对象注入进来(提供对象)。依赖注入:组件只声明使用某个或某些依赖,依赖相当于变成了参数,需要传入,在组件内部不会创建依赖对象,在使用组件的时候,给它注入依赖。

class B {
    sayHello() {console.log( 'Hello World')}
}

/*  不使用依赖注入 */
class A {
    constructor() {this.b = new B()} // 创建依赖的实例对象
    sayHello() { this.b.sayHello()}
}

(new A()).sayHello() // 直接使用A对象

/* 使用依赖注入 */
class A {
    constructor(b) {this.b = b;} // 声明使用的依赖,没有创建依赖对象
    sayHello() { this.b.sayHello()}
}

const b = new B();
const a = new A(b) // 创建A对象时,要把它的依赖注入进去
a.sayHello() // 再调用A对象的方法

  但手动管理依赖很麻烦,于是出现了控制翻转。依赖的管理(创建和注入)交由第三方,通常是一个容器。NestJs就是实现了这么一个容器。依赖注入如下图所示,Service类只声明了它依赖特定接口的dependency, injector(容器)创建了一个实现接口的实例,把它注入到service中。

  @Module中的provider是告诉NestJs,如果你碰到AppService,就把AppService的实例注入进去。什么叫碰到AppService, @inject

constructor(@Inject(AppService) private readonly appService: AppService) {}

  只是@Inject中的参数和方法参数都是AppService,前面的@Inject(AppService)被省略了。providers其实接受的是一个对象,对象的一个key是provide,表示提供什么,真实的作用就是一个token标示符,用于@inject。另外一个key是useValue或useClass或useFactory,当NestJs遇到provider指定的token后,就会用useValue指定的value,或useClass指定的类的实例对象,或useFactory返回的对象(工厂方法创建对象),来注入到@Inject(token)修饰的参数。

  写一个登录注册的模块。通常在服务端开发时,controller负责接受客户端请求,service负责处理业务逻辑,repository负责和数据库打交道,controller调用service,service调用repository。新建users目录,目录里面users.controller.ts,users.module.ts, users.service.ts, users.repository.ts。controller调用service,依赖service, users.controller.ts

import { Controller, Get, Post } from '@nestjs/common';
import { UsersService } from './users.service';

@Controller('users')
export class UsersController {
    constructor(private usersService: UsersService) { } // 声明需要的依赖

    @Post('/signup')
    async createUser() { }

    @Post('/signin')
    async singin() { }

    @Get('/:id')
    async getUser() { }
}

  Post请求要接受请求体,Get请求要接受params。做过Express开发,都知道到有请求对象和响应对象来获取这些内容。在Nest.js中,方法的参数用@Req和@Res进行装饰,就成了请求对象和响应对象。

@Post('/signup')
async createUser(@Req() request: Request, @Res() response: Response) { 
    const body = request.body;
    response.json({body})
}

  request.body, request.params太常见了,于是Nest.js又有了@Body,@Param装饰器来修饰方法的参数,直接就能获取到对应的内容。如果POST请求要接受一个复杂的对象作为请求体,参数的类型是一个类类型,定义请求体的形状和期望的数据。这个类也称为DTO(数据传输对象),创建用户需要email和password, 那就创建一个类,有email 和password属性,createUser参数就是这个类类型。user目录下创建User.ts

export class User {
    email: string;
    password: string
}

  然后

@Post('/signup')
async createUser(@Body() body: User) { }

  客户端通过JSON.stringfy()把email和password对象通过字符串的形式传递过来,服务端用JSON.parse进行解析,然后创建User类一个实例对象,把解析出来的数据赋值给对象,然后再把对象赋值给body,body是User的一个实例对象。此外还要对数据进行验证。NestJs有一个pipe的概念,数据到达路由处理函数之前要经过一系列过程,pipe就是其中之一,ValidationPipe用于验证。Class-validation 包使用注解验证,然后把验证结果给到validation pipe。npm install class-transformer class-validator, User 类添加验证

import { Transform } from "class-transformer";
import { IsNotEmpty, IsOptional, IsString } from "class-validator";

export class User {
    @IsString()
    @IsNotEmpty()
    // @IsNotEmpty({ groups: ['create'] }) groups限制 @IsNotEmpty() 规则何时应用。仅当{ groups: ['create']显式传递到验证管道时才会执行验证。
    email: string;

    @IsString()
    @IsNotEmpty()
    password: string;

    @IsOptional()
    description?: string; // 可选
@IsOptional() @Transform(({ value }) => value.toUpperCase()) // 简单的数据转换 title?: string; }

  然后

@Post('/signup')
async createUser(@Body(new ValidationPipe({
    transform: true, // 当validation 有transform 的时候,要开启transform
    // groups: ['create'] // 有条件时,开启条件判断
})) body: User) { }

  如果对每个客户端数据都进行验证,在每一个路由处理函数都添加ValidationPipe,比较麻烦,可以开启全局验证。useGlobalPipes(new ValidationPipe()), useGlobalPipes对所有的请求都应用它包含的pipe。ValidationPipe 验证每一个请求,如果DTO类没有添加验证规则,也不会对请求进行验证。main.ts

import { ValidationPipe } from "@nestjs/common";

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe({
   transform: true, whitelist: true })) await app.listen(3000); } bootstrap();

  有个问题,validation pipe是怎么知道使用哪个验证的规则的来验证哪一个路由的?尤其是typescript 编译成js,类型擦除后?ts 配置emitDecoratorMetadata, 把类型信息添加到js中, 编译后在js代码,dist目录,users下面的users.controller.js

exports.UsersController = UsersController;
__decorate([
    (0, common_1.Post)('/signup'),
    __param(0, (0, common_1.Body)(common_1.ValidationPipe)),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [create_user_1.CreateUser]),
    __metadata("design:returntype", Promise)
], UsersController.prototype, "createUser", null);

  现在把接受到的参数存储到数据库,调用userService中的create 方法,

async createUser(@Body(ValidationPipe) body: CreateUser) {
    await this.usersService.create(body.email, body.password)
    return {message: 'create successfully'}  // 当controller的方法返回基本数据类型时,直接返回。当返回对象或数组时,会序列化为json
}

  sign和createUser方法一样,getUser要接受id,

@Get('/:id') // 两种使用方式,不带参数,获取整个对象,带参数,获取指定的某个参数。
    async getUser(@Param('id') id: string, @Param('sex') sex: string, @Param() all: any) { } 

  users.service.ts创建UsersService类,添加create方法,servcie被依赖,所以它能够被注入,需要@Injectable()装饰,同时调用repository,需要声明依赖

import { Injectable } from "@nestjs/common";
import { UserRepo } from "./users.repository";

@Injectable()
export class UsersService {
    constructor(private userRepo: UserRepo) { this.userRepo = userRepo } // 声明需要的依赖,
    async create(email: string, password: string) {
        return this.userRepo.create({ email, password, admin: true });
    }
}

  repository比较麻烦,它需要连接数据库。数据库的连接是一个单独的功能,需要新建一个模块,可以看出模块起到组织代码的作用。创建db目录,再创建db.module.ts。但User模块下,怎么使用DB模块的功能?service(provider)都属于自己的module,如果要被其他module使用时,就要被export 出去

import { Module } from '@nestjs/common';
import {Pool} from 'pg'

const dbProvider = {
    provide: 'PG_CONNECTION',
    useValue: new Pool({
        user: 'postgres',
        host: 'localhost',
        database: 'postgres',
        password: '123456',
        port: 5432
    })
}

@Module({
  providers: [dbProvider],
  exports: [dbProvider]
})
export class DBModule {}

  其次一个模块要使用另一个模块的service,就要把另一个模块引入。import只能引入其他模块,所以DB功能,也只能创建模块,这也证明模块是创建NestJs应用的基础。引入一个模块,这个模块export出来的provider,就相当于自己模块写了这个provider,和本模块中的其他provider,没有什么区别。在本模块中碰到@inject的token,就会注入进来。users.repository.ts 需要从数据库中查数据

import { Inject, Injectable } from "@nestjs/common";

@Injectable()
export class UserRepo {
    constructor(@Inject('PG_CONNECTION') private connect: any) { }
    async create({ email, password, admin }: any) {
        /* pg中创建users表
        CREATE TABLE users (
        id SERIAL PRIMARY KEY,
        email VARCHAR(50),
        password VARCHAR(50),
        admin BOOLEAN
    );
        */
        const text = 'INSERT INTO users(email, password, admin) VALUES($1, $2, $3) RETURNING *'
        const values = [email, password, admin]

        const res = await this.connect.query(text, values)
        console.log(res.rows[0])
        return res.rows[0]
    }
}

    Users

import { Module } from '@nestjs/common';
import { DBModule } from 'src/db/db.module';
import { UsersController } from './users.controller';
import { UserRepo } from './users.repository';
import { UsersService } from './users.service';

@Module({
    imports: [DBModule], // 使用db模块的功能
    providers: [UsersService, UserRepo], //service和repository都是被注入,要在module的provider中,表明遇到哪个token才注入
    controllers: [UsersController]
})
export class UsersModule { }

  还差最后一步,怎么使用创建的Users模块?main.tsx启动文件中,只import了AppModule。AppModule的@Module装饰器中有import,可以引入其他module

import { Module } from '@nestjs/common';
import { UsersModule } from './users/users.module';

@Module({
  imports: [UsersModule]
})
export class AppModule {}

  用TypeORM操作MySQL,npm install --save @nestjs/typeorm typeorm mysql2 安装依赖。TypeORM有实体(Entity)的概念,对应MySql的一张表,它是一个类,代表表名,属性代表列。创建users目录,user目录下创建users.entity.ts

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({default: true})
  admin: boolean;

  @Column()
  email: string;

  @Column()
  password: string;
}

  连接数据库,在AppModule

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './users/users.entity';

@Module({
  imports: [TypeOrmModule.forRoot({
    type: 'mysql',
    host: 'localhost',
    port: 3306,
    username: 'root',
    password: '123',
    database: 'test',
    entities: [User],
    synchronize: true,
  })]
})
export class AppModule { }

  TypeORM模块的import方式不太一样,它是调用forRoot方法返回一个模块,这种模块称为动态模块,相对应的,MessagesModule 称为静态模块。静态模块功能是固定的,由@Module定义,import的时候,直接import 模块名,不用调用方法。不管是静态模块还是动态模块,模块一旦创建,它是单例的,存在某个地方,但又不全局可用(every module has its own set of imported modules in its scope)。Typeorm模块在AppMoule 中创建,它就已经存在,连接到数据库了,users模块中只使用user功能,所以在users中,Typeorm.forfeature('user'),让user功能在users模块中使用。当使用一个动态module 的时候,要么配置module的行为,要么引入某个feature。users目录下,创建users.module.ts

import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './users.entity';
import { UsersService } from './users.service';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [UsersController],
  providers: [UsersService]
})
export class UsersModule { }

 

  创建users.service.ts,本来是要创建users.repository.ts的,但有了实体,TypeORM会自动创建对应的repostiory对象来操作表,不用手动创建Repository类了,只需要在service中注入创建好的repostiory对象,users.service.ts 如下

import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { User } from "./users.entity";

@Injectable()
export class UsersService {
    constructor(
        @InjectRepository(User)
        private usersRepository: Repository<User>,
      ) {}
}

  Repositoy类型使用泛型Repository<User>。

   

,Typeorm生成的repository对象有create,save,insert,update,delete,find等方法。向数据库中插入数据,有两种实现方式,create之后调用save,

async create(email: string, password: string) {
  const user = this.usersRepository.create({ email, password, admin: true });
  await this.usersRepository.save(user);
}

  和直接调用insert。

async create(email: string, password: string) {await this.usersRepository.insert({ email, password, admin: false })
}

  在AppModule中import UserModule, npm run start:dev启动服务,

   同样的,update也有两种方式,先findOne,再调用save方式,或直接调用update 方法。delete也有两种方式,先findOne,再调用remove方法,或直接调用delete方法。为什么会有save和remove 方法呢?因为在Entity 中可以定义一些hooks,@AfterInsert, @AfterUpdate, 只有调用这两个方法的时候,它们才会执行,直接调用insert,update,delete不会执行,但save 方法,性能可能不高,因为,当实体不存在时,它执行insert操作,当实体存在时,它执行update操作,每天都要执行两次query查询,先find,再insert或update。

  登录singin,都会返回token,以后每一个请求都带有token,就知道谁在请求。npm install --save @nestjs/jwt。在UserModule中,

import { JwtModule } from '@nestjs/jwt';
export const secret = 'addfsdsfdf'

imports: [TypeOrmModule.forFeature([User]), JwtModule.register({
  global: true,
  secret: secret,
  signOptions: { expiresIn: '1h' },
})]

  再UserService中注入JwtService,实现signIn方法

import { JwtService } from '@nestjs/jwt';
import { Injectable, UnauthorizedException } from "@nestjs/common";

constructor(
  @InjectRepository(User)
  private usersRepository: Repository<User>,
  private jwtService: JwtService
) { }

async signIn( email: string, password: string) {
    const user = await this.usersRepository.findOne({where: { email }});
    if (user?.password !== password) {
      throw new UnauthorizedException();
    }
    const payload = { sub: user.id };
    return await this.jwtService.signAsync(payload)
  }

  在UserController 中

@Post('/signin')
async singin(@Body(ValidationPipe) body: CreateUser) {
    const token = await this.usersService.signIn(body.email, body.password)
    return {
        access_token: token
    };
}

  getUser方法,只有用户登录,才能调用访问,有些路由是要保护起来的,如果没有登录,就不能访问,这要用到guard,有一个canActivate(), 返回true or false,true表示允许访问,false表示不允许访问。守卫决定请求是否应该继续进行到其路由处理程序。用户登录就是验证token,创建AuthGuard.ts

import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { secret } from './users.module';
import { Request } from 'express';

@Injectable()
export class AuthGuard implements CanActivate {
    constructor(private jwtService: JwtService) { }

    async canActivate(context: ExecutionContext): Promise<boolean> {
        const request = context.switchToHttp().getRequest();
        const token = this.extractTokenFromHeader(request);
        if (!token) {
            throw new UnauthorizedException();
        }
        try {
            await this.jwtService.verifyAsync(token,{secret});
        } catch {
            throw new UnauthorizedException();
        }
        return true;
    }

    private extractTokenFromHeader(request: Request): string | undefined {
        const [type, token] = request.headers.authorization?.split(' ') ?? [];
        return type === 'Bearer' ? token : undefined;
    }
}

  getUser方法,那就使用UseGuard进行保护

@Get('/:id')
@UseGuards(AuthGuard)
async getUser() { }

  实现getUser,获取id参数,用@Param装饰器,

async getUser(@Param('id') id: string) {
    return await this.usersService.findOne(id);
}

  userService实现findOne,

findOne(id: number) {
  if (!id) return null;
  return this.usersRepository.findOneBy({ id });
}

  但这时有一个问题,controller中调用findOne的id是string类型,但service中,id接受的是number类型,这是可以用pipe,@param('id', ParseIntPipe) 把id转换成int 类型。

async getUser(@Param('id', ParseIntPipe) id: number) {
    return await this.usersService.findOne(id);
}

  pipe通常做两件事情,一个是类型转换,一个是验证用户的输入。在以上的方法中,抛出了异常,比如throw new UnauthorizedException(),NestJs有一层Exception filter,当应用程序中抛出了异常,而没有被捕获时,它会把异常转换成合适response,比如 throw NotFoundExeption 时, nextJs会返回404,not found。对exception 进行过滤,返回合适的响应。

  但返回值中有password,应该要去掉才对,这样用到拦截器。拦截器实现一个NestInterface,intercept 里面正常写,拦截请求,return next.handle() 对拦截响应,对路由处理器的返回值进行处理。它返回的是rxjs的observer,有map等操作,map中的data 就是路由处理器返回的data。 返回值去掉password,创建serialInteceptor.ts

import { CallHandler, ExecutionContext, NestInterceptor, UseInterceptors } from "@nestjs/common";
import { Observable, map } from "rxjs";

export class SerializeIntercepter implements NestInterceptor {
    intercept(context: ExecutionContext,
        next: CallHandler<any>): Observable<any> | Promise<Observable<any>> {
        return next.handle().pipe(
            map(data => {
                Reflect.deleteProperty(data, 'password'); // true
                return data;
            })
        )
    }
}

  getUser用userInterceptor.

@Get('/:id')
@UseGuards(AuthGuard)
@UseInterceptors(SerializeIntercepter)
async getUser(@Param('id', ParseIntPipe) id: number) {
    return await this.usersService.findOne(id);
}

  可以把拦截器包起来,形成一个装饰器,serialInteceptor.ts

export function Serialize() {
    return UseInterceptors(SerializeIntercepter);
}

  getUser 去掉@UseInterceptors(SerializeIntercepter), 直接使用@Serialize()。再创建一个report 模块,一辆汽车的报告,用户创建它,admin 用户批准它。nest cli 提供了一些命令来创建module,controller和service, nest g module reports,nest g controller reports, nest g service reports,手动在reports目录建reports.entity.ts

import { Entity, Column, PrimaryGeneratedColumn, ManyToOne } from 'typeorm';

@Entity()
export class Report {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({default: false})
  approved: boolean;

  @Column()
  price: number;

  @Column()
  year: number

  @Column()
  mileage: number
}

  然后在AppModule 中,Typeorm的配置项entities中,添加Report

 entities: [User, Report],

  用户创建report, createReport 中要知道用户的信息,admin批准report,那还要判断登录的用户是不是admin,如要不是,批准的api就不能被调用,需要创建AdminGuard。从客户端请求中,只能得到userId,所以其它信息还要从数据库里面取。这里要用到中间件,这是由中间件,guard,拦截器的执行顺序决定的。

   在中间件中,调用userService,获取到用户信息,然后把信息添加到request对象上,后面执行的guard,拦截器,路由处理器都能获取到request对象上在user信息。在src目录下,创建current-user.middlewire.ts

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { UsersService } from './users/users.service';
import { JwtService } from '@nestjs/jwt';
import { secret } from './users/users.module';

@Injectable()
export class CurrentUserMiddleware implements NestMiddleware {
    constructor(private user: UsersService, private jwtService: JwtService) { }

    async use(req: Request, res: Response, next: NextFunction) {
        const [, token] = req.headers.authorization?.split(' ') ?? [];
        if (token) {
            try {
                const result = await this.jwtService.verify(token, { secret });
                const user = await this.user.findOne(result.sub);
                // @ts-ignore
                req.currentUser = user;
            } catch (error) {
                console.log(error)
            }
        }
        next();
    }
}

  中间件的使用比较特别,使用中间件的module要实现NestModule, 在configure中配置,比如在AppModule中配置中间件

import { CurrentUserMiddleware } from './current-user.middlewire';
import { MiddlewareConsumer, NestModule } from '@nestjs/common';

export class AppModule implements NestModule  {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(CurrentUserMiddleware)
      .forRoutes('*'); // 或for('/reports')
  }
}

  由于中间件在AppModule中引入的,使用了UserService,UserModule还要exports 出去UserService。

@Module({
  // ....
  providers: [UsersService],
  exports: [UsersService]
})
export class UsersModule { }

  现在createReport可以获取到user信息了,但怎么在report中保存user信息呢?这涉及到了关系,report和user有1对多的关系,

  在user实体中, 添加属性

@Entity()
export class User {
  // ...
  @OneToMany(() => Report, (report) => report.user )
  reports: Report[] // 数组表示多个report
}

  在report 实体添加属性

@Entity()
export class Report {
  // ...
  @ManyToOne(() => User, (user) => user.reports)
  user: User
}

  oneToMany或ManyToOne为什么第一个参数是函数。这是因为,User Entity中使用Report Entity, Report Entity 中又使用User Entity,循环依赖了,不能直接使用,所以要用函数包起来,以后执行,而不是加载文件的时候执行。第二个函数参数的意思是关联的实体,返回值是定义的实体, 通过关联的实体report怎么找回到定义report的实体(User),report entity 有一个user字段,就是定义reports的实体(User实体中有reports属性)。Report实体有一个user字段,存储report时,给report的user属性赋值一个user实体,当真正存储到数据库时,会从user实体中取出id,存储到数据库。ReporstController

import { Body, Controller, Post, Req, UseGuards } from '@nestjs/common';
import { AuthGuard } from '../users/AuthGuard';
import { ReportsService } from './reports.service';

@Controller('reports')
export class ReportsController {
    constructor(private readonly reportsService: ReportsService) { }

    @Post()
    @UseGuards(AuthGuard)
    async createReport(@Body() body: any, @Req() req: any) { //body 的类型本来是一个DTO类型,简单起见,写了any
        const userReturn = await this.reportsService.create(body, req.currentUser)
        return userReturn
    }
}

  ReportsService

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from 'src/users/users.entity';
import { Repository } from 'typeorm';
import { Report } from './reports.entity'

@Injectable()
export class ReportsService {
    constructor(
        @InjectRepository(Report)
        private reportsRepository: Repository<Report>,
    ) { }
    create(reportDto: any, user: User) {
        const report = this.reportsRepository.create(reportDto);
        // @ts-ignore
        report.user = user;
        return this.reportsRepository.save(report)
    }
}

  由于在Service中注入了Report,所以在ReportsModule中 imports: [TypeOrmModule.forFeature([Report])],

  repository的save方法把整个关联的user 实体都返回了。还有就是controller 接收了@req参数,能不能也像@Body一样,直接获取user?这要自定义一个参数装饰器createParaDecorator. 在src目录下,currentUser.ts

import {createParamDecorator, ExecutionContext} from '@nestjs/common'

export const CurrentUser = createParamDecorator(
    (data: never, context: ExecutionContext) => {
        const request = context.switchToHttp().getRequest();
        return request.currentUser
    }
)

  controller

import { CurrentUser } from '../currentUser';
import { User } from '../users/users.entity';

@Post()
@UseGuards(AuthGuard)
async createReport(@Body() body: any,  @CurrentUser() user: User) {
    const userReturn = await this.reportsService.create(body, user)
    // @ts-ignore
    const newUser = { ...userReturn, userId: userReturn.user.id };
    // @ts-ignore
    delete newUser.user;
    return newUser
}

  现在写一个approve, 就是把report的approve属性,改成true.  它需要admin权限,写一个AdminGuard。在report目录下,admin.guard.ts

import { CanActivate,  ExecutionContext} from '@nestjs/common'

export class AdminGuard implements CanActivate {
    canActivate(context: ExecutionContext): boolean {
        const request = context.switchToHttp().getRequest();
        if(!request.currentUser) {
            return false
        }

        if(request.currentUser.admin) {
            return true
        } else {
            return false
        }
    }  
}

  ReportsController 添加一个patch 路由

@Patch('/:id')
@UseGuards(AdminGuard)
async approveReport(@Param('id') id: number, @Body() body: { approved: boolean }) {
    return await this.reportsService.changeApproval(id, body.approved);
}

  ReportsService 添加 changeApproval 方法

async changeApproval(id: number, approved: boolean) {
    const report = await this.reportsRepository.findOne({ where: { id } })
    if (!report) {
        throw new NotFoundException('not found')
    }

    report.approved = approved;

    return this.reportsRepository.save(report)
}

  当查询条件比较复杂的时候,就不能简单地用findOne和find了,就要使用createQueryBuilder,比如查询price是5000, mileage 也是5000等。在Controller 中,

@Get()
async getOneReport() {
   return this.reportsService.getReport();
}

  在Service 中

async getReport() {
  return await this.reportsRepository.createQueryBuilder('report')
        .where('report.price= :price', {price: 5000})
        .andWhere("report.mileage = :mileage", { mileage: 5000 })
        .getOne()

}

  当fetch reprot时,不会自动fetch user。同样的,当fetch user的时候,也不会自动fetch report。 

  配置环境变量,npm i @nestjs/config, @nestjs/config内部使用dotenv。Dotenv的目的是, 把不同的环境变量(命令行定义的环境变量, .env 文件定义的环境变量)收集起来, 形成一个对象(process.env),返回给你。 如果各个方法定义的环境变量有冲突,命令行中定义的优先级高。

"start:dev": "cross-env ENV=dev nest start --watch",
"start:prod": "cross-env ENV=prod node dist/main",

 

 

A good approach for using this technique in Nest is to create a ConfigModule that exposes a ConfigService which loads the appropriate .env file. 

   @nestjs/config 提供了依赖注入的功能。 每一个环境不同的.env 文件,然后,configroot.forRoot() 加载不同的配置文件(命令行配置env环境变量),

  isGlobal全局模块。envFilePath加载哪一个环境变量配置文件。

    // my-service.service.ts
    import { Injectable } from '@nestjs/common';
    import { ConfigService } from '@nestjs/config';

    @Injectable()
    export class MyService {
      constructor(private readonly configService: ConfigService) {}

      getDatabaseHost(): string {
        return this.configService.get<string>('DATABASE_HOST');
      }
    }

 

异常过滤器让您可以控制应用程序抛出的异常。通过扩展 ExceptionFilter 基类,您可以定义发生错误时的自定义响应。Nest’s hierarchical injector
NestJS not only leverages DI but also elevates it with its hierarchical injector system. This system
works in layers, ensuring that each module and its components get their dependencies from the closest
injector, be it module-specific or global.

NestJS provides a mechanism called forwardRef() that allows you to
reference classes before they are defined, hence resolving the cyclic dependency issue.

Once defined
as global, a module doesn’t need to be imported into other modules; its providers and controllers
become instantly accessible everywhere:

 Middleware is great for tasks that do not involve decision-making concerning the continuation of the request-response cycle based on business logic. 中间件非常适合那些不涉及基于业务逻辑的请求-响应周期延续的决策的任务。

 

Route guards are a fundamental part of the NestJS framework, crucial for ensuring that a certain set of
logic is fulfilled before a route handler is executed. These guards are particularly vital for implementing
authorization and authentication logic in an application.

  日志的作用,记录错误并定位问题,比如什么时候发生的,发生了什么事情,错误是什么?日志的等级,waring,error。按照功能分,错误日志,调试日志,请求日志,通常记录到文件中,比较敏感的日志,记录到数据库中。生产环境通常用第三方日志库,Winston。

 

  VS Code 调试

  点击调试图标(Ctrl+Shift+D),简单的,面板中点击"Show all automatic debug configurations",  选Node.js...,弹出Run Script命令,选“Run Script: start:debug” 开启调试。复杂的,面板中点击“创建launch.json文件”,选Node.js,自己配置。configurations 数组中,输入npm,弹出提示

图片

   选择Node.js: Launch via npm

{
    "name": "Launch via NPM",
    "request": "launch",
    "runtimeArgs": [
        "run-script",
        "start:debug" // npm 要执行的命令
    ],
    "runtimeExecutable": "npm",
    "runtimeVersion": "18", // 如果是nvm切换过版本,要指定本项目使用的node版本
    "internalConsoleOptions": "neverOpen", // 要不要打开VS code 内置的debug console
    "skipFiles": [
        "<node_internals>/**"
    ],
    "type": "node"
}

 

posted @ 2024-08-30 18:38  SamWeb  阅读(135)  评论(0)    收藏  举报