feat: 开发broker相关代码,开发全局代码
This commit is contained in:
@@ -1,8 +1,9 @@
|
|||||||
# 开发环境配置
|
# 开发环境配置
|
||||||
PORT=3200
|
PORT=3200
|
||||||
|
|
||||||
DB_HOST=127.0.0.1
|
DB_HOST=localhost
|
||||||
DB_PORT=5432
|
DB_PORT=5432
|
||||||
DB_USER=joey
|
DB_USER=
|
||||||
DB_PASSWORD=vest_mind_0228
|
DB_PASSWORD=
|
||||||
DB_DATABASE=vest_mind
|
DB_DATABASE=vest_mind_test
|
||||||
|
# DB_DATABASE_TEST=vest_mind_test
|
||||||
|
|||||||
714
apps/api/NESTJS-GENERATE-GUIDE.md
Normal file
714
apps/api/NESTJS-GENERATE-GUIDE.md
Normal file
@@ -0,0 +1,714 @@
|
|||||||
|
# NestJS 模块生成指南
|
||||||
|
|
||||||
|
## 快速生成完整模块
|
||||||
|
|
||||||
|
### 方法一:使用 `resource` 命令(推荐)
|
||||||
|
|
||||||
|
一次性生成包含 module、controller、service、entity 的完整 CRUD 资源:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 生成 user 模块(会在 src/modules/user 目录下创建所有文件)
|
||||||
|
pnpm nest g resource modules/user
|
||||||
|
|
||||||
|
# 或者使用别名
|
||||||
|
pnpm nest g res modules/user
|
||||||
|
```
|
||||||
|
|
||||||
|
**交互式选项:**
|
||||||
|
|
||||||
|
- `What transport layer do you use?` → 选择 `REST API`
|
||||||
|
- `Would you like to generate CRUD entry points?` → 选择 `Yes`
|
||||||
|
|
||||||
|
**生成的文件结构:**
|
||||||
|
|
||||||
|
```
|
||||||
|
src/modules/user/
|
||||||
|
├── user.controller.ts # 控制器
|
||||||
|
├── user.controller.spec.ts # 控制器测试
|
||||||
|
├── user.service.ts # 服务
|
||||||
|
├── user.service.spec.ts # 服务测试
|
||||||
|
├── user.module.ts # 模块
|
||||||
|
└── entities/
|
||||||
|
└── user.entity.ts # 实体(TypeORM)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方法二:分别生成各个文件
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 生成模块
|
||||||
|
pnpm nest g module modules/user
|
||||||
|
|
||||||
|
# 生成控制器
|
||||||
|
pnpm nest g controller modules/user
|
||||||
|
|
||||||
|
# 生成服务
|
||||||
|
pnpm nest g service modules/user
|
||||||
|
|
||||||
|
# 生成实体(需要手动创建,或使用 TypeORM CLI)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方法三:使用 TypeORM 生成实体
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装 TypeORM CLI(如果还没有)
|
||||||
|
pnpm add -D typeorm
|
||||||
|
|
||||||
|
# 生成实体(需要先配置 TypeORM)
|
||||||
|
pnpm typeorm entity:create -n User
|
||||||
|
```
|
||||||
|
|
||||||
|
## 文件组织模式
|
||||||
|
|
||||||
|
### 模式一:按功能模块组织(推荐)⭐
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── modules/
|
||||||
|
│ ├── user/
|
||||||
|
│ │ ├── user.module.ts
|
||||||
|
│ │ ├── user.controller.ts
|
||||||
|
│ │ ├── user.service.ts
|
||||||
|
│ │ ├── user.entity.ts
|
||||||
|
│ │ ├── user.dto.ts # DTO 文件
|
||||||
|
│ │ ├── user.controller.spec.ts
|
||||||
|
│ │ └── user.service.spec.ts
|
||||||
|
│ ├── order/
|
||||||
|
│ │ ├── order.module.ts
|
||||||
|
│ │ ├── order.controller.ts
|
||||||
|
│ │ ├── order.service.ts
|
||||||
|
│ │ ├── order.entity.ts
|
||||||
|
│ │ └── ...
|
||||||
|
│ └── product/
|
||||||
|
│ └── ...
|
||||||
|
├── database/
|
||||||
|
│ ├── database.module.ts
|
||||||
|
│ └── database.config.ts
|
||||||
|
└── app.module.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
**优点:**
|
||||||
|
|
||||||
|
- 模块化清晰,每个功能独立
|
||||||
|
- 易于维护和扩展
|
||||||
|
- 符合 NestJS 最佳实践
|
||||||
|
|
||||||
|
**适用场景:**
|
||||||
|
|
||||||
|
- 大型企业级应用
|
||||||
|
- 需要复杂业务逻辑
|
||||||
|
- 需要清晰的架构分层
|
||||||
|
|
||||||
|
## 推荐的文件组织(当前项目)
|
||||||
|
|
||||||
|
基于你的项目结构,推荐使用**模式一**:
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── modules/ # 业务模块
|
||||||
|
│ ├── user/
|
||||||
|
│ │ ├── user.module.ts
|
||||||
|
│ │ ├── user.controller.ts
|
||||||
|
│ │ ├── user.service.ts
|
||||||
|
│ │ ├── user.entity.ts
|
||||||
|
│ │ ├── dto/ # DTO 文件(可选)
|
||||||
|
│ │ │ ├── create-user.dto.ts
|
||||||
|
│ │ │ └── update-user.dto.ts
|
||||||
|
│ │ └── interfaces/ # 接口定义(可选)
|
||||||
|
│ │ └── user.interface.ts
|
||||||
|
│ ├── order/
|
||||||
|
│ └── product/
|
||||||
|
├── database/ # 数据库配置
|
||||||
|
│ ├── database.module.ts
|
||||||
|
│ └── database.config.ts
|
||||||
|
├── common/ # 公共模块(可选)
|
||||||
|
│ ├── filters/
|
||||||
|
│ ├── guards/
|
||||||
|
│ ├── interceptors/
|
||||||
|
│ └── pipes/
|
||||||
|
└── app.module.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## 实际生成示例
|
||||||
|
|
||||||
|
### 生成 user 模块
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 生成完整资源
|
||||||
|
cd /Users/joey-xd/sites/vest-mind/vest-mind-backend/apps/api
|
||||||
|
pnpm nest g resource modules/user
|
||||||
|
|
||||||
|
# 2. 选择选项:
|
||||||
|
# - REST API
|
||||||
|
# - Yes (生成 CRUD)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 生成后的文件内容示例
|
||||||
|
|
||||||
|
**user.entity.ts:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class User {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**user.service.ts:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { CreateUserDto } from './dto/create-user.dto';
|
||||||
|
import { UpdateUserDto } from './dto/update-user.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserService {
|
||||||
|
create(createUserDto: CreateUserDto) {
|
||||||
|
return 'This action adds a new user';
|
||||||
|
}
|
||||||
|
|
||||||
|
findAll() {
|
||||||
|
return `This action returns all user`;
|
||||||
|
}
|
||||||
|
|
||||||
|
findOne(id: number) {
|
||||||
|
return `This action returns a #${id} user`;
|
||||||
|
}
|
||||||
|
|
||||||
|
update(id: number, updateUserDto: UpdateUserDto) {
|
||||||
|
return `This action updates a #${id} user`;
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(id: number) {
|
||||||
|
return `This action removes a #${id} user`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**user.controller.ts:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
Patch,
|
||||||
|
Param,
|
||||||
|
Delete,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { UserService } from './user.service';
|
||||||
|
import { CreateUserDto } from './dto/create-user.dto';
|
||||||
|
import { UpdateUserDto } from './dto/update-user.dto';
|
||||||
|
|
||||||
|
@Controller('user')
|
||||||
|
export class UserController {
|
||||||
|
constructor(private readonly userService: UserService) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
create(@Body() createUserDto: CreateUserDto) {
|
||||||
|
return this.userService.create(createUserDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
findAll() {
|
||||||
|
return this.userService.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
findOne(@Param('id') id: string) {
|
||||||
|
return this.userService.findOne(+id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':id')
|
||||||
|
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
|
||||||
|
return this.userService.update(+id, updateUserDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
remove(@Param('id') id: string) {
|
||||||
|
return this.userService.remove(+id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常用生成命令速查
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 生成资源(完整 CRUD)
|
||||||
|
pnpm nest g resource modules/user
|
||||||
|
|
||||||
|
# 生成模块
|
||||||
|
pnpm nest g module modules/user
|
||||||
|
|
||||||
|
# 生成控制器
|
||||||
|
pnpm nest g controller modules/user
|
||||||
|
|
||||||
|
# 生成服务
|
||||||
|
pnpm nest g service modules/user
|
||||||
|
|
||||||
|
# 生成守卫
|
||||||
|
pnpm nest g guard modules/user/guards/auth
|
||||||
|
|
||||||
|
# 生成拦截器
|
||||||
|
pnpm nest g interceptor modules/user/interceptors/logging
|
||||||
|
|
||||||
|
# 生成过滤器
|
||||||
|
pnpm nest g filter modules/user/filters/http-exception
|
||||||
|
|
||||||
|
# 生成管道
|
||||||
|
pnpm nest g pipe modules/user/pipes/validation
|
||||||
|
|
||||||
|
# 生成装饰器
|
||||||
|
pnpm nest g decorator modules/user/decorators/roles
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **路径规范:**
|
||||||
|
- 使用 `modules/模块名` 作为路径
|
||||||
|
- 会自动创建目录结构
|
||||||
|
|
||||||
|
2. **自动导入:**
|
||||||
|
- 生成的文件会自动导入到相应的模块中
|
||||||
|
- 如果不想自动导入,使用 `--skip-import` 选项
|
||||||
|
|
||||||
|
3. **测试文件:**
|
||||||
|
- 默认会生成 `.spec.ts` 测试文件
|
||||||
|
- 使用 `--no-spec` 可以跳过测试文件生成
|
||||||
|
|
||||||
|
4. **扁平结构:**
|
||||||
|
- 使用 `--flat` 可以生成扁平结构(所有文件在同一目录)
|
||||||
|
- 不推荐使用,会破坏模块化结构
|
||||||
|
|
||||||
|
## DTO(Data Transfer Object)详解
|
||||||
|
|
||||||
|
### 什么是 DTO?
|
||||||
|
|
||||||
|
DTO(Data Transfer Object)是数据传输对象,用于在不同层之间传输数据。在 NestJS 中,DTO 主要用于:
|
||||||
|
|
||||||
|
1. **定义 API 请求和响应的数据结构**
|
||||||
|
2. **数据验证**(结合 `class-validator`)
|
||||||
|
3. **类型安全**
|
||||||
|
4. **API 文档生成**(结合 Swagger)
|
||||||
|
|
||||||
|
### 为什么需要 DTO?
|
||||||
|
|
||||||
|
#### 1. 数据验证
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 没有 DTO - 不安全
|
||||||
|
@Post()
|
||||||
|
create(@Body() body: any) {
|
||||||
|
// body 可能是任何数据,没有验证
|
||||||
|
return this.userService.create(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 DTO - 安全
|
||||||
|
@Post()
|
||||||
|
create(@Body() createUserDto: CreateUserDto) {
|
||||||
|
// 数据已经验证,类型安全
|
||||||
|
return this.userService.create(createUserDto);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 类型安全
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// DTO 定义了明确的数据结构
|
||||||
|
export class CreateUserDto {
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
age: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TypeScript 会检查类型
|
||||||
|
const user = new CreateUserDto();
|
||||||
|
user.username = 'john'; // ✅ 正确
|
||||||
|
user.age = '25'; // ❌ TypeScript 错误
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. API 文档
|
||||||
|
|
||||||
|
使用 Swagger 时,DTO 会自动生成 API 文档:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class CreateUserDto {
|
||||||
|
@ApiProperty({ description: '用户名', example: 'john' })
|
||||||
|
username: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '邮箱', example: 'john@example.com' })
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### DTO 的类型
|
||||||
|
|
||||||
|
#### 1. Create DTO(创建数据)
|
||||||
|
|
||||||
|
用于创建新资源时的数据验证:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// dto/create-user.dto.ts
|
||||||
|
import {
|
||||||
|
IsString,
|
||||||
|
IsEmail,
|
||||||
|
IsOptional,
|
||||||
|
MinLength,
|
||||||
|
MaxLength,
|
||||||
|
} from 'class-validator';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class CreateUserDto {
|
||||||
|
@ApiProperty({ description: '用户名', example: 'john' })
|
||||||
|
@IsString()
|
||||||
|
@MinLength(3)
|
||||||
|
@MaxLength(20)
|
||||||
|
username: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '邮箱', example: 'john@example.com' })
|
||||||
|
@IsEmail()
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '密码', example: 'password123' })
|
||||||
|
@IsString()
|
||||||
|
@MinLength(6)
|
||||||
|
password: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '年龄', example: 25, required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
age?: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Update DTO(更新数据)
|
||||||
|
|
||||||
|
用于更新资源时的数据验证:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// dto/update-user.dto.ts
|
||||||
|
import { PartialType } from '@nestjs/mapped-types';
|
||||||
|
import { CreateUserDto } from './create-user.dto';
|
||||||
|
|
||||||
|
// 方式一:使用 PartialType(推荐)
|
||||||
|
export class UpdateUserDto extends PartialType(CreateUserDto) {}
|
||||||
|
|
||||||
|
// 方式二:手动定义(更灵活)
|
||||||
|
// export class UpdateUserDto {
|
||||||
|
// @IsOptional()
|
||||||
|
// @IsString()
|
||||||
|
// username?: string;
|
||||||
|
//
|
||||||
|
// @IsOptional()
|
||||||
|
// @IsEmail()
|
||||||
|
// email?: string;
|
||||||
|
// }
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Query DTO(查询参数)
|
||||||
|
|
||||||
|
用于查询列表时的参数验证:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// dto/query-user.dto.ts
|
||||||
|
import { IsOptional, IsNumber, Min, Max } from 'class-validator';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
|
||||||
|
export class QueryUserDto {
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsNumber()
|
||||||
|
@Min(1)
|
||||||
|
page?: number = 1;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsNumber()
|
||||||
|
@Min(1)
|
||||||
|
@Max(100)
|
||||||
|
limit?: number = 10;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
search?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
sortBy?: string = 'createdAt';
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
sortOrder?: 'ASC' | 'DESC' = 'DESC';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Response DTO(响应数据)
|
||||||
|
|
||||||
|
用于定义 API 响应的数据结构:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// dto/user-response.dto.ts
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class UserResponseDto {
|
||||||
|
@ApiProperty({ description: '用户ID', example: 1 })
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '用户名', example: 'john' })
|
||||||
|
username: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '邮箱', example: 'john@example.com' })
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '创建时间', example: '2024-01-01T00:00:00Z' })
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '更新时间', example: '2024-01-01T00:00:00Z' })
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 在 Controller 中使用 DTO
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
Patch,
|
||||||
|
Param,
|
||||||
|
Query,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { UserService } from './user.service';
|
||||||
|
import { CreateUserDto } from './dto/create-user.dto';
|
||||||
|
import { UpdateUserDto } from './dto/update-user.dto';
|
||||||
|
import { QueryUserDto } from './dto/query-user.dto';
|
||||||
|
|
||||||
|
@Controller('users')
|
||||||
|
export class UserController {
|
||||||
|
constructor(private readonly userService: UserService) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
create(@Body() createUserDto: CreateUserDto) {
|
||||||
|
return this.userService.create(createUserDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
findAll(@Query() queryUserDto: QueryUserDto) {
|
||||||
|
return this.userService.findAll(queryUserDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
findOne(@Param('id') id: string) {
|
||||||
|
return this.userService.findOne(+id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':id')
|
||||||
|
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
|
||||||
|
return this.userService.update(+id, updateUserDto);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 启用全局验证管道
|
||||||
|
|
||||||
|
在 `main.ts` 中启用全局验证:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ValidationPipe } from '@nestjs/common';
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
const app = await NestFactory.create(AppModule);
|
||||||
|
|
||||||
|
// 启用全局验证管道
|
||||||
|
app.useGlobalPipes(
|
||||||
|
new ValidationPipe({
|
||||||
|
whitelist: true, // 自动删除不在 DTO 中的属性
|
||||||
|
forbidNonWhitelisted: true, // 如果请求包含未定义的属性,抛出错误
|
||||||
|
transform: true, // 自动转换类型(如字符串转数字)
|
||||||
|
transformOptions: {
|
||||||
|
enableImplicitConversion: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await app.listen(3000);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 常用验证装饰器
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
IsString, // 字符串
|
||||||
|
IsNumber, // 数字
|
||||||
|
IsBoolean, // 布尔值
|
||||||
|
IsEmail, // 邮箱
|
||||||
|
IsUrl, // URL
|
||||||
|
IsDate, // 日期
|
||||||
|
IsOptional, // 可选字段
|
||||||
|
IsNotEmpty, // 非空
|
||||||
|
MinLength, // 最小长度
|
||||||
|
MaxLength, // 最大长度
|
||||||
|
Min, // 最小值
|
||||||
|
Max, // 最大值
|
||||||
|
IsEnum, // 枚举
|
||||||
|
IsArray, // 数组
|
||||||
|
IsObject, // 对象
|
||||||
|
ValidateNested, // 嵌套对象验证
|
||||||
|
IsUUID, // UUID
|
||||||
|
Matches, // 正则匹配
|
||||||
|
} from 'class-validator';
|
||||||
|
```
|
||||||
|
|
||||||
|
### DTO 示例:完整的用户模块
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// dto/create-user.dto.ts
|
||||||
|
import {
|
||||||
|
IsString,
|
||||||
|
IsEmail,
|
||||||
|
IsOptional,
|
||||||
|
MinLength,
|
||||||
|
IsNumber,
|
||||||
|
Min,
|
||||||
|
Max,
|
||||||
|
} from 'class-validator';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class CreateUserDto {
|
||||||
|
@ApiProperty({ description: '用户名', example: 'john' })
|
||||||
|
@IsString()
|
||||||
|
@MinLength(3)
|
||||||
|
@MaxLength(20)
|
||||||
|
username: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '邮箱', example: 'john@example.com' })
|
||||||
|
@IsEmail()
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '密码', example: 'password123' })
|
||||||
|
@IsString()
|
||||||
|
@MinLength(6)
|
||||||
|
password: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '年龄', example: 25, required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
@Min(18)
|
||||||
|
@Max(100)
|
||||||
|
age?: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// dto/update-user.dto.ts
|
||||||
|
import { PartialType } from '@nestjs/mapped-types';
|
||||||
|
import { CreateUserDto } from './create-user.dto';
|
||||||
|
|
||||||
|
export class UpdateUserDto extends PartialType(CreateUserDto) {}
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// dto/query-user.dto.ts
|
||||||
|
import { IsOptional, IsNumber, IsString, Min, Max } from 'class-validator';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
|
||||||
|
export class QueryUserDto {
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsNumber()
|
||||||
|
@Min(1)
|
||||||
|
page?: number = 1;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsNumber()
|
||||||
|
@Min(1)
|
||||||
|
@Max(100)
|
||||||
|
limit?: number = 10;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
search?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 安装必要的依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装验证库
|
||||||
|
pnpm add class-validator class-transformer
|
||||||
|
|
||||||
|
# 如果使用 Swagger(可选)
|
||||||
|
pnpm add @nestjs/swagger swagger-ui-express
|
||||||
|
```
|
||||||
|
|
||||||
|
### DTO vs Entity
|
||||||
|
|
||||||
|
| 特性 | DTO | Entity |
|
||||||
|
| -------- | ---------------------- | ------------------- |
|
||||||
|
| **用途** | 数据传输和验证 | 数据库模型 |
|
||||||
|
| **位置** | `dto/` 目录 | `entities/` 目录 |
|
||||||
|
| **验证** | 使用 `class-validator` | 使用 TypeORM 装饰器 |
|
||||||
|
| **暴露** | 暴露给 API 客户端 | 不直接暴露 |
|
||||||
|
| **示例** | `CreateUserDto` | `User` entity |
|
||||||
|
|
||||||
|
**最佳实践:**
|
||||||
|
|
||||||
|
- ✅ Entity 包含数据库字段(如 `id`, `createdAt`)
|
||||||
|
- ✅ DTO 只包含客户端需要传递的字段
|
||||||
|
- ✅ 使用 DTO 转换 Entity,避免暴露敏感信息
|
||||||
|
|
||||||
|
### DTO 转换示例
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// service 中使用
|
||||||
|
@Injectable()
|
||||||
|
export class UserService {
|
||||||
|
async create(createUserDto: CreateUserDto): Promise<User> {
|
||||||
|
// DTO → Entity
|
||||||
|
const user = this.userRepository.create({
|
||||||
|
username: createUserDto.username,
|
||||||
|
email: createUserDto.email,
|
||||||
|
// 不直接传递 password,需要加密
|
||||||
|
passwordHash: await this.hashPassword(createUserDto.password),
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.userRepository.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll(queryDto: QueryUserDto): Promise<User[]> {
|
||||||
|
// 使用 DTO 中的查询参数
|
||||||
|
return this.userRepository.find({
|
||||||
|
skip: (queryDto.page - 1) * queryDto.limit,
|
||||||
|
take: queryDto.limit,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
1. ✅ **使用 `resource` 命令生成完整模块**
|
||||||
|
2. ✅ **按功能模块组织文件**
|
||||||
|
3. ✅ **每个模块包含:module、controller、service、entity**
|
||||||
|
4. ✅ **DTO 文件放在模块目录下的 `dto/` 子目录**
|
||||||
|
5. ✅ **为每个操作创建对应的 DTO(Create、Update、Query、Response)**
|
||||||
|
6. ✅ **使用 `class-validator` 进行数据验证**
|
||||||
|
7. ✅ **使用 `PartialType` 创建 Update DTO**
|
||||||
|
8. ✅ **启用全局验证管道**
|
||||||
|
9. ✅ **区分 DTO 和 Entity,不要混用**
|
||||||
|
10. ✅ **公共组件放在 `common/` 目录**
|
||||||
|
11. ❌ **避免使用扁平结构**
|
||||||
|
12. ❌ **避免按类型组织文件**
|
||||||
|
13. ❌ **避免在 Entity 中直接暴露给 API**
|
||||||
@@ -28,6 +28,7 @@ export default tseslint.config(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
rules: {
|
rules: {
|
||||||
|
'@typescript-eslint/no-unsafe-call': 'off',
|
||||||
'@typescript-eslint/no-explicit-any': 'off',
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
'@typescript-eslint/no-floating-promises': 'warn',
|
'@typescript-eslint/no-floating-promises': 'warn',
|
||||||
'@typescript-eslint/no-unsafe-argument': 'warn',
|
'@typescript-eslint/no-unsafe-argument': 'warn',
|
||||||
|
|||||||
@@ -1,76 +1,90 @@
|
|||||||
{
|
{
|
||||||
"name": "api",
|
"name": "api",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nest build",
|
"build": "nest build",
|
||||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
"start": "nest start",
|
"start": "nest start",
|
||||||
"start:dev": "nest start --watch",
|
"start:dev": "nest start --watch",
|
||||||
"start:debug": "nest start --debug --watch",
|
"start:debug": "nest start --debug --watch",
|
||||||
"start:prod": "node dist/main",
|
"start:prod": "node dist/main",
|
||||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"test:cov": "jest --coverage",
|
"test:cov": "jest --coverage",
|
||||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||||
},
|
"test:integration": "jest --config ./test/jest-integration.json"
|
||||||
"dependencies": {
|
|
||||||
"@nestjs/common": "^11.0.1",
|
|
||||||
"@nestjs/config": "^4.0.2",
|
|
||||||
"@nestjs/core": "^11.0.1",
|
|
||||||
"@nestjs/platform-express": "^11.0.1",
|
|
||||||
"@nestjs/typeorm": "^11.0.0",
|
|
||||||
"pg": "^8.16.3",
|
|
||||||
"reflect-metadata": "^0.2.2",
|
|
||||||
"rxjs": "^7.8.1",
|
|
||||||
"typeorm": "^0.3.27"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@eslint/eslintrc": "^3.2.0",
|
|
||||||
"@eslint/js": "^9.18.0",
|
|
||||||
"@nestjs/cli": "^11.0.0",
|
|
||||||
"@nestjs/schematics": "^11.0.0",
|
|
||||||
"@nestjs/testing": "^11.0.1",
|
|
||||||
"@types/express": "^5.0.0",
|
|
||||||
"@types/jest": "^30.0.0",
|
|
||||||
"@types/node": "^22.10.7",
|
|
||||||
"@types/pg": "^8.15.6",
|
|
||||||
"@types/supertest": "^6.0.2",
|
|
||||||
"eslint": "^9.18.0",
|
|
||||||
"eslint-config-prettier": "^10.0.1",
|
|
||||||
"eslint-plugin-prettier": "^5.2.2",
|
|
||||||
"globals": "^16.0.0",
|
|
||||||
"jest": "^30.0.0",
|
|
||||||
"prettier": "^3.4.2",
|
|
||||||
"source-map-support": "^0.5.21",
|
|
||||||
"supertest": "^7.0.0",
|
|
||||||
"ts-jest": "^29.2.5",
|
|
||||||
"ts-loader": "^9.5.2",
|
|
||||||
"ts-node": "^10.9.2",
|
|
||||||
"tsconfig-paths": "^4.2.0",
|
|
||||||
"typescript": "^5.7.3",
|
|
||||||
"typescript-eslint": "^8.20.0"
|
|
||||||
},
|
|
||||||
"jest": {
|
|
||||||
"moduleFileExtensions": [
|
|
||||||
"js",
|
|
||||||
"json",
|
|
||||||
"ts"
|
|
||||||
],
|
|
||||||
"rootDir": "src",
|
|
||||||
"testRegex": ".*\\.spec\\.ts$",
|
|
||||||
"transform": {
|
|
||||||
"^.+\\.(t|j)s$": "ts-jest"
|
|
||||||
},
|
},
|
||||||
"collectCoverageFrom": [
|
"dependencies": {
|
||||||
"**/*.(t|j)s"
|
"@nestjs/common": "^11.0.1",
|
||||||
],
|
"@nestjs/config": "^4.0.2",
|
||||||
"coverageDirectory": "../coverage",
|
"@nestjs/core": "^11.0.1",
|
||||||
"testEnvironment": "node"
|
"@nestjs/mapped-types": "^2.1.0",
|
||||||
}
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
|
"@nestjs/swagger": "^11.2.2",
|
||||||
|
"@nestjs/typeorm": "^11.0.0",
|
||||||
|
"body-parser": "^2.2.0",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
|
"class-validator": "^0.14.2",
|
||||||
|
"compression": "^1.8.1",
|
||||||
|
"express-rate-limit": "^8.2.1",
|
||||||
|
"helmet": "^8.1.0",
|
||||||
|
"pg": "^8.16.3",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"rxjs": "^7.8.1",
|
||||||
|
"typeorm": "^0.3.27"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
|
"@eslint/js": "^9.18.0",
|
||||||
|
"@nestjs/cli": "^11.0.0",
|
||||||
|
"@nestjs/schematics": "^11.0.0",
|
||||||
|
"@nestjs/testing": "^11.0.1",
|
||||||
|
"@types/body-parser": "^1.19.6",
|
||||||
|
"@types/compression": "^1.8.1",
|
||||||
|
"@types/express": "^5.0.0",
|
||||||
|
"@types/jest": "^30.0.0",
|
||||||
|
"@types/node": "^22.10.7",
|
||||||
|
"@types/pg": "^8.15.6",
|
||||||
|
"@types/supertest": "^6.0.2",
|
||||||
|
"eslint": "^9.18.0",
|
||||||
|
"eslint-config-prettier": "^10.0.1",
|
||||||
|
"eslint-plugin-prettier": "^5.2.2",
|
||||||
|
"globals": "^16.0.0",
|
||||||
|
"jest": "^30.0.0",
|
||||||
|
"prettier": "^3.4.2",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
|
"supertest": "^7.0.0",
|
||||||
|
"ts-jest": "^29.2.5",
|
||||||
|
"ts-loader": "^9.5.2",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"tsconfig-paths": "^4.2.0",
|
||||||
|
"typescript": "^5.7.3",
|
||||||
|
"typescript-eslint": "^8.20.0"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"js",
|
||||||
|
"json",
|
||||||
|
"ts"
|
||||||
|
],
|
||||||
|
"rootDir": "src",
|
||||||
|
"testMatch": [
|
||||||
|
"**/__tests__/**/*.spec.ts",
|
||||||
|
"**/*.spec.ts"
|
||||||
|
],
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
},
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"**/*.(t|j)s"
|
||||||
|
],
|
||||||
|
"coverageDirectory": "../coverage",
|
||||||
|
"testEnvironment": "node"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import { AppService } from './app.service';
|
|||||||
|
|
||||||
@Controller()
|
@Controller()
|
||||||
export class AppController {
|
export class AppController {
|
||||||
constructor(private readonly appService: AppService) {}
|
constructor(private readonly appService: AppService) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
getHello(): string {
|
getHello(): string {
|
||||||
return this.appService.getHello();
|
return this.appService.getHello();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { DatabaseModule } from './database/database.module';
|
import { DatabaseModule } from './database/database.module';
|
||||||
|
import { CoreModule } from './core/core.module';
|
||||||
|
import { BrokerModule } from './modules/broker/broker.module';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
|
|
||||||
@@ -13,7 +15,9 @@ import { AppService } from './app.service';
|
|||||||
'.env',
|
'.env',
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
CoreModule,
|
||||||
DatabaseModule,
|
DatabaseModule,
|
||||||
|
BrokerModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService],
|
providers: [AppService],
|
||||||
|
|||||||
96
apps/api/src/common/filters/http-exception.filter.ts
Normal file
96
apps/api/src/common/filters/http-exception.filter.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import {
|
||||||
|
ExceptionFilter,
|
||||||
|
Catch,
|
||||||
|
ArgumentsHost,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Response } from 'express';
|
||||||
|
|
||||||
|
export interface ErrorResponse {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data: null;
|
||||||
|
timestamp: string;
|
||||||
|
errors?: string[] | Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Catch()
|
||||||
|
export class HttpExceptionFilter implements ExceptionFilter {
|
||||||
|
catch(exception: unknown, host: ArgumentsHost) {
|
||||||
|
const ctx = host.switchToHttp();
|
||||||
|
const response = ctx.getResponse<Response>();
|
||||||
|
|
||||||
|
let status = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||||
|
let message = 'Internal server error';
|
||||||
|
let code = -1; // 默认错误码
|
||||||
|
let errors: string[] | Record<string, unknown> | null = null;
|
||||||
|
|
||||||
|
if (exception instanceof HttpException) {
|
||||||
|
status = exception.getStatus();
|
||||||
|
const exceptionResponse = exception.getResponse();
|
||||||
|
|
||||||
|
if (typeof exceptionResponse === 'string') {
|
||||||
|
message = exceptionResponse;
|
||||||
|
} else if (
|
||||||
|
typeof exceptionResponse === 'object' &&
|
||||||
|
exceptionResponse !== null
|
||||||
|
) {
|
||||||
|
const responseObj = exceptionResponse as {
|
||||||
|
message?: string | string[];
|
||||||
|
errors?: string[] | Record<string, unknown>;
|
||||||
|
code?: number;
|
||||||
|
};
|
||||||
|
message =
|
||||||
|
typeof responseObj.message === 'string'
|
||||||
|
? responseObj.message
|
||||||
|
: Array.isArray(responseObj.message)
|
||||||
|
? responseObj.message.join(', ')
|
||||||
|
: message;
|
||||||
|
errors = responseObj.errors || null;
|
||||||
|
|
||||||
|
// 如果有自定义的 code,使用它
|
||||||
|
if (responseObj.code !== undefined) {
|
||||||
|
code = responseObj.code;
|
||||||
|
} else {
|
||||||
|
// 根据 HTTP 状态码设置业务错误码
|
||||||
|
code = this.getBusinessCode(status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (exception instanceof Error) {
|
||||||
|
message = exception.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorResponse: ErrorResponse = {
|
||||||
|
code,
|
||||||
|
message,
|
||||||
|
data: null,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 如果是验证错误,添加详细错误信息
|
||||||
|
if (errors) {
|
||||||
|
errorResponse.errors = errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
response.status(status).json(errorResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 HTTP 状态码返回业务错误码
|
||||||
|
*/
|
||||||
|
private getBusinessCode(httpStatus: number): number {
|
||||||
|
const codeMap: Record<number, number> = {
|
||||||
|
[HttpStatus.BAD_REQUEST]: 400, // 请求参数错误
|
||||||
|
[HttpStatus.UNAUTHORIZED]: 401, // 未授权
|
||||||
|
[HttpStatus.FORBIDDEN]: 403, // 禁止访问
|
||||||
|
[HttpStatus.NOT_FOUND]: 404, // 资源不存在
|
||||||
|
[HttpStatus.CONFLICT]: 409, // 资源冲突
|
||||||
|
[HttpStatus.UNPROCESSABLE_ENTITY]: 422, // 验证失败
|
||||||
|
[HttpStatus.TOO_MANY_REQUESTS]: 429, // 请求过多
|
||||||
|
[HttpStatus.INTERNAL_SERVER_ERROR]: 500, // 服务器错误
|
||||||
|
};
|
||||||
|
|
||||||
|
return codeMap[httpStatus] ?? -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
49
apps/api/src/common/interceptors/transform.interceptor.ts
Normal file
49
apps/api/src/common/interceptors/transform.interceptor.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
NestInterceptor,
|
||||||
|
ExecutionContext,
|
||||||
|
CallHandler,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
|
export interface Response<T> {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data: T;
|
||||||
|
timestamp: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TransformInterceptor<T>
|
||||||
|
implements NestInterceptor<T, Response<T>>
|
||||||
|
{
|
||||||
|
intercept(
|
||||||
|
context: ExecutionContext,
|
||||||
|
next: CallHandler,
|
||||||
|
): Observable<Response<T>> {
|
||||||
|
return next.handle().pipe(
|
||||||
|
map((data) => {
|
||||||
|
// 如果响应已经是统一格式,直接返回
|
||||||
|
if (
|
||||||
|
data &&
|
||||||
|
typeof data === 'object' &&
|
||||||
|
'code' in data &&
|
||||||
|
'message' in data &&
|
||||||
|
'data' in data
|
||||||
|
) {
|
||||||
|
return data as Response<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一包装响应格式
|
||||||
|
// code: 0 表示业务处理成功(HTTP 状态码 200 表示请求成功)
|
||||||
|
return {
|
||||||
|
code: 0,
|
||||||
|
message: 'success',
|
||||||
|
data: (data ?? null) as T,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
34
apps/api/src/core/core.module.ts
Normal file
34
apps/api/src/core/core.module.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { Module, Global } from '@nestjs/common';
|
||||||
|
import { APP_FILTER, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core';
|
||||||
|
import { ValidationPipe } from '@nestjs/common';
|
||||||
|
import { TransformInterceptor } from '../common/interceptors/transform.interceptor';
|
||||||
|
import { HttpExceptionFilter } from '../common/filters/http-exception.filter';
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
providers: [
|
||||||
|
// 全局异常过滤器
|
||||||
|
{
|
||||||
|
provide: APP_FILTER,
|
||||||
|
useClass: HttpExceptionFilter,
|
||||||
|
},
|
||||||
|
// 全局响应拦截器
|
||||||
|
{
|
||||||
|
provide: APP_INTERCEPTOR,
|
||||||
|
useClass: TransformInterceptor,
|
||||||
|
},
|
||||||
|
// 全局验证管道
|
||||||
|
{
|
||||||
|
provide: APP_PIPE,
|
||||||
|
useValue: new ValidationPipe({
|
||||||
|
whitelist: true,
|
||||||
|
forbidNonWhitelisted: true,
|
||||||
|
transform: true,
|
||||||
|
transformOptions: {
|
||||||
|
enableImplicitConversion: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class CoreModule {}
|
||||||
@@ -1,12 +1,61 @@
|
|||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
|
import compression from 'compression';
|
||||||
|
import helmet from 'helmet';
|
||||||
|
import rateLimit from 'express-rate-limit';
|
||||||
|
import bodyParser from 'body-parser';
|
||||||
|
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule);
|
const configService = new ConfigService();
|
||||||
const configService = app.get(ConfigService);
|
// const isProduction = configService.get('NODE_ENV') === 'production';
|
||||||
|
const app = await NestFactory.create(AppModule, {
|
||||||
|
bodyParser: false, // 禁用默认 bodyParser,使用自定义配置
|
||||||
|
});
|
||||||
|
|
||||||
|
// 安全头设置(必须在其他中间件之前)
|
||||||
|
app.use(helmet());
|
||||||
|
|
||||||
|
// 速率限制
|
||||||
|
app.use(
|
||||||
|
rateLimit({
|
||||||
|
windowMs: 60 * 1000, // 1 分钟
|
||||||
|
max: 100, // 限制每个 IP 在 windowMs 时间内最多 10000 个请求
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 请求体解析(设置大小限制为 10mb)
|
||||||
|
app.use(bodyParser.json({ limit: '10mb' }));
|
||||||
|
app.use(bodyParser.urlencoded({ limit: '10mb', extended: true }));
|
||||||
|
|
||||||
|
app.enableCors();
|
||||||
|
/* app.enableCors({
|
||||||
|
// 允许的域名
|
||||||
|
origin: ['http://localhost:3200'],
|
||||||
|
}); */
|
||||||
|
app.setGlobalPrefix('api');
|
||||||
|
// 响应压缩(在 helmet 之后)
|
||||||
|
app.use(compression());
|
||||||
|
|
||||||
|
// Swagger API 文档配置
|
||||||
|
const swaggerConfig = new DocumentBuilder()
|
||||||
|
.setTitle('Vest Mind API')
|
||||||
|
.setDescription('Vest Mind 投资管理系统 API 文档')
|
||||||
|
.setVersion('1.0')
|
||||||
|
.addTag('broker', '券商管理相关接口')
|
||||||
|
.addBearerAuth()
|
||||||
|
.build();
|
||||||
|
const document = SwaggerModule.createDocument(app, swaggerConfig);
|
||||||
|
SwaggerModule.setup('api-docs', app, document, {
|
||||||
|
swaggerOptions: {
|
||||||
|
persistAuthorization: true, // 保持授权状态
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const port = configService.get<number>('PORT', 3200);
|
const port = configService.get<number>('PORT', 3200);
|
||||||
await app.listen(port);
|
await app.listen(port);
|
||||||
console.log(`Application is running on: http://localhost:${port}`);
|
console.log(`Application is running on: http://localhost:${port}`);
|
||||||
|
console.log(`Swagger API Docs: http://localhost:${port}/api-docs`);
|
||||||
}
|
}
|
||||||
void bootstrap();
|
void bootstrap();
|
||||||
|
|||||||
222
apps/api/src/modules/broker/__tests__/broker.controller.spec.ts
Normal file
222
apps/api/src/modules/broker/__tests__/broker.controller.spec.ts
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { BrokerController } from '../broker.controller';
|
||||||
|
import { BrokerService } from '../broker.service';
|
||||||
|
import { Broker } from '../broker.entity';
|
||||||
|
import { CreateBrokerDto } from '../dto/create-broker.dto';
|
||||||
|
import { BatchCreateBrokerDto } from '../dto/batch-create-broker.dto';
|
||||||
|
|
||||||
|
describe('BrokerController', () => {
|
||||||
|
let controller: BrokerController;
|
||||||
|
|
||||||
|
const mockBrokerService = {
|
||||||
|
create: jest.fn(),
|
||||||
|
batchCreate: jest.fn(),
|
||||||
|
findAll: jest.fn(),
|
||||||
|
findOne: jest.fn(),
|
||||||
|
update: jest.fn(),
|
||||||
|
remove: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [BrokerController],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: BrokerService,
|
||||||
|
useValue: mockBrokerService,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
controller = module.get<BrokerController>(BrokerController);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('create', () => {
|
||||||
|
const createBrokerDto: CreateBrokerDto = {
|
||||||
|
brokerCode: 'HTZQ',
|
||||||
|
brokerName: '华泰证券',
|
||||||
|
region: 'CN',
|
||||||
|
sortOrder: 1,
|
||||||
|
isActive: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockBroker: Broker = {
|
||||||
|
brokerId: 1,
|
||||||
|
...createBrokerDto,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
it('应该成功创建券商并返回 201 状态码', async () => {
|
||||||
|
mockBrokerService.create.mockResolvedValue(mockBroker);
|
||||||
|
|
||||||
|
const result = await controller.create(createBrokerDto);
|
||||||
|
|
||||||
|
expect(result).toEqual(mockBroker);
|
||||||
|
expect(mockBrokerService.create).toHaveBeenCalledWith(
|
||||||
|
createBrokerDto,
|
||||||
|
);
|
||||||
|
expect(mockBrokerService.create).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该正确传递 DTO 到服务层', async () => {
|
||||||
|
const dto: CreateBrokerDto = {
|
||||||
|
brokerCode: 'ZSZQ',
|
||||||
|
brokerName: '招商证券',
|
||||||
|
region: 'CN',
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockResult: Broker = {
|
||||||
|
brokerId: 2,
|
||||||
|
...dto,
|
||||||
|
sortOrder: 0,
|
||||||
|
isActive: true,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
mockBrokerService.create.mockResolvedValue(mockResult);
|
||||||
|
|
||||||
|
await controller.create(dto);
|
||||||
|
|
||||||
|
expect(mockBrokerService.create).toHaveBeenCalledWith(dto);
|
||||||
|
expect(mockBrokerService.create).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
brokerCode: 'ZSZQ',
|
||||||
|
brokerName: '招商证券',
|
||||||
|
region: 'CN',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('batchCreate', () => {
|
||||||
|
const batchCreateDto: BatchCreateBrokerDto = {
|
||||||
|
brokers: [
|
||||||
|
{
|
||||||
|
brokerCode: 'HTZQ',
|
||||||
|
brokerName: '华泰证券',
|
||||||
|
region: 'CN',
|
||||||
|
sortOrder: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brokerCode: 'ZSZQ',
|
||||||
|
brokerName: '招商证券',
|
||||||
|
region: 'CN',
|
||||||
|
sortOrder: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockBrokers: Broker[] = [
|
||||||
|
{
|
||||||
|
brokerId: 1,
|
||||||
|
...batchCreateDto.brokers[0],
|
||||||
|
isActive: true,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brokerId: 2,
|
||||||
|
...batchCreateDto.brokers[1],
|
||||||
|
isActive: true,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
it('应该成功批量创建券商并返回 201 状态码', async () => {
|
||||||
|
mockBrokerService.batchCreate.mockResolvedValue(mockBrokers);
|
||||||
|
|
||||||
|
const result = await controller.batchCreate(batchCreateDto);
|
||||||
|
|
||||||
|
expect(result).toEqual(mockBrokers);
|
||||||
|
expect(result).toHaveLength(2);
|
||||||
|
expect(mockBrokerService.batchCreate).toHaveBeenCalledWith(
|
||||||
|
batchCreateDto,
|
||||||
|
);
|
||||||
|
expect(mockBrokerService.batchCreate).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该正确传递批量 DTO 到服务层', async () => {
|
||||||
|
const dto: BatchCreateBrokerDto = {
|
||||||
|
brokers: [
|
||||||
|
{
|
||||||
|
brokerCode: 'HTZQ',
|
||||||
|
brokerName: '华泰证券',
|
||||||
|
region: 'CN',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockResult: Broker[] = [
|
||||||
|
{
|
||||||
|
brokerId: 1,
|
||||||
|
...dto.brokers[0],
|
||||||
|
sortOrder: 0,
|
||||||
|
isActive: true,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
mockBrokerService.batchCreate.mockResolvedValue(mockResult);
|
||||||
|
|
||||||
|
await controller.batchCreate(dto);
|
||||||
|
|
||||||
|
expect(mockBrokerService.batchCreate).toHaveBeenCalledWith(dto);
|
||||||
|
// 验证调用了 batchCreate,并且参数包含正确的数据
|
||||||
|
expect(mockBrokerService.batchCreate).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该处理空数组的情况', async () => {
|
||||||
|
const emptyDto: BatchCreateBrokerDto = {
|
||||||
|
brokers: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
mockBrokerService.batchCreate.mockResolvedValue([]);
|
||||||
|
|
||||||
|
const result = await controller.batchCreate(emptyDto);
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
expect(result).toHaveLength(0);
|
||||||
|
expect(mockBrokerService.batchCreate).toHaveBeenCalledWith(
|
||||||
|
emptyDto,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该处理大量券商批量创建', async () => {
|
||||||
|
const largeBatchDto: BatchCreateBrokerDto = {
|
||||||
|
brokers: Array.from({ length: 10 }, (_, i) => ({
|
||||||
|
brokerCode: `CODE${i}`,
|
||||||
|
brokerName: `券商${i}`,
|
||||||
|
region: 'CN',
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockLargeResult: Broker[] = largeBatchDto.brokers.map(
|
||||||
|
(broker, i) => ({
|
||||||
|
brokerId: i + 1,
|
||||||
|
...broker,
|
||||||
|
sortOrder: 0,
|
||||||
|
isActive: true,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
mockBrokerService.batchCreate.mockResolvedValue(mockLargeResult);
|
||||||
|
|
||||||
|
const result = await controller.batchCreate(largeBatchDto);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(10);
|
||||||
|
expect(mockBrokerService.batchCreate).toHaveBeenCalledWith(
|
||||||
|
largeBatchDto,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
349
apps/api/src/modules/broker/__tests__/broker.service.spec.ts
Normal file
349
apps/api/src/modules/broker/__tests__/broker.service.spec.ts
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { ConflictException } from '@nestjs/common';
|
||||||
|
import { BrokerService } from '../broker.service';
|
||||||
|
import { Broker } from '../broker.entity';
|
||||||
|
import { CreateBrokerDto } from '../dto/create-broker.dto';
|
||||||
|
import { BatchCreateBrokerDto } from '../dto/batch-create-broker.dto';
|
||||||
|
|
||||||
|
describe('BrokerService', () => {
|
||||||
|
let service: BrokerService;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
let repository: Repository<Broker>;
|
||||||
|
|
||||||
|
const mockRepository = {
|
||||||
|
create: jest.fn(),
|
||||||
|
save: jest.fn(),
|
||||||
|
findOne: jest.fn(),
|
||||||
|
find: jest.fn(),
|
||||||
|
remove: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
BrokerService,
|
||||||
|
{
|
||||||
|
provide: getRepositoryToken(Broker),
|
||||||
|
useValue: mockRepository,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<BrokerService>(BrokerService);
|
||||||
|
repository = module.get<Repository<Broker>>(getRepositoryToken(Broker));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('create', () => {
|
||||||
|
const createBrokerDto: CreateBrokerDto = {
|
||||||
|
brokerCode: 'HTZQ',
|
||||||
|
brokerName: '华泰证券',
|
||||||
|
region: 'CN',
|
||||||
|
sortOrder: 1,
|
||||||
|
isActive: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockBroker: Broker = {
|
||||||
|
brokerId: 1,
|
||||||
|
...createBrokerDto,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
it('应该成功创建一个券商', async () => {
|
||||||
|
// 模拟数据库中不存在相同 code 和 region 的券商
|
||||||
|
mockRepository.findOne.mockResolvedValue(null);
|
||||||
|
mockRepository.create.mockReturnValue(mockBroker);
|
||||||
|
mockRepository.save.mockResolvedValue(mockBroker);
|
||||||
|
|
||||||
|
const result = await service.create(createBrokerDto);
|
||||||
|
|
||||||
|
expect(result).toEqual(mockBroker);
|
||||||
|
expect(mockRepository.findOne).toHaveBeenCalledTimes(2); // 检查 code 和 name
|
||||||
|
expect(mockRepository.findOne).toHaveBeenCalledWith({
|
||||||
|
where: {
|
||||||
|
brokerCode: createBrokerDto.brokerCode,
|
||||||
|
region: createBrokerDto.region,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(mockRepository.create).toHaveBeenCalledWith({
|
||||||
|
...createBrokerDto,
|
||||||
|
sortOrder: 1,
|
||||||
|
isActive: true,
|
||||||
|
});
|
||||||
|
expect(mockRepository.save).toHaveBeenCalledWith(mockBroker);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该使用默认值当 sortOrder 和 isActive 未提供时', async () => {
|
||||||
|
const dtoWithoutDefaults: CreateBrokerDto = {
|
||||||
|
brokerCode: 'ZSZQ',
|
||||||
|
brokerName: '招商证券',
|
||||||
|
region: 'CN',
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockBrokerWithDefaults: Broker = {
|
||||||
|
brokerId: 2,
|
||||||
|
...dtoWithoutDefaults,
|
||||||
|
sortOrder: 0,
|
||||||
|
isActive: true,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
mockRepository.findOne.mockResolvedValue(null);
|
||||||
|
mockRepository.create.mockReturnValue(mockBrokerWithDefaults);
|
||||||
|
mockRepository.save.mockResolvedValue(mockBrokerWithDefaults);
|
||||||
|
|
||||||
|
const result = await service.create(dtoWithoutDefaults);
|
||||||
|
|
||||||
|
expect(result.sortOrder).toBe(0);
|
||||||
|
expect(result.isActive).toBe(true);
|
||||||
|
expect(mockRepository.create).toHaveBeenCalledWith({
|
||||||
|
...dtoWithoutDefaults,
|
||||||
|
sortOrder: 0,
|
||||||
|
isActive: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该抛出 ConflictException 当 broker_code 已存在时', async () => {
|
||||||
|
const existingBroker: Broker = {
|
||||||
|
brokerId: 1,
|
||||||
|
brokerCode: 'HTZQ',
|
||||||
|
brokerName: '华泰证券',
|
||||||
|
region: 'CN',
|
||||||
|
sortOrder: 0,
|
||||||
|
isActive: true,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
mockRepository.findOne.mockResolvedValueOnce(existingBroker);
|
||||||
|
|
||||||
|
await expect(service.create(createBrokerDto)).rejects.toThrow(
|
||||||
|
ConflictException,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockRepository.save).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该抛出 ConflictException 当 broker_name 已存在时', async () => {
|
||||||
|
mockRepository.findOne
|
||||||
|
.mockResolvedValueOnce(null) // 第一次检查 code,不存在
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
// 第二次检查 name,已存在
|
||||||
|
brokerId: 1,
|
||||||
|
brokerCode: 'OTHER',
|
||||||
|
brokerName: '华泰证券',
|
||||||
|
region: 'CN',
|
||||||
|
sortOrder: 0,
|
||||||
|
isActive: true,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(service.create(createBrokerDto)).rejects.toThrow(
|
||||||
|
ConflictException,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockRepository.save).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('batchCreate', () => {
|
||||||
|
const batchCreateDto: BatchCreateBrokerDto = {
|
||||||
|
brokers: [
|
||||||
|
{
|
||||||
|
brokerCode: 'HTZQ',
|
||||||
|
brokerName: '华泰证券',
|
||||||
|
region: 'CN',
|
||||||
|
sortOrder: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brokerCode: 'ZSZQ',
|
||||||
|
brokerName: '招商证券',
|
||||||
|
region: 'CN',
|
||||||
|
sortOrder: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockBrokers: Broker[] = [
|
||||||
|
{
|
||||||
|
brokerId: 1,
|
||||||
|
...batchCreateDto.brokers[0],
|
||||||
|
brokerImage: 'https://example.com/broker1.jpg',
|
||||||
|
isActive: true,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brokerId: 2,
|
||||||
|
...batchCreateDto.brokers[1],
|
||||||
|
isActive: true,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
it('应该成功批量创建券商', async () => {
|
||||||
|
// 模拟数据库中不存在这些券商
|
||||||
|
mockRepository.find.mockResolvedValue([]);
|
||||||
|
mockRepository.create
|
||||||
|
.mockReturnValueOnce(mockBrokers[0])
|
||||||
|
.mockReturnValueOnce(mockBrokers[1]);
|
||||||
|
mockRepository.save.mockResolvedValue(mockBrokers);
|
||||||
|
|
||||||
|
const result = await service.batchCreate(batchCreateDto);
|
||||||
|
|
||||||
|
expect(result).toEqual(mockBrokers);
|
||||||
|
expect(result).toHaveLength(2);
|
||||||
|
expect(mockRepository.create).toHaveBeenCalledTimes(2);
|
||||||
|
expect(mockRepository.save).toHaveBeenCalledWith(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
brokerCode: 'HTZQ',
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
brokerCode: 'ZSZQ',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该为每个券商设置默认值', async () => {
|
||||||
|
const dtoWithoutDefaults: BatchCreateBrokerDto = {
|
||||||
|
brokers: [
|
||||||
|
{
|
||||||
|
brokerCode: 'HTZQ',
|
||||||
|
brokerName: '华泰证券',
|
||||||
|
region: 'CN',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockBroker: Broker = {
|
||||||
|
brokerId: 1,
|
||||||
|
...dtoWithoutDefaults.brokers[0],
|
||||||
|
sortOrder: 0,
|
||||||
|
isActive: true,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
mockRepository.find.mockResolvedValue([]);
|
||||||
|
mockRepository.create.mockReturnValue(mockBroker);
|
||||||
|
mockRepository.save.mockResolvedValue([mockBroker]);
|
||||||
|
|
||||||
|
const result = await service.batchCreate(dtoWithoutDefaults);
|
||||||
|
|
||||||
|
expect(result[0].sortOrder).toBe(0);
|
||||||
|
expect(result[0].isActive).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该抛出 ConflictException 当批量数据中有已存在的券商时', async () => {
|
||||||
|
const existingBroker: Broker = {
|
||||||
|
brokerId: 1,
|
||||||
|
brokerCode: 'HTZQ',
|
||||||
|
brokerName: '华泰证券',
|
||||||
|
region: 'CN',
|
||||||
|
sortOrder: 0,
|
||||||
|
isActive: true,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
mockRepository.find.mockResolvedValue([existingBroker]);
|
||||||
|
|
||||||
|
await expect(service.batchCreate(batchCreateDto)).rejects.toThrow(
|
||||||
|
ConflictException,
|
||||||
|
);
|
||||||
|
await expect(service.batchCreate(batchCreateDto)).rejects.toThrow(
|
||||||
|
expect.stringContaining('already exist'),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockRepository.save).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该抛出 ConflictException 当批量数据内部有重复的 code+region 组合时', async () => {
|
||||||
|
const duplicateDto: BatchCreateBrokerDto = {
|
||||||
|
brokers: [
|
||||||
|
{
|
||||||
|
brokerCode: 'HTZQ',
|
||||||
|
brokerName: '华泰证券',
|
||||||
|
region: 'CN',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brokerCode: 'HTZQ', // 重复的 code
|
||||||
|
brokerName: '华泰证券2',
|
||||||
|
region: 'CN', // 相同的 region
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
mockRepository.find.mockResolvedValue([]);
|
||||||
|
|
||||||
|
await expect(service.batchCreate(duplicateDto)).rejects.toThrow(
|
||||||
|
ConflictException,
|
||||||
|
);
|
||||||
|
await expect(service.batchCreate(duplicateDto)).rejects.toThrow(
|
||||||
|
'Duplicate broker_code and region combinations in batch data',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockRepository.save).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该成功创建不同地区的相同 code', async () => {
|
||||||
|
const differentRegionDto: BatchCreateBrokerDto = {
|
||||||
|
brokers: [
|
||||||
|
{
|
||||||
|
brokerCode: 'HTZQ',
|
||||||
|
brokerName: '华泰证券',
|
||||||
|
region: 'CN',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brokerCode: 'HTZQ', // 相同的 code
|
||||||
|
brokerName: 'Huatai Securities',
|
||||||
|
region: 'US', // 不同的 region
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockBrokersDifferentRegion: Broker[] = [
|
||||||
|
{
|
||||||
|
brokerId: 1,
|
||||||
|
...differentRegionDto.brokers[0],
|
||||||
|
sortOrder: 0,
|
||||||
|
isActive: true,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brokerId: 2,
|
||||||
|
...differentRegionDto.brokers[1],
|
||||||
|
sortOrder: 0,
|
||||||
|
isActive: true,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
mockRepository.find.mockResolvedValue([]);
|
||||||
|
mockRepository.create
|
||||||
|
.mockReturnValueOnce(mockBrokersDifferentRegion[0])
|
||||||
|
.mockReturnValueOnce(mockBrokersDifferentRegion[1]);
|
||||||
|
mockRepository.save.mockResolvedValue(mockBrokersDifferentRegion);
|
||||||
|
|
||||||
|
const result = await service.batchCreate(differentRegionDto);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(2);
|
||||||
|
expect(result[0].region).toBe('CN');
|
||||||
|
expect(result[1].region).toBe('US');
|
||||||
|
expect(mockRepository.save).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
141
apps/api/src/modules/broker/broker.controller.ts
Normal file
141
apps/api/src/modules/broker/broker.controller.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
Patch,
|
||||||
|
Param,
|
||||||
|
Delete,
|
||||||
|
Query,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger';
|
||||||
|
import { BrokerService } from './broker.service';
|
||||||
|
import { CreateBrokerDto } from './dto/create-broker.dto';
|
||||||
|
import { UpdateBrokerDto } from './dto/update-broker.dto';
|
||||||
|
import { QueryBrokerDto } from './dto/query-broker.dto';
|
||||||
|
import { BatchCreateBrokerDto } from './dto/batch-create-broker.dto';
|
||||||
|
import { Broker } from './broker.entity';
|
||||||
|
|
||||||
|
@ApiTags('broker')
|
||||||
|
@Controller('broker')
|
||||||
|
export class BrokerController {
|
||||||
|
constructor(private readonly brokerService: BrokerService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单独创建 broker
|
||||||
|
*/
|
||||||
|
@Post()
|
||||||
|
@HttpCode(HttpStatus.CREATED)
|
||||||
|
@ApiOperation({ summary: '创建券商', description: '创建单个券商信息' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 201,
|
||||||
|
description: '创建成功',
|
||||||
|
type: Broker,
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 400, description: '请求参数错误' })
|
||||||
|
@ApiResponse({ status: 409, description: '券商代码或名称已存在' })
|
||||||
|
create(@Body() createBrokerDto: CreateBrokerDto): Promise<Broker> {
|
||||||
|
return this.brokerService.create(createBrokerDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量创建 broker
|
||||||
|
*/
|
||||||
|
@Post('batch')
|
||||||
|
@HttpCode(HttpStatus.CREATED)
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '批量创建券商',
|
||||||
|
description: '一次性创建多个券商信息',
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 201,
|
||||||
|
description: '批量创建成功',
|
||||||
|
type: [Broker],
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 400, description: '请求参数错误' })
|
||||||
|
@ApiResponse({ status: 409, description: '存在重复的券商代码或名称' })
|
||||||
|
batchCreate(
|
||||||
|
@Body() batchCreateBrokerDto: BatchCreateBrokerDto,
|
||||||
|
): Promise<Broker[]> {
|
||||||
|
return this.brokerService.batchCreate(batchCreateBrokerDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询 broker(支持多种查询条件)
|
||||||
|
* 支持按 broker_id、broker_code、broker_name、region 查询
|
||||||
|
* 返回一个或多个 broker
|
||||||
|
*/
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '查询券商列表',
|
||||||
|
description: '支持按多个条件查询券商,支持分页和排序',
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: '查询成功',
|
||||||
|
type: [Broker],
|
||||||
|
})
|
||||||
|
findAll(@Query() queryDto: QueryBrokerDto): Promise<Broker[]> {
|
||||||
|
return this.brokerService.findAll(queryDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 ID 查询单个 broker
|
||||||
|
*/
|
||||||
|
@Get(':id')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '根据ID查询券商',
|
||||||
|
description: '根据券商ID获取详细信息',
|
||||||
|
})
|
||||||
|
@ApiParam({ name: 'id', description: '券商ID', type: Number })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: '查询成功',
|
||||||
|
type: Broker,
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 404, description: '券商不存在' })
|
||||||
|
findOne(@Param('id') id: string): Promise<Broker> {
|
||||||
|
return this.brokerService.findOne(+id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新 broker
|
||||||
|
*/
|
||||||
|
@Patch(':id')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '更新券商',
|
||||||
|
description: '更新券商的部分或全部信息',
|
||||||
|
})
|
||||||
|
@ApiParam({ name: 'id', description: '券商ID', type: Number })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: '更新成功',
|
||||||
|
type: Broker,
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 404, description: '券商不存在' })
|
||||||
|
@ApiResponse({ status: 409, description: '更新后的券商代码或名称已存在' })
|
||||||
|
update(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() updateBrokerDto: UpdateBrokerDto,
|
||||||
|
): Promise<Broker> {
|
||||||
|
return this.brokerService.update(+id, updateBrokerDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除 broker
|
||||||
|
*/
|
||||||
|
@Delete(':id')
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '删除券商',
|
||||||
|
description: '根据券商ID删除券商信息',
|
||||||
|
})
|
||||||
|
@ApiParam({ name: 'id', description: '券商ID', type: Number })
|
||||||
|
@ApiResponse({ status: 204, description: '删除成功' })
|
||||||
|
@ApiResponse({ status: 404, description: '券商不存在' })
|
||||||
|
remove(@Param('id') id: string): Promise<void> {
|
||||||
|
return this.brokerService.remove(+id);
|
||||||
|
}
|
||||||
|
}
|
||||||
96
apps/api/src/modules/broker/broker.entity.ts
Normal file
96
apps/api/src/modules/broker/broker.entity.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
Column,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
CreateDateColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
Index,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
@Entity('broker')
|
||||||
|
export class Broker {
|
||||||
|
@ApiProperty({ description: '券商ID', example: 1 })
|
||||||
|
@PrimaryGeneratedColumn({ name: 'broker_id' })
|
||||||
|
brokerId: number;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: '券商代码',
|
||||||
|
example: 'HTZQ',
|
||||||
|
maxLength: 50,
|
||||||
|
})
|
||||||
|
@Column({ name: 'broker_code', type: 'varchar', length: 50 })
|
||||||
|
@Index()
|
||||||
|
brokerCode: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: '券商名称',
|
||||||
|
example: '华泰证券',
|
||||||
|
maxLength: 100,
|
||||||
|
})
|
||||||
|
@Column({ name: 'broker_name', type: 'varchar', length: 100 })
|
||||||
|
@Index()
|
||||||
|
brokerName: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: '券商图片地址',
|
||||||
|
example: 'https://example.com/broker-image.jpg',
|
||||||
|
maxLength: 200,
|
||||||
|
})
|
||||||
|
@Column({
|
||||||
|
name: 'broker_image',
|
||||||
|
type: 'varchar',
|
||||||
|
length: 200,
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
brokerImage?: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: '地区/国家代码',
|
||||||
|
example: 'CN',
|
||||||
|
enum: ['CN', 'US', 'HK', 'SG', 'JP', 'UK', 'AU', 'CA', 'OTHER'],
|
||||||
|
default: 'CN',
|
||||||
|
})
|
||||||
|
@Column({ name: 'region', type: 'varchar', length: 50, default: 'CN' })
|
||||||
|
region: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: '排序顺序',
|
||||||
|
example: 1,
|
||||||
|
default: 0,
|
||||||
|
})
|
||||||
|
@Column({
|
||||||
|
name: 'sort_order',
|
||||||
|
type: 'integer',
|
||||||
|
default: 0,
|
||||||
|
nullable: false,
|
||||||
|
})
|
||||||
|
sortOrder?: number; // 可选,数据库有默认值
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: '是否启用',
|
||||||
|
example: true,
|
||||||
|
default: true,
|
||||||
|
})
|
||||||
|
@Column({
|
||||||
|
name: 'is_active',
|
||||||
|
type: 'boolean',
|
||||||
|
default: true,
|
||||||
|
nullable: false,
|
||||||
|
})
|
||||||
|
isActive?: boolean; // 可选,数据库有默认值
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: '创建时间',
|
||||||
|
example: '2024-01-01T00:00:00.000Z',
|
||||||
|
})
|
||||||
|
@CreateDateColumn({ name: 'created_at' })
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: '更新时间',
|
||||||
|
example: '2024-01-01T00:00:00.000Z',
|
||||||
|
})
|
||||||
|
@UpdateDateColumn({ name: 'updated_at' })
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
13
apps/api/src/modules/broker/broker.module.ts
Normal file
13
apps/api/src/modules/broker/broker.module.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { BrokerService } from './broker.service';
|
||||||
|
import { BrokerController } from './broker.controller';
|
||||||
|
import { Broker } from './broker.entity';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([Broker])],
|
||||||
|
controllers: [BrokerController],
|
||||||
|
providers: [BrokerService],
|
||||||
|
exports: [BrokerService],
|
||||||
|
})
|
||||||
|
export class BrokerModule {}
|
||||||
256
apps/api/src/modules/broker/broker.service.ts
Normal file
256
apps/api/src/modules/broker/broker.service.ts
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
NotFoundException,
|
||||||
|
ConflictException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository, FindOptionsWhere } from 'typeorm';
|
||||||
|
import { Broker } from './broker.entity';
|
||||||
|
import { CreateBrokerDto } from './dto/create-broker.dto';
|
||||||
|
import { UpdateBrokerDto } from './dto/update-broker.dto';
|
||||||
|
import { QueryBrokerDto } from './dto/query-broker.dto';
|
||||||
|
import { BatchCreateBrokerDto } from './dto/batch-create-broker.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BrokerService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Broker)
|
||||||
|
private readonly brokerRepository: Repository<Broker>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单独创建 broker
|
||||||
|
*/
|
||||||
|
async create(createBrokerDto: CreateBrokerDto): Promise<Broker> {
|
||||||
|
// 检查同一地区的 broker_code 是否已存在
|
||||||
|
const existingByCode = await this.brokerRepository.findOne({
|
||||||
|
where: {
|
||||||
|
brokerCode: createBrokerDto.brokerCode,
|
||||||
|
region: createBrokerDto.region,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingByCode) {
|
||||||
|
throw new ConflictException(
|
||||||
|
`Broker with code "${createBrokerDto.brokerCode}" already exists in region "${createBrokerDto.region}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查同一地区的 broker_name 是否已存在
|
||||||
|
const existingByName = await this.brokerRepository.findOne({
|
||||||
|
where: {
|
||||||
|
brokerName: createBrokerDto.brokerName,
|
||||||
|
region: createBrokerDto.region,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingByName) {
|
||||||
|
throw new ConflictException(
|
||||||
|
`Broker with name "${createBrokerDto.brokerName}" already exists in region "${createBrokerDto.region}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const broker = this.brokerRepository.create({
|
||||||
|
...createBrokerDto,
|
||||||
|
sortOrder: createBrokerDto.sortOrder ?? 0,
|
||||||
|
isActive: createBrokerDto.isActive ?? true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.brokerRepository.save(broker);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量创建 broker
|
||||||
|
*/
|
||||||
|
async batchCreate(
|
||||||
|
batchCreateBrokerDto: BatchCreateBrokerDto,
|
||||||
|
): Promise<Broker[]> {
|
||||||
|
const brokers = batchCreateBrokerDto.brokers.map((dto) =>
|
||||||
|
this.brokerRepository.create({
|
||||||
|
...dto,
|
||||||
|
sortOrder: dto.sortOrder ?? 0,
|
||||||
|
isActive: dto.isActive ?? true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 检查是否有重复的 broker_code + region 组合
|
||||||
|
const codeRegionPairs = brokers.map((b) => ({
|
||||||
|
brokerCode: b.brokerCode,
|
||||||
|
region: b.region,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const existingBrokers = await this.brokerRepository.find({
|
||||||
|
where: codeRegionPairs.map((pair) => ({
|
||||||
|
brokerCode: pair.brokerCode,
|
||||||
|
region: pair.region,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingBrokers.length > 0) {
|
||||||
|
const conflicts = existingBrokers.map(
|
||||||
|
(b) => `${b.brokerCode} in ${b.region}`,
|
||||||
|
);
|
||||||
|
throw new ConflictException(
|
||||||
|
`The following brokers already exist: ${conflicts.join(', ')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查批量数据内部是否有重复
|
||||||
|
const uniquePairs = new Set(
|
||||||
|
codeRegionPairs.map((p) => `${p.brokerCode}-${p.region}`),
|
||||||
|
);
|
||||||
|
if (uniquePairs.size !== codeRegionPairs.length) {
|
||||||
|
throw new ConflictException(
|
||||||
|
'Duplicate broker_code and region combinations in batch data',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.brokerRepository.save(brokers);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询 broker(支持多种查询条件)
|
||||||
|
*/
|
||||||
|
async findAll(queryDto: QueryBrokerDto): Promise<Broker[]> {
|
||||||
|
const where: FindOptionsWhere<Broker> = {};
|
||||||
|
|
||||||
|
if (queryDto.brokerId) {
|
||||||
|
where.brokerId = queryDto.brokerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryDto.brokerCode) {
|
||||||
|
where.brokerCode = queryDto.brokerCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryDto.brokerName) {
|
||||||
|
where.brokerName = queryDto.brokerName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryDto.region) {
|
||||||
|
where.region = queryDto.region;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryDto.isActive !== undefined) {
|
||||||
|
where.isActive = queryDto.isActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.brokerRepository.find({
|
||||||
|
where,
|
||||||
|
order: {
|
||||||
|
sortOrder: 'ASC',
|
||||||
|
brokerId: 'ASC',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 ID 查询单个 broker
|
||||||
|
*/
|
||||||
|
async findOne(id: number): Promise<Broker> {
|
||||||
|
const broker = await this.brokerRepository.findOne({
|
||||||
|
where: { brokerId: id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!broker) {
|
||||||
|
throw new NotFoundException(`Broker with ID ${id} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return broker;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据条件查询单个 broker(返回第一个匹配的)
|
||||||
|
*/
|
||||||
|
async findOneByCondition(queryDto: QueryBrokerDto): Promise<Broker> {
|
||||||
|
const where: FindOptionsWhere<Broker> = {};
|
||||||
|
|
||||||
|
if (queryDto.brokerId) {
|
||||||
|
where.brokerId = queryDto.brokerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryDto.brokerCode) {
|
||||||
|
where.brokerCode = queryDto.brokerCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryDto.brokerName) {
|
||||||
|
where.brokerName = queryDto.brokerName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryDto.region) {
|
||||||
|
where.region = queryDto.region;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryDto.isActive !== undefined) {
|
||||||
|
where.isActive = queryDto.isActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
const broker = await this.brokerRepository.findOne({ where });
|
||||||
|
|
||||||
|
if (!broker) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
'Broker not found with the given conditions',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return broker;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新 broker
|
||||||
|
*/
|
||||||
|
async update(
|
||||||
|
id: number,
|
||||||
|
updateBrokerDto: UpdateBrokerDto,
|
||||||
|
): Promise<Broker> {
|
||||||
|
const broker = await this.findOne(id);
|
||||||
|
|
||||||
|
// 如果更新 broker_code 或 region,检查是否冲突
|
||||||
|
if ('brokerCode' in updateBrokerDto || 'region' in updateBrokerDto) {
|
||||||
|
const newCode = updateBrokerDto.brokerCode ?? broker.brokerCode;
|
||||||
|
const newRegion = updateBrokerDto.region ?? broker.region;
|
||||||
|
|
||||||
|
const existing = await this.brokerRepository.findOne({
|
||||||
|
where: {
|
||||||
|
brokerCode: newCode,
|
||||||
|
region: newRegion,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existing && existing.brokerId !== id) {
|
||||||
|
throw new ConflictException(
|
||||||
|
`Broker with code "${newCode}" already exists in region "${newRegion}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果更新 broker_name 或 region,检查是否冲突
|
||||||
|
if ('brokerName' in updateBrokerDto || 'region' in updateBrokerDto) {
|
||||||
|
const newName = updateBrokerDto.brokerName ?? broker.brokerName;
|
||||||
|
const newRegion = updateBrokerDto.region ?? broker.region;
|
||||||
|
|
||||||
|
const existing = await this.brokerRepository.findOne({
|
||||||
|
where: {
|
||||||
|
brokerName: newName,
|
||||||
|
region: newRegion,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existing && existing.brokerId !== id) {
|
||||||
|
throw new ConflictException(
|
||||||
|
`Broker with name "${newName}" already exists in region "${newRegion}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(broker, updateBrokerDto);
|
||||||
|
return this.brokerRepository.save(broker);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除 broker
|
||||||
|
*/
|
||||||
|
async remove(id: number): Promise<void> {
|
||||||
|
const broker = await this.findOne(id);
|
||||||
|
await this.brokerRepository.remove(broker);
|
||||||
|
}
|
||||||
|
}
|
||||||
165
apps/api/src/modules/broker/config/brokers.config.ts
Normal file
165
apps/api/src/modules/broker/config/brokers.config.ts
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
import { CreateBrokerDto } from '../dto/create-broker.dto';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主要券商配置数据
|
||||||
|
* 包含A股、港股和美股的主要券商信息
|
||||||
|
*/
|
||||||
|
export const brokersConfig: CreateBrokerDto[] = [
|
||||||
|
// A股券商
|
||||||
|
{
|
||||||
|
brokerCode: 'HTZQ',
|
||||||
|
brokerName: '华泰证券',
|
||||||
|
region: 'CN',
|
||||||
|
sortOrder: 1,
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brokerCode: 'ZSZQ',
|
||||||
|
brokerName: '招商证券',
|
||||||
|
region: 'CN',
|
||||||
|
sortOrder: 2,
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brokerCode: 'GTJA',
|
||||||
|
brokerName: '国泰君安',
|
||||||
|
region: 'CN',
|
||||||
|
sortOrder: 3,
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brokerCode: 'ZXZQ',
|
||||||
|
brokerName: '中信证券',
|
||||||
|
region: 'CN',
|
||||||
|
sortOrder: 4,
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brokerCode: 'HXZQ',
|
||||||
|
brokerName: '海通证券',
|
||||||
|
region: 'CN',
|
||||||
|
sortOrder: 5,
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brokerCode: 'GFZQ',
|
||||||
|
brokerName: '广发证券',
|
||||||
|
region: 'CN',
|
||||||
|
sortOrder: 6,
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brokerCode: 'ZJZQ',
|
||||||
|
brokerName: '中金公司',
|
||||||
|
region: 'CN',
|
||||||
|
sortOrder: 7,
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brokerCode: 'DFZQ',
|
||||||
|
brokerName: '东方证券',
|
||||||
|
region: 'CN',
|
||||||
|
sortOrder: 8,
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brokerCode: 'XZQ',
|
||||||
|
brokerName: '兴业证券',
|
||||||
|
region: 'CN',
|
||||||
|
sortOrder: 9,
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brokerCode: 'SWZQ',
|
||||||
|
brokerName: '申万宏源',
|
||||||
|
region: 'CN',
|
||||||
|
sortOrder: 10,
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
// 港股券商 从21开始
|
||||||
|
{
|
||||||
|
brokerCode: 'FUTU',
|
||||||
|
brokerName: '富途证券',
|
||||||
|
region: 'HK',
|
||||||
|
sortOrder: 21,
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brokerCode: 'TIGER',
|
||||||
|
brokerName: '老虎证券',
|
||||||
|
region: 'HK',
|
||||||
|
sortOrder: 22,
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brokerCode: 'HSBC',
|
||||||
|
brokerName: '汇丰银行',
|
||||||
|
region: 'HK',
|
||||||
|
sortOrder: 23,
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brokerCode: 'CITIC',
|
||||||
|
brokerName: '中信里昂',
|
||||||
|
region: 'HK',
|
||||||
|
sortOrder: 24,
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brokerCode: 'UBS',
|
||||||
|
brokerName: '瑞银证券',
|
||||||
|
region: 'HK',
|
||||||
|
sortOrder: 25,
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
// 美股券商 从31开始
|
||||||
|
{
|
||||||
|
brokerCode: 'IBKR',
|
||||||
|
brokerName: 'Interactive Brokers',
|
||||||
|
region: 'US',
|
||||||
|
sortOrder: 31,
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brokerCode: 'SCHWAB',
|
||||||
|
brokerName: 'Charles Schwab',
|
||||||
|
region: 'US',
|
||||||
|
sortOrder: 32,
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brokerCode: 'FIDELITY',
|
||||||
|
brokerName: 'Fidelity',
|
||||||
|
region: 'US',
|
||||||
|
sortOrder: 33,
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brokerCode: 'TD',
|
||||||
|
brokerName: 'TD Ameritrade',
|
||||||
|
region: 'US',
|
||||||
|
sortOrder: 34,
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brokerCode: 'ETRADE',
|
||||||
|
brokerName: 'E*TRADE',
|
||||||
|
region: 'US',
|
||||||
|
sortOrder: 35,
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brokerCode: 'ROBINHOOD',
|
||||||
|
brokerName: 'Robinhood',
|
||||||
|
region: 'US',
|
||||||
|
sortOrder: 36,
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brokerCode: 'WEBULL',
|
||||||
|
brokerName: 'Webull',
|
||||||
|
region: 'US',
|
||||||
|
sortOrder: 37,
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
29
apps/api/src/modules/broker/dto/batch-create-broker.dto.ts
Normal file
29
apps/api/src/modules/broker/dto/batch-create-broker.dto.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { IsArray, ValidateNested } from 'class-validator';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { CreateBrokerDto } from './create-broker.dto';
|
||||||
|
|
||||||
|
export class BatchCreateBrokerDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: '券商列表',
|
||||||
|
type: [CreateBrokerDto],
|
||||||
|
example: [
|
||||||
|
{
|
||||||
|
brokerCode: 'HTZQ',
|
||||||
|
brokerName: '华泰证券',
|
||||||
|
region: 'CN',
|
||||||
|
sortOrder: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brokerCode: 'ZSZQ',
|
||||||
|
brokerName: '招商证券',
|
||||||
|
region: 'CN',
|
||||||
|
sortOrder: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => CreateBrokerDto)
|
||||||
|
brokers: CreateBrokerDto[];
|
||||||
|
}
|
||||||
76
apps/api/src/modules/broker/dto/create-broker.dto.ts
Normal file
76
apps/api/src/modules/broker/dto/create-broker.dto.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import {
|
||||||
|
IsString,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsOptional,
|
||||||
|
IsNumber,
|
||||||
|
IsBoolean,
|
||||||
|
MinLength,
|
||||||
|
MaxLength,
|
||||||
|
IsIn,
|
||||||
|
Min,
|
||||||
|
} from 'class-validator';
|
||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class CreateBrokerDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: '券商代码',
|
||||||
|
example: 'HTZQ',
|
||||||
|
maxLength: 50,
|
||||||
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@MinLength(1)
|
||||||
|
@MaxLength(50)
|
||||||
|
brokerCode: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: '券商名称',
|
||||||
|
example: '华泰证券',
|
||||||
|
maxLength: 100,
|
||||||
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@MinLength(1)
|
||||||
|
@MaxLength(100)
|
||||||
|
brokerName: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: '地区/国家代码',
|
||||||
|
example: 'CN',
|
||||||
|
enum: ['CN', 'US', 'HK', 'SG', 'JP', 'UK', 'AU', 'CA', 'OTHER'],
|
||||||
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsIn(['CN', 'US', 'HK', 'SG', 'JP', 'UK', 'AU', 'CA', 'OTHER'])
|
||||||
|
region: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: '排序顺序',
|
||||||
|
example: 1,
|
||||||
|
minimum: 0,
|
||||||
|
default: 0,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
@Min(0)
|
||||||
|
sortOrder?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: '是否启用',
|
||||||
|
example: true,
|
||||||
|
default: true,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
isActive?: boolean;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: '券商图片地址',
|
||||||
|
example: 'https://example.com/broker-image.jpg',
|
||||||
|
maxLength: 200,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(200)
|
||||||
|
brokerImage?: string;
|
||||||
|
}
|
||||||
99
apps/api/src/modules/broker/dto/query-broker.dto.ts
Normal file
99
apps/api/src/modules/broker/dto/query-broker.dto.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import {
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
IsNumber,
|
||||||
|
IsBoolean,
|
||||||
|
Min,
|
||||||
|
} from 'class-validator';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class QueryBrokerDto {
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: '券商ID',
|
||||||
|
example: 1,
|
||||||
|
minimum: 1,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsNumber()
|
||||||
|
@Min(1)
|
||||||
|
brokerId?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: '券商代码',
|
||||||
|
example: 'HTZQ',
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
brokerCode?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: '券商名称',
|
||||||
|
example: '华泰证券',
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
brokerName?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: '地区/国家代码',
|
||||||
|
example: 'CN',
|
||||||
|
enum: ['CN', 'US', 'HK', 'SG', 'JP', 'UK', 'AU', 'CA', 'OTHER'],
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
region?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: '是否启用',
|
||||||
|
example: true,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Boolean)
|
||||||
|
@IsBoolean()
|
||||||
|
isActive?: boolean;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: '页码',
|
||||||
|
example: 1,
|
||||||
|
minimum: 1,
|
||||||
|
default: 1,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsNumber()
|
||||||
|
@Min(1)
|
||||||
|
page?: number = 1;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: '每页数量',
|
||||||
|
example: 10,
|
||||||
|
minimum: 1,
|
||||||
|
default: 10,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsNumber()
|
||||||
|
@Min(1)
|
||||||
|
limit?: number = 10;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: '排序字段',
|
||||||
|
example: 'createdAt',
|
||||||
|
default: 'createdAt',
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
sortBy?: string = 'createdAt';
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: '排序方向',
|
||||||
|
example: 'DESC',
|
||||||
|
enum: ['ASC', 'DESC'],
|
||||||
|
default: 'DESC',
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
sortOrder?: 'ASC' | 'DESC' = 'DESC';
|
||||||
|
}
|
||||||
4
apps/api/src/modules/broker/dto/update-broker.dto.ts
Normal file
4
apps/api/src/modules/broker/dto/update-broker.dto.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { PartialType } from '@nestjs/swagger';
|
||||||
|
import { CreateBrokerDto } from './create-broker.dto';
|
||||||
|
|
||||||
|
export class UpdateBrokerDto extends PartialType(CreateBrokerDto) {}
|
||||||
4
apps/api/src/modules/user/user.module.ts
Normal file
4
apps/api/src/modules/user/user.module.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Module({})
|
||||||
|
export class UserModule {}
|
||||||
7
apps/api/src/modules/user/user.spec.ts
Normal file
7
apps/api/src/modules/user/user.spec.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { User } from './user';
|
||||||
|
|
||||||
|
describe('User', () => {
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(new User()).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
1
apps/api/src/modules/user/user.ts
Normal file
1
apps/api/src/modules/user/user.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export class User {}
|
||||||
12
apps/api/test/jest-integration.json
Normal file
12
apps/api/test/jest-integration.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"moduleFileExtensions": ["js", "json", "ts"],
|
||||||
|
"rootDir": "..",
|
||||||
|
"testEnvironment": "node",
|
||||||
|
"testMatch": ["**/test/**/*.integration.spec.ts"],
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
},
|
||||||
|
"moduleNameMapper": {
|
||||||
|
"^src/(.*)$": "<rootDir>/src/$1"
|
||||||
|
}
|
||||||
|
}
|
||||||
240
apps/api/test/modules/broker/broker.e2e-spec.ts
Normal file
240
apps/api/test/modules/broker/broker.e2e-spec.ts
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { INestApplication } from '@nestjs/common';
|
||||||
|
import request from 'supertest';
|
||||||
|
import { AppModule } from '../../../src/app.module';
|
||||||
|
import { Broker } from '../../../src/modules/broker/broker.entity';
|
||||||
|
import { Response } from '../../../src/common/interceptors/transform.interceptor';
|
||||||
|
|
||||||
|
describe('BrokerController (e2e)', () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
jest.setTimeout(30000);
|
||||||
|
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||||
|
imports: [AppModule], // 导入整个应用模块
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
app = moduleFixture.createNestApplication();
|
||||||
|
await app.init(); // 初始化应用(包括所有中间件、管道、拦截器等)
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /api/broker', () => {
|
||||||
|
it('应该成功创建券商(完整流程)', () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post('/api/broker')
|
||||||
|
.send({
|
||||||
|
brokerCode: 'E2E_TEST',
|
||||||
|
brokerName: 'E2E测试券商',
|
||||||
|
region: 'CN',
|
||||||
|
sortOrder: 1,
|
||||||
|
isActive: true,
|
||||||
|
})
|
||||||
|
.expect(201)
|
||||||
|
.expect((res) => {
|
||||||
|
// 验证响应格式(经过 TransformInterceptor 处理)
|
||||||
|
expect(res.body).toHaveProperty('code', 0);
|
||||||
|
expect(res.body).toHaveProperty('message', 'success');
|
||||||
|
expect(res.body).toHaveProperty('data');
|
||||||
|
expect(res.body.data).toHaveProperty(
|
||||||
|
'brokerCode',
|
||||||
|
'E2E_TEST',
|
||||||
|
);
|
||||||
|
expect(res.body.data).toHaveProperty(
|
||||||
|
'brokerName',
|
||||||
|
'E2E测试券商',
|
||||||
|
);
|
||||||
|
expect(res.body.data).toHaveProperty('brokerId');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该返回 400 当数据验证失败时', () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post('/api/broker')
|
||||||
|
.send({
|
||||||
|
// 缺少必需字段
|
||||||
|
brokerCode: 'TEST',
|
||||||
|
})
|
||||||
|
.expect(400)
|
||||||
|
.expect((res) => {
|
||||||
|
// 验证错误响应格式(经过 HttpExceptionFilter 处理)
|
||||||
|
expect(res.body).toHaveProperty('code');
|
||||||
|
expect(res.body).toHaveProperty('message');
|
||||||
|
expect(res.body.code).not.toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该返回 409 当券商代码已存在时', async () => {
|
||||||
|
// 先创建一个券商
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.post('/api/broker')
|
||||||
|
.send({
|
||||||
|
brokerCode: 'DUPLICATE_TEST',
|
||||||
|
brokerName: '重复测试',
|
||||||
|
region: 'CN',
|
||||||
|
})
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
// 尝试创建重复的券商
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post('/api/broker')
|
||||||
|
.send({
|
||||||
|
brokerCode: 'DUPLICATE_TEST',
|
||||||
|
brokerName: '重复测试2',
|
||||||
|
region: 'CN',
|
||||||
|
})
|
||||||
|
.expect(409)
|
||||||
|
.expect((res) => {
|
||||||
|
expect(res.body.code).toBe(409);
|
||||||
|
expect(res.body.message).toContain('already exists');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /api/broker', () => {
|
||||||
|
it('应该返回券商列表', async () => {
|
||||||
|
// 先创建一个券商
|
||||||
|
const createResponse = await request(app.getHttpServer())
|
||||||
|
.post('/api/broker')
|
||||||
|
.send({
|
||||||
|
brokerCode: 'GET_TEST',
|
||||||
|
brokerName: '查询测试',
|
||||||
|
region: 'CN',
|
||||||
|
})
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
// 查询券商列表
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get('/api/broker')
|
||||||
|
.expect(200)
|
||||||
|
.expect((res) => {
|
||||||
|
expect(res.body).toHaveProperty('code', 0);
|
||||||
|
expect(res.body).toHaveProperty('data');
|
||||||
|
expect(Array.isArray(res.body.data)).toBe(true);
|
||||||
|
// 验证创建的券商在列表中
|
||||||
|
const found = res.body.data.find(
|
||||||
|
(b: { brokerCode: string }) =>
|
||||||
|
b.brokerCode === 'GET_TEST',
|
||||||
|
);
|
||||||
|
expect(found).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该支持按条件查询', async () => {
|
||||||
|
// 先创建一个券商
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.post('/api/broker')
|
||||||
|
.send({
|
||||||
|
brokerCode: 'QUERY_TEST',
|
||||||
|
brokerName: '条件查询测试',
|
||||||
|
region: 'CN',
|
||||||
|
})
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
// 按 brokerCode 查询
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get('/api/broker?brokerCode=QUERY_TEST')
|
||||||
|
.expect(200)
|
||||||
|
.expect((res) => {
|
||||||
|
expect(res.body.code).toBe(0);
|
||||||
|
expect(res.body.data).toHaveLength(1);
|
||||||
|
expect(res.body.data[0].brokerCode).toBe('QUERY_TEST');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /api/broker/:id', () => {
|
||||||
|
it('应该返回单个券商', async () => {
|
||||||
|
// 先创建一个券商
|
||||||
|
const createResponse = await request(app.getHttpServer())
|
||||||
|
.post('/api/broker')
|
||||||
|
.send({
|
||||||
|
brokerCode: 'GET_ONE_TEST',
|
||||||
|
brokerName: '单个查询测试',
|
||||||
|
region: 'CN',
|
||||||
|
})
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
const brokerId = (createResponse.body as Response<Broker>).data
|
||||||
|
.brokerId;
|
||||||
|
|
||||||
|
// 查询单个券商
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/api/broker/${brokerId}`)
|
||||||
|
.expect(200)
|
||||||
|
.expect((res) => {
|
||||||
|
expect(res.body.code).toBe(0);
|
||||||
|
expect(res.body.data.brokerId).toBe(brokerId);
|
||||||
|
expect(res.body.data.brokerCode).toBe('GET_ONE_TEST');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该返回 404 当券商不存在时', () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get('/api/broker/99999')
|
||||||
|
.expect(404)
|
||||||
|
.expect((res) => {
|
||||||
|
expect(res.body.code).toBe(404);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PATCH /api/broker/:id', () => {
|
||||||
|
it('应该成功更新券商', async () => {
|
||||||
|
// 先创建一个券商
|
||||||
|
const createResponse = await request(app.getHttpServer())
|
||||||
|
.post('/api/broker')
|
||||||
|
.send({
|
||||||
|
brokerCode: 'UPDATE_TEST',
|
||||||
|
brokerName: '更新测试',
|
||||||
|
region: 'CN',
|
||||||
|
})
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
const brokerId = (createResponse.body as Response<Broker>).data
|
||||||
|
.brokerId;
|
||||||
|
|
||||||
|
// 更新券商
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.patch(`/api/broker/${brokerId}`)
|
||||||
|
.send({
|
||||||
|
brokerName: '更新后的名称',
|
||||||
|
})
|
||||||
|
.expect(200)
|
||||||
|
.expect((res) => {
|
||||||
|
expect(res.body.code).toBe(0);
|
||||||
|
expect(res.body.data.brokerName).toBe('更新后的名称');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('DELETE /api/broker/:id', () => {
|
||||||
|
it('应该成功删除券商', async () => {
|
||||||
|
// 先创建一个券商
|
||||||
|
const createResponse = await request(app.getHttpServer())
|
||||||
|
.post('/api/broker')
|
||||||
|
.send({
|
||||||
|
brokerCode: 'DELETE_TEST',
|
||||||
|
brokerName: '删除测试',
|
||||||
|
region: 'CN',
|
||||||
|
})
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
const brokerId = (createResponse.body as Response<Broker>).data
|
||||||
|
.brokerId;
|
||||||
|
|
||||||
|
// 删除券商
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.delete(`/api/broker/${brokerId}`)
|
||||||
|
.expect(204);
|
||||||
|
|
||||||
|
// 验证已删除
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.get(`/api/broker/${brokerId}`)
|
||||||
|
.expect(404);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
150
apps/api/test/modules/broker/broker.service.integration.spec.ts
Normal file
150
apps/api/test/modules/broker/broker.service.integration.spec.ts
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
|
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { BrokerService } from '../../../src/modules/broker/broker.service';
|
||||||
|
import { Broker } from '../../../src/modules/broker/broker.entity';
|
||||||
|
import { BrokerModule } from '../../../src/modules/broker/broker.module';
|
||||||
|
import { CreateBrokerDto } from '../../../src/modules/broker/dto/create-broker.dto';
|
||||||
|
import { BatchCreateBrokerDto } from '../../../src/modules/broker/dto/batch-create-broker.dto';
|
||||||
|
import { getDatabaseConfig } from '../../../src/database/database.config';
|
||||||
|
import { brokersConfig } from '../../../src/modules/broker/config/brokers.config';
|
||||||
|
|
||||||
|
describe('BrokerService (集成测试)', () => {
|
||||||
|
let service: BrokerService;
|
||||||
|
let repository: Repository<Broker>;
|
||||||
|
let module: TestingModule;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
jest.setTimeout(30000); // 设置超时为 30 秒
|
||||||
|
// 创建测试模块,使用真实数据库连接
|
||||||
|
module = await Test.createTestingModule({
|
||||||
|
imports: [
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
isGlobal: true,
|
||||||
|
envFilePath: ['.env.development', '.env'],
|
||||||
|
}),
|
||||||
|
TypeOrmModule.forRootAsync({
|
||||||
|
imports: [ConfigModule],
|
||||||
|
useFactory: (configService: ConfigService) => {
|
||||||
|
const config = getDatabaseConfig(configService);
|
||||||
|
// 使用测试数据库(如果配置了),否则使用开发数据库
|
||||||
|
const testDatabase =
|
||||||
|
configService.get<string>('DB_DATABASE') ||
|
||||||
|
(config.database as string);
|
||||||
|
return {
|
||||||
|
...config,
|
||||||
|
database: testDatabase,
|
||||||
|
synchronize: true, // 测试环境允许同步
|
||||||
|
dropSchema: false, // 不删除现有数据
|
||||||
|
} as typeof config;
|
||||||
|
},
|
||||||
|
inject: [ConfigService],
|
||||||
|
}),
|
||||||
|
BrokerModule,
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<BrokerService>(BrokerService);
|
||||||
|
repository = module.get<Repository<Broker>>(getRepositoryToken(Broker));
|
||||||
|
|
||||||
|
// 清理测试数据(可选)
|
||||||
|
await repository.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
// 测试结束后清理
|
||||||
|
// await repository.clear();
|
||||||
|
await module.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.skip('create - 集成测试', () => {
|
||||||
|
it('应该成功在数据库中创建券商', async () => {
|
||||||
|
const createBrokerDto: CreateBrokerDto = {
|
||||||
|
brokerCode: 'HTZQ_TEST',
|
||||||
|
brokerName: '华泰证券测试',
|
||||||
|
region: 'CN',
|
||||||
|
sortOrder: 1,
|
||||||
|
isActive: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await service.create(createBrokerDto);
|
||||||
|
console.log(result);
|
||||||
|
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.brokerId).toBeDefined();
|
||||||
|
expect(result.brokerCode).toBe(createBrokerDto.brokerCode);
|
||||||
|
expect(result.brokerName).toBe(createBrokerDto.brokerName);
|
||||||
|
|
||||||
|
// 验证数据确实保存到数据库
|
||||||
|
const savedBroker = await repository.findOne({
|
||||||
|
where: { brokerId: result.brokerId },
|
||||||
|
});
|
||||||
|
expect(savedBroker).toBeDefined();
|
||||||
|
expect(savedBroker?.brokerCode).toBe(createBrokerDto.brokerCode);
|
||||||
|
|
||||||
|
// 清理
|
||||||
|
// await repository.remove(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该使用数据库默认值', async () => {
|
||||||
|
const createBrokerDto: CreateBrokerDto = {
|
||||||
|
brokerCode: 'ZSZQ_TEST',
|
||||||
|
brokerName: '招商证券测试',
|
||||||
|
region: 'CN',
|
||||||
|
// 不提供 sortOrder 和 isActive
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await service.create(createBrokerDto);
|
||||||
|
|
||||||
|
expect(result.sortOrder).toBe(0); // 数据库默认值
|
||||||
|
expect(result.isActive).toBe(true); // 数据库默认值
|
||||||
|
|
||||||
|
// 清理
|
||||||
|
// await repository.remove(result);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.only('batchCreate - 集成测试', () => {
|
||||||
|
it('应该成功从配置文件批量创建券商', async () => {
|
||||||
|
// 从配置文件读取券商数据
|
||||||
|
const batchCreateDto: BatchCreateBrokerDto = {
|
||||||
|
brokers: brokersConfig,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await service.batchCreate(batchCreateDto);
|
||||||
|
|
||||||
|
// 验证创建成功
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(Array.isArray(result)).toBe(true);
|
||||||
|
expect(result.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// 验证数据确实保存到数据库
|
||||||
|
const savedBrokers = await repository.find({
|
||||||
|
where: brokersConfig.map((broker) => ({
|
||||||
|
brokerCode: broker.brokerCode,
|
||||||
|
region: broker.region,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(savedBrokers.length).toBe(brokersConfig.length);
|
||||||
|
|
||||||
|
// 验证部分数据
|
||||||
|
const cnBrokers = savedBrokers.filter((b) => b.region === 'CN');
|
||||||
|
const hkBrokers = savedBrokers.filter((b) => b.region === 'HK');
|
||||||
|
const usBrokers = savedBrokers.filter((b) => b.region === 'US');
|
||||||
|
|
||||||
|
expect(cnBrokers.length).toBeGreaterThan(0);
|
||||||
|
expect(hkBrokers.length).toBeGreaterThan(0);
|
||||||
|
expect(usBrokers.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// 验证 brokerImage 为 null 或 undefined
|
||||||
|
savedBrokers.forEach((broker) => {
|
||||||
|
expect(broker.brokerImage).toBeFalsy(); // null 或 undefined 都通过
|
||||||
|
});
|
||||||
|
|
||||||
|
// 注意:不删除数据,保留在数据库中
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
261
pnpm-lock.yaml
generated
261
pnpm-lock.yaml
generated
@@ -12,19 +12,43 @@ importers:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@nestjs/common':
|
'@nestjs/common':
|
||||||
specifier: ^11.0.1
|
specifier: ^11.0.1
|
||||||
version: 11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
version: 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
'@nestjs/config':
|
'@nestjs/config':
|
||||||
specifier: ^4.0.2
|
specifier: ^4.0.2
|
||||||
version: 4.0.2(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)
|
version: 4.0.2(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)
|
||||||
'@nestjs/core':
|
'@nestjs/core':
|
||||||
specifier: ^11.0.1
|
specifier: ^11.0.1
|
||||||
version: 11.1.9(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
version: 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
|
'@nestjs/mapped-types':
|
||||||
|
specifier: ^2.1.0
|
||||||
|
version: 2.1.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)
|
||||||
'@nestjs/platform-express':
|
'@nestjs/platform-express':
|
||||||
specifier: ^11.0.1
|
specifier: ^11.0.1
|
||||||
version: 11.1.9(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)
|
version: 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)
|
||||||
|
'@nestjs/swagger':
|
||||||
|
specifier: ^11.2.2
|
||||||
|
version: 11.2.2(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)
|
||||||
'@nestjs/typeorm':
|
'@nestjs/typeorm':
|
||||||
specifier: ^11.0.0
|
specifier: ^11.0.0
|
||||||
version: 11.0.0(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(pg@8.16.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))
|
version: 11.0.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(pg@8.16.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))
|
||||||
|
body-parser:
|
||||||
|
specifier: ^2.2.0
|
||||||
|
version: 2.2.0
|
||||||
|
class-transformer:
|
||||||
|
specifier: ^0.5.1
|
||||||
|
version: 0.5.1
|
||||||
|
class-validator:
|
||||||
|
specifier: ^0.14.2
|
||||||
|
version: 0.14.2
|
||||||
|
compression:
|
||||||
|
specifier: ^1.8.1
|
||||||
|
version: 1.8.1
|
||||||
|
express-rate-limit:
|
||||||
|
specifier: ^8.2.1
|
||||||
|
version: 8.2.1(express@5.1.0)
|
||||||
|
helmet:
|
||||||
|
specifier: ^8.1.0
|
||||||
|
version: 8.1.0
|
||||||
pg:
|
pg:
|
||||||
specifier: ^8.16.3
|
specifier: ^8.16.3
|
||||||
version: 8.16.3
|
version: 8.16.3
|
||||||
@@ -52,7 +76,13 @@ importers:
|
|||||||
version: 11.0.9(chokidar@4.0.3)(typescript@5.9.3)
|
version: 11.0.9(chokidar@4.0.3)(typescript@5.9.3)
|
||||||
'@nestjs/testing':
|
'@nestjs/testing':
|
||||||
specifier: ^11.0.1
|
specifier: ^11.0.1
|
||||||
version: 11.1.9(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-express@11.1.9)
|
version: 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-express@11.1.9)
|
||||||
|
'@types/body-parser':
|
||||||
|
specifier: ^1.19.6
|
||||||
|
version: 1.19.6
|
||||||
|
'@types/compression':
|
||||||
|
specifier: ^1.8.1
|
||||||
|
version: 1.8.1
|
||||||
'@types/express':
|
'@types/express':
|
||||||
specifier: ^5.0.0
|
specifier: ^5.0.0
|
||||||
version: 5.0.5
|
version: 5.0.5
|
||||||
@@ -654,6 +684,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==}
|
resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
'@microsoft/tsdoc@0.16.0':
|
||||||
|
resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==}
|
||||||
|
|
||||||
'@napi-rs/wasm-runtime@0.2.12':
|
'@napi-rs/wasm-runtime@0.2.12':
|
||||||
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
|
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
|
||||||
|
|
||||||
@@ -707,6 +740,19 @@ packages:
|
|||||||
'@nestjs/websockets':
|
'@nestjs/websockets':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@nestjs/mapped-types@2.1.0':
|
||||||
|
resolution: {integrity: sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@nestjs/common': ^10.0.0 || ^11.0.0
|
||||||
|
class-transformer: ^0.4.0 || ^0.5.0
|
||||||
|
class-validator: ^0.13.0 || ^0.14.0
|
||||||
|
reflect-metadata: ^0.1.12 || ^0.2.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
class-transformer:
|
||||||
|
optional: true
|
||||||
|
class-validator:
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@nestjs/platform-express@11.1.9':
|
'@nestjs/platform-express@11.1.9':
|
||||||
resolution: {integrity: sha512-GVd3+0lO0mJq2m1kl9hDDnVrX3Nd4oH3oDfklz0pZEVEVS0KVSp63ufHq2Lu9cyPdSBuelJr9iPm2QQ1yX+Kmw==}
|
resolution: {integrity: sha512-GVd3+0lO0mJq2m1kl9hDDnVrX3Nd4oH3oDfklz0pZEVEVS0KVSp63ufHq2Lu9cyPdSBuelJr9iPm2QQ1yX+Kmw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -718,6 +764,23 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: '>=4.8.2'
|
typescript: '>=4.8.2'
|
||||||
|
|
||||||
|
'@nestjs/swagger@11.2.2':
|
||||||
|
resolution: {integrity: sha512-i16GRaZ7vlTHIqk8C1UvV/WwQYbWwQymocTvU8mr6QIUBZ6fJc1uGEsw0Mu/JWC0kaV3nbsTj1hZbXrc5Ui4NA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@fastify/static': ^8.0.0
|
||||||
|
'@nestjs/common': ^11.0.1
|
||||||
|
'@nestjs/core': ^11.0.1
|
||||||
|
class-transformer: '*'
|
||||||
|
class-validator: '*'
|
||||||
|
reflect-metadata: ^0.1.12 || ^0.2.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@fastify/static':
|
||||||
|
optional: true
|
||||||
|
class-transformer:
|
||||||
|
optional: true
|
||||||
|
class-validator:
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@nestjs/testing@11.1.9':
|
'@nestjs/testing@11.1.9':
|
||||||
resolution: {integrity: sha512-UFxerBDdb0RUNxQNj25pvkvNE7/vxKhXYWBt3QuwBFnYISzRIzhVlyIqLfoV5YI3zV0m0Nn4QAn1KM0zzwfEng==}
|
resolution: {integrity: sha512-UFxerBDdb0RUNxQNj25pvkvNE7/vxKhXYWBt3QuwBFnYISzRIzhVlyIqLfoV5YI3zV0m0Nn4QAn1KM0zzwfEng==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -772,6 +835,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==}
|
resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==}
|
||||||
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
||||||
|
|
||||||
|
'@scarf/scarf@1.4.0':
|
||||||
|
resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==}
|
||||||
|
|
||||||
'@sinclair/typebox@0.34.41':
|
'@sinclair/typebox@0.34.41':
|
||||||
resolution: {integrity: sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==}
|
resolution: {integrity: sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==}
|
||||||
|
|
||||||
@@ -821,6 +887,9 @@ packages:
|
|||||||
'@types/body-parser@1.19.6':
|
'@types/body-parser@1.19.6':
|
||||||
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
|
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
|
||||||
|
|
||||||
|
'@types/compression@1.8.1':
|
||||||
|
resolution: {integrity: sha512-kCFuWS0ebDbmxs0AXYn6e2r2nrGAb5KwQhknjSPSPgJcGd8+HVSILlUyFhGqML2gk39HcG7D1ydW9/qpYkN00Q==}
|
||||||
|
|
||||||
'@types/connect@3.4.38':
|
'@types/connect@3.4.38':
|
||||||
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
|
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
|
||||||
|
|
||||||
@@ -896,6 +965,9 @@ packages:
|
|||||||
'@types/supertest@6.0.3':
|
'@types/supertest@6.0.3':
|
||||||
resolution: {integrity: sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==}
|
resolution: {integrity: sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==}
|
||||||
|
|
||||||
|
'@types/validator@13.15.10':
|
||||||
|
resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==}
|
||||||
|
|
||||||
'@types/yargs-parser@21.0.3':
|
'@types/yargs-parser@21.0.3':
|
||||||
resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==}
|
resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==}
|
||||||
|
|
||||||
@@ -1369,6 +1441,12 @@ packages:
|
|||||||
cjs-module-lexer@2.1.1:
|
cjs-module-lexer@2.1.1:
|
||||||
resolution: {integrity: sha512-+CmxIZ/L2vNcEfvNtLdU0ZQ6mbq3FZnwAP2PPTiKP+1QOoKwlKlPgb8UKV0Dds7QVaMnHm+FwSft2VB0s/SLjQ==}
|
resolution: {integrity: sha512-+CmxIZ/L2vNcEfvNtLdU0ZQ6mbq3FZnwAP2PPTiKP+1QOoKwlKlPgb8UKV0Dds7QVaMnHm+FwSft2VB0s/SLjQ==}
|
||||||
|
|
||||||
|
class-transformer@0.5.1:
|
||||||
|
resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==}
|
||||||
|
|
||||||
|
class-validator@0.14.2:
|
||||||
|
resolution: {integrity: sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==}
|
||||||
|
|
||||||
cli-cursor@3.1.0:
|
cli-cursor@3.1.0:
|
||||||
resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==}
|
resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -1425,6 +1503,14 @@ packages:
|
|||||||
component-emitter@1.3.1:
|
component-emitter@1.3.1:
|
||||||
resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==}
|
resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==}
|
||||||
|
|
||||||
|
compressible@2.0.18:
|
||||||
|
resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
compression@1.8.1:
|
||||||
|
resolution: {integrity: sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==}
|
||||||
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
concat-map@0.0.1:
|
concat-map@0.0.1:
|
||||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||||
|
|
||||||
@@ -1484,6 +1570,14 @@ packages:
|
|||||||
dayjs@1.11.19:
|
dayjs@1.11.19:
|
||||||
resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==}
|
resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==}
|
||||||
|
|
||||||
|
debug@2.6.9:
|
||||||
|
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
|
||||||
|
peerDependencies:
|
||||||
|
supports-color: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
supports-color:
|
||||||
|
optional: true
|
||||||
|
|
||||||
debug@4.4.3:
|
debug@4.4.3:
|
||||||
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
||||||
engines: {node: '>=6.0'}
|
engines: {node: '>=6.0'}
|
||||||
@@ -1705,6 +1799,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==}
|
resolution: {integrity: sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==}
|
||||||
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
|
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||||
|
|
||||||
|
express-rate-limit@8.2.1:
|
||||||
|
resolution: {integrity: sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==}
|
||||||
|
engines: {node: '>= 16'}
|
||||||
|
peerDependencies:
|
||||||
|
express: '>= 4.11'
|
||||||
|
|
||||||
express@5.1.0:
|
express@5.1.0:
|
||||||
resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==}
|
resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==}
|
||||||
engines: {node: '>= 18'}
|
engines: {node: '>= 18'}
|
||||||
@@ -1910,6 +2010,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
helmet@8.1.0:
|
||||||
|
resolution: {integrity: sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==}
|
||||||
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
html-escaper@2.0.2:
|
html-escaper@2.0.2:
|
||||||
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
|
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
|
||||||
|
|
||||||
@@ -1960,6 +2064,10 @@ packages:
|
|||||||
inherits@2.0.4:
|
inherits@2.0.4:
|
||||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||||
|
|
||||||
|
ip-address@10.0.1:
|
||||||
|
resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==}
|
||||||
|
engines: {node: '>= 12'}
|
||||||
|
|
||||||
ipaddr.js@1.9.1:
|
ipaddr.js@1.9.1:
|
||||||
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
@@ -2232,6 +2340,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
|
libphonenumber-js@1.12.26:
|
||||||
|
resolution: {integrity: sha512-MagMOuqEXB2Pa90cWE+BoCmcKJx+de5uBIicaUkQ+uiEslZ0OBMNOkSZT/36syXNHu68UeayTxPm3DYM2IHoLQ==}
|
||||||
|
|
||||||
lines-and-columns@1.2.4:
|
lines-and-columns@1.2.4:
|
||||||
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
||||||
|
|
||||||
@@ -2369,6 +2480,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
|
resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
ms@2.0.0:
|
||||||
|
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
||||||
|
|
||||||
ms@2.1.3:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
@@ -2388,6 +2502,10 @@ packages:
|
|||||||
natural-compare@1.4.0:
|
natural-compare@1.4.0:
|
||||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||||
|
|
||||||
|
negotiator@0.6.4:
|
||||||
|
resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
negotiator@1.0.0:
|
negotiator@1.0.0:
|
||||||
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
|
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@@ -2427,6 +2545,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
|
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
on-headers@1.1.0:
|
||||||
|
resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
once@1.4.0:
|
once@1.4.0:
|
||||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||||
|
|
||||||
@@ -2871,6 +2993,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
|
resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
swagger-ui-dist@5.30.2:
|
||||||
|
resolution: {integrity: sha512-HWCg1DTNE/Nmapt+0m2EPXFwNKNeKK4PwMjkwveN/zn1cV2Kxi9SURd+m0SpdcSgWEK/O64sf8bzXdtUhigtHA==}
|
||||||
|
|
||||||
symbol-observable@4.0.0:
|
symbol-observable@4.0.0:
|
||||||
resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==}
|
resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==}
|
||||||
engines: {node: '>=0.10'}
|
engines: {node: '>=0.10'}
|
||||||
@@ -3150,6 +3275,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==}
|
resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==}
|
||||||
engines: {node: '>=10.12.0'}
|
engines: {node: '>=10.12.0'}
|
||||||
|
|
||||||
|
validator@13.15.23:
|
||||||
|
resolution: {integrity: sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==}
|
||||||
|
engines: {node: '>= 0.10'}
|
||||||
|
|
||||||
vary@1.1.2:
|
vary@1.1.2:
|
||||||
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@@ -3950,6 +4079,8 @@ snapshots:
|
|||||||
|
|
||||||
'@lukeed/csprng@1.1.0': {}
|
'@lukeed/csprng@1.1.0': {}
|
||||||
|
|
||||||
|
'@microsoft/tsdoc@0.16.0': {}
|
||||||
|
|
||||||
'@napi-rs/wasm-runtime@0.2.12':
|
'@napi-rs/wasm-runtime@0.2.12':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@emnapi/core': 1.7.1
|
'@emnapi/core': 1.7.1
|
||||||
@@ -3984,7 +4115,7 @@ snapshots:
|
|||||||
- uglify-js
|
- uglify-js
|
||||||
- webpack-cli
|
- webpack-cli
|
||||||
|
|
||||||
'@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2)':
|
'@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
file-type: 21.1.0
|
file-type: 21.1.0
|
||||||
iterare: 1.2.1
|
iterare: 1.2.1
|
||||||
@@ -3993,20 +4124,23 @@ snapshots:
|
|||||||
rxjs: 7.8.2
|
rxjs: 7.8.2
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
uid: 2.0.2
|
uid: 2.0.2
|
||||||
|
optionalDependencies:
|
||||||
|
class-transformer: 0.5.1
|
||||||
|
class-validator: 0.14.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@nestjs/config@4.0.2(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)':
|
'@nestjs/config@4.0.2(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nestjs/common': 11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
'@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
dotenv: 16.4.7
|
dotenv: 16.4.7
|
||||||
dotenv-expand: 12.0.1
|
dotenv-expand: 12.0.1
|
||||||
lodash: 4.17.21
|
lodash: 4.17.21
|
||||||
rxjs: 7.8.2
|
rxjs: 7.8.2
|
||||||
|
|
||||||
'@nestjs/core@11.1.9(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)':
|
'@nestjs/core@11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nestjs/common': 11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
'@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
'@nuxt/opencollective': 0.4.1
|
'@nuxt/opencollective': 0.4.1
|
||||||
fast-safe-stringify: 2.1.1
|
fast-safe-stringify: 2.1.1
|
||||||
iterare: 1.2.1
|
iterare: 1.2.1
|
||||||
@@ -4016,12 +4150,20 @@ snapshots:
|
|||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
uid: 2.0.2
|
uid: 2.0.2
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@nestjs/platform-express': 11.1.9(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)
|
'@nestjs/platform-express': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)
|
||||||
|
|
||||||
'@nestjs/platform-express@11.1.9(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)':
|
'@nestjs/mapped-types@2.1.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nestjs/common': 11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
'@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
'@nestjs/core': 11.1.9(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
reflect-metadata: 0.2.2
|
||||||
|
optionalDependencies:
|
||||||
|
class-transformer: 0.5.1
|
||||||
|
class-validator: 0.14.2
|
||||||
|
|
||||||
|
'@nestjs/platform-express@11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)':
|
||||||
|
dependencies:
|
||||||
|
'@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
|
'@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
cors: 2.8.5
|
cors: 2.8.5
|
||||||
express: 5.1.0
|
express: 5.1.0
|
||||||
multer: 2.0.2
|
multer: 2.0.2
|
||||||
@@ -4052,18 +4194,33 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- chokidar
|
- chokidar
|
||||||
|
|
||||||
'@nestjs/testing@11.1.9(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-express@11.1.9)':
|
'@nestjs/swagger@11.2.2(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nestjs/common': 11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
'@microsoft/tsdoc': 0.16.0
|
||||||
'@nestjs/core': 11.1.9(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
'@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
|
'@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
|
'@nestjs/mapped-types': 2.1.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)
|
||||||
|
js-yaml: 4.1.1
|
||||||
|
lodash: 4.17.21
|
||||||
|
path-to-regexp: 8.3.0
|
||||||
|
reflect-metadata: 0.2.2
|
||||||
|
swagger-ui-dist: 5.30.2
|
||||||
|
optionalDependencies:
|
||||||
|
class-transformer: 0.5.1
|
||||||
|
class-validator: 0.14.2
|
||||||
|
|
||||||
|
'@nestjs/testing@11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-express@11.1.9)':
|
||||||
|
dependencies:
|
||||||
|
'@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
|
'@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@nestjs/platform-express': 11.1.9(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)
|
'@nestjs/platform-express': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)
|
||||||
|
|
||||||
'@nestjs/typeorm@11.0.0(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(pg@8.16.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))':
|
'@nestjs/typeorm@11.0.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(pg@8.16.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nestjs/common': 11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
'@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
'@nestjs/core': 11.1.9(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
'@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
reflect-metadata: 0.2.2
|
reflect-metadata: 0.2.2
|
||||||
rxjs: 7.8.2
|
rxjs: 7.8.2
|
||||||
typeorm: 0.3.27(pg@8.16.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
|
typeorm: 0.3.27(pg@8.16.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
|
||||||
@@ -4095,6 +4252,8 @@ snapshots:
|
|||||||
|
|
||||||
'@pkgr/core@0.2.9': {}
|
'@pkgr/core@0.2.9': {}
|
||||||
|
|
||||||
|
'@scarf/scarf@1.4.0': {}
|
||||||
|
|
||||||
'@sinclair/typebox@0.34.41': {}
|
'@sinclair/typebox@0.34.41': {}
|
||||||
|
|
||||||
'@sinonjs/commons@3.0.1':
|
'@sinonjs/commons@3.0.1':
|
||||||
@@ -4156,6 +4315,11 @@ snapshots:
|
|||||||
'@types/connect': 3.4.38
|
'@types/connect': 3.4.38
|
||||||
'@types/node': 22.19.1
|
'@types/node': 22.19.1
|
||||||
|
|
||||||
|
'@types/compression@1.8.1':
|
||||||
|
dependencies:
|
||||||
|
'@types/express': 5.0.5
|
||||||
|
'@types/node': 22.19.1
|
||||||
|
|
||||||
'@types/connect@3.4.38':
|
'@types/connect@3.4.38':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.19.1
|
'@types/node': 22.19.1
|
||||||
@@ -4253,6 +4417,8 @@ snapshots:
|
|||||||
'@types/methods': 1.1.4
|
'@types/methods': 1.1.4
|
||||||
'@types/superagent': 8.1.9
|
'@types/superagent': 8.1.9
|
||||||
|
|
||||||
|
'@types/validator@13.15.10': {}
|
||||||
|
|
||||||
'@types/yargs-parser@21.0.3': {}
|
'@types/yargs-parser@21.0.3': {}
|
||||||
|
|
||||||
'@types/yargs@17.0.35':
|
'@types/yargs@17.0.35':
|
||||||
@@ -4761,6 +4927,14 @@ snapshots:
|
|||||||
|
|
||||||
cjs-module-lexer@2.1.1: {}
|
cjs-module-lexer@2.1.1: {}
|
||||||
|
|
||||||
|
class-transformer@0.5.1: {}
|
||||||
|
|
||||||
|
class-validator@0.14.2:
|
||||||
|
dependencies:
|
||||||
|
'@types/validator': 13.15.10
|
||||||
|
libphonenumber-js: 1.12.26
|
||||||
|
validator: 13.15.23
|
||||||
|
|
||||||
cli-cursor@3.1.0:
|
cli-cursor@3.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
restore-cursor: 3.1.0
|
restore-cursor: 3.1.0
|
||||||
@@ -4809,6 +4983,22 @@ snapshots:
|
|||||||
|
|
||||||
component-emitter@1.3.1: {}
|
component-emitter@1.3.1: {}
|
||||||
|
|
||||||
|
compressible@2.0.18:
|
||||||
|
dependencies:
|
||||||
|
mime-db: 1.54.0
|
||||||
|
|
||||||
|
compression@1.8.1:
|
||||||
|
dependencies:
|
||||||
|
bytes: 3.1.2
|
||||||
|
compressible: 2.0.18
|
||||||
|
debug: 2.6.9
|
||||||
|
negotiator: 0.6.4
|
||||||
|
on-headers: 1.1.0
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
vary: 1.1.2
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
concat-map@0.0.1: {}
|
concat-map@0.0.1: {}
|
||||||
|
|
||||||
concat-stream@2.0.0:
|
concat-stream@2.0.0:
|
||||||
@@ -4860,6 +5050,10 @@ snapshots:
|
|||||||
|
|
||||||
dayjs@1.11.19: {}
|
dayjs@1.11.19: {}
|
||||||
|
|
||||||
|
debug@2.6.9:
|
||||||
|
dependencies:
|
||||||
|
ms: 2.0.0
|
||||||
|
|
||||||
debug@4.4.3:
|
debug@4.4.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
@@ -5069,6 +5263,11 @@ snapshots:
|
|||||||
jest-mock: 30.2.0
|
jest-mock: 30.2.0
|
||||||
jest-util: 30.2.0
|
jest-util: 30.2.0
|
||||||
|
|
||||||
|
express-rate-limit@8.2.1(express@5.1.0):
|
||||||
|
dependencies:
|
||||||
|
express: 5.1.0
|
||||||
|
ip-address: 10.0.1
|
||||||
|
|
||||||
express@5.1.0:
|
express@5.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
accepts: 2.0.0
|
accepts: 2.0.0
|
||||||
@@ -5333,6 +5532,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
function-bind: 1.1.2
|
function-bind: 1.1.2
|
||||||
|
|
||||||
|
helmet@8.1.0: {}
|
||||||
|
|
||||||
html-escaper@2.0.2: {}
|
html-escaper@2.0.2: {}
|
||||||
|
|
||||||
http-errors@2.0.0:
|
http-errors@2.0.0:
|
||||||
@@ -5378,6 +5579,8 @@ snapshots:
|
|||||||
|
|
||||||
inherits@2.0.4: {}
|
inherits@2.0.4: {}
|
||||||
|
|
||||||
|
ip-address@10.0.1: {}
|
||||||
|
|
||||||
ipaddr.js@1.9.1: {}
|
ipaddr.js@1.9.1: {}
|
||||||
|
|
||||||
is-arrayish@0.2.1: {}
|
is-arrayish@0.2.1: {}
|
||||||
@@ -5817,6 +6020,8 @@ snapshots:
|
|||||||
prelude-ls: 1.2.1
|
prelude-ls: 1.2.1
|
||||||
type-check: 0.4.0
|
type-check: 0.4.0
|
||||||
|
|
||||||
|
libphonenumber-js@1.12.26: {}
|
||||||
|
|
||||||
lines-and-columns@1.2.4: {}
|
lines-and-columns@1.2.4: {}
|
||||||
|
|
||||||
load-esm@1.0.3: {}
|
load-esm@1.0.3: {}
|
||||||
@@ -5923,6 +6128,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
minimist: 1.2.8
|
minimist: 1.2.8
|
||||||
|
|
||||||
|
ms@2.0.0: {}
|
||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
multer@2.0.2:
|
multer@2.0.2:
|
||||||
@@ -5941,6 +6148,8 @@ snapshots:
|
|||||||
|
|
||||||
natural-compare@1.4.0: {}
|
natural-compare@1.4.0: {}
|
||||||
|
|
||||||
|
negotiator@0.6.4: {}
|
||||||
|
|
||||||
negotiator@1.0.0: {}
|
negotiator@1.0.0: {}
|
||||||
|
|
||||||
neo-async@2.6.2: {}
|
neo-async@2.6.2: {}
|
||||||
@@ -5969,6 +6178,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ee-first: 1.1.1
|
ee-first: 1.1.1
|
||||||
|
|
||||||
|
on-headers@1.1.0: {}
|
||||||
|
|
||||||
once@1.4.0:
|
once@1.4.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
wrappy: 1.0.2
|
wrappy: 1.0.2
|
||||||
@@ -6418,6 +6629,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
has-flag: 4.0.0
|
has-flag: 4.0.0
|
||||||
|
|
||||||
|
swagger-ui-dist@5.30.2:
|
||||||
|
dependencies:
|
||||||
|
'@scarf/scarf': 1.4.0
|
||||||
|
|
||||||
symbol-observable@4.0.0: {}
|
symbol-observable@4.0.0: {}
|
||||||
|
|
||||||
synckit@0.11.11:
|
synckit@0.11.11:
|
||||||
@@ -6666,6 +6881,8 @@ snapshots:
|
|||||||
'@types/istanbul-lib-coverage': 2.0.6
|
'@types/istanbul-lib-coverage': 2.0.6
|
||||||
convert-source-map: 2.0.0
|
convert-source-map: 2.0.0
|
||||||
|
|
||||||
|
validator@13.15.23: {}
|
||||||
|
|
||||||
vary@1.1.2: {}
|
vary@1.1.2: {}
|
||||||
|
|
||||||
walker@1.0.8:
|
walker@1.0.8:
|
||||||
|
|||||||
Reference in New Issue
Block a user