@onebun/docs 0.1.1 → 0.1.3
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 +154 -121
- package/package.json +3 -2
- package/src/decorators.ts +36 -2
- package/src/docs-examples.test.ts +341 -233
package/README.md
CHANGED
|
@@ -4,12 +4,11 @@ Documentation generation package for OneBun framework with OpenAPI/Swagger suppo
|
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
- ⚡ **Effect.js Integration** - Seamless integration with Effect.js ecosystem
|
|
7
|
+
- OpenAPI Generation - Automatic OpenAPI 3.1 specification from decorators
|
|
8
|
+
- Swagger UI - Built-in Swagger UI HTML generation for interactive API documentation
|
|
9
|
+
- JSON Schema Converter - Convert ArkType validation schemas to JSON Schema format
|
|
10
|
+
- Decorator-based - Use decorators to add metadata for documentation
|
|
11
|
+
- Type-safe - Full TypeScript support with type inference
|
|
13
12
|
|
|
14
13
|
## Installation
|
|
15
14
|
|
|
@@ -21,178 +20,212 @@ bun add @onebun/docs
|
|
|
21
20
|
|
|
22
21
|
### Basic Usage
|
|
23
22
|
|
|
23
|
+
The `@onebun/docs` package provides decorators for adding documentation metadata to your controllers. Note that `@ApiResponse` for response validation is provided by `@onebun/core`.
|
|
24
|
+
|
|
25
|
+
**Important: Decorator Order Matters!**
|
|
26
|
+
- `@ApiTags` must be placed **above** `@Controller` (because `@Controller` wraps the class)
|
|
27
|
+
- `@ApiOperation` must be placed **above** route decorators (`@Get`, `@Post`, etc.)
|
|
28
|
+
- `@ApiResponse` must be placed **below** route decorators
|
|
29
|
+
|
|
24
30
|
```typescript
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
31
|
+
import { Controller, Get, Post, Body, BaseController, ApiResponse } from '@onebun/core';
|
|
32
|
+
import { ApiTags, ApiOperation } from '@onebun/docs';
|
|
33
|
+
import { type } from 'arktype';
|
|
34
|
+
|
|
35
|
+
const createUserSchema = type({
|
|
36
|
+
name: 'string',
|
|
37
|
+
email: 'string.email',
|
|
38
|
+
});
|
|
27
39
|
|
|
40
|
+
// @ApiTags ABOVE @Controller
|
|
41
|
+
@ApiTags('Users')
|
|
28
42
|
@Controller('/users')
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
@Get()
|
|
43
|
+
export class UserController extends BaseController {
|
|
44
|
+
// @ApiOperation ABOVE @Get, @ApiResponse BELOW @Get
|
|
32
45
|
@ApiOperation({ summary: 'Get all users', description: 'Returns a list of all users' })
|
|
33
|
-
@
|
|
34
|
-
|
|
35
|
-
|
|
46
|
+
@Get('/')
|
|
47
|
+
@ApiResponse(200, { description: 'List of users returned successfully' })
|
|
48
|
+
async getUsers(): Promise<Response> {
|
|
49
|
+
return this.success({ users: [] });
|
|
36
50
|
}
|
|
37
51
|
|
|
38
|
-
@Post()
|
|
39
52
|
@ApiOperation({ summary: 'Create user', description: 'Creates a new user' })
|
|
40
|
-
@
|
|
41
|
-
@ApiResponse({
|
|
42
|
-
@ApiResponse(
|
|
43
|
-
async createUser(@Body() userData:
|
|
44
|
-
return { id: '1', ...userData };
|
|
53
|
+
@Post('/')
|
|
54
|
+
@ApiResponse(201, { schema: createUserSchema, description: 'User created successfully' })
|
|
55
|
+
@ApiResponse(400, { description: 'Invalid input data' })
|
|
56
|
+
async createUser(@Body(createUserSchema) userData: typeof createUserSchema.infer): Promise<Response> {
|
|
57
|
+
return this.success({ id: '1', ...userData });
|
|
45
58
|
}
|
|
46
59
|
}
|
|
47
60
|
```
|
|
48
61
|
|
|
49
|
-
|
|
62
|
+
## Decorators
|
|
63
|
+
|
|
64
|
+
### From @onebun/docs
|
|
65
|
+
|
|
66
|
+
#### @ApiTags(...tags)
|
|
67
|
+
|
|
68
|
+
Group endpoints under tags. Can be used on controller class or individual methods.
|
|
50
69
|
|
|
51
70
|
```typescript
|
|
52
|
-
import {
|
|
53
|
-
import { AppModule } from './app.module';
|
|
54
|
-
|
|
55
|
-
const app = new OneBunApplication(AppModule, {
|
|
56
|
-
docs: {
|
|
57
|
-
enabled: true,
|
|
58
|
-
path: '/docs',
|
|
59
|
-
title: 'My API',
|
|
60
|
-
version: '1.0.0',
|
|
61
|
-
description: 'API documentation for my service'
|
|
62
|
-
}
|
|
63
|
-
});
|
|
71
|
+
import { ApiTags } from '@onebun/docs';
|
|
64
72
|
|
|
65
|
-
|
|
66
|
-
|
|
73
|
+
// @ApiTags must be ABOVE @Controller
|
|
74
|
+
@ApiTags('Users', 'User Management')
|
|
75
|
+
@Controller('/users')
|
|
76
|
+
export class UserController extends BaseController {
|
|
77
|
+
// All endpoints in this controller will be tagged with 'Users' and 'User Management'
|
|
78
|
+
}
|
|
67
79
|
```
|
|
68
80
|
|
|
69
|
-
|
|
81
|
+
#### @ApiOperation(options)
|
|
70
82
|
|
|
71
|
-
|
|
83
|
+
Describe an API operation with summary, description, and additional tags.
|
|
72
84
|
|
|
73
|
-
|
|
85
|
+
```typescript
|
|
86
|
+
import { ApiOperation } from '@onebun/docs';
|
|
87
|
+
|
|
88
|
+
// @ApiOperation must be ABOVE route decorator
|
|
89
|
+
@ApiOperation({
|
|
90
|
+
summary: 'Get user by ID',
|
|
91
|
+
description: 'Returns a single user by their unique identifier',
|
|
92
|
+
tags: ['Users'], // Additional tags for this specific endpoint
|
|
93
|
+
})
|
|
94
|
+
@Get('/:id')
|
|
95
|
+
async getUser(@Param('id') id: string): Promise<Response> {
|
|
96
|
+
// ...
|
|
97
|
+
}
|
|
98
|
+
```
|
|
74
99
|
|
|
75
|
-
###
|
|
100
|
+
### From @onebun/core
|
|
76
101
|
|
|
77
|
-
|
|
78
|
-
- `@ApiResponse(options)` - Document response types
|
|
79
|
-
- `@ApiBody(options)` - Document request body
|
|
80
|
-
- `@ApiParam(options)` - Document path parameters
|
|
81
|
-
- `@ApiQuery(options)` - Document query parameters
|
|
82
|
-
- `@ApiHeader(options)` - Document header parameters
|
|
102
|
+
#### @ApiResponse(statusCode, options?)
|
|
83
103
|
|
|
84
|
-
|
|
104
|
+
Document and validate response types. This decorator is provided by `@onebun/core`.
|
|
85
105
|
|
|
86
106
|
```typescript
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
// Contact information
|
|
104
|
-
contact?: {
|
|
105
|
-
name?: string;
|
|
106
|
-
email?: string;
|
|
107
|
-
url?: string;
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
// License information
|
|
111
|
-
license?: {
|
|
112
|
-
name: string;
|
|
113
|
-
url?: string;
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
// External documentation
|
|
117
|
-
externalDocs?: {
|
|
118
|
-
description?: string;
|
|
119
|
-
url: string;
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
// Server URLs
|
|
123
|
-
servers?: Array<{
|
|
124
|
-
url: string;
|
|
125
|
-
description?: string;
|
|
126
|
-
}>;
|
|
107
|
+
import { ApiResponse } from '@onebun/core';
|
|
108
|
+
import { type } from 'arktype';
|
|
109
|
+
|
|
110
|
+
const userSchema = type({
|
|
111
|
+
id: 'string',
|
|
112
|
+
name: 'string',
|
|
113
|
+
email: 'string.email',
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// @ApiResponse must be BELOW route decorator
|
|
117
|
+
@Get('/:id')
|
|
118
|
+
@ApiResponse(200, { schema: userSchema, description: 'User found' })
|
|
119
|
+
@ApiResponse(404, { description: 'User not found' })
|
|
120
|
+
async getUser(@Param('id') id: string): Promise<Response> {
|
|
121
|
+
// Response will be validated against userSchema for 200 responses
|
|
127
122
|
}
|
|
128
123
|
```
|
|
129
124
|
|
|
130
125
|
## OpenAPI Generation
|
|
131
126
|
|
|
132
|
-
###
|
|
127
|
+
### Generate OpenAPI Spec from Controllers
|
|
133
128
|
|
|
134
129
|
```typescript
|
|
135
130
|
import { generateOpenApiSpec } from '@onebun/docs';
|
|
131
|
+
import { UserController } from './user.controller';
|
|
132
|
+
import { OrderController } from './order.controller';
|
|
136
133
|
|
|
137
|
-
const spec = generateOpenApiSpec(
|
|
134
|
+
const spec = generateOpenApiSpec([UserController, OrderController], {
|
|
138
135
|
title: 'My API',
|
|
139
|
-
version: '1.0.0'
|
|
136
|
+
version: '1.0.0',
|
|
137
|
+
description: 'API documentation for my service',
|
|
140
138
|
});
|
|
141
139
|
|
|
142
140
|
// Save to file
|
|
143
141
|
await Bun.write('openapi.json', JSON.stringify(spec, null, 2));
|
|
144
142
|
```
|
|
145
143
|
|
|
146
|
-
###
|
|
144
|
+
### Generate Swagger UI HTML
|
|
147
145
|
|
|
148
146
|
```typescript
|
|
149
|
-
import {
|
|
150
|
-
import { S } from '@onebun/core';
|
|
147
|
+
import { generateSwaggerUiHtml } from '@onebun/docs';
|
|
151
148
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
email: S.string().email(),
|
|
155
|
-
age: S.number().min(0).max(150)
|
|
156
|
-
});
|
|
149
|
+
// Generate HTML that loads OpenAPI spec from a URL
|
|
150
|
+
const html = generateSwaggerUiHtml('/openapi.json');
|
|
157
151
|
|
|
158
|
-
|
|
159
|
-
|
|
152
|
+
// Serve as an endpoint
|
|
153
|
+
@Get('/docs')
|
|
154
|
+
async getDocs(): Promise<Response> {
|
|
155
|
+
return new Response(html, {
|
|
156
|
+
headers: { 'Content-Type': 'text/html' },
|
|
157
|
+
});
|
|
158
|
+
}
|
|
160
159
|
```
|
|
161
160
|
|
|
162
|
-
|
|
161
|
+
### JSON Schema Conversion
|
|
163
162
|
|
|
164
|
-
|
|
163
|
+
Convert ArkType schemas to JSON Schema format for OpenAPI documentation:
|
|
165
164
|
|
|
166
165
|
```typescript
|
|
167
|
-
import {
|
|
168
|
-
import {
|
|
166
|
+
import { arktypeToJsonSchema } from '@onebun/docs';
|
|
167
|
+
import { type } from 'arktype';
|
|
169
168
|
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
email:
|
|
173
|
-
age:
|
|
169
|
+
const userSchema = type({
|
|
170
|
+
id: 'string',
|
|
171
|
+
email: 'string.email',
|
|
172
|
+
age: 'number > 0',
|
|
174
173
|
});
|
|
175
174
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
@Post()
|
|
179
|
-
@ApiOperation({ summary: 'Create user' })
|
|
180
|
-
@ApiBody({ schema: CreateUserSchema })
|
|
181
|
-
@ApiResponse({ status: 201, schema: CreateUserSchema })
|
|
182
|
-
async createUser(@Body(CreateUserSchema) userData: typeof CreateUserSchema.infer) {
|
|
183
|
-
return userData;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
175
|
+
const jsonSchema = arktypeToJsonSchema(userSchema);
|
|
176
|
+
// Returns JSON Schema compatible object
|
|
186
177
|
```
|
|
187
178
|
|
|
179
|
+
## Integration with @onebun/core
|
|
180
|
+
|
|
181
|
+
The docs package reads metadata from `@onebun/core` decorators automatically:
|
|
182
|
+
|
|
183
|
+
- Route information from `@Get`, `@Post`, `@Put`, `@Delete`, `@Patch`
|
|
184
|
+
- Path parameters from `@Param`
|
|
185
|
+
- Query parameters from `@Query`
|
|
186
|
+
- Request body from `@Body`
|
|
187
|
+
- Response schemas from `@ApiResponse`
|
|
188
|
+
|
|
189
|
+
This means your existing controller decorators contribute to the generated OpenAPI spec.
|
|
190
|
+
|
|
188
191
|
## Best Practices
|
|
189
192
|
|
|
190
|
-
1. **
|
|
191
|
-
2. **Use
|
|
192
|
-
3. **
|
|
193
|
-
4. **
|
|
193
|
+
1. **Use @ApiOperation for all endpoints** - Add summary and description
|
|
194
|
+
2. **Use @ApiTags for grouping** - Group related endpoints logically
|
|
195
|
+
3. **Document response types** - Use `@ApiResponse` with schemas from `@onebun/core`
|
|
196
|
+
4. **Use meaningful descriptions** - Help API consumers understand your endpoints
|
|
194
197
|
5. **Keep documentation updated** - Decorators ensure docs stay in sync with code
|
|
195
198
|
|
|
199
|
+
## API Reference
|
|
200
|
+
|
|
201
|
+
### generateOpenApiSpec(controllers, options)
|
|
202
|
+
|
|
203
|
+
Generate OpenAPI 3.1 specification from controller classes.
|
|
204
|
+
|
|
205
|
+
**Parameters:**
|
|
206
|
+
- `controllers: Function[]` - Array of controller classes
|
|
207
|
+
- `options: { title?: string; version?: string; description?: string }` - OpenAPI info options
|
|
208
|
+
|
|
209
|
+
**Returns:** `OpenApiSpec` - OpenAPI 3.1 compliant specification object
|
|
210
|
+
|
|
211
|
+
### generateSwaggerUiHtml(specUrl)
|
|
212
|
+
|
|
213
|
+
Generate HTML page with Swagger UI.
|
|
214
|
+
|
|
215
|
+
**Parameters:**
|
|
216
|
+
- `specUrl: string` - URL to the OpenAPI JSON specification
|
|
217
|
+
|
|
218
|
+
**Returns:** `string` - HTML string
|
|
219
|
+
|
|
220
|
+
### arktypeToJsonSchema(schema)
|
|
221
|
+
|
|
222
|
+
Convert ArkType schema to JSON Schema.
|
|
223
|
+
|
|
224
|
+
**Parameters:**
|
|
225
|
+
- `schema: Type<unknown>` - ArkType schema
|
|
226
|
+
|
|
227
|
+
**Returns:** `Record<string, unknown>` - JSON Schema object
|
|
228
|
+
|
|
196
229
|
## License
|
|
197
230
|
|
|
198
231
|
[LGPL-3.0](../../LICENSE)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onebun/docs",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Documentation generation for OneBun framework - OpenAPI, Swagger UI",
|
|
5
5
|
"license": "LGPL-3.0",
|
|
6
6
|
"author": "RemRyahirev",
|
|
@@ -37,10 +37,11 @@
|
|
|
37
37
|
"test": "bun test"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@onebun/core": "workspace
|
|
40
|
+
"@onebun/core": "workspace:^",
|
|
41
41
|
"swagger-ui-dist": "^5.17.14"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
+
"arktype": "^2.0.0",
|
|
44
45
|
"bun-types": "1.2.2"
|
|
45
46
|
},
|
|
46
47
|
"engines": {
|
package/src/decorators.ts
CHANGED
|
@@ -72,24 +72,58 @@ export function ApiTags(...tags: string[]): ClassDecorator & MethodDecorator {
|
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
74
|
* Get API operation metadata for a method
|
|
75
|
+
* Walks up the prototype chain to find metadata (needed because @Controller wraps classes)
|
|
75
76
|
*/
|
|
76
77
|
export function getApiOperationMetadata(
|
|
77
78
|
target: object,
|
|
78
79
|
propertyKey: string | symbol,
|
|
79
80
|
): { summary?: string; description?: string; tags?: string[] } | undefined {
|
|
80
|
-
|
|
81
|
+
// Try to get metadata from target directly
|
|
82
|
+
let result = getMetadata(API_OPERATION_METADATA, target, propertyKey);
|
|
83
|
+
if (result) {
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Walk up prototype chain (needed for wrapped classes)
|
|
88
|
+
let proto = Object.getPrototypeOf(target);
|
|
89
|
+
while (proto && proto !== Object.prototype) {
|
|
90
|
+
result = getMetadata(API_OPERATION_METADATA, proto, propertyKey);
|
|
91
|
+
if (result) {
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
proto = Object.getPrototypeOf(proto);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return undefined;
|
|
81
98
|
}
|
|
82
99
|
|
|
83
100
|
/**
|
|
84
101
|
* Get API tags for a class or method
|
|
102
|
+
* Walks up the prototype chain to find metadata (needed because @Controller wraps classes)
|
|
85
103
|
*/
|
|
86
104
|
export function getApiTagsMetadata(
|
|
87
105
|
target: object | Function,
|
|
88
106
|
propertyKey?: string | symbol,
|
|
89
107
|
): string[] | undefined {
|
|
90
108
|
if (propertyKey !== undefined) {
|
|
91
|
-
|
|
109
|
+
// Method-level tags - walk prototype chain
|
|
110
|
+
let result = getMetadata(API_TAGS_METADATA, target, propertyKey);
|
|
111
|
+
if (result) {
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let proto = Object.getPrototypeOf(target);
|
|
116
|
+
while (proto && proto !== Object.prototype) {
|
|
117
|
+
result = getMetadata(API_TAGS_METADATA, proto, propertyKey);
|
|
118
|
+
if (result) {
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
proto = Object.getPrototypeOf(proto);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return undefined;
|
|
92
125
|
}
|
|
93
126
|
|
|
127
|
+
// Class-level tags
|
|
94
128
|
return getMetadata(API_TAGS_METADATA, target);
|
|
95
129
|
}
|
|
@@ -4,160 +4,186 @@
|
|
|
4
4
|
* This file tests code examples from:
|
|
5
5
|
* - packages/docs/README.md
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* The \@onebun/docs package provides:
|
|
8
|
+
* - \@ApiTags - for grouping endpoints
|
|
9
|
+
* - \@ApiOperation - for describing operations
|
|
10
|
+
* - generateOpenApiSpec - for generating OpenAPI specs
|
|
11
|
+
* - generateSwaggerUiHtml - for generating Swagger UI
|
|
12
|
+
* - arktypeToJsonSchema - for converting schemas
|
|
13
|
+
*
|
|
14
|
+
* Note: \@ApiResponse is provided by \@onebun/core for response validation
|
|
9
15
|
*/
|
|
10
16
|
|
|
17
|
+
import { type } from 'arktype';
|
|
11
18
|
import {
|
|
12
19
|
describe,
|
|
13
|
-
it,
|
|
14
20
|
expect,
|
|
21
|
+
it,
|
|
15
22
|
} from 'bun:test';
|
|
16
23
|
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const ApiParam = (_options: { name: string; description?: string }) =>
|
|
27
|
-
(_target: object, _propertyKey?: string | symbol) => {};
|
|
28
|
-
const ApiQuery = (_options: { name: string; description?: string }) =>
|
|
29
|
-
(_target: object, _propertyKey?: string | symbol) => {};
|
|
30
|
-
const ApiHeader = (_options: { name: string; description?: string }) =>
|
|
31
|
-
(_target: object, _propertyKey?: string | symbol) => {};
|
|
32
|
-
const ApiTag = ApiTags; // Alias for the actual implementation
|
|
33
|
-
/* eslint-enable @typescript-eslint/naming-convention, @typescript-eslint/no-empty-function */
|
|
34
|
-
|
|
35
|
-
describe('Docs README Examples', () => {
|
|
36
|
-
describe('Controller Level Decorators (README)', () => {
|
|
37
|
-
it('should have @ApiTag decorator available', () => {
|
|
38
|
-
// From README: Controller Level - @ApiTag
|
|
39
|
-
expect(ApiTag).toBeDefined();
|
|
40
|
-
expect(typeof ApiTag).toBe('function');
|
|
41
|
-
});
|
|
24
|
+
import {
|
|
25
|
+
ApiResponse,
|
|
26
|
+
BaseController,
|
|
27
|
+
Body,
|
|
28
|
+
Controller,
|
|
29
|
+
Get,
|
|
30
|
+
Param,
|
|
31
|
+
Post,
|
|
32
|
+
} from '@onebun/core';
|
|
42
33
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
34
|
+
import {
|
|
35
|
+
ApiOperation,
|
|
36
|
+
ApiTags,
|
|
37
|
+
getApiOperationMetadata,
|
|
38
|
+
getApiTagsMetadata,
|
|
39
|
+
} from './decorators';
|
|
40
|
+
import { generateOpenApiSpec } from './openapi/generator';
|
|
41
|
+
import { arktypeToJsonSchema } from './openapi/json-schema-converter';
|
|
42
|
+
import { generateSwaggerUiHtml } from './openapi/swagger-ui';
|
|
47
43
|
|
|
48
|
-
|
|
44
|
+
describe('Docs README Examples', () => {
|
|
45
|
+
describe('Decorators from @onebun/docs', () => {
|
|
46
|
+
it('should have @ApiTags decorator available', () => {
|
|
47
|
+
expect(ApiTags).toBeDefined();
|
|
48
|
+
expect(typeof ApiTags).toBe('function');
|
|
49
49
|
});
|
|
50
|
-
});
|
|
51
50
|
|
|
52
|
-
describe('Method Level Decorators (README)', () => {
|
|
53
51
|
it('should have @ApiOperation decorator available', () => {
|
|
54
|
-
// From README: Method Level - @ApiOperation
|
|
55
52
|
expect(ApiOperation).toBeDefined();
|
|
56
53
|
expect(typeof ApiOperation).toBe('function');
|
|
57
54
|
});
|
|
58
55
|
|
|
59
|
-
it('should
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
expect(typeof ApiResponse).toBe('function');
|
|
63
|
-
});
|
|
56
|
+
it('should use @ApiTags on controller class', () => {
|
|
57
|
+
@ApiTags('Users')
|
|
58
|
+
class UserController {}
|
|
64
59
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
expect(
|
|
68
|
-
expect(
|
|
60
|
+
const tags = getApiTagsMetadata(UserController);
|
|
61
|
+
expect(tags).toBeDefined();
|
|
62
|
+
expect(tags!.length).toBeGreaterThan(0);
|
|
63
|
+
expect(tags).toContain('Users');
|
|
69
64
|
});
|
|
70
65
|
|
|
71
|
-
it('should
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
66
|
+
it('should use @ApiTags with multiple tags', () => {
|
|
67
|
+
@ApiTags('Users', 'User Management')
|
|
68
|
+
class UserController {}
|
|
69
|
+
|
|
70
|
+
const tags = getApiTagsMetadata(UserController);
|
|
71
|
+
expect(tags).toBeDefined();
|
|
72
|
+
expect(tags).toContain('Users');
|
|
73
|
+
expect(tags).toContain('User Management');
|
|
75
74
|
});
|
|
76
75
|
|
|
77
|
-
it('should
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
76
|
+
it('should use @ApiOperation on method', () => {
|
|
77
|
+
class TestController {
|
|
78
|
+
@ApiOperation({
|
|
79
|
+
summary: 'Get all users',
|
|
80
|
+
description: 'Returns a list of all users',
|
|
81
|
+
})
|
|
82
|
+
async getUsers() {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const metadata = getApiOperationMetadata(
|
|
88
|
+
TestController.prototype,
|
|
89
|
+
'getUsers',
|
|
90
|
+
);
|
|
91
|
+
expect(metadata?.summary).toBe('Get all users');
|
|
92
|
+
expect(metadata?.description).toBe('Returns a list of all users');
|
|
81
93
|
});
|
|
82
94
|
|
|
83
|
-
it('should
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
95
|
+
it('should use @ApiOperation with tags', () => {
|
|
96
|
+
class TestController {
|
|
97
|
+
@ApiOperation({
|
|
98
|
+
summary: 'Get user',
|
|
99
|
+
tags: ['Users', 'Admin'],
|
|
100
|
+
})
|
|
101
|
+
async getUser() {
|
|
102
|
+
return {};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const metadata = getApiOperationMetadata(
|
|
107
|
+
TestController.prototype,
|
|
108
|
+
'getUser',
|
|
109
|
+
);
|
|
110
|
+
expect(metadata?.tags).toContain('Users');
|
|
111
|
+
expect(metadata?.tags).toContain('Admin');
|
|
87
112
|
});
|
|
88
113
|
});
|
|
89
114
|
|
|
90
115
|
describe('Basic Usage Example (README)', () => {
|
|
91
|
-
it('should use decorators
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
116
|
+
it('should use decorators from both packages', () => {
|
|
117
|
+
const createUserSchema = type({
|
|
118
|
+
name: 'string',
|
|
119
|
+
email: 'string.email',
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Note: @ApiTags must be ABOVE @Controller because @Controller wraps the class
|
|
123
|
+
// Decorators are applied bottom-to-top, so @ApiTags runs last and gets the wrapped class
|
|
124
|
+
@ApiTags('Users')
|
|
125
|
+
@Controller('/users')
|
|
126
|
+
class UserController extends BaseController {
|
|
127
|
+
// Note: @ApiOperation must be ABOVE @Get for the same reason
|
|
100
128
|
@ApiOperation({
|
|
101
129
|
summary: 'Get all users',
|
|
102
130
|
description: 'Returns a list of all users',
|
|
103
131
|
})
|
|
104
|
-
@ApiResponse(
|
|
105
|
-
|
|
106
|
-
|
|
132
|
+
@ApiResponse(200, { description: 'List of users returned successfully' })
|
|
133
|
+
@Get('/')
|
|
134
|
+
async getUsers(): Promise<Response> {
|
|
135
|
+
return this.success({ users: [] });
|
|
107
136
|
}
|
|
108
137
|
|
|
109
|
-
@ApiOperation({
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
138
|
+
@ApiOperation({
|
|
139
|
+
summary: 'Create user',
|
|
140
|
+
description: 'Creates a new user',
|
|
141
|
+
})
|
|
142
|
+
@ApiResponse(201, {
|
|
143
|
+
schema: createUserSchema,
|
|
144
|
+
description: 'User created successfully',
|
|
145
|
+
})
|
|
146
|
+
@ApiResponse(400, { description: 'Invalid input data' })
|
|
147
|
+
@Post('/')
|
|
148
|
+
async createUser(
|
|
149
|
+
@Body(createUserSchema) userData: typeof createUserSchema.infer,
|
|
150
|
+
): Promise<Response> {
|
|
151
|
+
return this.success({ id: '1', ...userData });
|
|
115
152
|
}
|
|
116
153
|
}
|
|
117
154
|
|
|
118
155
|
expect(UserController).toBeDefined();
|
|
156
|
+
|
|
157
|
+
// Verify tags are set on controller
|
|
158
|
+
const tags = getApiTagsMetadata(UserController);
|
|
159
|
+
expect(tags).toBeDefined();
|
|
160
|
+
expect(tags!.length).toBeGreaterThan(0);
|
|
119
161
|
});
|
|
120
162
|
});
|
|
121
163
|
|
|
122
164
|
describe('Configuration Options (README)', () => {
|
|
123
165
|
it('should define valid DocsOptions interface', () => {
|
|
124
|
-
//
|
|
166
|
+
// This matches the DocsApplicationOptions interface in core
|
|
125
167
|
const docsOptions = {
|
|
126
|
-
// Enable/disable documentation (default: true)
|
|
127
168
|
enabled: true,
|
|
128
|
-
|
|
129
|
-
// Path for Swagger UI (default: '/docs')
|
|
130
169
|
path: '/docs',
|
|
131
|
-
|
|
132
|
-
// API title
|
|
170
|
+
jsonPath: '/openapi.json',
|
|
133
171
|
title: 'My API',
|
|
134
|
-
|
|
135
|
-
// API version
|
|
136
172
|
version: '1.0.0',
|
|
137
|
-
|
|
138
|
-
// API description
|
|
139
173
|
description: 'API documentation for my service',
|
|
140
|
-
|
|
141
|
-
// Contact information
|
|
142
174
|
contact: {
|
|
143
175
|
name: 'API Support',
|
|
144
176
|
email: 'support@example.com',
|
|
145
177
|
url: 'https://example.com/support',
|
|
146
178
|
},
|
|
147
|
-
|
|
148
|
-
// License information
|
|
149
179
|
license: {
|
|
150
180
|
name: 'MIT',
|
|
151
181
|
url: 'https://opensource.org/licenses/MIT',
|
|
152
182
|
},
|
|
153
|
-
|
|
154
|
-
// External documentation
|
|
155
183
|
externalDocs: {
|
|
156
184
|
description: 'More information',
|
|
157
185
|
url: 'https://example.com/docs',
|
|
158
186
|
},
|
|
159
|
-
|
|
160
|
-
// Server URLs
|
|
161
187
|
servers: [
|
|
162
188
|
{
|
|
163
189
|
url: 'https://api.example.com',
|
|
@@ -172,196 +198,278 @@ describe('Docs README Examples', () => {
|
|
|
172
198
|
|
|
173
199
|
expect(docsOptions.enabled).toBe(true);
|
|
174
200
|
expect(docsOptions.path).toBe('/docs');
|
|
201
|
+
expect(docsOptions.jsonPath).toBe('/openapi.json');
|
|
175
202
|
expect(docsOptions.title).toBe('My API');
|
|
176
203
|
expect(docsOptions.version).toBe('1.0.0');
|
|
177
204
|
expect(docsOptions.servers).toHaveLength(2);
|
|
178
205
|
});
|
|
179
206
|
});
|
|
207
|
+
});
|
|
180
208
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
@
|
|
189
|
-
|
|
190
|
-
|
|
209
|
+
describe('OpenAPI Generation', () => {
|
|
210
|
+
describe('generateOpenApiSpec', () => {
|
|
211
|
+
it('should generate spec from controllers', () => {
|
|
212
|
+
// Note: @ApiTags must be ABOVE @Controller
|
|
213
|
+
@ApiTags('Users')
|
|
214
|
+
@Controller('/users')
|
|
215
|
+
class UserController extends BaseController {
|
|
216
|
+
@ApiOperation({ summary: 'Get all users' })
|
|
217
|
+
@Get('/')
|
|
218
|
+
async getUsers(): Promise<Response> {
|
|
219
|
+
return this.success([]);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
@ApiOperation({ summary: 'Get user by ID' })
|
|
223
|
+
@Get('/:id')
|
|
224
|
+
async getUser(@Param('id') id: string): Promise<Response> {
|
|
225
|
+
return this.success({ id });
|
|
191
226
|
}
|
|
192
227
|
}
|
|
193
228
|
|
|
194
|
-
|
|
229
|
+
const spec = generateOpenApiSpec([UserController], {
|
|
230
|
+
title: 'Test API',
|
|
231
|
+
version: '1.0.0',
|
|
232
|
+
description: 'Test API description',
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
expect(spec.openapi).toBe('3.1.0');
|
|
236
|
+
expect(spec.info.title).toBe('Test API');
|
|
237
|
+
expect(spec.info.version).toBe('1.0.0');
|
|
238
|
+
expect(spec.info.description).toBe('Test API description');
|
|
239
|
+
expect(spec.paths).toBeDefined();
|
|
240
|
+
// Controller path + route path = /users + / = /users/
|
|
241
|
+
expect(spec.paths['/users/']).toBeDefined();
|
|
242
|
+
expect(spec.paths['/users/:id']).toBeDefined();
|
|
195
243
|
});
|
|
196
244
|
|
|
197
|
-
it('should
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
class
|
|
201
|
-
@ApiOperation({
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
'The email must be unique and a valid email format. ' +
|
|
206
|
-
'Password must be at least 8 characters.',
|
|
207
|
-
})
|
|
208
|
-
@ApiResponse({
|
|
209
|
-
status: 201,
|
|
210
|
-
description: 'User account created successfully. Returns the new user object.',
|
|
211
|
-
})
|
|
212
|
-
@ApiResponse({
|
|
213
|
-
status: 400,
|
|
214
|
-
description: 'Invalid input - email format incorrect or password too short.',
|
|
215
|
-
})
|
|
216
|
-
@ApiResponse({
|
|
217
|
-
status: 409,
|
|
218
|
-
description: 'Conflict - a user with this email already exists.',
|
|
219
|
-
})
|
|
220
|
-
async createUser() {
|
|
221
|
-
return {};
|
|
245
|
+
it('should include tags from @ApiTags', () => {
|
|
246
|
+
@ApiTags('Orders')
|
|
247
|
+
@Controller('/orders')
|
|
248
|
+
class OrderController extends BaseController {
|
|
249
|
+
@ApiOperation({ summary: 'Get orders' })
|
|
250
|
+
@Get('/')
|
|
251
|
+
async getOrders(): Promise<Response> {
|
|
252
|
+
return this.success([]);
|
|
222
253
|
}
|
|
223
254
|
}
|
|
224
255
|
|
|
225
|
-
|
|
256
|
+
const spec = generateOpenApiSpec([OrderController], {
|
|
257
|
+
title: 'Test API',
|
|
258
|
+
version: '1.0.0',
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// Path is /orders/ (controller path + route path)
|
|
262
|
+
const getOperation = spec.paths['/orders/']?.get;
|
|
263
|
+
expect(getOperation).toBeDefined();
|
|
264
|
+
expect(getOperation?.tags).toBeDefined();
|
|
265
|
+
expect(getOperation?.tags).toContain('Orders');
|
|
226
266
|
});
|
|
227
267
|
|
|
228
|
-
it('should
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
async updateUser() {
|
|
240
|
-
return {};
|
|
241
|
-
}
|
|
242
|
-
async deleteUser() {
|
|
243
|
-
return {};
|
|
268
|
+
it('should include operation metadata from @ApiOperation', () => {
|
|
269
|
+
@Controller('/products')
|
|
270
|
+
class ProductController extends BaseController {
|
|
271
|
+
@ApiOperation({
|
|
272
|
+
summary: 'List products',
|
|
273
|
+
description: 'Returns paginated list of products',
|
|
274
|
+
tags: ['Products', 'Catalog'],
|
|
275
|
+
})
|
|
276
|
+
@Get('/')
|
|
277
|
+
async listProducts(): Promise<Response> {
|
|
278
|
+
return this.success([]);
|
|
244
279
|
}
|
|
245
280
|
}
|
|
246
281
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
282
|
+
const spec = generateOpenApiSpec([ProductController], {
|
|
283
|
+
title: 'Test API',
|
|
284
|
+
version: '1.0.0',
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const getOperation = spec.paths['/products/']?.get;
|
|
288
|
+
expect(getOperation).toBeDefined();
|
|
289
|
+
expect(getOperation?.summary).toBe('List products');
|
|
290
|
+
expect(getOperation?.description).toBe(
|
|
291
|
+
'Returns paginated list of products',
|
|
292
|
+
);
|
|
293
|
+
expect(getOperation?.tags).toContain('Products');
|
|
294
|
+
expect(getOperation?.tags).toContain('Catalog');
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should include response schemas from @ApiResponse', () => {
|
|
298
|
+
const productSchema = type({
|
|
299
|
+
id: 'string',
|
|
300
|
+
name: 'string',
|
|
301
|
+
price: 'number',
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
@Controller('/products')
|
|
305
|
+
class ProductController extends BaseController {
|
|
306
|
+
// Note: @Get must be ABOVE @ApiResponse because @Get reads response schemas when it runs
|
|
307
|
+
// and decorators apply bottom-to-top
|
|
308
|
+
@Get('/:id')
|
|
309
|
+
@ApiResponse(200, {
|
|
310
|
+
schema: productSchema,
|
|
311
|
+
description: 'Product found',
|
|
312
|
+
})
|
|
313
|
+
@ApiResponse(404, { description: 'Product not found' })
|
|
314
|
+
async getProduct(@Param('id') id: string): Promise<Response> {
|
|
315
|
+
return this.success({ id, name: 'Test', price: 10 });
|
|
254
316
|
}
|
|
255
317
|
}
|
|
256
318
|
|
|
257
|
-
|
|
258
|
-
|
|
319
|
+
const spec = generateOpenApiSpec([ProductController], {
|
|
320
|
+
title: 'Test API',
|
|
321
|
+
version: '1.0.0',
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const responses = spec.paths['/products/:id']?.get?.responses;
|
|
325
|
+
expect(responses?.['200']).toBeDefined();
|
|
326
|
+
expect(responses?.['200']?.description).toBe('Product found');
|
|
327
|
+
expect(responses?.['404']).toBeDefined();
|
|
328
|
+
expect(responses?.['404']?.description).toBe('Product not found');
|
|
259
329
|
});
|
|
260
330
|
|
|
261
|
-
it('should
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
@ApiResponse({ status: 400, description: 'Invalid request body' })
|
|
268
|
-
@ApiResponse({ status: 401, description: 'Authentication required' })
|
|
269
|
-
@ApiResponse({ status: 403, description: 'Permission denied' })
|
|
270
|
-
@ApiResponse({ status: 404, description: 'User not found' })
|
|
271
|
-
@ApiResponse({ status: 422, description: 'Validation error' })
|
|
272
|
-
@ApiResponse({ status: 500, description: 'Internal server error' })
|
|
273
|
-
async updateUser() {
|
|
274
|
-
return {};
|
|
331
|
+
it('should use default options when not provided', () => {
|
|
332
|
+
@Controller('/api')
|
|
333
|
+
class ApiController extends BaseController {
|
|
334
|
+
@Get('/health')
|
|
335
|
+
async health(): Promise<Response> {
|
|
336
|
+
return this.success({ status: 'ok' });
|
|
275
337
|
}
|
|
276
338
|
}
|
|
277
339
|
|
|
278
|
-
|
|
340
|
+
const spec = generateOpenApiSpec([ApiController]);
|
|
341
|
+
|
|
342
|
+
expect(spec.info.title).toBe('OneBun API');
|
|
343
|
+
expect(spec.info.version).toBe('1.0.0');
|
|
279
344
|
});
|
|
280
345
|
});
|
|
281
|
-
});
|
|
282
346
|
|
|
283
|
-
describe('
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
class TestController {
|
|
287
|
-
@ApiOperation({
|
|
288
|
-
summary: 'Short summary',
|
|
289
|
-
description: 'Longer description with more details',
|
|
290
|
-
})
|
|
291
|
-
async testMethod() {}
|
|
292
|
-
}
|
|
347
|
+
describe('generateSwaggerUiHtml', () => {
|
|
348
|
+
it('should generate valid HTML', () => {
|
|
349
|
+
const html = generateSwaggerUiHtml('/openapi.json');
|
|
293
350
|
|
|
294
|
-
expect(
|
|
351
|
+
expect(html).toContain('<!DOCTYPE html>');
|
|
352
|
+
expect(html).toContain('swagger-ui');
|
|
353
|
+
expect(html).toContain('/openapi.json');
|
|
295
354
|
});
|
|
296
|
-
});
|
|
297
355
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
class TestController {
|
|
301
|
-
@ApiResponse({
|
|
302
|
-
status: 200,
|
|
303
|
-
description: 'Success response',
|
|
304
|
-
})
|
|
305
|
-
async testMethod() {}
|
|
306
|
-
}
|
|
356
|
+
it('should include spec URL in SwaggerUI config', () => {
|
|
357
|
+
const html = generateSwaggerUiHtml('/api/v1/openapi.json');
|
|
307
358
|
|
|
308
|
-
expect(
|
|
359
|
+
expect(html).toContain('url: "/api/v1/openapi.json"');
|
|
309
360
|
});
|
|
310
361
|
});
|
|
311
362
|
|
|
312
|
-
describe('
|
|
313
|
-
it('should
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
}
|
|
363
|
+
describe('arktypeToJsonSchema', () => {
|
|
364
|
+
it('should convert simple object schema', () => {
|
|
365
|
+
const userSchema = type({
|
|
366
|
+
name: 'string',
|
|
367
|
+
age: 'number',
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
const jsonSchema = arktypeToJsonSchema(userSchema);
|
|
321
371
|
|
|
322
|
-
expect(
|
|
372
|
+
expect(jsonSchema.type).toBe('object');
|
|
373
|
+
expect(jsonSchema.properties).toBeDefined();
|
|
323
374
|
});
|
|
324
|
-
});
|
|
325
375
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
376
|
+
it('should convert schema with string constraints', () => {
|
|
377
|
+
const emailSchema = type('string.email');
|
|
378
|
+
|
|
379
|
+
const jsonSchema = arktypeToJsonSchema(emailSchema);
|
|
380
|
+
|
|
381
|
+
expect(jsonSchema).toBeDefined();
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('should convert nested object schema', () => {
|
|
385
|
+
const addressSchema = type({
|
|
386
|
+
street: 'string',
|
|
387
|
+
city: 'string',
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
const userSchema = type({
|
|
391
|
+
name: 'string',
|
|
392
|
+
address: addressSchema,
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
const jsonSchema = arktypeToJsonSchema(userSchema);
|
|
335
396
|
|
|
336
|
-
expect(
|
|
397
|
+
expect(jsonSchema.type).toBe('object');
|
|
398
|
+
expect(jsonSchema.properties).toBeDefined();
|
|
337
399
|
});
|
|
338
400
|
});
|
|
401
|
+
});
|
|
339
402
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
403
|
+
describe('Best Practices (README)', () => {
|
|
404
|
+
it('should use @ApiOperation for all endpoints', () => {
|
|
405
|
+
@Controller('/resources')
|
|
406
|
+
class ResourceController extends BaseController {
|
|
407
|
+
@ApiOperation({ summary: 'List resources' })
|
|
408
|
+
@ApiResponse(200, { description: 'Resources listed' })
|
|
409
|
+
@Get('/')
|
|
410
|
+
async list(): Promise<Response> {
|
|
411
|
+
return this.success([]);
|
|
348
412
|
}
|
|
349
413
|
|
|
350
|
-
|
|
351
|
-
|
|
414
|
+
@ApiOperation({ summary: 'Get resource by ID' })
|
|
415
|
+
@ApiResponse(200, { description: 'Resource found' })
|
|
416
|
+
@ApiResponse(404, { description: 'Resource not found' })
|
|
417
|
+
@Get('/:id')
|
|
418
|
+
async get(@Param('id') id: string): Promise<Response> {
|
|
419
|
+
return this.success({ id });
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
expect(ResourceController).toBeDefined();
|
|
352
424
|
});
|
|
353
425
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
426
|
+
it('should use @ApiTags for grouping', () => {
|
|
427
|
+
// Note: @ApiTags MUST be placed ABOVE @Controller
|
|
428
|
+
// because @Controller wraps the class and decorators apply bottom-to-top
|
|
429
|
+
@ApiTags('Users')
|
|
430
|
+
@Controller('/users')
|
|
431
|
+
class UsersController extends BaseController {
|
|
432
|
+
@Get('/')
|
|
433
|
+
async list(): Promise<Response> {
|
|
434
|
+
return this.success([]);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
@ApiTags('Orders')
|
|
439
|
+
@Controller('/orders')
|
|
440
|
+
class OrdersController extends BaseController {
|
|
441
|
+
@Get('/')
|
|
442
|
+
async list(): Promise<Response> {
|
|
443
|
+
return this.success([]);
|
|
362
444
|
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const usersTags = getApiTagsMetadata(UsersController);
|
|
448
|
+
const ordersTags = getApiTagsMetadata(OrdersController);
|
|
449
|
+
|
|
450
|
+
expect(usersTags).toBeDefined();
|
|
451
|
+
expect(usersTags).toContain('Users');
|
|
452
|
+
expect(ordersTags).toBeDefined();
|
|
453
|
+
expect(ordersTags).toContain('Orders');
|
|
454
|
+
});
|
|
363
455
|
|
|
364
|
-
|
|
456
|
+
it('should document response types with @ApiResponse from core', () => {
|
|
457
|
+
const userSchema = type({
|
|
458
|
+
id: 'string',
|
|
459
|
+
name: 'string',
|
|
460
|
+
email: 'string.email',
|
|
365
461
|
});
|
|
462
|
+
|
|
463
|
+
@Controller('/users')
|
|
464
|
+
class UserController extends BaseController {
|
|
465
|
+
@ApiResponse(200, { schema: userSchema, description: 'User found' })
|
|
466
|
+
@ApiResponse(404, { description: 'User not found' })
|
|
467
|
+
@Get('/:id')
|
|
468
|
+
async getUser(@Param('id') id: string): Promise<Response> {
|
|
469
|
+
return this.success({ id, name: 'Test', email: 'test@test.com' });
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
expect(UserController).toBeDefined();
|
|
366
474
|
});
|
|
367
475
|
});
|