npm package: @lark-apaas/fullstack-nestjs-core
Description: FullStack Nestjs Core
Latest version: 1.1.38

# @lark-apaas/fullstack-nestjs-core

FullStack NestJS Core 是一个为 NestJS 全栈应用提供核心功能的工具包，包括平台集成、CSRF 保护、用户上下文管理和开发工具等。

## 特性

- **平台模块**: 一站式集成 Config、Logger、Database、Auth 等核心功能
- **HTTP 客户端**: 自动集成 @nestjs/axios，提供 HTTP 请求能力，自动打印请求日志
- **CSRF 保护**: 提供完整的 CSRF Token 生成和验证机制
- **用户上下文**: 自动从请求头中提取并注入用户上下文信息
- **视图上下文**: 将用户信息和 CSRF Token 注入到模板渲染上下文
- **旧路径兼容**: 灰度迁移期间自动将旧路径 302 重定向到新路径，实现平滑过渡
- **开发工具**: 自动生成 Swagger 文档、OpenAPI JSON 和 TypeScript 客户端 SDK
- **应用配置**: 一键配置 Logger、Cookie Parser、全局前缀等

## 安装

```bash
npm install @lark-apaas/fullstack-nestjs-core
```

或使用 yarn:

```bash
yarn add @lark-apaas/fullstack-nestjs-core
```

## 环境要求

- Node.js >= 18.0.0
- NestJS >= 10.4.20

## 快速开始

### 方案 1: 使用 PlatformModule（推荐）

最简单的方式是使用 `PlatformModule`，它会自动集成所有核心功能：

```typescript
import { Module } from '@nestjs/common';
import { PlatformModule } from '@lark-apaas/fullstack-nestjs-core';

@Module({
  imports: [
    PlatformModule.forRoot({
      enableCsrf: true,           // 是否启用 CSRF 保护，默认 true
      csrfRoutes: '/api/*',       // CSRF 保护的路由，默认 '/api/*'
      alwaysNeedLogin: true,      // 是否所有路由都需要登录，默认 true
    }),
  ],
})
export class AppModule {}
```

`PlatformModule` 自动集成：
- ✅ ConfigModule (环境变量配置)
- ✅ LoggerModule (日志系统)
- ✅ HttpModule (HTTP 客户端，自动打印请求日志)
- ✅ DataPaasModule (数据库连接)
- ✅ AuthNPaasModule (认证系统)
- ✅ UserContextMiddleware (用户上下文)
- ✅ LegacyPathRedirectMiddleware (旧路径兼容重定向)
- ✅ CsrfTokenMiddleware + CsrfMiddleware (CSRF 保护)
- ✅ ViewContextMiddleware (视图上下文)
- ✅ ValidationPipe (全局验证管道)

### 方案 2: 使用 configureApp 辅助函数

在 `main.ts` 中使用 `configureApp` 快速配置应用：

```typescript
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { configureApp } from '@lark-apaas/fullstack-nestjs-core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);

  // 一键配置：Logger、CookieParser、GlobalPrefix、DevTools
  await configureApp(app);

  await app.listen(3000);
}
bootstrap();
```

`configureApp` 会自动：
- ✅ 注册 AppLogger
- ✅ 注册 cookie-parser 中间件
- ✅ 根据 `CLIENT_BASE_PATH` 环境变量设置全局前缀
- ✅ 在非生产环境自动挂载 `DevToolsV2Module`

### 方案 3: 手动配置各个模块

如果需要更细粒度的控制，可以单独使用各个模块：

```typescript
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import {
  CsrfTokenMiddleware,
  CsrfMiddleware,
  UserContextMiddleware,
  ViewContextMiddleware,
} from '@lark-apaas/fullstack-nestjs-core';

@Module({})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    // 用户上下文中间件
    consumer
      .apply(UserContextMiddleware)
      .forRoutes('*');

    // CSRF Token 生成（用于视图渲染）
    consumer
      .apply(CsrfTokenMiddleware)
      .exclude('/api/(.*)')
      .forRoutes('*');

    // 视图上下文注入
    consumer
      .apply(ViewContextMiddleware)
      .exclude('/api/(.*)')
      .forRoutes('*');

    // CSRF 验证（用于 API 保护）
    consumer
      .apply(CsrfMiddleware)
      .forRoutes('/api/*');
  }
}
```

## 核心模块

### PlatformModule

全局平台模块，集成了所有核心功能。

#### 配置选项

| 选项 | 类型 | 默认值 | 描述 |
|------|------|--------|------|
| enableCsrf | boolean | `true` | 是否启用 CSRF 保护 |
| csrfRoutes | string \| string[] | `'/api/*'` | CSRF 保护应用的路由 |
| alwaysNeedLogin | boolean | `true` | 是否所有路由都需要登录 |

#### 使用示例

```typescript
@Module({
  imports: [
    PlatformModule.forRoot({
      enableCsrf: true,
      csrfRoutes: ['/api/*', '/admin/*'],
      alwaysNeedLogin: false,
    }),
  ],
})
export class AppModule {}
```

### DevToolsV2Module（推荐）

使用 `@hey-api/openapi-ts` 生成高质量的 TypeScript 客户端 SDK。

**新特性：**
- ✅ 使用最新的 `@hey-api/openapi-ts` 生成器
- ✅ 自动注入 `baseURL` 配置到生成的客户端
- ✅ 自动给所有生成的文件添加 `// @ts-nocheck` 注释

#### 配置选项

| 选项 | 类型 | 默认值 | 描述 |
|------|------|--------|------|
| basePath | string | `'/'` | API 基础路径 |
| docsPath | string | `'/api/docs'` | Swagger UI 的挂载路径 |
| openapiOut | string | `'./client/src/api/gen/openapi.json'` | OpenAPI JSON 输出路径 |
| needSetupServer | boolean | `false` | 是否挂载 Swagger UI 服务器 |
| needGenerateClientSdk | boolean | `true` | 是否生成客户端 SDK |
| clientSdkOut | string | `'./client/src/api/gen'` | 客户端 SDK 输出目录 |
| swaggerOptions | object | - | Swagger 文档配置 |

#### 使用示例

```typescript
import { NestFactory } from '@nestjs/core';
import { DevToolsV2Module } from '@lark-apaas/fullstack-nestjs-core';
import { AppModule } from './app.module';

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

  // 在开发环境挂载开发工具
  if (process.env.NODE_ENV !== 'production') {
    await DevToolsV2Module.mount(app, {
      basePath: '/app',
      docsPath: '/api_docs',
      needSetupServer: true,
      needGenerateClientSdk: true,
      swaggerOptions: {
        title: 'My API',
        version: '1.0.0',
      },
    });
  }

  await app.listen(3000);
}
bootstrap();
```

#### 生成的文件

```
client/src/api/gen/
├── openapi.json              # OpenAPI 规范文件
├── client.config.ts          # 客户端配置（自动生成 baseURL）
├── types.gen.ts              # TypeScript 类型定义
├── sdk.gen.ts                # SDK 函数
└── client/
    └── client.gen.ts         # Axios 客户端
```

所有生成的 `.ts` 文件都会自动添加 `// @ts-nocheck` 注释，避免类型检查错误。

### DevToolsModule（旧版）

使用 `openapi-typescript-codegen` 生成客户端 SDK（不推荐，建议迁移到 `DevToolsV2Module`）。

```typescript
import { DevToolsModule } from '@lark-apaas/fullstack-nestjs-core';

await DevToolsModule.mount(app, {
  docsPath: 'api-docs',
  openapiOut: './openapi.json',
  needSetupServer: true,
  needGenerateClientSdk: true,
  clientSdkOut: './src/sdk',
});
```

## HTTP 客户端

PlatformModule 自动集成了 `@nestjs/axios`，提供开箱即用的 HTTP 请求能力。

### 特性

- ✅ **自动集成**: 导入 PlatformModule 后即可直接使用，无需额外配置
- ✅ **自动日志**: 所有 HTTP 请求和响应自动打印到日志系统
- ✅ **标准 API**: 完全遵循 @nestjs/axios 的标准用法
- ✅ **默认配置**: 自动设置 5 秒超时和最多 5 次重定向

### 基础使用

```typescript
import { Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { firstValueFrom } from 'rxjs';

interface User {
  id: string;
  name: string;
  email: string;
}

@Injectable()
export class UserService {
  constructor(private readonly httpService: HttpService) {}

  async getUserById(id: string): Promise<User> {
    const { data } = await firstValueFrom(
      this.httpService.get<User>(`https://api.example.com/users/${id}`)
    );
    return data;
  }

  async createUser(userData: Partial<User>): Promise<User> {
    const { data } = await firstValueFrom(
      this.httpService.post<User>('https://api.example.com/users', userData)
    );
    return data;
  }

  async updateUser(id: string, userData: Partial<User>): Promise<User> {
    const { data } = await firstValueFrom(
      this.httpService.put<User>(`https://api.example.com/users/${id}`, userData)
    );
    return data;
  }

  async deleteUser(id: string): Promise<void> {
    await firstValueFrom(
      this.httpService.delete(`https://api.example.com/users/${id}`)
    );
  }
}
```

### 带配置的请求

```typescript
async getUserWithHeaders(id: string) {
  const { data } = await firstValueFrom(
    this.httpService.get(`https://api.example.com/users/${id}`, {
      headers: {
        'Authorization': 'Bearer token',
        'X-Custom-Header': 'value',
      },
      params: {
        include: 'profile',
      },
      timeout: 10000, // 10秒超时
    })
  );
  return data;
}
```

### 自动日志输出

所有 HTTP 请求会自动打印到日志系统：

```
[HttpService] HTTP Request {
  method: 'GET',
  url: 'https://api.example.com/users/123',
  headers: { ... },
  params: { include: 'profile' },
  data: undefined
}

[HttpService] HTTP Response {
  method: 'GET',
  url: 'https://api.example.com/users/123',
  status: 200,
  statusText: 'OK',
  data: { id: '123', name: 'John', email: 'john@example.com' }
}
```

错误请求也会自动记录：

```
[HttpService] HTTP Response Error {
  method: 'GET',
  url: 'https://api.example.com/users/999',
  status: 404,
  statusText: 'Not Found',
  data: { message: 'User not found' },
  message: 'Request failed with status code 404'
}
```

### 高级用法

#### 直接访问 Axios 实例

```typescript
constructor(private readonly httpService: HttpService) {
  // 访问底层的 axios 实例
  const axiosInstance = this.httpService.axiosRef;

  // 添加自定义拦截器
  axiosInstance.interceptors.request.use((config) => {
    config.headers['X-Custom'] = 'my-value';
    return config;
  });
}
```

#### 使用 RxJS 操作符

```typescript
import { map, catchError } from 'rxjs/operators';
import { of } from 'rxjs';

async getUsers() {
  return this.httpService.get<User[]>('https://api.example.com/users').pipe(
    map(response => response.data),
    catchError(error => {
      console.error('Error fetching users:', error);
      return of([]);
    })
  );
}
```

### 默认配置

PlatformModule 注册 HttpModule 时使用以下默认配置：

```typescript
HttpModule.register({
  timeout: 5000,      // 5 秒超时
  maxRedirects: 5,    // 最多 5 次重定向
})
```

### 注意事项

1. **记得使用 firstValueFrom**: @nestjs/axios 返回的是 Observable，需要转换为 Promise
2. **记得解构 data**: 响应数据在 `response.data` 中
3. **类型安全**: 使用泛型指定响应数据类型 `httpService.get<User>(...)`

## 中间件

### LegacyPathRedirectMiddleware

`CLIENT_BASE_PATH` 灰度切换到新路径格式（`/app/:appId`）后，将旧路径的页面请求 302 重定向到新路径，实现平滑过渡。已内置于 `PlatformModule`，无需手动注册。

#### 重定向规则

| 环境 (`req.userContext.env`) | 旧路径前缀 | 重定向目标 |
|---|---|---|
| `preview` | `/af/p/:appId/...` | `CLIENT_BASE_PATH/...` |
| `runtime` | `/spark/faas/:appId/...` | `CLIENT_BASE_PATH/...` |

#### 触发条件

- `CLIENT_BASE_PATH` 以 `/app/:appId` 格式开头（新路径格式）
- 请求路径匹配对应环境的旧前缀
- 请求路径不以 `/api` 开头（仅拦截页面请求）

#### 示例

```
CLIENT_BASE_PATH = /app/cli_abc123

# 预览态旧链接访问
GET /af/p/cli_abc123/pages/home?tab=1
→ 302 /app/cli_abc123/pages/home?tab=1

# 运行态旧链接访问
GET /spark/faas/cli_abc123/pages/home
→ 302 /app/cli_abc123/pages/home
```

#### 注意事项

- 中间件依赖 `UserContextMiddleware` 已填充 `req.userContext`，注册顺序不可颠倒
- 旧路径格式（`CLIENT_BASE_PATH` 不是 `/app/:appId`）下该中间件是空操作，没有额外开销
- 路径后缀和 query string 完整保留

### CsrfTokenMiddleware

生成 CSRF Token 并设置到 Cookie 中，用于视图渲染场景。

#### 配置选项

| 选项 | 类型 | 默认值 | 描述 |
|------|------|--------|------|
| cookieKey | string | `'suda-csrf-token'` | Cookie 中存储 Token 的键名 |
| cookieMaxAge | number | `2592000000` (30天) | Cookie 过期时间（毫秒） |
| cookiePath | string | `'/'` | Cookie 路径 |

#### 使用示例

```typescript
CsrfTokenMiddleware.configure({
  cookieKey: 'my-csrf-token',
  cookieMaxAge: 86400000, // 1天
});

consumer
  .apply(CsrfTokenMiddleware)
  .exclude('/api/(.*)')
  .forRoutes('*');
```

### CsrfMiddleware

验证请求中的 CSRF Token，用于保护 API 接口。

#### 配置选项

| 选项 | 类型 | 默认值 | 描述 |
|------|------|--------|------|
| headerKey | string | `'x-suda-csrf-token'` | 请求头中 Token 的键名 |
| cookieKey | string | `'suda-csrf-token'` | Cookie 中 Token 的键名 |

#### 使用示例

```typescript
CsrfMiddleware.configure({
  headerKey: 'x-my-csrf-token',
  cookieKey: 'my-csrf-token',
});

consumer
  .apply(CsrfMiddleware)
  .forRoutes('/api/*');
```

**注意**: 确保 `CsrfTokenMiddleware` 和 `CsrfMiddleware` 的 `cookieKey` 配置一致。

### UserContextMiddleware

从请求头中提取用户上下文信息并注入到 `req.userContext`。

#### 注入的上下文

```typescript
req.userContext = {
  userId: string | undefined;    // 用户 ID（来自 x-user-id 请求头）
  tenantId: string | undefined;  // 租户 ID（来自 x-tenant-id 请求头）
  appId: string;                 // 应用 ID（来自 x-app-id 请求头）
}
```

#### 使用示例

```typescript
@Controller()
export class AppController {
  @Get('profile')
  getProfile(@Req() req: Request) {
    const { userId, tenantId, appId } = req.userContext;
    return { userId, tenantId, appId };
  }
}
```

### ViewContextMiddleware

将用户上下文和 CSRF Token 注入到 `res.locals`，用于模板渲染。

#### 注入的变量

```typescript
res.locals = {
  csrfToken: string;   // CSRF Token
  userId: string;      // 用户 ID
  tenantId: string;    // 租户 ID
  appId: string;       // 应用 ID
}
```

#### 使用示例

```typescript
// 在模板引擎中可以直接使用这些变量
// EJS 示例：
<input type="hidden" name="_csrf" value="<%= csrfToken %>">
<div>User ID: <%= userId %></div>
```

## TypeScript 支持

本包完全使用 TypeScript 编写，并提供完整的类型定义。

### 扩展 Express Request 类型

```typescript
declare global {
  namespace Express {
    interface Request {
      csrfToken?: string;
      userContext: {
        userId?: string;
        tenantId?: string;
        appId: string;
      };
    }
  }
}
```

## 环境变量

| 变量名 | 描述 | 默认值 |
|--------|------|--------|
| CLIENT_BASE_PATH | 应用的全局路径前缀 | - |
| NODE_ENV | 运行环境 | - |

## 许可证

MIT

## 关键字

- plugin
- nestjs
- server
- typescript
- fullstack