NestJS 问题记录

以前写后端都是用 Express 的,但现在流行的是 NestJS,为了跟上潮流,也只能尝试 NestJS 了。对于新的技术栈,碰到问题是难免的,所以这里简要记录碰到的问题。

TypeORM

TypeORM 是一个统一和简化数据库操作的库,比手动拼 SQL 方便一些(熟练以后)。

实体字段如何与数据库不同

很多时候,后端返回给前端的数据名称需要与数据库不同,所以最好有一种映射方法,查询之后自动转换。TypeORM 可以在定义实体(Entity)的时候指定映射字段——Column 装饰器里指定 name。

@Column({ name: 'created_at' })
created: string;

连接多个数据库

网上有很多方法的介绍,比如 app.module 里定义多个 TypeOrmModule.forRoot,以此创建多个连接。我没用这种方式,因为我的库都在一台机器上,这种情况只需要在定义实体的时候指定数据库就行。

@Entity('user', {
  database: 'lab',
})

where 条件嵌套查询

手拼 SQL 的时候我是根据条件拼接不同的 OR 或者 AND 语句,用了 TypeORM 之后方便多了。比如根据条件是否存在调用 where 或者 andWhere 或者 orWhere。

// 关键字查询
if (keyword) {
  qb.where(
    '(poem.title LIKE :keyword OR poem.author LIKE :keyword OR poem.content LIKE :keyword)',
    { keyword: `%${keyword}%` },
  );
}
// 作者
if (author) {
  qb.andWhere('poem.author = :author', { author });
}
// 类型
if (type) {
  qb.andWhere('poem.type LIKE :type', { type: `%#${type}#%` });
}

调用很方便,并且即使 keyword 不存在,也不影响后续 andWhere 的调用。而对于嵌套的条件,直接用括号就可以,和手拼一样的(代码里 qb.where 部分)。

还有就是 LIKE 查询的写法。一开始我是这样写的:LIKE `’%#${type}#%’`,这种写法不行,即使把单引号去掉也不行,因为 type 部分会自动替换成包含单引号的实际值。因此,需要直接将值处理成最终的查询条件字符串——{ type: `%#${type}#%` }。

查询结果自动映射实体对象字段

对于创建的查询器,可以调用 execute() 方法执行,但这种方式得到的结果字段是类似于 poem_created_at 这种带有前缀的,并且没有将数据库字段转成我们我需要的字段。并且,如果需要 count,还需要手动调用 getCount() 方法。

其实,为了得到最终结果,只需要调用 getManyAndCount() 方法就可以了,这个方法返回一个数组,数组第一个元素是结果列表,第二个元素是 count 结果。

另外,如果只需要一个结果,直接 getOne() 方法就可以。

分页查询

对于分页,可以调用查询器的 limit() 方法和 offset() 方法。

class-validator

class-validator 是一个配合 NestJS 使用的校验库。

请求参数校验

一开始我以为请求参数直接根据 dto 的 ts 类型就可以校验了,比如:

export class ListReqDto {
  readonly keyword?: string;

  readonly page?: number;

  readonly size?: number;
}

但实际上,这样好像并不能触发校验,或者说实际上并没有定义校验条件。字段的具体限制需要添加各种装饰器实现。

export class ListReqDto {
  @IsString()
  @IsOptional()
  readonly keyword?: string;

  @IsNumber()
  @IsOptional()
  readonly page?: number;
}

另外,如果需要校验参数为枚举,可以使用 IsIn([xxx, xxx]) 装饰器。

多种类型校验

一个字段可能存在多种类型,比如查询列表,可以 POST 也可以 GET,这时如果用一个 DTO,那么 page、size 这种字段就是数字或者数字字符类型。但是,class-validator 没有提供这种联合类型的校验方法。对此,有热心网友在相关 issue(How to describe validators for two possible types: array or string?) 里提供了解决方案——自定义校验器。

import { registerDecorator } from 'class-validator';

export const isWhether = (
  value: string,
  ...validators: ((value: any) => boolean)[]
) => {
  return validators.map((validate) => validate(value)).some((valid) => valid);
};

export function IsWhether(...validators: [string, (value: any) => boolean][]) {
  return function (object: any, propertyName: string) {
    registerDecorator({
      name: 'isWhetherType',
      target: object.constructor,
      propertyName: propertyName,
      options: {},
      validator: {
        validate(value: any) {
          const validatorsFns = validators
            // eslint-disable-next-line @typescript-eslint/no-unused-vars
            .map(([_, validate]) => validate)
            .flat();
          return isWhether(value, ...validatorsFns);
        },
        defaultMessage(/* validationArguments?: ValidationArguments */) {
          const types = validators.map(([type]) => type).flat();
          const lastType = types.pop();
          if (types.length === 0)
            return `${propertyName} has to be ${lastType}`;
          return `${propertyName} can only be ${types.join(', ')} or ${lastType}.`;
        },
      },
    });
  };
}

JWT

JWT 流行很久了,用起来也还算方便吧,毕竟可以把用户基本信息扔到 token 里,然后直接解出 token 就可以拿到用户信息,省得每次查表。

但是,我还是需要每次查表,因为用户信息是会变动的,比如被禁用了,而已经签发的 token 信息是固定 的,所以,我的 token 里唯一需要的就是用户 id。

解密出来的 sub 为空

jwt 的 payload 里有个 sub 字段(主题),我把用户 id 赋给了这个字段,但是用 verifyAsync 这个方法验证得到的 payload 里 sub 是空的,其他的自定义字段就没问题。后来改成了 decode 方法才拿到需要的 sub。