create-hest-app 0.1.0 → 0.1.2

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.
@@ -17,8 +17,6 @@ export default [
17
17
  '*.d.ts',
18
18
  '.bun/**',
19
19
  'bun.lockb',
20
- // shadcn 目录
21
- 'src/components/ui/**',
22
20
  // scripts 目录
23
21
  'scripts/**',
24
22
  ],
@@ -30,9 +30,10 @@
30
30
  "author": "aqz236",
31
31
  "license": "MIT",
32
32
  "dependencies": {
33
- "@hestjs/core": "^0.1.8",
33
+ "@hestjs/core": "^0.1.9",
34
34
  "@hestjs/validation": "^0.1.5",
35
- "@hestjs/cqrs": "^0.1.2",
35
+ "@hestjs/cqrs": "^0.1.3",
36
+ "@hestjs/scalar": "^0.1.4",
36
37
  "hono": "^4.8.9",
37
38
  "reflect-metadata": "^0.2.2"
38
39
  },
@@ -1,15 +1,58 @@
1
1
  import { Controller, Get } from '@hestjs/core';
2
+ import { ApiOperation, ApiResponse, ApiTags } from '@hestjs/scalar';
2
3
  import { AppService } from './app.service';
3
4
 
4
5
  @Controller('/')
6
+ @ApiTags('Application')
5
7
  export class AppController {
6
8
  constructor(private readonly appService: AppService) {}
7
9
 
8
10
  @Get('/')
11
+ @ApiOperation({
12
+ summary: 'Get application info',
13
+ description: 'Returns application information and available endpoints',
14
+ tags: ['Health Check', 'Application'],
15
+ })
16
+ @ApiResponse('200', {
17
+ description: 'Application information',
18
+ content: {
19
+ 'application/json': {
20
+ schema: {
21
+ type: 'object',
22
+ properties: {
23
+ message: {
24
+ type: 'string',
25
+ example: 'Hello World from HestJS CQRS!',
26
+ },
27
+ description: {
28
+ type: 'string',
29
+ example:
30
+ 'HestJS CQRS Demo - A demonstration of CQRS pattern using HestJS framework',
31
+ },
32
+ endpoints: {
33
+ type: 'object',
34
+ properties: {
35
+ users: {
36
+ type: 'object',
37
+ properties: {
38
+ getAll: { type: 'string', example: 'GET /users' },
39
+ getById: { type: 'string', example: 'GET /users/:id' },
40
+ create: { type: 'string', example: 'POST /users' },
41
+ update: { type: 'string', example: 'PUT /users/:id' },
42
+ },
43
+ },
44
+ },
45
+ },
46
+ },
47
+ },
48
+ },
49
+ },
50
+ })
9
51
  getHello() {
10
52
  return {
11
53
  message: this.appService.getHello(),
12
- description: 'HestJS CQRS Demo - A demonstration of CQRS pattern using HestJS framework',
54
+ description:
55
+ 'HestJS CQRS Demo - A demonstration of CQRS pattern using HestJS framework',
13
56
  endpoints: {
14
57
  users: {
15
58
  getAll: 'GET /users',
@@ -22,6 +65,26 @@ export class AppController {
22
65
  }
23
66
 
24
67
  @Get('/error')
68
+ @ApiOperation({
69
+ summary: 'Test error endpoint',
70
+ description: 'Throws an error for testing exception handling and filters',
71
+ })
72
+ @ApiResponse('500', {
73
+ description: 'Internal server error',
74
+ content: {
75
+ 'application/json': {
76
+ schema: {
77
+ type: 'object',
78
+ properties: {
79
+ message: {
80
+ type: 'string',
81
+ example: 'This is a test error for exception handling',
82
+ },
83
+ },
84
+ },
85
+ },
86
+ },
87
+ })
25
88
  throwError() {
26
89
  throw new Error('This is a test error for exception handling');
27
90
  }
@@ -2,11 +2,11 @@ import { Module } from '@hestjs/core';
2
2
  import { CqrsModule } from '@hestjs/cqrs';
3
3
  import { AppController } from './app.controller';
4
4
  import { AppService } from './app.service';
5
- import { UserModule } from './users';
5
+ import { UserController, UserModule } from './users';
6
6
 
7
7
  @Module({
8
8
  imports: [CqrsModule.forRoot(), UserModule],
9
- controllers: [AppController],
9
+ controllers: [AppController, UserController],
10
10
  providers: [AppService],
11
11
  })
12
12
  export class AppModule {}
@@ -19,13 +19,12 @@ export class ResponseInterceptor implements Interceptor {
19
19
  const startTime = Date.now();
20
20
  const request = context.switchToHttp().getRequest();
21
21
 
22
- logger.info(`🚀 Request: ${request.method} ${request.url}`);
23
22
 
24
23
  const result = await next.handle();
25
-
24
+
26
25
  const duration = Date.now() - startTime;
27
26
  logger.info(
28
- `✅ Response: ${request.method} ${request.url} - ${duration}ms`,
27
+ `🚀 Response: ${request.method} ${request.url} - ${duration}ms`,
29
28
  );
30
29
 
31
30
  return {
@@ -1,7 +1,7 @@
1
1
  import { HestFactory, logger } from '@hestjs/core';
2
+ import '@hestjs/scalar'; // 导入scalar扩展
2
3
  import { ValidationInterceptor } from '@hestjs/validation';
3
4
  import { cors } from 'hono/cors';
4
- import { logger as log } from 'hono/logger';
5
5
  import { AppModule } from './app.module';
6
6
  import { HttpExceptionFilter } from './common/filters/http-exception.filter';
7
7
  import { ResponseInterceptor } from './common/interceptors/response.interceptor';
@@ -12,7 +12,7 @@ async function bootstrap() {
12
12
 
13
13
  const app = await HestFactory.create(AppModule);
14
14
  app.hono().use(cors()); // 使用 Hono 的 CORS 中间件
15
- app.hono().use('*', log()); // 使用 Hono 的日志中间件
15
+ // app.hono().use('*', log()); // 使用 Hono 的日志中间件
16
16
 
17
17
  // 全局拦截器 - 验证拦截器应该在响应拦截器之前
18
18
  app.useGlobalInterceptors(new ValidationInterceptor());
@@ -21,6 +21,60 @@ async function bootstrap() {
21
21
  // 全局异常过滤器
22
22
  app.useGlobalFilters(new HttpExceptionFilter());
23
23
 
24
+ // 设置OpenAPI规范端点
25
+ app.useSwagger(
26
+ {
27
+ info: {
28
+ title: 'HestJS CQRS Demo API',
29
+ version: '1.0.0',
30
+ description:
31
+ 'A demonstration of HestJS CQRS framework capabilities with Scalar API documentation (Auto-discovered controllers)',
32
+ },
33
+ servers: [
34
+ {
35
+ url: 'http://localhost:3002',
36
+ description: 'Development server',
37
+ },
38
+ ],
39
+ },
40
+ {
41
+ path: '/docs',
42
+ theme: 'elysia', // 使用elysia主题
43
+ enableMarkdown: true,
44
+ markdownPath: '/api-docs.md',
45
+ },
46
+ );
47
+
48
+ // 可选:仍然支持手动指定控制器的方式
49
+ // app.useScalarWithControllers(
50
+ // [AppController, UserController], // 传入需要生成文档的控制器
51
+ // {
52
+ // info: {
53
+ // title: 'HestJS CQRS Demo API',
54
+ // version: '1.0.0',
55
+ // description:
56
+ // 'A demonstration of HestJS CQRS framework capabilities with Scalar API documentation',
57
+ // },
58
+ // servers: [
59
+ // {
60
+ // url: 'http://localhost:3002',
61
+ // description: 'Development server',
62
+ // },
63
+ // ],
64
+ // },
65
+ // {
66
+ // path: '/docs',
67
+ // theme: 'elysia', // 使用elysia主题
68
+ // enableMarkdown: true,
69
+ // markdownPath: '/api-docs.md',
70
+ // },
71
+ // );
72
+
73
+ logger.info('📚 API Documentation available at:');
74
+ logger.info(' • Scalar UI: http://localhost:3002/docs');
75
+ logger.info(' • OpenAPI JSON: http://localhost:3002/openapi.json');
76
+ logger.info(' • Markdown (for LLMs): http://localhost:3002/api-docs.md');
77
+
24
78
  const server = Bun.serve({
25
79
  port: 3002,
26
80
  fetch: app.hono().fetch,
@@ -4,7 +4,46 @@ import { User, CreateUserData, UpdateUserData } from '../entities';
4
4
  @Injectable()
5
5
  export class UserRepository {
6
6
  private users: Map<string, User> = new Map();
7
- private nextId = 1;
7
+ private nextId = 4; // 从4开始,因为我们预填充了3个用户
8
+
9
+ constructor() {
10
+ // 初始化一些 mock 数据
11
+ this.initializeMockData();
12
+ }
13
+
14
+ private initializeMockData() {
15
+ const mockUsers: User[] = [
16
+ {
17
+ id: '1',
18
+ name: 'Alice Johnson',
19
+ email: 'alice@example.com',
20
+ age: 28,
21
+ createdAt: new Date('2024-01-15T08:30:00Z'),
22
+ updatedAt: new Date('2024-01-15T08:30:00Z'),
23
+ },
24
+ {
25
+ id: '2',
26
+ name: 'Bob Smith',
27
+ email: 'bob@example.com',
28
+ age: 32,
29
+ createdAt: new Date('2024-01-16T10:15:00Z'),
30
+ updatedAt: new Date('2024-01-16T10:15:00Z'),
31
+ },
32
+ {
33
+ id: '3',
34
+ name: 'Charlie Brown',
35
+ email: 'charlie@example.com',
36
+ age: 25,
37
+ createdAt: new Date('2024-01-17T14:20:00Z'),
38
+ updatedAt: new Date('2024-01-17T14:20:00Z'),
39
+ },
40
+ ];
41
+
42
+ // 将 mock 数据添加到内存存储中
43
+ mockUsers.forEach(user => {
44
+ this.users.set(user.id, user);
45
+ });
46
+ }
8
47
 
9
48
  async create(userData: CreateUserData): Promise<User> {
10
49
  const id = this.nextId.toString();
@@ -1,11 +1,19 @@
1
- import { Controller, Get, Post, Put, Context } from '@hestjs/core';
2
1
  import type { HestContext } from '@hestjs/core';
2
+ import { Context, Controller, Get, Post, Put } from '@hestjs/core';
3
3
  import { CommandBus, QueryBus } from '@hestjs/cqrs';
4
+ import {
5
+ ApiBody,
6
+ ApiOperation,
7
+ ApiParam,
8
+ ApiResponse,
9
+ ApiTags,
10
+ } from '@hestjs/scalar';
4
11
  import { CreateUserCommand, UpdateUserCommand } from './commands';
5
12
  import { CreateUserData, UpdateUserData } from './entities';
6
13
  import { GetAllUsersQuery, GetUserQuery } from './queries';
7
14
 
8
15
  @Controller('/users')
16
+ @ApiTags('Users')
9
17
  export class UserController {
10
18
  constructor(
11
19
  private readonly commandBus: CommandBus,
@@ -13,6 +21,85 @@ export class UserController {
13
21
  ) {}
14
22
 
15
23
  @Post('/')
24
+ @ApiOperation({
25
+ summary: 'Create a new user',
26
+ description: 'Creates a new user using CQRS pattern with command bus',
27
+ })
28
+ @ApiBody(
29
+ {
30
+ 'application/json': {
31
+ schema: {
32
+ type: 'object',
33
+ required: ['name', 'email'],
34
+ properties: {
35
+ name: {
36
+ type: 'string',
37
+ example: 'John Doe',
38
+ description: 'User full name',
39
+ },
40
+ email: {
41
+ type: 'string',
42
+ format: 'email',
43
+ example: 'john@example.com',
44
+ description: 'User email address',
45
+ },
46
+ age: {
47
+ type: 'number',
48
+ example: 25,
49
+ description: 'User age (optional)',
50
+ minimum: 0,
51
+ maximum: 150,
52
+ },
53
+ },
54
+ },
55
+ },
56
+ },
57
+ {
58
+ description: 'User creation data',
59
+ required: true,
60
+ },
61
+ )
62
+ @ApiResponse('201', {
63
+ description: 'User created successfully',
64
+ content: {
65
+ 'application/json': {
66
+ schema: {
67
+ type: 'object',
68
+ properties: {
69
+ success: { type: 'boolean', example: true },
70
+ data: {
71
+ type: 'object',
72
+ properties: {
73
+ id: { type: 'string', example: '1' },
74
+ name: { type: 'string', example: 'John Doe' },
75
+ email: { type: 'string', example: 'john@example.com' },
76
+ age: { type: 'number', example: 25 },
77
+ createdAt: { type: 'string', format: 'date-time' },
78
+ updatedAt: { type: 'string', format: 'date-time' },
79
+ },
80
+ },
81
+ },
82
+ },
83
+ },
84
+ },
85
+ })
86
+ @ApiResponse('400', {
87
+ description: 'Invalid input data',
88
+ content: {
89
+ 'application/json': {
90
+ schema: {
91
+ type: 'object',
92
+ properties: {
93
+ success: { type: 'boolean', example: false },
94
+ error: {
95
+ type: 'string',
96
+ example: 'Name and email are required',
97
+ },
98
+ },
99
+ },
100
+ },
101
+ },
102
+ })
16
103
  async createUser(@Context() c: HestContext) {
17
104
  const userData = await c.req.json<CreateUserData>();
18
105
 
@@ -25,16 +112,69 @@ export class UserController {
25
112
  }
26
113
 
27
114
  const user = await this.commandBus.execute(new CreateUserCommand(userData));
28
- return c.json({ success: true, data: user }, 201);
115
+ // 直接返回数据,让拦截器处理响应格式
116
+ // 注意:这里我们无法通过拦截器设置状态码为201,如果需要可以抛出特殊异常或使用其他方式
117
+ return user;
29
118
  }
30
119
 
31
120
  @Get('/')
32
- async getAllUsers(@Context() c: HestContext) {
121
+ async getAllUsers() {
122
+
123
+ // 使用查询总线获取所有用户
33
124
  const result = await this.queryBus.execute(new GetAllUsersQuery());
34
- return c.json({ success: true, data: result.users });
125
+
126
+ // 直接返回数据,让拦截器处理响应格式
127
+ return result.users;
35
128
  }
36
129
 
37
130
  @Get('/:id')
131
+ @ApiOperation({
132
+ summary: 'Get user by ID',
133
+ description: 'Retrieves a specific user by their ID using CQRS pattern',
134
+ })
135
+ @ApiParam('id', {
136
+ description: 'User unique identifier',
137
+ schema: { type: 'string' },
138
+ example: '1',
139
+ })
140
+ @ApiResponse('200', {
141
+ description: 'User found successfully',
142
+ content: {
143
+ 'application/json': {
144
+ schema: {
145
+ type: 'object',
146
+ properties: {
147
+ success: { type: 'boolean', example: true },
148
+ data: {
149
+ type: 'object',
150
+ properties: {
151
+ id: { type: 'string', example: '1' },
152
+ name: { type: 'string', example: 'John Doe' },
153
+ email: { type: 'string', example: 'john@example.com' },
154
+ age: { type: 'number', example: 25 },
155
+ createdAt: { type: 'string', format: 'date-time' },
156
+ updatedAt: { type: 'string', format: 'date-time' },
157
+ },
158
+ },
159
+ },
160
+ },
161
+ },
162
+ },
163
+ })
164
+ @ApiResponse('404', {
165
+ description: 'User not found',
166
+ content: {
167
+ 'application/json': {
168
+ schema: {
169
+ type: 'object',
170
+ properties: {
171
+ success: { type: 'boolean', example: false },
172
+ error: { type: 'string', example: 'User not found' },
173
+ },
174
+ },
175
+ },
176
+ },
177
+ })
38
178
  async getUserById(@Context() c: HestContext) {
39
179
  const id = c.req.param('id');
40
180
  const result = await this.queryBus.execute(new GetUserQuery(id));
@@ -43,10 +183,108 @@ export class UserController {
43
183
  return c.json({ success: false, error: 'User not found' }, 404);
44
184
  }
45
185
 
46
- return c.json({ success: true, data: result.user });
186
+ // 直接返回数据,让拦截器处理响应格式
187
+ return result.user;
47
188
  }
48
189
 
49
190
  @Put('/:id')
191
+ @ApiOperation({
192
+ summary: 'Update user by ID',
193
+ description: 'Updates an existing user using CQRS pattern with command bus',
194
+ })
195
+ @ApiParam('id', {
196
+ description: 'User unique identifier',
197
+ schema: { type: 'string' },
198
+ example: '1',
199
+ })
200
+ @ApiBody(
201
+ {
202
+ 'application/json': {
203
+ schema: {
204
+ type: 'object',
205
+ properties: {
206
+ name: {
207
+ type: 'string',
208
+ example: 'Jane Doe',
209
+ description: 'User full name (optional)',
210
+ },
211
+ email: {
212
+ type: 'string',
213
+ format: 'email',
214
+ example: 'jane@example.com',
215
+ description: 'User email address (optional)',
216
+ },
217
+ age: {
218
+ type: 'number',
219
+ example: 30,
220
+ description: 'User age (optional)',
221
+ minimum: 0,
222
+ maximum: 150,
223
+ },
224
+ },
225
+ },
226
+ },
227
+ },
228
+ {
229
+ description: 'User update data (all fields are optional)',
230
+ required: true,
231
+ },
232
+ )
233
+ @ApiResponse('200', {
234
+ description: 'User updated successfully',
235
+ content: {
236
+ 'application/json': {
237
+ schema: {
238
+ type: 'object',
239
+ properties: {
240
+ success: { type: 'boolean', example: true },
241
+ data: {
242
+ type: 'object',
243
+ properties: {
244
+ id: { type: 'string', example: '1' },
245
+ name: { type: 'string', example: 'Jane Doe' },
246
+ email: { type: 'string', example: 'jane@example.com' },
247
+ age: { type: 'number', example: 30 },
248
+ createdAt: { type: 'string', format: 'date-time' },
249
+ updatedAt: { type: 'string', format: 'date-time' },
250
+ },
251
+ },
252
+ },
253
+ },
254
+ },
255
+ },
256
+ })
257
+ @ApiResponse('404', {
258
+ description: 'User not found',
259
+ content: {
260
+ 'application/json': {
261
+ schema: {
262
+ type: 'object',
263
+ properties: {
264
+ success: { type: 'boolean', example: false },
265
+ error: { type: 'string', example: 'User not found' },
266
+ },
267
+ },
268
+ },
269
+ },
270
+ })
271
+ @ApiResponse('500', {
272
+ description: 'Internal server error',
273
+ content: {
274
+ 'application/json': {
275
+ schema: {
276
+ type: 'object',
277
+ properties: {
278
+ success: { type: 'boolean', example: false },
279
+ error: {
280
+ type: 'string',
281
+ example: 'An error occurred while updating the user',
282
+ },
283
+ },
284
+ },
285
+ },
286
+ },
287
+ })
50
288
  async updateUser(@Context() c: HestContext) {
51
289
  const id = c.req.param('id');
52
290
  const userData = await c.req.json<UpdateUserData>();
@@ -55,7 +293,8 @@ export class UserController {
55
293
  const user = await this.commandBus.execute(
56
294
  new UpdateUserCommand(id, userData),
57
295
  );
58
- return c.json({ success: true, data: user });
296
+ // 直接返回数据,让拦截器处理响应格式
297
+ return user;
59
298
  } catch (error) {
60
299
  if (error instanceof Error && error.message.includes('not found')) {
61
300
  return c.json({ success: false, error: 'User not found' }, 404);