create-hest-app 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +211 -0
- package/dist/index.js +127 -0
- package/package.json +50 -0
- package/templates/base/.prettierrc +33 -0
- package/templates/base/.vscode/extensions.json +79 -0
- package/templates/base/.vscode/settings.json +70 -0
- package/templates/base/README.md +562 -0
- package/templates/base/eslint.config.ts +26 -0
- package/templates/base/package.json +62 -0
- package/templates/base/src/app.controller.ts +39 -0
- package/templates/base/src/app.module.ts +12 -0
- package/templates/base/src/app.service.ts +30 -0
- package/templates/base/src/common/filters/http-exception.filter.ts +34 -0
- package/templates/base/src/common/interceptors/response.interceptor.ts +38 -0
- package/templates/base/src/index.ts +35 -0
- package/templates/base/src/modules/custom-validation/custom-validation.controller.ts +146 -0
- package/templates/base/src/modules/custom-validation/custom-validation.module.ts +10 -0
- package/templates/base/src/modules/custom-validation/custom-validation.service.ts +33 -0
- package/templates/base/src/modules/custom-validation/dto/custom-validation.dto.ts +132 -0
- package/templates/base/src/modules/users/dto/user.dto.ts +64 -0
- package/templates/base/src/modules/users/entities/user.entity.ts +9 -0
- package/templates/base/src/modules/users/users.controller.ts +68 -0
- package/templates/base/src/modules/users/users.module.ts +10 -0
- package/templates/base/src/modules/users/users.service.ts +55 -0
- package/templates/base/tsconfig.json +19 -0
- package/templates/base_scalar/.prettierrc +32 -0
- package/templates/base_scalar/.vscode/extensions.json +79 -0
- package/templates/base_scalar/.vscode/settings.json +70 -0
- package/templates/base_scalar/README.md +562 -0
- package/templates/base_scalar/eslint.config.ts +26 -0
- package/templates/base_scalar/package.json +63 -0
- package/templates/base_scalar/src/app.controller.ts +196 -0
- package/templates/base_scalar/src/app.module.ts +12 -0
- package/templates/base_scalar/src/app.service.ts +30 -0
- package/templates/base_scalar/src/common/filters/http-exception.filter.ts +34 -0
- package/templates/base_scalar/src/common/interceptors/response.interceptor.ts +38 -0
- package/templates/base_scalar/src/index.ts +67 -0
- package/templates/base_scalar/src/modules/custom-validation/custom-validation.controller.ts +146 -0
- package/templates/base_scalar/src/modules/custom-validation/custom-validation.module.ts +10 -0
- package/templates/base_scalar/src/modules/custom-validation/custom-validation.service.ts +33 -0
- package/templates/base_scalar/src/modules/custom-validation/dto/custom-validation.dto.ts +132 -0
- package/templates/base_scalar/src/modules/users/dto/user.dto.ts +64 -0
- package/templates/base_scalar/src/modules/users/entities/user.entity.ts +9 -0
- package/templates/base_scalar/src/modules/users/users.controller.ts +68 -0
- package/templates/base_scalar/src/modules/users/users.module.ts +10 -0
- package/templates/base_scalar/src/modules/users/users.service.ts +55 -0
- package/templates/base_scalar/tsconfig.json +19 -0
- package/templates/cqrs/.prettierrc +33 -0
- package/templates/cqrs/.vscode/extensions.json +79 -0
- package/templates/cqrs/.vscode/settings.json +70 -0
- package/templates/cqrs/README.md +234 -0
- package/templates/cqrs/eslint.config.ts +26 -0
- package/templates/cqrs/package.json +62 -0
- package/templates/cqrs/src/app.controller.ts +28 -0
- package/templates/cqrs/src/app.module.ts +12 -0
- package/templates/cqrs/src/app.service.ts +8 -0
- package/templates/cqrs/src/common/filters/http-exception.filter.ts +34 -0
- package/templates/cqrs/src/common/interceptors/response.interceptor.ts +38 -0
- package/templates/cqrs/src/index.ts +38 -0
- package/templates/cqrs/src/modules/custom-validation/custom-validation.controller.ts +146 -0
- package/templates/cqrs/src/modules/custom-validation/custom-validation.module.ts +10 -0
- package/templates/cqrs/src/modules/custom-validation/custom-validation.service.ts +33 -0
- package/templates/cqrs/src/modules/custom-validation/dto/custom-validation.dto.ts +132 -0
- package/templates/cqrs/src/modules/users/dto/user.dto.ts +64 -0
- package/templates/cqrs/src/modules/users/entities/user.entity.ts +9 -0
- package/templates/cqrs/src/modules/users/users.controller.ts +68 -0
- package/templates/cqrs/src/modules/users/users.module.ts +10 -0
- package/templates/cqrs/src/modules/users/users.service.ts +55 -0
- package/templates/cqrs/src/test-error-scenarios.ts +54 -0
- package/templates/cqrs/src/users/commands/create-user.command.ts +8 -0
- package/templates/cqrs/src/users/commands/index.ts +2 -0
- package/templates/cqrs/src/users/commands/update-user.command.ts +11 -0
- package/templates/cqrs/src/users/entities/index.ts +1 -0
- package/templates/cqrs/src/users/entities/user.entity.ts +22 -0
- package/templates/cqrs/src/users/events/index.ts +2 -0
- package/templates/cqrs/src/users/events/user-created.event.ts +8 -0
- package/templates/cqrs/src/users/events/user-updated.event.ts +8 -0
- package/templates/cqrs/src/users/handlers/create-user.handler.ts +26 -0
- package/templates/cqrs/src/users/handlers/get-all-users.handler.ts +15 -0
- package/templates/cqrs/src/users/handlers/get-user.handler.ts +15 -0
- package/templates/cqrs/src/users/handlers/index.ts +6 -0
- package/templates/cqrs/src/users/handlers/update-user.handler.ts +33 -0
- package/templates/cqrs/src/users/handlers/user-created.handler.ts +15 -0
- package/templates/cqrs/src/users/handlers/user-updated.handler.ts +15 -0
- package/templates/cqrs/src/users/index.ts +8 -0
- package/templates/cqrs/src/users/queries/get-all-users.query.ts +8 -0
- package/templates/cqrs/src/users/queries/get-user.query.ts +12 -0
- package/templates/cqrs/src/users/queries/index.ts +2 -0
- package/templates/cqrs/src/users/repositories/index.ts +1 -0
- package/templates/cqrs/src/users/repositories/user.repository.ts +51 -0
- package/templates/cqrs/src/users/user.controller.ts +66 -0
- package/templates/cqrs/src/users/user.module.ts +30 -0
- package/templates/cqrs/tsconfig.json +19 -0
- package/templates/cqrs_scalar/.prettierrc +33 -0
- package/templates/cqrs_scalar/.vscode/extensions.json +79 -0
- package/templates/cqrs_scalar/.vscode/settings.json +70 -0
- package/templates/cqrs_scalar/README.md +234 -0
- package/templates/cqrs_scalar/eslint.config.ts +26 -0
- package/templates/cqrs_scalar/package.json +62 -0
- package/templates/cqrs_scalar/src/app.controller.ts +28 -0
- package/templates/cqrs_scalar/src/app.module.ts +12 -0
- package/templates/cqrs_scalar/src/app.service.ts +8 -0
- package/templates/cqrs_scalar/src/common/filters/http-exception.filter.ts +34 -0
- package/templates/cqrs_scalar/src/common/interceptors/response.interceptor.ts +38 -0
- package/templates/cqrs_scalar/src/index.ts +38 -0
- package/templates/cqrs_scalar/src/modules/custom-validation/custom-validation.controller.ts +146 -0
- package/templates/cqrs_scalar/src/modules/custom-validation/custom-validation.module.ts +10 -0
- package/templates/cqrs_scalar/src/modules/custom-validation/custom-validation.service.ts +33 -0
- package/templates/cqrs_scalar/src/modules/custom-validation/dto/custom-validation.dto.ts +132 -0
- package/templates/cqrs_scalar/src/modules/users/dto/user.dto.ts +64 -0
- package/templates/cqrs_scalar/src/modules/users/entities/user.entity.ts +9 -0
- package/templates/cqrs_scalar/src/modules/users/users.controller.ts +68 -0
- package/templates/cqrs_scalar/src/modules/users/users.module.ts +10 -0
- package/templates/cqrs_scalar/src/modules/users/users.service.ts +55 -0
- package/templates/cqrs_scalar/src/test-error-scenarios.ts +54 -0
- package/templates/cqrs_scalar/src/users/commands/create-user.command.ts +8 -0
- package/templates/cqrs_scalar/src/users/commands/index.ts +2 -0
- package/templates/cqrs_scalar/src/users/commands/update-user.command.ts +11 -0
- package/templates/cqrs_scalar/src/users/entities/index.ts +1 -0
- package/templates/cqrs_scalar/src/users/entities/user.entity.ts +22 -0
- package/templates/cqrs_scalar/src/users/events/index.ts +2 -0
- package/templates/cqrs_scalar/src/users/events/user-created.event.ts +8 -0
- package/templates/cqrs_scalar/src/users/events/user-updated.event.ts +8 -0
- package/templates/cqrs_scalar/src/users/handlers/create-user.handler.ts +26 -0
- package/templates/cqrs_scalar/src/users/handlers/get-all-users.handler.ts +15 -0
- package/templates/cqrs_scalar/src/users/handlers/get-user.handler.ts +15 -0
- package/templates/cqrs_scalar/src/users/handlers/index.ts +6 -0
- package/templates/cqrs_scalar/src/users/handlers/update-user.handler.ts +33 -0
- package/templates/cqrs_scalar/src/users/handlers/user-created.handler.ts +15 -0
- package/templates/cqrs_scalar/src/users/handlers/user-updated.handler.ts +15 -0
- package/templates/cqrs_scalar/src/users/index.ts +8 -0
- package/templates/cqrs_scalar/src/users/queries/get-all-users.query.ts +8 -0
- package/templates/cqrs_scalar/src/users/queries/get-user.query.ts +12 -0
- package/templates/cqrs_scalar/src/users/queries/index.ts +2 -0
- package/templates/cqrs_scalar/src/users/repositories/index.ts +1 -0
- package/templates/cqrs_scalar/src/users/repositories/user.repository.ts +51 -0
- package/templates/cqrs_scalar/src/users/user.controller.ts +66 -0
- package/templates/cqrs_scalar/src/users/user.module.ts +30 -0
- package/templates/cqrs_scalar/tsconfig.json +19 -0
@@ -0,0 +1,62 @@
|
|
1
|
+
{
|
2
|
+
"name": "@hestjs/cqrs-demo",
|
3
|
+
"version": "0.1.1",
|
4
|
+
"description": "HestJS Demo Application - A demonstration of HestJS framework capabilities",
|
5
|
+
"main": "dist/index.js",
|
6
|
+
"types": "dist/index.d.ts",
|
7
|
+
"files": [
|
8
|
+
"dist",
|
9
|
+
"README.md"
|
10
|
+
],
|
11
|
+
"scripts": {
|
12
|
+
"dev": "bun run --hot src/index.ts",
|
13
|
+
"build": "bun build src/index.ts --outdir=dist --target=bun --format=esm --splitting --external pino --external pino-pretty --external sonic-boom --external thread-stream",
|
14
|
+
"build:external": "bun build src/index.ts --outdir=dist --target=bun --format=esm --external reflect-metadata --external pino --external pino-pretty --external sonic-boom --external thread-stream",
|
15
|
+
"build:binary": "bun build src/index.ts --compile --outfile=dist/hest-demo --external pino-pretty",
|
16
|
+
"start": "bun run dist/index.js | pino-pretty",
|
17
|
+
"start:binary": "./dist/hest-demo | pino-pretty",
|
18
|
+
"start:prod": "NODE_ENV=production bun run dist/index.js | pino-pretty",
|
19
|
+
"clean": "rm -rf dist",
|
20
|
+
"check-types": "tsc --noEmit"
|
21
|
+
},
|
22
|
+
"repository": {
|
23
|
+
"type": "git",
|
24
|
+
"url": "https://github.com/aqz236/hestjs-cqrs-demo.git"
|
25
|
+
},
|
26
|
+
"homepage": "https://github.com/aqz236/hestjs-cqrs-demo#readme",
|
27
|
+
"bugs": {
|
28
|
+
"url": "https://github.com/aqz236/hestjs-cqrs-demo/issues"
|
29
|
+
},
|
30
|
+
"author": "aqz236",
|
31
|
+
"license": "MIT",
|
32
|
+
"dependencies": {
|
33
|
+
"@hestjs/core": "^0.1.8",
|
34
|
+
"@hestjs/validation": "^0.1.5",
|
35
|
+
"@hestjs/cqrs": "^0.1.2",
|
36
|
+
"hono": "^4.8.9",
|
37
|
+
"reflect-metadata": "^0.2.2"
|
38
|
+
},
|
39
|
+
"devDependencies": {
|
40
|
+
"@hestjs/eslint-config": "^0.1.1",
|
41
|
+
"@hestjs/typescript-config": "^0.1.0",
|
42
|
+
"@types/bun": "^1.2.19",
|
43
|
+
"jiti": "^2.5.1",
|
44
|
+
"typescript": "5.8.3"
|
45
|
+
},
|
46
|
+
"exports": {
|
47
|
+
".": {
|
48
|
+
"import": "./dist/index.js",
|
49
|
+
"require": "./dist/index.js",
|
50
|
+
"types": "./dist/index.d.ts"
|
51
|
+
}
|
52
|
+
},
|
53
|
+
"keywords": [
|
54
|
+
"hestjs",
|
55
|
+
"demo",
|
56
|
+
"framework",
|
57
|
+
"typescript",
|
58
|
+
"bun",
|
59
|
+
"hono",
|
60
|
+
"validation"
|
61
|
+
]
|
62
|
+
}
|
@@ -0,0 +1,28 @@
|
|
1
|
+
import { Controller, Get } from '@hestjs/core';
|
2
|
+
import { AppService } from './app.service';
|
3
|
+
|
4
|
+
@Controller('/')
|
5
|
+
export class AppController {
|
6
|
+
constructor(private readonly appService: AppService) {}
|
7
|
+
|
8
|
+
@Get('/')
|
9
|
+
getHello() {
|
10
|
+
return {
|
11
|
+
message: this.appService.getHello(),
|
12
|
+
description: 'HestJS CQRS Demo - A demonstration of CQRS pattern using HestJS framework',
|
13
|
+
endpoints: {
|
14
|
+
users: {
|
15
|
+
getAll: 'GET /users',
|
16
|
+
getById: 'GET /users/:id',
|
17
|
+
create: 'POST /users',
|
18
|
+
update: 'PUT /users/:id',
|
19
|
+
},
|
20
|
+
},
|
21
|
+
};
|
22
|
+
}
|
23
|
+
|
24
|
+
@Get('/error')
|
25
|
+
throwError() {
|
26
|
+
throw new Error('This is a test error for exception handling');
|
27
|
+
}
|
28
|
+
}
|
@@ -0,0 +1,12 @@
|
|
1
|
+
import { Module } from '@hestjs/core';
|
2
|
+
import { CqrsModule } from '@hestjs/cqrs';
|
3
|
+
import { AppController } from './app.controller';
|
4
|
+
import { AppService } from './app.service';
|
5
|
+
import { UserModule } from './users';
|
6
|
+
|
7
|
+
@Module({
|
8
|
+
imports: [CqrsModule.forRoot(), UserModule],
|
9
|
+
controllers: [AppController],
|
10
|
+
providers: [AppService],
|
11
|
+
})
|
12
|
+
export class AppModule {}
|
@@ -0,0 +1,34 @@
|
|
1
|
+
import type {
|
2
|
+
ArgumentsHost,
|
3
|
+
ExceptionFilter,
|
4
|
+
HttpException,
|
5
|
+
} from '@hestjs/core';
|
6
|
+
import { createLogger } from '@hestjs/core';
|
7
|
+
|
8
|
+
const logger = createLogger('HttpExceptionFilter');
|
9
|
+
|
10
|
+
/**
|
11
|
+
* 自定义 HTTP 异常过滤器
|
12
|
+
*/
|
13
|
+
export class HttpExceptionFilter implements ExceptionFilter<HttpException> {
|
14
|
+
catch(exception: HttpException, host: ArgumentsHost) {
|
15
|
+
const ctx = host.getContext();
|
16
|
+
const request = host.getRequest();
|
17
|
+
|
18
|
+
const status = exception.status;
|
19
|
+
const response = {
|
20
|
+
statusCode: status,
|
21
|
+
timestamp: new Date().toISOString(),
|
22
|
+
path: request.url,
|
23
|
+
message: exception.message,
|
24
|
+
error: exception.error || 'Http Exception',
|
25
|
+
};
|
26
|
+
|
27
|
+
logger.error(`🔥 HTTP Exception [${status}]: ${exception.message}`, {
|
28
|
+
requestUrl: request.url,
|
29
|
+
stack: exception.stack,
|
30
|
+
});
|
31
|
+
|
32
|
+
return ctx.json(response, status);
|
33
|
+
}
|
34
|
+
}
|
@@ -0,0 +1,38 @@
|
|
1
|
+
import type { CallHandler, ExecutionContext, Interceptor } from '@hestjs/core';
|
2
|
+
import { createLogger } from '@hestjs/core';
|
3
|
+
|
4
|
+
const logger = createLogger('ResponseInterceptor');
|
5
|
+
|
6
|
+
/**
|
7
|
+
* 响应转换拦截器
|
8
|
+
*/
|
9
|
+
export class ResponseInterceptor implements Interceptor {
|
10
|
+
async intercept(
|
11
|
+
context: ExecutionContext,
|
12
|
+
next: CallHandler,
|
13
|
+
): Promise<{
|
14
|
+
success: boolean;
|
15
|
+
data: unknown;
|
16
|
+
timestamp: string;
|
17
|
+
duration: string;
|
18
|
+
}> {
|
19
|
+
const startTime = Date.now();
|
20
|
+
const request = context.switchToHttp().getRequest();
|
21
|
+
|
22
|
+
logger.info(`🚀 Request: ${request.method} ${request.url}`);
|
23
|
+
|
24
|
+
const result = await next.handle();
|
25
|
+
|
26
|
+
const duration = Date.now() - startTime;
|
27
|
+
logger.info(
|
28
|
+
`✅ Response: ${request.method} ${request.url} - ${duration}ms`,
|
29
|
+
);
|
30
|
+
|
31
|
+
return {
|
32
|
+
success: true,
|
33
|
+
data: result,
|
34
|
+
timestamp: new Date().toISOString(),
|
35
|
+
duration: `${duration}ms`,
|
36
|
+
};
|
37
|
+
}
|
38
|
+
}
|
@@ -0,0 +1,38 @@
|
|
1
|
+
import { HestFactory, logger } from '@hestjs/core';
|
2
|
+
import { ValidationInterceptor } from '@hestjs/validation';
|
3
|
+
import { cors } from 'hono/cors';
|
4
|
+
import { logger as log } from 'hono/logger';
|
5
|
+
import { AppModule } from './app.module';
|
6
|
+
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
|
7
|
+
import { ResponseInterceptor } from './common/interceptors/response.interceptor';
|
8
|
+
|
9
|
+
async function bootstrap() {
|
10
|
+
try {
|
11
|
+
logger.info('🚀 Starting HestJS application...');
|
12
|
+
|
13
|
+
const app = await HestFactory.create(AppModule);
|
14
|
+
app.hono().use(cors()); // 使用 Hono 的 CORS 中间件
|
15
|
+
app.hono().use('*', log()); // 使用 Hono 的日志中间件
|
16
|
+
|
17
|
+
// 全局拦截器 - 验证拦截器应该在响应拦截器之前
|
18
|
+
app.useGlobalInterceptors(new ValidationInterceptor());
|
19
|
+
app.useGlobalInterceptors(new ResponseInterceptor());
|
20
|
+
|
21
|
+
// 全局异常过滤器
|
22
|
+
app.useGlobalFilters(new HttpExceptionFilter());
|
23
|
+
|
24
|
+
const server = Bun.serve({
|
25
|
+
port: 3002,
|
26
|
+
fetch: app.hono().fetch,
|
27
|
+
reusePort: true, // 启用端口复用
|
28
|
+
});
|
29
|
+
|
30
|
+
logger.info(`🎉 Server is running on http://localhost:${server.port}`);
|
31
|
+
} catch (error) {
|
32
|
+
// 使用新的简化语法直接传递错误对象
|
33
|
+
logger.error('❌ Failed to start application:', error);
|
34
|
+
process.exit(1);
|
35
|
+
}
|
36
|
+
}
|
37
|
+
|
38
|
+
bootstrap();
|
@@ -0,0 +1,146 @@
|
|
1
|
+
import { Controller, Get, Post } from '@hestjs/core';
|
2
|
+
import { Body } from '@hestjs/validation';
|
3
|
+
import { CustomValidationDto, SearchQueryDto } from './dto/custom-validation.dto';
|
4
|
+
import { CustomValidationService } from './custom-validation.service';
|
5
|
+
|
6
|
+
/**
|
7
|
+
* 自定义验证控制器 - 展示 TypeBox 自定义验证功能
|
8
|
+
*/
|
9
|
+
@Controller('/api/custom')
|
10
|
+
export class CustomValidationController {
|
11
|
+
constructor(
|
12
|
+
private readonly customValidationService: CustomValidationService,
|
13
|
+
) {}
|
14
|
+
|
15
|
+
@Get('/')
|
16
|
+
async getInfo() {
|
17
|
+
return {
|
18
|
+
success: true,
|
19
|
+
message: 'HestJS 自定义验证功能示例',
|
20
|
+
features: [
|
21
|
+
'🔧 TypeBox 自定义 Schema 验证',
|
22
|
+
'🏗️ SchemaFactory 便捷构建器',
|
23
|
+
'📦 CommonValidators 常用验证',
|
24
|
+
'🔗 联合类型验证',
|
25
|
+
'📐 嵌套对象验证',
|
26
|
+
'📱 中国手机号验证',
|
27
|
+
'🔗 UUID 验证',
|
28
|
+
'📍 地理坐标验证',
|
29
|
+
],
|
30
|
+
endpoints: {
|
31
|
+
'POST /api/custom/validate': '测试自定义验证',
|
32
|
+
'POST /api/custom/search': '测试搜索参数验证',
|
33
|
+
'GET /api/custom/examples': '获取验证示例',
|
34
|
+
},
|
35
|
+
};
|
36
|
+
}
|
37
|
+
|
38
|
+
@Post('/validate')
|
39
|
+
async validateCustomData(
|
40
|
+
@Body(CustomValidationDto) data: CustomValidationDto,
|
41
|
+
) {
|
42
|
+
const result = this.customValidationService.processCustomData(data);
|
43
|
+
return {
|
44
|
+
success: true,
|
45
|
+
message: '自定义验证通过!',
|
46
|
+
result,
|
47
|
+
validationInfo: {
|
48
|
+
username: `用户名 "${data.username}" 通过正则验证`,
|
49
|
+
role: `角色 "${data.role}" 通过联合类型验证`,
|
50
|
+
userId: `UUID "${data.userId}" 格式验证通过`,
|
51
|
+
phoneNumber: data.phoneNumber
|
52
|
+
? `手机号 "${data.phoneNumber}" 通过中国手机号验证`
|
53
|
+
: '未提供手机号',
|
54
|
+
location: data.location
|
55
|
+
? `坐标 (${data.location.lat}, ${data.location.lng}) 验证通过`
|
56
|
+
: '未提供坐标',
|
57
|
+
emails: data.emails
|
58
|
+
? `邮箱列表包含 ${data.emails.length} 个地址`
|
59
|
+
: '未提供邮箱',
|
60
|
+
},
|
61
|
+
};
|
62
|
+
}
|
63
|
+
|
64
|
+
@Post('/search')
|
65
|
+
async searchWithValidation(@Body(SearchQueryDto) query: SearchQueryDto) {
|
66
|
+
const result = this.customValidationService.processSearch(query);
|
67
|
+
return {
|
68
|
+
success: true,
|
69
|
+
message: '搜索参数验证通过!',
|
70
|
+
result,
|
71
|
+
validationInfo: {
|
72
|
+
query: query.q || '无搜索关键词',
|
73
|
+
pagination: query.pagination || '使用默认分页',
|
74
|
+
},
|
75
|
+
};
|
76
|
+
}
|
77
|
+
|
78
|
+
@Get('/examples')
|
79
|
+
async getValidationExamples() {
|
80
|
+
return {
|
81
|
+
success: true,
|
82
|
+
message: '自定义验证示例',
|
83
|
+
examples: {
|
84
|
+
customValidation: {
|
85
|
+
description: 'POST /api/custom/validate',
|
86
|
+
validExample: {
|
87
|
+
username: 'john_doe123',
|
88
|
+
role: 'user',
|
89
|
+
userId: '123e4567-e89b-12d3-a456-426614174000',
|
90
|
+
phoneNumber: '13812345678',
|
91
|
+
location: { lat: 39.9042, lng: 116.4074 },
|
92
|
+
emails: ['john@example.com', 'john.doe@company.com'],
|
93
|
+
},
|
94
|
+
invalidExample: {
|
95
|
+
username: 'a', // 太短
|
96
|
+
role: 'invalid_role', // 不在联合类型中
|
97
|
+
userId: 'not-a-uuid', // 无效 UUID
|
98
|
+
phoneNumber: '123456', // 无效手机号
|
99
|
+
location: { lat: 200, lng: 200 }, // 超出范围
|
100
|
+
emails: ['invalid-email', 'another@invalid'], // 无效邮箱
|
101
|
+
},
|
102
|
+
},
|
103
|
+
searchValidation: {
|
104
|
+
description: 'POST /api/custom/search',
|
105
|
+
validExample: {
|
106
|
+
q: 'TypeScript',
|
107
|
+
pagination: {
|
108
|
+
page: 1,
|
109
|
+
limit: 10,
|
110
|
+
sort: 'createdAt',
|
111
|
+
order: 'desc',
|
112
|
+
},
|
113
|
+
},
|
114
|
+
invalidExample: {
|
115
|
+
q: 123, // 应该是字符串
|
116
|
+
pagination: {
|
117
|
+
page: 0, // 应该 >= 1
|
118
|
+
limit: 1000, // 应该 <= 100
|
119
|
+
order: 'invalid', // 应该是 'asc' 或 'desc'
|
120
|
+
},
|
121
|
+
},
|
122
|
+
},
|
123
|
+
},
|
124
|
+
typeboxFeatures: {
|
125
|
+
basicTypes: [
|
126
|
+
'Type.String()',
|
127
|
+
'Type.Number()',
|
128
|
+
'Type.Boolean()',
|
129
|
+
'Type.Array()',
|
130
|
+
],
|
131
|
+
advancedTypes: [
|
132
|
+
'Type.Union()',
|
133
|
+
'Type.Object()',
|
134
|
+
'Type.Intersect()',
|
135
|
+
'Type.Optional()',
|
136
|
+
],
|
137
|
+
formats: ['email', 'date', 'date-time', 'uri', 'uuid'],
|
138
|
+
patterns: ['正则表达式验证', '长度限制', '数值范围', '枚举值'],
|
139
|
+
customValidators: [
|
140
|
+
'SchemaFactory 便捷方法',
|
141
|
+
'CommonValidators 常用验证',
|
142
|
+
],
|
143
|
+
},
|
144
|
+
};
|
145
|
+
}
|
146
|
+
}
|
@@ -0,0 +1,10 @@
|
|
1
|
+
import { Module } from '@hestjs/core';
|
2
|
+
import { CustomValidationController } from './custom-validation.controller';
|
3
|
+
import { CustomValidationService } from './custom-validation.service';
|
4
|
+
|
5
|
+
@Module({
|
6
|
+
controllers: [CustomValidationController],
|
7
|
+
providers: [CustomValidationService],
|
8
|
+
exports: [CustomValidationService],
|
9
|
+
})
|
10
|
+
export class CustomValidationModule {}
|
@@ -0,0 +1,33 @@
|
|
1
|
+
import { Injectable } from '@hestjs/core';
|
2
|
+
import type { CustomValidationDto, SearchQueryDto } from './dto/custom-validation.dto';
|
3
|
+
|
4
|
+
/**
|
5
|
+
* 自定义验证服务
|
6
|
+
*/
|
7
|
+
@Injectable()
|
8
|
+
export class CustomValidationService {
|
9
|
+
processCustomData(data: CustomValidationDto) {
|
10
|
+
// 模拟处理逻辑
|
11
|
+
return {
|
12
|
+
processed: true,
|
13
|
+
data: {
|
14
|
+
...data,
|
15
|
+
processedAt: new Date().toISOString(),
|
16
|
+
validation: 'TypeBox 自定义验证通过',
|
17
|
+
},
|
18
|
+
};
|
19
|
+
}
|
20
|
+
|
21
|
+
processSearch(query: SearchQueryDto) {
|
22
|
+
// 模拟搜索逻辑
|
23
|
+
return {
|
24
|
+
results: [
|
25
|
+
{ id: 1, title: '搜索结果 1', relevance: 0.95 },
|
26
|
+
{ id: 2, title: '搜索结果 2', relevance: 0.87 },
|
27
|
+
],
|
28
|
+
query,
|
29
|
+
total: 2,
|
30
|
+
searchedAt: new Date().toISOString(),
|
31
|
+
};
|
32
|
+
}
|
33
|
+
}
|
@@ -0,0 +1,132 @@
|
|
1
|
+
import {
|
2
|
+
CommonValidators,
|
3
|
+
Custom,
|
4
|
+
IsOptional,
|
5
|
+
IsString,
|
6
|
+
SchemaFactory,
|
7
|
+
} from '@hestjs/validation';
|
8
|
+
import { Type } from '@sinclair/typebox';
|
9
|
+
|
10
|
+
/**
|
11
|
+
* 展示自定义验证功能的简化 DTO
|
12
|
+
*/
|
13
|
+
export class CustomValidationDto {
|
14
|
+
// 使用基础的 TypeBox API
|
15
|
+
@Custom(
|
16
|
+
Type.String({ minLength: 3, maxLength: 20, pattern: '^[a-zA-Z0-9_]+$' }),
|
17
|
+
{
|
18
|
+
message: '用户名必须是3-20位字母、数字或下划线',
|
19
|
+
},
|
20
|
+
)
|
21
|
+
username!: string;
|
22
|
+
|
23
|
+
// 使用联合类型
|
24
|
+
@Custom(
|
25
|
+
Type.Union([
|
26
|
+
Type.Literal('admin'),
|
27
|
+
Type.Literal('user'),
|
28
|
+
Type.Literal('guest'),
|
29
|
+
]),
|
30
|
+
{
|
31
|
+
message: '角色必须是 admin、user 或 guest',
|
32
|
+
},
|
33
|
+
)
|
34
|
+
role!: 'admin' | 'user' | 'guest';
|
35
|
+
|
36
|
+
// 使用 CommonValidators 的便捷方法
|
37
|
+
@CommonValidators.UUID({ message: '必须是有效的 UUID' })
|
38
|
+
userId!: string;
|
39
|
+
|
40
|
+
// 使用 SchemaFactory 的便捷方法
|
41
|
+
@Custom(SchemaFactory.chinesePhoneNumber(), {
|
42
|
+
message: '必须是有效的中国手机号',
|
43
|
+
optional: true,
|
44
|
+
})
|
45
|
+
phoneNumber?: string;
|
46
|
+
|
47
|
+
// 自定义对象验证
|
48
|
+
@Custom(
|
49
|
+
Type.Object({
|
50
|
+
lat: Type.Number({ minimum: -90, maximum: 90 }),
|
51
|
+
lng: Type.Number({ minimum: -180, maximum: 180 }),
|
52
|
+
}),
|
53
|
+
{ optional: true, message: '坐标必须在有效范围内' },
|
54
|
+
)
|
55
|
+
location?: {
|
56
|
+
lat: number;
|
57
|
+
lng: number;
|
58
|
+
};
|
59
|
+
|
60
|
+
// 数组验证
|
61
|
+
@Custom(Type.Array(Type.String({ format: 'email' })), {
|
62
|
+
optional: true,
|
63
|
+
message: '邮箱列表必须是有效的邮箱地址数组',
|
64
|
+
})
|
65
|
+
emails?: string[];
|
66
|
+
}
|
67
|
+
|
68
|
+
/**
|
69
|
+
* 搜索查询 DTO - 展示复杂对象验证
|
70
|
+
*/
|
71
|
+
export class SearchQueryDto {
|
72
|
+
@IsOptional()
|
73
|
+
@IsString({ message: '搜索关键词必须是字符串' })
|
74
|
+
q?: string;
|
75
|
+
|
76
|
+
// 使用 SchemaFactory 构建复杂的分页对象
|
77
|
+
@Custom(
|
78
|
+
Type.Object({
|
79
|
+
page: Type.Number({ minimum: 1 }),
|
80
|
+
limit: Type.Number({ minimum: 1, maximum: 100 }),
|
81
|
+
sort: Type.Optional(Type.String()),
|
82
|
+
order: Type.Optional(
|
83
|
+
Type.Union([Type.Literal('asc'), Type.Literal('desc')]),
|
84
|
+
),
|
85
|
+
}),
|
86
|
+
{
|
87
|
+
optional: true,
|
88
|
+
message: '分页参数格式错误',
|
89
|
+
},
|
90
|
+
)
|
91
|
+
pagination?: {
|
92
|
+
page: number;
|
93
|
+
limit: number;
|
94
|
+
sort?: string;
|
95
|
+
order?: 'asc' | 'desc';
|
96
|
+
};
|
97
|
+
|
98
|
+
// 使用联合类型进行复杂验证
|
99
|
+
@Custom(
|
100
|
+
Type.Union([
|
101
|
+
Type.Array(Type.String()),
|
102
|
+
Type.String(),
|
103
|
+
Type.Null(),
|
104
|
+
Type.Undefined(),
|
105
|
+
]),
|
106
|
+
{
|
107
|
+
optional: true,
|
108
|
+
message: '标签可以是字符串、字符串数组或空值',
|
109
|
+
},
|
110
|
+
)
|
111
|
+
tags?: string | string[] | null;
|
112
|
+
|
113
|
+
// 使用数值范围验证
|
114
|
+
@Custom(Type.Number({ minimum: 0, maximum: 100 }), {
|
115
|
+
optional: true,
|
116
|
+
message: '评分必须在 0-100 之间',
|
117
|
+
})
|
118
|
+
score?: number;
|
119
|
+
|
120
|
+
// 使用日期字符串验证
|
121
|
+
@Custom(Type.String({ format: 'date' }), {
|
122
|
+
optional: true,
|
123
|
+
message: '开始日期格式必须是 YYYY-MM-DD',
|
124
|
+
})
|
125
|
+
startDate?: string;
|
126
|
+
|
127
|
+
@Custom(Type.String({ format: 'date' }), {
|
128
|
+
optional: true,
|
129
|
+
message: '结束日期格式必须是 YYYY-MM-DD',
|
130
|
+
})
|
131
|
+
endDate?: string;
|
132
|
+
}
|
@@ -0,0 +1,64 @@
|
|
1
|
+
import {
|
2
|
+
IsEmail,
|
3
|
+
IsNumber,
|
4
|
+
IsOptional,
|
5
|
+
IsString,
|
6
|
+
Length,
|
7
|
+
Max,
|
8
|
+
Min,
|
9
|
+
} from '@hestjs/validation';
|
10
|
+
|
11
|
+
/**
|
12
|
+
* 创建用户 DTO
|
13
|
+
*/
|
14
|
+
export class CreateUserDto {
|
15
|
+
@IsString({
|
16
|
+
minLength: 2,
|
17
|
+
maxLength: 50,
|
18
|
+
message: '用户名长度必须在2-50字符之间',
|
19
|
+
})
|
20
|
+
name!: string;
|
21
|
+
|
22
|
+
@IsEmail({ message: '请输入有效的邮箱地址' })
|
23
|
+
email!: string;
|
24
|
+
|
25
|
+
@IsNumber({ message: '年龄必须是数字' })
|
26
|
+
@Min(0, { message: '年龄不能小于0' })
|
27
|
+
@Max(120, { message: '年龄不能大于120' })
|
28
|
+
age!: number;
|
29
|
+
|
30
|
+
@IsString({ message: '密码必须是字符串' })
|
31
|
+
@Length(8, 100, { message: '密码长度必须在8-100字符之间' })
|
32
|
+
password!: string;
|
33
|
+
|
34
|
+
@IsOptional()
|
35
|
+
@IsString({ message: '个人简介必须是字符串' })
|
36
|
+
bio?: string;
|
37
|
+
}
|
38
|
+
|
39
|
+
/**
|
40
|
+
* 更新用户 DTO
|
41
|
+
*/
|
42
|
+
export class UpdateUserDto {
|
43
|
+
@IsOptional()
|
44
|
+
@IsString({
|
45
|
+
minLength: 2,
|
46
|
+
maxLength: 50,
|
47
|
+
message: '用户名长度必须在2-50字符之间',
|
48
|
+
})
|
49
|
+
name?: string;
|
50
|
+
|
51
|
+
@IsOptional()
|
52
|
+
@IsEmail({ message: '请输入有效的邮箱地址' })
|
53
|
+
email?: string;
|
54
|
+
|
55
|
+
@IsOptional()
|
56
|
+
@IsNumber({ message: '年龄必须是数字' })
|
57
|
+
@Min(0, { message: '年龄不能小于0' })
|
58
|
+
@Max(120, { message: '年龄不能大于120' })
|
59
|
+
age?: number;
|
60
|
+
|
61
|
+
@IsOptional()
|
62
|
+
@IsString({ message: '个人简介必须是字符串' })
|
63
|
+
bio?: string;
|
64
|
+
}
|
@@ -0,0 +1,68 @@
|
|
1
|
+
import type { HestContext } from '@hestjs/core';
|
2
|
+
import { Context, Controller, Get, Post } from '@hestjs/core';
|
3
|
+
import { Body } from '@hestjs/validation';
|
4
|
+
import { CreateUserDto, UpdateUserDto } from './dto/user.dto';
|
5
|
+
import { UsersService } from './users.service';
|
6
|
+
|
7
|
+
/**
|
8
|
+
* 用户控制器 - 展示验证功能
|
9
|
+
*/
|
10
|
+
@Controller('/users')
|
11
|
+
export class UsersController {
|
12
|
+
constructor(private readonly usersService: UsersService) {}
|
13
|
+
|
14
|
+
@Get('/')
|
15
|
+
async getAllUsers() {
|
16
|
+
return {
|
17
|
+
success: true,
|
18
|
+
data: this.usersService.findAll(),
|
19
|
+
message: 'Users retrieved successfully',
|
20
|
+
};
|
21
|
+
}
|
22
|
+
|
23
|
+
@Get('/:id')
|
24
|
+
async getUser(@Context() c: HestContext) {
|
25
|
+
const id = parseInt(c.req.param('id'));
|
26
|
+
const user = this.usersService.findOne(id);
|
27
|
+
|
28
|
+
if (!user) {
|
29
|
+
return c.json(
|
30
|
+
{
|
31
|
+
success: false,
|
32
|
+
message: 'User not found',
|
33
|
+
},
|
34
|
+
404,
|
35
|
+
);
|
36
|
+
}
|
37
|
+
|
38
|
+
return {
|
39
|
+
success: true,
|
40
|
+
data: user,
|
41
|
+
message: 'User retrieved successfully',
|
42
|
+
};
|
43
|
+
}
|
44
|
+
|
45
|
+
@Post('/')
|
46
|
+
async createUser(@Body(CreateUserDto) createUserDto: CreateUserDto) {
|
47
|
+
const newUser = this.usersService.create(createUserDto);
|
48
|
+
return {
|
49
|
+
success: true,
|
50
|
+
data: newUser,
|
51
|
+
message: 'User created successfully',
|
52
|
+
};
|
53
|
+
}
|
54
|
+
|
55
|
+
@Post('/:id')
|
56
|
+
async updateUser(
|
57
|
+
@Context() c: HestContext,
|
58
|
+
@Body(UpdateUserDto) updateUserDto: UpdateUserDto,
|
59
|
+
) {
|
60
|
+
const id = parseInt(c.req.param('id'));
|
61
|
+
const updatedUser = this.usersService.update(id, updateUserDto);
|
62
|
+
return {
|
63
|
+
success: true,
|
64
|
+
data: updatedUser,
|
65
|
+
message: 'User updated successfully',
|
66
|
+
};
|
67
|
+
}
|
68
|
+
}
|