Skip to content

Commit 81c9f48

Browse files
committed
feat: response openapi schema & example
1 parent e6dcda9 commit 81c9f48

10 files changed

Lines changed: 576 additions & 7 deletions

examples/complete-example/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const server = new MyServer({
3737
port: 3000,
3838
maxPayloadSizeMB: 10,
3939
log: customLogger,
40+
swaggerEnabled: true,
4041
});
4142

4243
server

examples/complete-example/users/create-user-endpoint.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ApiRequest, ApiResponse, PostEndpoint } from '../../../src/index';
22
import { usersRepo, User } from './users-repository';
3-
import { ObjectSanitizer, EmailValidator } from 'valsan';
3+
import { ObjectSanitizer, EmailValidator, IntegerValidator } from 'valsan';
44
import { NameValSan } from './name-valsan';
55

66
/**
@@ -11,11 +11,29 @@ import { NameValSan } from './name-valsan';
1111
export class CreateUserEndpoint extends PostEndpoint {
1212
override path = '/';
1313

14+
override bodyExample = {
15+
name: 'John Doe',
16+
email: 'john@example.com',
17+
};
18+
1419
override body = new ObjectSanitizer({
1520
name: new NameValSan(),
1621
email: new EmailValidator(),
1722
});
1823

24+
override responseExample = {
25+
id: 3,
26+
name: 'John Doe',
27+
email: 'john@example.com',
28+
created: new Date(),
29+
};
30+
31+
override response = new ObjectSanitizer({
32+
id: new IntegerValidator(),
33+
name: new NameValSan(),
34+
email: new EmailValidator(),
35+
});
36+
1937
async handle(request: ApiRequest, response: ApiResponse) {
2038
const { name, email } = request.body;
2139

examples/complete-example/users/get-user-endpoint.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { ApiRequest, ApiResponse, GetEndpoint } from '../../../src/index';
22
import { NotFoundError } from '../../../src/error';
33
import { usersRepo } from './users-repository';
4+
import { ObjectSanitizer, IntegerValidator, EmailValidator } from 'valsan';
5+
import { NameValSan } from './name-valsan';
46

57
/**
68
* Complete Example - Get User Endpoint
@@ -11,6 +13,19 @@ import { usersRepo } from './users-repository';
1113
export class GetUserEndpoint extends GetEndpoint {
1214
override path = '/:id';
1315

16+
override responseExample = {
17+
id: 1,
18+
name: 'Alice',
19+
email: 'alice@example.com',
20+
created: new Date('2023-01-01'),
21+
};
22+
23+
override response = new ObjectSanitizer({
24+
id: new IntegerValidator(),
25+
name: new NameValSan(),
26+
email: new EmailValidator(),
27+
});
28+
1429
async handle(request: ApiRequest, response: ApiResponse) {
1530
const userId = parseInt(request.params['id'], 10);
1631
const user = usersRepo[userId];

src/oas/oas-endpoint-component-converter.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,16 @@ export class OasEndpointComponentConverter {
2222
});
2323
}
2424

25+
const responseSanitizer = endpoint.response;
26+
27+
if (responseSanitizer) {
28+
this.addSchema({
29+
name: `${endpoint.name}Response`,
30+
sanitizer: responseSanitizer,
31+
example: endpoint.responseExample,
32+
});
33+
}
34+
2535
return this.schemas;
2636
}
2737

src/oas/oas-endpoint-converter.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
PathItemObject,
44
ParameterObject,
55
RequestBodyObject,
6+
ResponsesObject,
67
} from 'auto-oas/oas/v3.1';
78

89
import { BaseApiEndpoint } from '../router';
@@ -60,9 +61,28 @@ export class OasEndpointConverter {
6061
// Use statusCode for response
6162
const status = endpoint.statusCode;
6263

63-
const responses = {
64-
[status]: { description: 'Success' },
65-
};
64+
const responses: ResponsesObject = {};
65+
66+
// Generate success response with schema if available
67+
const responseSanitizer = endpoint.response;
68+
if (responseSanitizer && responseSanitizer.schema) {
69+
responses[status] = {
70+
description: 'Success',
71+
content: {
72+
'application/json': {
73+
schema: {
74+
$ref:
75+
'#/components/schemas/' +
76+
endpoint.getName() +
77+
'Response',
78+
},
79+
},
80+
},
81+
};
82+
}
83+
else {
84+
responses[status] = { description: 'Success' };
85+
}
6686

6787
const errors = endpoint.getErrors();
6888
for (const error in errors) {

src/router/endpoint.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ export abstract class BaseApiEndpoint extends BaseApiRoute {
4949
// eslint-disable-next-line @typescript-eslint/no-explicit-any
5050
public headersExample?: any;
5151

52+
public response?: ObjectSanitizer;
53+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
54+
public responseExample?: any;
55+
5256
public getErrors(): { [key: string]: HTTPError } {
5357
return {
5458
parse: new BadRequestError(),

src/router/endpoints/health-check-endpoint.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,29 @@
1-
import { ApiRequest, ApiResponse } from '../endpoint';
1+
import {
2+
Iso8601TimestampValSan,
3+
MinLengthValidator,
4+
ObjectSanitizer,
5+
StringToNumberValSan,
6+
} from 'valsan';
27
import { GetEndpoint } from './get-endpoint';
38

49
export class HealthCheckEndpoint extends GetEndpoint {
510
override path = '/health';
611

7-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
8-
async handle(_request: ApiRequest, _response: ApiResponse) {
12+
override responseExample = {
13+
status: 'ok',
14+
timestamp: new Date().toISOString(),
15+
uptime: 12345,
16+
environment: 'development',
17+
};
18+
19+
override response = new ObjectSanitizer({
20+
status: new MinLengthValidator(),
21+
timestamp: new Iso8601TimestampValSan(),
22+
uptime: new StringToNumberValSan(),
23+
environment: new MinLengthValidator(),
24+
});
25+
26+
async handle() {
927
return {
1028
status: await this.getStatus(),
1129
timestamp: await this.getTimestamp(),

test/spec/api/openapi-server.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,14 @@ class CreateUserEndpoint extends PostEndpoint {
4747
email: 'john.doe@example.com',
4848
};
4949

50+
override response = new ObjectSanitizer({
51+
id: new LengthValidator({ minLength: 1, maxLength: 50 }),
52+
});
53+
54+
override responseExample = {
55+
id: 'abc123',
56+
};
57+
5058
async handle(request: ApiRequest) {
5159
const id = Math.random().toString(36).slice(2);
5260
usersDb[id] = { id, ...request.body };
@@ -80,6 +88,16 @@ class GetUserEndpoint extends GetEndpoint {
8088
'x-request-id': 'req-12345',
8189
};
8290

91+
override responseExample = {
92+
id: 'abc123',
93+
name: 'John Doe',
94+
email: 'john.doe@example.com',
95+
};
96+
97+
override response = new ObjectSanitizer({
98+
id: idValidator,
99+
});
100+
83101
async handle(request: ApiRequest) {
84102
const user = usersDb[request.params['id']];
85103

@@ -107,6 +125,20 @@ class ListUsersEndpoint extends GetEndpoint {
107125
email: emailValidator.copy({ isOptional: true }),
108126
});
109127

128+
override response = new ObjectSanitizer({
129+
limit: new RangeValidator({ min: 1, max: 100 }),
130+
name: nameValidator,
131+
email: emailValidator,
132+
});
133+
134+
override responseExample = [
135+
{
136+
id: 'abc123',
137+
name: 'John Doe',
138+
email: 'john.doe@example.com',
139+
},
140+
];
141+
110142
async handle(request: ApiRequest) {
111143
const users = Object.values(usersDb);
112144

0 commit comments

Comments
 (0)