npm package: @libeilong/nestjs
Description: 基于 NestJS 的常用工具集合，主要包含：
Author: lblblong
Latest version: 0.7.1

# @libeilong/nestjs

基于 NestJS 的常用工具集合，主要包含：

- HTTP 日志拦截器
- 分页 DTO、拦截器与分页参数转换工具
- 自动继承验证 / 转换 / Swagger 元数据的 DTO 辅助类型（Create / Update / Search）
- 若干验证相关装饰器与工具

适合在日常后端项目中快速搭建统一的接口风格和 DTO 规范。

## 安装

```bash
pnpm add @libeilong/nestjs
# 或者
npm install @libeilong/nestjs
```

### peerDependencies

本包依赖以下 Nest/验证相关库，请在项目中自行安装合适版本：

- `@nestjs/common`
- `@nestjs/swagger`
- `@nestjs/mapped-types`
- `class-transformer`
- `class-validator`
- `dayjs`
- `colors`
- `rxjs`
- `express`

## 功能概览

### 1. HTTP 日志拦截器 `HttpLoggerInterceptor`

拦截所有 HTTP 请求，输出包含：

- 状态码
- 请求方法（带颜色）
- 路径
- 耗时（毫秒）

默认使用 Nest 的 `Logger('HTTP')`，并基于 `dayjs` 计算耗时，`colors` 高亮输出。

#### 全局启用示例

```ts
// main.ts
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
import { HttpLoggerInterceptor } from '@libeilong/nestjs'

async function bootstrap() {
  const app = await NestFactory.create(AppModule)

  // 全局拦截器
  app.useGlobalInterceptors(new HttpLoggerInterceptor())

  await app.listen(3000)
}

bootstrap()
```

---

### 2. 分页工具与拦截器

相关导出在 `@libeilong/nestjs/pagination` 中：

- `PaginationDto`：统一的分页入参 DTO
- `PageinationInterceptor`：将控制器返回的 `[list, count]` 结构转换为 `{ list, count }`
- `getPagination(dto)`：从 `PaginationDto` 中计算数据库查询用的 `limit`/`offset`
- `convertOptions(dto)`：从 DTO 中拆分出 `where` 与分页选项

#### `PaginationDto`

```ts
import { PaginationDto } from '@libeilong/nestjs/pagination'

export class UserListQueryDto extends PaginationDto {
  // 你的其他查询字段
}
```

- 字段：
  - `page`：页码，默认 `1`；特殊约定：
    - `page = -1`：表示不分页（`getPagination` 返回空对象）
    - `page = 0`：抛出异常，防止非法页码
  - `limit`：每页数量，默认 `10`

#### `PageinationInterceptor` 使用示例

控制器返回 `[list, count]`，拦截器会自动转换为 `{ list, count }`：

```ts
// users.controller.ts
import { Controller, Get, Query, UseInterceptors } from '@nestjs/common'
import { PageinationInterceptor, PaginationDto } from '@libeilong/nestjs/pagination'
import { UsersService } from './users.service'

@Controller('users')
@UseInterceptors(PageinationInterceptor)
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get()
  async list(@Query() query: PaginationDto) {
    const { limit, offset } = this.usersService.getPagination(query)
    const [list, count] = await this.usersService.findAndCount({ limit, offset })
    return [list, count]
  }
}
```

> 注意：拦截器只对返回值是 `[list, count]` 形式的数组生效。

#### `getPagination` / `convertOptions`

在 service 中可使用：

```ts
// users.service.ts
import { Injectable } from '@nestjs/common'
import { convertOptions } from '@libeilong/nestjs/pagination'
import { UserListQueryDto } from './dto/user-list-query.dto'

@Injectable()
export class UsersService {
  async findList(dto: UserListQueryDto) {
    const { options, where } = convertOptions(dto)
    const { limit, offset } = options

    // 示例：对接 TypeORM / Prisma 等
    const [list, count] = await this.findAndCountFromDb({ where, limit, offset })
    return [list, count] as const
  }
}
```

---

### 3. DTO 辅助类型

位于 `@libeilong/nestjs/validator` 目录，核心导出：

- `CreateType<T>`：生成“创建用” DTO
- `UpdateType<T>`：生成“更新用” DTO（所有字段可选）
- `SearchType<T>`：生成“搜索用” DTO，自动做查询字符串到正确类型的转换
- `setPaginationDto(dtoClass)`：自定义 SearchType 使用的分页 DTO（默认 `PaginationDto`）

所有类型都会尽量继承：

- 原始类上的 `class-validator` 元数据
- `class-transformer` 元数据
- Swagger 的 `@ApiProperty` / `@ApiPropertyOptional` 等元数据

#### 3.1 `CreateType<T>`

适用于“创建实体”场景：以实体类为基础，复制其字段与验证 / Swagger 配置。

```ts
// user.entity.ts
import { IsString, IsEmail } from 'class-validator'
import { ApiProperty } from '@nestjs/swagger'

export class UserEntity {
  @ApiProperty()
  @IsString()
  name: string

  @ApiProperty()
  @IsEmail()
  email: string
}

// dto/create-user.dto.ts
import { CreateType } from '@libeilong/nestjs/validator/create.type'
import { UserEntity } from '../entities/user.entity'

export class CreateUserDto extends CreateType(UserEntity) {}
```

`CreateUserDto` 会继承 `UserEntity` 上的校验规则与 Swagger 文档配置。

#### 3.2 `UpdateType<T>`

在 `CreateType` 基础上，再通过 Swagger 的 `PartialType` 将所有字段变为可选，适用于“更新”场景：

```ts
// dto/update-user.dto.ts
import { UpdateType } from '@libeilong/nestjs/validator/update.type'
import { UserEntity } from '../entities/user.entity'

export class UpdateUserDto extends UpdateType(UserEntity) {}
```

这样可以保持创建 / 更新 DTO 在字段与文档上的一致性，同时更新时字段全部可选。

#### 3.3 `SearchType<T>`

用于“列表搜索”的 Query DTO，特点：

- 自动继承原类的验证与 Swagger 元数据
- 自动将字符串 Query 参数转换为合适的类型（number / boolean / Date / JSON / string）
- 默认为所有字段 `Partial`（可选）
- 与分页 DTO 进行交叉类型：`SearchType(BaseDto)` + `PaginationDto`

```ts
// dto/user-search.dto.ts
import { SearchType } from '@libeilong/nestjs/validator/search.type'
import { UserEntity } from '../entities/user.entity'

export class UserSearchDto extends SearchType(UserEntity) {}
```

在控制器中：

```ts
// users.controller.ts
import { Controller, Get, Query, UseInterceptors } from '@nestjs/common'
import { PageinationInterceptor } from '@libeilong/nestjs/pagination'
import { UserSearchDto } from './dto/user-search.dto'
import { UsersService } from './users.service'

@Controller('users')
@UseInterceptors(PageinationInterceptor)
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get()
  async search(@Query() query: UserSearchDto) {
    const [list, count] = await this.usersService.search(query)
    return [list, count]
  }
}
```

在 service 中可以配合 `convertOptions` 使用：

```ts
// users.service.ts
import { Injectable } from '@nestjs/common'
import { convertOptions } from '@libeilong/nestjs/pagination'
import { UserSearchDto } from './dto/user-search.dto'

@Injectable()
export class UsersService {
  async search(dto: UserSearchDto) {
    const { options, where } = convertOptions(dto)
    const [list, count] = await this.findAndCountFromDb({ where, ...options })
    return [list, count] as const
  }
}
```

##### 查询参数自动转换行为

`SearchType` 内部会：

1. 根据属性上绑定的 validator / design type 推断字段类型
2. 对字符串 Query 参数进行转换：
   - 若看起来是 JSON（如 `{"a":1}`、`[1,2,3]`），尝试 `JSON.parse`
   - 若目标类型为 `Number`，尝试转为数字
   - 若为 `Boolean`，将 `'true'`/`'false'` 转为布尔
   - 若为 `Date`，尝试用 `new Date(value)` 解析
   - 其他情况保持字符串

这使得复杂查询条件（如 `where` 对象、数组条件等）可以通过 URL 查询参数以 JSON 形式传入。

#### 3.4 自定义分页 DTO：`setPaginationDto`

如果需要在 Search DTO 中使用自定义分页类，可以调用：

```ts
import { setPaginationDto } from '@libeilong/nestjs/validator/search.type'
import { MyPaginationDto } from './dto/my-pagination.dto'

setPaginationDto(MyPaginationDto)
```

之后 `SearchType` 会与 `MyPaginationDto` 而不是默认 `PaginationDto` 进行组合。

---

### 4. 验证相关装饰器

本包还导出了一些验证辅助：

- `PropertyOptional`：统一可选属性的 Swagger / 验证元数据
- 自定义 `IsArray` 装饰器：在 query 场景下更友好地处理数组参数

典型用法：

```ts
// dto/example.dto.ts
import { PropertyOptional } from '@libeilong/nestjs/validator/property-optional'
import { IsArray } from '@libeilong/nestjs/validator/property/IsArray'

export class ExampleDto {
  @PropertyOptional()
  keyword?: string

  @PropertyOptional()
  @IsArray({ each: true })
  tags?: string[]
}
```

`PropertyOptional` 会同步处理：

- Swagger：将字段标记为可选
- class-validator：允许字段缺省

`IsArray` 则封装了对数组类型字段的验证与（在部分场景下）字符串 -> 数组的转换逻辑。

---

## 使用建议

- 在新项目中，可优先使用 `CreateType` / `UpdateType` / `SearchType` 来生成 DTO，尽量减少重复的验证与 Swagger 配置。
- 建议统一使用 `PageinationInterceptor` 和 `PaginationDto`，让列表接口始终返回 `{ list, count }` 的结构。
- `SearchType` 支持自动类型转换与 JSON 解析，前端可直接通过查询字符串传递复杂条件，但需要注意：
  - 非法 JSON 会抛出解析错误
  - 类型推断依赖已配置的 validator / design type，必要时请显式声明。

---

## License

MIT