@hg-ts/http-controller 0.5.17 → 0.5.19
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/package.json +15 -15
- package/src/decorators/decorators.ts +43 -0
- package/src/decorators/index.ts +1 -0
- package/src/exceptions/exception-schema-already-exists.exception.ts +7 -0
- package/src/exceptions/exception-schema-not-found.exception.ts +7 -0
- package/src/exceptions/exception-status-already-defined.exception.ts +7 -0
- package/src/exceptions/index.ts +3 -0
- package/src/http-controller.module.ts +49 -0
- package/src/index.ts +3 -0
- package/src/middlewares/database.interceptor.ts +40 -0
- package/src/middlewares/exception-mapper.interceptor.ts +83 -0
- package/src/middlewares/index.ts +3 -0
- package/src/middlewares/validation.pipe.ts +39 -0
- package/src/services/exception.service.ts +34 -0
- package/src/services/index.ts +2 -0
- package/src/services/swagger.service.ts +42 -0
- package/src/tests/abstracts/base-controller-suite.ts +47 -0
- package/src/tests/abstracts/index.ts +1 -0
- package/src/tests/echo/controllers/dto/echo-get.query.ts +5 -0
- package/src/tests/echo/controllers/dto/echo-post.body.ts +5 -0
- package/src/tests/echo/controllers/dto/index.ts +2 -0
- package/src/tests/echo/controllers/echo.controller.ts +45 -0
- package/src/tests/echo/controllers/index.ts +1 -0
- package/src/tests/echo/echo.test.module.ts +6 -0
- package/src/tests/echo/echo.test.ts +73 -0
- package/src/tests/exception/controllers/dtos/common-exception.dto.ts +10 -0
- package/src/tests/exception/controllers/dtos/index.ts +1 -0
- package/src/tests/exception/controllers/exception.controller.ts +25 -0
- package/src/tests/exception/controllers/index.ts +1 -0
- package/src/tests/exception/exception.test.module.ts +6 -0
- package/src/tests/exception/exception.test.ts +45 -0
- package/src/tests/exception/exceptions/index.ts +2 -0
- package/src/tests/exception/exceptions/mock-conflict.exception.ts +7 -0
- package/src/tests/exception/exceptions/mock-not-found.exception.ts +7 -0
- package/src/tests/http-methods/controllers/empty-response.controller.ts +59 -0
- package/src/tests/http-methods/controllers/index.ts +1 -0
- package/src/tests/http-methods/http-methods.test.module.ts +6 -0
- package/src/tests/http-methods/http-methods.test.ts +75 -0
- package/src/types.ts +9 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hg-ts/http-controller",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.19",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -18,15 +18,15 @@
|
|
|
18
18
|
"test:dev": "vitest watch"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
|
-
"@hg-ts-config/typescript": "0.5.
|
|
22
|
-
"@hg-ts/exception": "0.5.
|
|
23
|
-
"@hg-ts/execution-mode": "0.5.
|
|
24
|
-
"@hg-ts/knex": "0.5.
|
|
25
|
-
"@hg-ts/linter": "0.5.
|
|
26
|
-
"@hg-ts/logger": "0.5.
|
|
27
|
-
"@hg-ts/tests": "0.5.
|
|
28
|
-
"@hg-ts/types": "0.5.
|
|
29
|
-
"@hg-ts/validation": "0.5.
|
|
21
|
+
"@hg-ts-config/typescript": "0.5.19",
|
|
22
|
+
"@hg-ts/exception": "0.5.19",
|
|
23
|
+
"@hg-ts/execution-mode": "0.5.19",
|
|
24
|
+
"@hg-ts/knex": "0.5.19",
|
|
25
|
+
"@hg-ts/linter": "0.5.19",
|
|
26
|
+
"@hg-ts/logger": "0.5.19",
|
|
27
|
+
"@hg-ts/tests": "0.5.19",
|
|
28
|
+
"@hg-ts/types": "0.5.19",
|
|
29
|
+
"@hg-ts/validation": "0.5.19",
|
|
30
30
|
"@nestjs/common": "11.1.0",
|
|
31
31
|
"@nestjs/core": "11.1.0",
|
|
32
32
|
"@nestjs/platform-fastify": "11.1.0",
|
|
@@ -47,11 +47,11 @@
|
|
|
47
47
|
"vitest": "4.0.14"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"@hg-ts/exception": "0.5.
|
|
51
|
-
"@hg-ts/execution-mode": "0.5.
|
|
52
|
-
"@hg-ts/knex": "0.5.
|
|
53
|
-
"@hg-ts/logger": "0.5.
|
|
54
|
-
"@hg-ts/validation": "0.5.
|
|
50
|
+
"@hg-ts/exception": "0.5.19",
|
|
51
|
+
"@hg-ts/execution-mode": "0.5.19",
|
|
52
|
+
"@hg-ts/knex": "0.5.19",
|
|
53
|
+
"@hg-ts/logger": "0.5.19",
|
|
54
|
+
"@hg-ts/validation": "0.5.19",
|
|
55
55
|
"@nestjs/common": "*",
|
|
56
56
|
"@nestjs/core": "*",
|
|
57
57
|
"@nestjs/platform-fastify": "*",
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { BaseException } from '@hg-ts/exception';
|
|
2
|
+
import { createSchema } from '@hg-ts/validation';
|
|
3
|
+
|
|
4
|
+
import { ApiResponse } from '@nestjs/swagger';
|
|
5
|
+
|
|
6
|
+
import { ExceptionStatusAlreadyDefinedException } from '../exceptions/index.js';
|
|
7
|
+
import { ExceptionService } from '../services/index.js';
|
|
8
|
+
|
|
9
|
+
export type ExceptionMap = Map<Class<BaseException, any[]>, number>;
|
|
10
|
+
|
|
11
|
+
export const EXCEPTION_METADATA_KEY = Symbol('EXCEPTION_METADATA_KEY');
|
|
12
|
+
|
|
13
|
+
export class Http {
|
|
14
|
+
private constructor() {}
|
|
15
|
+
|
|
16
|
+
public static ExceptionStatus(exception: Class<BaseException, any[]>, statusCode: number): MethodDecorator {
|
|
17
|
+
return (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
|
|
18
|
+
const statusMap: ExceptionMap =
|
|
19
|
+
Reflect.getMetadata(EXCEPTION_METADATA_KEY, target, propertyKey) ?? new Map();
|
|
20
|
+
|
|
21
|
+
if (statusMap.has(exception)) {
|
|
22
|
+
throw new ExceptionStatusAlreadyDefinedException(exception);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
statusMap.set(exception, statusCode);
|
|
26
|
+
Reflect.defineMetadata(EXCEPTION_METADATA_KEY, statusMap, target, propertyKey);
|
|
27
|
+
|
|
28
|
+
const exceptionVariants = [...statusMap.entries()];
|
|
29
|
+
|
|
30
|
+
const schemas = exceptionVariants
|
|
31
|
+
.filter(([, exceptionStatusCode]) => exceptionStatusCode === statusCode)
|
|
32
|
+
.map(([item]) => ExceptionService.getSchema(item))
|
|
33
|
+
.map(item => item.schema)
|
|
34
|
+
.map(item => createSchema(item, { io: 'input' }))
|
|
35
|
+
.map(item => item.schema as any);
|
|
36
|
+
|
|
37
|
+
ApiResponse({
|
|
38
|
+
schema: schemas.length === 1 ? schemas[0] : { oneOf: schemas },
|
|
39
|
+
status: statusCode,
|
|
40
|
+
})(target, propertyKey, descriptor);
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './decorators.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { BaseException } from '@hg-ts/exception';
|
|
2
|
+
|
|
3
|
+
export class ExceptionStatusAlreadyDefinedException extends BaseException {
|
|
4
|
+
public constructor(exception: Class<BaseException, any[]>) {
|
|
5
|
+
super(`Status for exception "${exception.name}" already defined`);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DynamicModule,
|
|
3
|
+
Module,
|
|
4
|
+
} from '@nestjs/common';
|
|
5
|
+
import {
|
|
6
|
+
APP_INTERCEPTOR,
|
|
7
|
+
APP_PIPE,
|
|
8
|
+
} from '@nestjs/core';
|
|
9
|
+
import {
|
|
10
|
+
DatabaseInterceptor,
|
|
11
|
+
ExceptionMapperInterceptor,
|
|
12
|
+
ValidationPipe,
|
|
13
|
+
} from './middlewares/index.js';
|
|
14
|
+
|
|
15
|
+
import { SwaggerService } from './services/index.js';
|
|
16
|
+
|
|
17
|
+
type HttpControllerModuleOptions = {
|
|
18
|
+
disableTransactions?: boolean;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
@Module({
|
|
22
|
+
providers: [
|
|
23
|
+
SwaggerService,
|
|
24
|
+
{
|
|
25
|
+
provide: APP_INTERCEPTOR,
|
|
26
|
+
useClass: ExceptionMapperInterceptor,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
provide: APP_PIPE,
|
|
30
|
+
useClass: ValidationPipe,
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
})
|
|
34
|
+
export class HttpControllerModule {
|
|
35
|
+
public static forRoot(options: HttpControllerModuleOptions = {}): DynamicModule {
|
|
36
|
+
const { disableTransactions = false } = options;
|
|
37
|
+
return {
|
|
38
|
+
module: HttpControllerModule,
|
|
39
|
+
providers: disableTransactions
|
|
40
|
+
? []
|
|
41
|
+
: [
|
|
42
|
+
{
|
|
43
|
+
provide: APP_INTERCEPTOR,
|
|
44
|
+
useClass: DatabaseInterceptor,
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { KnexService } from '@hg-ts/knex';
|
|
2
|
+
import { Logger } from '@hg-ts/logger';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
CallHandler,
|
|
6
|
+
Catch,
|
|
7
|
+
ExecutionContext,
|
|
8
|
+
Inject,
|
|
9
|
+
NestInterceptor,
|
|
10
|
+
Optional,
|
|
11
|
+
} from '@nestjs/common';
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
from,
|
|
15
|
+
lastValueFrom,
|
|
16
|
+
Observable,
|
|
17
|
+
} from 'rxjs';
|
|
18
|
+
|
|
19
|
+
@Catch()
|
|
20
|
+
export class DatabaseInterceptor implements NestInterceptor {
|
|
21
|
+
@Inject()
|
|
22
|
+
protected readonly logger: Logger;
|
|
23
|
+
|
|
24
|
+
@Optional()
|
|
25
|
+
@Inject()
|
|
26
|
+
private readonly postgresService?: KnexService;
|
|
27
|
+
|
|
28
|
+
public intercept(
|
|
29
|
+
_context: ExecutionContext,
|
|
30
|
+
next: CallHandler,
|
|
31
|
+
): Observable<any> {
|
|
32
|
+
if (!this.postgresService) {
|
|
33
|
+
this.logger.warning('PostgresService not provided');
|
|
34
|
+
return next.handle();
|
|
35
|
+
}
|
|
36
|
+
const transactionalPromise = this.postgresService.runInTransaction(async() => lastValueFrom(next.handle()));
|
|
37
|
+
|
|
38
|
+
return from(transactionalPromise);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BaseException,
|
|
3
|
+
ErrorException,
|
|
4
|
+
} from '@hg-ts/exception';
|
|
5
|
+
import { Logger } from '@hg-ts/logger';
|
|
6
|
+
import { ValidationException } from '@hg-ts/validation';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
CallHandler,
|
|
10
|
+
Catch,
|
|
11
|
+
ExecutionContext,
|
|
12
|
+
Inject,
|
|
13
|
+
NestInterceptor,
|
|
14
|
+
} from '@nestjs/common';
|
|
15
|
+
import { isAxiosError } from 'axios';
|
|
16
|
+
import { FastifyReply } from 'fastify';
|
|
17
|
+
import {
|
|
18
|
+
Observable,
|
|
19
|
+
tap,
|
|
20
|
+
} from 'rxjs';
|
|
21
|
+
import {
|
|
22
|
+
EXCEPTION_METADATA_KEY,
|
|
23
|
+
ExceptionMap,
|
|
24
|
+
} from '../decorators/index.js';
|
|
25
|
+
|
|
26
|
+
@Catch()
|
|
27
|
+
export class ExceptionMapperInterceptor implements NestInterceptor {
|
|
28
|
+
@Inject()
|
|
29
|
+
protected readonly logger: Logger;
|
|
30
|
+
|
|
31
|
+
public intercept(
|
|
32
|
+
context: ExecutionContext,
|
|
33
|
+
next: CallHandler,
|
|
34
|
+
): Observable<any> {
|
|
35
|
+
return next.handle().pipe(tap({
|
|
36
|
+
error: error => {
|
|
37
|
+
const response = context.switchToHttp().getResponse<FastifyReply>();
|
|
38
|
+
|
|
39
|
+
if (isAxiosError(error)) {
|
|
40
|
+
response.status(error.status ?? 500).send(error.response?.data);
|
|
41
|
+
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const exception = this.getException(error);
|
|
45
|
+
const exceptionMap = this.getExceptionMap(context);
|
|
46
|
+
const exceptionCtor = this.getExceptionCtor(exception);
|
|
47
|
+
const status = exceptionMap.get(exceptionCtor);
|
|
48
|
+
|
|
49
|
+
response.status(status ?? 500).send(exception.toJSON());
|
|
50
|
+
},
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private getExceptionMap(context: ExecutionContext): ExceptionMap {
|
|
55
|
+
const exceptionMap: ExceptionMap = Reflect.getMetadata(
|
|
56
|
+
EXCEPTION_METADATA_KEY,
|
|
57
|
+
context.getClass().prototype,
|
|
58
|
+
context.getHandler().name,
|
|
59
|
+
) ?? new Map();
|
|
60
|
+
|
|
61
|
+
if (!exceptionMap.has(ValidationException)) {
|
|
62
|
+
exceptionMap.set(ValidationException, 404);
|
|
63
|
+
}
|
|
64
|
+
return exceptionMap;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private getExceptionCtor<T extends BaseException>(exception: T): Class<T, any[]> {
|
|
68
|
+
return Object.getPrototypeOf(exception)!.constructor as Class<T, any[]>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
private getException(exception: unknown): BaseException {
|
|
73
|
+
if (exception instanceof BaseException) {
|
|
74
|
+
return exception;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (exception instanceof Error) {
|
|
78
|
+
return new ErrorException(exception);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return new ErrorException(new Error(String(exception)));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isZodDto,
|
|
3
|
+
ZodType,
|
|
4
|
+
} from '@hg-ts/validation';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
ArgumentMetadata,
|
|
8
|
+
PipeTransform,
|
|
9
|
+
} from '@nestjs/common';
|
|
10
|
+
|
|
11
|
+
import assert from 'node:assert/strict';
|
|
12
|
+
|
|
13
|
+
export class ValidationPipe implements PipeTransform<unknown, unknown> {
|
|
14
|
+
private readonly dto: ZodType | null;
|
|
15
|
+
|
|
16
|
+
public constructor(dto?: ZodType) {
|
|
17
|
+
this.dto = dto ?? null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public transform(value: unknown, { metatype }: ArgumentMetadata): unknown {
|
|
21
|
+
const schema = this.getSchema(metatype);
|
|
22
|
+
|
|
23
|
+
return schema.parse(value);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public static getInstance(): ValidationPipe {
|
|
27
|
+
return new ValidationPipe();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private getSchema(metatype?: Class<any, any[]>): ZodType {
|
|
31
|
+
if (this.dto) {
|
|
32
|
+
return this.dto;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
assert.ok(isZodDto(metatype));
|
|
36
|
+
|
|
37
|
+
return metatype.schema;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { BaseException } from '@hg-ts/exception';
|
|
2
|
+
import { ZodDto } from '@hg-ts/validation';
|
|
3
|
+
import {
|
|
4
|
+
ExceptionSchemaAlreadyExistsException,
|
|
5
|
+
ExceptionSchemaNotFoundException,
|
|
6
|
+
} from '../exceptions/index.js';
|
|
7
|
+
|
|
8
|
+
export class ExceptionService {
|
|
9
|
+
private static readonly exceptionMap = new Map<Class<BaseException>, ZodDto>();
|
|
10
|
+
|
|
11
|
+
public static registerSchema<T extends BaseException>(
|
|
12
|
+
this: typeof ExceptionService,
|
|
13
|
+
exception: Class<T, any[]>,
|
|
14
|
+
schema: ZodDto<ReturnType<T['toJSON']>>,
|
|
15
|
+
): void {
|
|
16
|
+
if (this.exceptionMap.has(exception)) {
|
|
17
|
+
throw new ExceptionSchemaAlreadyExistsException(exception);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
this.exceptionMap.set(exception, schema);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public static getSchema<T extends BaseException>(
|
|
24
|
+
this: typeof ExceptionService,
|
|
25
|
+
exception: Class<T, any[]>,
|
|
26
|
+
): ZodDto<ReturnType<T['toJSON']>> {
|
|
27
|
+
const schema = this.exceptionMap.get(exception);
|
|
28
|
+
if (!schema) {
|
|
29
|
+
throw new ExceptionSchemaNotFoundException(exception);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return schema;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { WillNeverHappenedException } from '@hg-ts/exception';
|
|
2
|
+
import {
|
|
3
|
+
createSchema,
|
|
4
|
+
isZodDto,
|
|
5
|
+
} from '@hg-ts/validation';
|
|
6
|
+
|
|
7
|
+
import { INestApplication } from '@nestjs/common';
|
|
8
|
+
import {
|
|
9
|
+
DocumentBuilder,
|
|
10
|
+
SwaggerModule,
|
|
11
|
+
} from '@nestjs/swagger';
|
|
12
|
+
import { SchemaObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface.js';
|
|
13
|
+
import { SchemaObjectFactory } from '@nestjs/swagger/dist/services/schema-object-factory.js';
|
|
14
|
+
|
|
15
|
+
export class SwaggerService {
|
|
16
|
+
public setup(app: INestApplication): void {
|
|
17
|
+
this.patchSwaggerModule();
|
|
18
|
+
|
|
19
|
+
const config = new DocumentBuilder()
|
|
20
|
+
.setOpenAPIVersion('3.0.0')
|
|
21
|
+
.build();
|
|
22
|
+
|
|
23
|
+
const document = SwaggerModule.createDocument(app, config);
|
|
24
|
+
|
|
25
|
+
SwaggerModule.setup('/api', app, document, { jsonDocumentUrl: '/api/json' });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private patchSwaggerModule(): void {
|
|
29
|
+
SchemaObjectFactory.prototype.exploreModelSchema = function(
|
|
30
|
+
type: Class | Function | any,
|
|
31
|
+
schemas: any | Record<string, SchemaObject>,
|
|
32
|
+
): string {
|
|
33
|
+
if (!isZodDto(type)) {
|
|
34
|
+
throw new WillNeverHappenedException('Type will be zod schema');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
schemas[type.name] = createSchema(type.schema, { io: 'input' }).schema;
|
|
38
|
+
|
|
39
|
+
return type.name;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { LoggerModule } from '@hg-ts/logger';
|
|
2
|
+
import { Suite } from '@hg-ts/tests';
|
|
3
|
+
|
|
4
|
+
import { INestApplication } from '@nestjs/common';
|
|
5
|
+
import { FastifyAdapter } from '@nestjs/platform-fastify';
|
|
6
|
+
import {
|
|
7
|
+
Test as TestFactory,
|
|
8
|
+
TestingModule,
|
|
9
|
+
} from '@nestjs/testing';
|
|
10
|
+
|
|
11
|
+
import axios, { AxiosInstance } from 'axios';
|
|
12
|
+
import { HttpControllerModule } from '../../http-controller.module.js';
|
|
13
|
+
|
|
14
|
+
export abstract class BaseControllerSuite extends Suite {
|
|
15
|
+
protected module: TestingModule;
|
|
16
|
+
protected container: INestApplication;
|
|
17
|
+
protected client: AxiosInstance;
|
|
18
|
+
private readonly moduleCtor: Class<any, any[]>;
|
|
19
|
+
|
|
20
|
+
public constructor(moduleCtor: Class<any, any[]>) {
|
|
21
|
+
super();
|
|
22
|
+
|
|
23
|
+
this.moduleCtor = moduleCtor;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public override async setUp(): Promise<void> {
|
|
27
|
+
this.module = await TestFactory
|
|
28
|
+
.createTestingModule({ imports: [this.moduleCtor, LoggerModule, HttpControllerModule] })
|
|
29
|
+
.compile();
|
|
30
|
+
|
|
31
|
+
const port = this.getPort();
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
this.container = this.module.createNestApplication(new FastifyAdapter());
|
|
35
|
+
|
|
36
|
+
await this.container.init();
|
|
37
|
+
await this.container.listen(port, 'localhost');
|
|
38
|
+
|
|
39
|
+
this.client = axios.create({ baseURL: `http://localhost:${port}` });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public override async tearDown(): Promise<void> {
|
|
43
|
+
await this.container.close();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
protected abstract getPort(): number;
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './base-controller-suite.js';
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Body,
|
|
3
|
+
Controller,
|
|
4
|
+
Get,
|
|
5
|
+
Param,
|
|
6
|
+
Post,
|
|
7
|
+
Query,
|
|
8
|
+
} from '@nestjs/common';
|
|
9
|
+
import { ApiTags } from '@nestjs/swagger';
|
|
10
|
+
import {
|
|
11
|
+
EchoGetQuery,
|
|
12
|
+
EchoPostBody,
|
|
13
|
+
} from './dto/index.js';
|
|
14
|
+
|
|
15
|
+
@ApiTags('Echo')
|
|
16
|
+
@Controller('/echo')
|
|
17
|
+
export class EchoController {
|
|
18
|
+
@Get('/query')
|
|
19
|
+
public async getEchoQuery(
|
|
20
|
+
@Query() query: EchoGetQuery,
|
|
21
|
+
): Promise<EchoGetQuery> {
|
|
22
|
+
return query;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@Get('/param/:test')
|
|
26
|
+
public async getEchoParam(
|
|
27
|
+
@Param() params: EchoGetQuery,
|
|
28
|
+
): Promise<EchoGetQuery> {
|
|
29
|
+
return params;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@Post('/query')
|
|
33
|
+
public async postEchoQuery(
|
|
34
|
+
@Query() query: EchoGetQuery,
|
|
35
|
+
): Promise<EchoGetQuery> {
|
|
36
|
+
return query;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@Post('/body')
|
|
40
|
+
public async postEchoBody(
|
|
41
|
+
@Body() body: EchoPostBody,
|
|
42
|
+
): Promise<EchoPostBody> {
|
|
43
|
+
return body;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './echo.controller.js';
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Describe,
|
|
3
|
+
expect,
|
|
4
|
+
ExpectException,
|
|
5
|
+
Test,
|
|
6
|
+
} from '@hg-ts/tests';
|
|
7
|
+
|
|
8
|
+
import { BaseControllerSuite } from '../abstracts/index.js';
|
|
9
|
+
import { EchoTestModule } from './echo.test.module.js';
|
|
10
|
+
|
|
11
|
+
@Describe()
|
|
12
|
+
export class EchoTest extends BaseControllerSuite {
|
|
13
|
+
public constructor() {
|
|
14
|
+
super(EchoTestModule);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@Test()
|
|
18
|
+
public async echoGet(): Promise<void> {
|
|
19
|
+
const value = '3.12';
|
|
20
|
+
const params: Record<string, unknown> = { test: value };
|
|
21
|
+
const response = await this.client.get('/echo/query', { params });
|
|
22
|
+
|
|
23
|
+
expect(response.data).toMatchObject(params);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@Test()
|
|
27
|
+
public async echoPostQuery(): Promise<void> {
|
|
28
|
+
const value = '3.12';
|
|
29
|
+
const params: Record<string, unknown> = { test: value };
|
|
30
|
+
const response = await this.client.post('/echo/query', null, { params });
|
|
31
|
+
|
|
32
|
+
expect(response.data).toMatchObject(params);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@Test()
|
|
36
|
+
public async echoPostBody(): Promise<void> {
|
|
37
|
+
const value = 3.12;
|
|
38
|
+
const data: Record<string, unknown> = { test: value };
|
|
39
|
+
const response = await this.client.post('/echo/body', data);
|
|
40
|
+
|
|
41
|
+
expect(response.data).toMatchObject(data);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Test()
|
|
45
|
+
public async echoGetParam(): Promise<void> {
|
|
46
|
+
const value = '3.12';
|
|
47
|
+
const params: Record<string, unknown> = { test: value };
|
|
48
|
+
const response = await this.client.get(`/echo/param/${value}`);
|
|
49
|
+
|
|
50
|
+
expect(response.data).toMatchObject(params);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@Test()
|
|
54
|
+
@ExpectException()
|
|
55
|
+
public async echoGetInvalidQuery(): Promise<void> {
|
|
56
|
+
const value = '5.16';
|
|
57
|
+
const params: Record<string, unknown> = { unknownKey: value };
|
|
58
|
+
await this.client.get('/echo/query', { params });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@Test()
|
|
62
|
+
@ExpectException()
|
|
63
|
+
public async echoGetFieldInvalidQuery(): Promise<void> {
|
|
64
|
+
const value = '9.10 unknown text';
|
|
65
|
+
const params: Record<string, unknown> = { unknownField: value };
|
|
66
|
+
|
|
67
|
+
await this.client.get('/echo/query/field', { params });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
protected override getPort(): number {
|
|
71
|
+
return 50003;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './common-exception.dto.js';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { baseExceptionSchema } from '@hg-ts/validation';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Controller,
|
|
5
|
+
Get,
|
|
6
|
+
} from '@nestjs/common';
|
|
7
|
+
import { ApiTags } from '@nestjs/swagger';
|
|
8
|
+
|
|
9
|
+
import { Http } from '../../../decorators/index.js';
|
|
10
|
+
import { ExceptionService } from '../../../services/index.js';
|
|
11
|
+
import { MockNotFoundException } from '../exceptions/index.js';
|
|
12
|
+
|
|
13
|
+
ExceptionService.registerSchema(MockNotFoundException, baseExceptionSchema.toClass());
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@ApiTags('Exception')
|
|
17
|
+
@Controller('/exception')
|
|
18
|
+
export class ExceptionController {
|
|
19
|
+
@Http.ExceptionStatus(MockNotFoundException, 404)
|
|
20
|
+
@Get('/404')
|
|
21
|
+
public async getNotFound(
|
|
22
|
+
): Promise<never> {
|
|
23
|
+
throw new MockNotFoundException();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './exception.controller.js';
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Describe,
|
|
3
|
+
expect,
|
|
4
|
+
ExpectException,
|
|
5
|
+
Test,
|
|
6
|
+
} from '@hg-ts/tests';
|
|
7
|
+
import {
|
|
8
|
+
AxiosError,
|
|
9
|
+
isAxiosError,
|
|
10
|
+
} from 'axios';
|
|
11
|
+
|
|
12
|
+
import { BaseControllerSuite } from '../abstracts/index.js';
|
|
13
|
+
import { ExceptionTestModule } from './exception.test.module.js';
|
|
14
|
+
|
|
15
|
+
@Describe()
|
|
16
|
+
export class ExceptionTest extends BaseControllerSuite {
|
|
17
|
+
public constructor() {
|
|
18
|
+
super(ExceptionTestModule);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@Test()
|
|
22
|
+
@ExpectException(AxiosError)
|
|
23
|
+
public async echoGetFieldInvalidQuery(): Promise<void> {
|
|
24
|
+
const value = '9.10 unknown text';
|
|
25
|
+
const params: Record<string, unknown> = { unknownField: value };
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
await this.client.get('/exception/404', { params });
|
|
29
|
+
} catch (error) {
|
|
30
|
+
this.checkIsAxiosError(isAxiosError(error));
|
|
31
|
+
|
|
32
|
+
expect(error.status).toBe(404);
|
|
33
|
+
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
protected override getPort(): number {
|
|
39
|
+
return 50001;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private checkIsAxiosError(isAxiosError: boolean): asserts isAxiosError {
|
|
43
|
+
expect(isAxiosError).toBe(true);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { expect } from '@hg-ts/tests';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
All,
|
|
5
|
+
Controller,
|
|
6
|
+
Delete,
|
|
7
|
+
Get,
|
|
8
|
+
Options,
|
|
9
|
+
Patch,
|
|
10
|
+
Post,
|
|
11
|
+
Put,
|
|
12
|
+
Search,
|
|
13
|
+
} from '@nestjs/common';
|
|
14
|
+
import { ApiTags } from '@nestjs/swagger';
|
|
15
|
+
|
|
16
|
+
@ApiTags('Empty Response')
|
|
17
|
+
@Controller('/empty-response')
|
|
18
|
+
export class EmptyResponseController {
|
|
19
|
+
@Get()
|
|
20
|
+
public async getEmptyReturn(): Promise<void> {
|
|
21
|
+
expect(true).toBe(true);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@Post()
|
|
25
|
+
public async postEmptyReturn(): Promise<void> {
|
|
26
|
+
expect(true).toBe(true);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@Put()
|
|
30
|
+
public async putEmptyReturn(): Promise<void> {
|
|
31
|
+
expect(true).toBe(true);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@Patch()
|
|
35
|
+
public async patchEmptyReturn(): Promise<void> {
|
|
36
|
+
expect(true).toBe(true);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@Options()
|
|
40
|
+
public async optionsEmptyReturn(): Promise<void> {
|
|
41
|
+
expect(true).toBe(true);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Delete()
|
|
45
|
+
public async deleteEmptyReturn(): Promise<void> {
|
|
46
|
+
expect(true).toBe(true);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@Search()
|
|
50
|
+
public async searchEmptyReturn(): Promise<void> {
|
|
51
|
+
expect(true).toBe(true);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@ApiTags('Empty Response All')
|
|
55
|
+
@All('/all')
|
|
56
|
+
public async allEmptyReturn(): Promise<void> {
|
|
57
|
+
expect(true).toBe(true);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './empty-response.controller.js';
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Describe,
|
|
3
|
+
expect,
|
|
4
|
+
Test,
|
|
5
|
+
} from '@hg-ts/tests';
|
|
6
|
+
|
|
7
|
+
import { HttpMethod } from '../../types.js';
|
|
8
|
+
|
|
9
|
+
import { BaseControllerSuite } from '../abstracts/index.js';
|
|
10
|
+
import { HttpMethodsTestModule } from './http-methods.test.module.js';
|
|
11
|
+
|
|
12
|
+
@Describe()
|
|
13
|
+
export class HttpMethodsTest extends BaseControllerSuite {
|
|
14
|
+
public constructor() {
|
|
15
|
+
super(HttpMethodsTestModule);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@Test()
|
|
19
|
+
public async emptyGetTest(): Promise<void> {
|
|
20
|
+
await this.testEmptyResponseTest(HttpMethod.GET);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@Test()
|
|
24
|
+
public async emptyPostTest(): Promise<void> {
|
|
25
|
+
await this.testEmptyResponseTest(HttpMethod.POST);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@Test()
|
|
29
|
+
public async emptyPutTest(): Promise<void> {
|
|
30
|
+
await this.testEmptyResponseTest(HttpMethod.PUT);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@Test()
|
|
34
|
+
public async emptyPatchTest(): Promise<void> {
|
|
35
|
+
await this.testEmptyResponseTest(HttpMethod.PATCH);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@Test()
|
|
39
|
+
public async emptyDeleteTest(): Promise<void> {
|
|
40
|
+
await this.testEmptyResponseTest(HttpMethod.DELETE);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@Test()
|
|
44
|
+
public async emptyOptionsTest(): Promise<void> {
|
|
45
|
+
await this.testEmptyResponseTest(HttpMethod.OPTIONS);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Test()
|
|
49
|
+
public async emptySearchTest(): Promise<void> {
|
|
50
|
+
await this.testEmptyResponseTest(HttpMethod.SEARCH);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@Test()
|
|
54
|
+
public async emptyAllTest(): Promise<void> {
|
|
55
|
+
const methods = Object.values(HttpMethod);
|
|
56
|
+
|
|
57
|
+
await Promise.all(methods.map(async method => this.testEmptyResponseTest(method, '/all')));
|
|
58
|
+
|
|
59
|
+
expect.assertions(methods.length * 2);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
protected override getPort(): number {
|
|
63
|
+
return 50002;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private async testEmptyResponseTest(method: HttpMethod, suffixPath = ''): Promise<void> {
|
|
67
|
+
const response = await this.client.request({
|
|
68
|
+
method,
|
|
69
|
+
url: `/empty-response${suffixPath}`,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(response.data).toBe('');
|
|
73
|
+
expect.assertions(2);
|
|
74
|
+
}
|
|
75
|
+
}
|