@dismissible/nestjs-dismissible 0.0.2-canary.5daf195.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 +490 -0
- package/jest.config.ts +29 -0
- package/package.json +62 -0
- package/project.json +42 -0
- package/src/api/dismissible-item-response.dto.ts +30 -0
- package/src/api/dismissible-item.mapper.spec.ts +51 -0
- package/src/api/dismissible-item.mapper.ts +27 -0
- package/src/api/index.ts +6 -0
- package/src/api/use-cases/api-tags.constants.ts +4 -0
- package/src/api/use-cases/dismiss/dismiss.controller.spec.ts +41 -0
- package/src/api/use-cases/dismiss/dismiss.controller.ts +62 -0
- package/src/api/use-cases/dismiss/dismiss.response.dto.ts +7 -0
- package/src/api/use-cases/dismiss/index.ts +2 -0
- package/src/api/use-cases/get-or-create/get-or-create.controller.spec.ts +36 -0
- package/src/api/use-cases/get-or-create/get-or-create.controller.ts +59 -0
- package/src/api/use-cases/get-or-create/get-or-create.response.dto.ts +7 -0
- package/src/api/use-cases/get-or-create/index.ts +2 -0
- package/src/api/use-cases/index.ts +3 -0
- package/src/api/use-cases/restore/index.ts +2 -0
- package/src/api/use-cases/restore/restore.controller.spec.ts +41 -0
- package/src/api/use-cases/restore/restore.controller.ts +62 -0
- package/src/api/use-cases/restore/restore.response.dto.ts +7 -0
- package/src/api/validation/index.ts +2 -0
- package/src/api/validation/param-validation.pipe.spec.ts +313 -0
- package/src/api/validation/param-validation.pipe.ts +38 -0
- package/src/api/validation/param.decorators.ts +32 -0
- package/src/core/dismissible-core.service.spec.ts +403 -0
- package/src/core/dismissible-core.service.ts +173 -0
- package/src/core/dismissible.service.spec.ts +226 -0
- package/src/core/dismissible.service.ts +227 -0
- package/src/core/hook-runner.service.spec.ts +745 -0
- package/src/core/hook-runner.service.ts +368 -0
- package/src/core/index.ts +5 -0
- package/src/core/lifecycle-hook.interface.ts +7 -0
- package/src/core/service-responses.interface.ts +34 -0
- package/src/dismissible.module.integration.spec.ts +704 -0
- package/src/dismissible.module.ts +82 -0
- package/src/events/dismissible.events.ts +82 -0
- package/src/events/events.constants.ts +21 -0
- package/src/events/index.ts +2 -0
- package/src/exceptions/dismissible.exceptions.spec.ts +50 -0
- package/src/exceptions/dismissible.exceptions.ts +69 -0
- package/src/exceptions/index.ts +1 -0
- package/src/index.ts +8 -0
- package/src/response/dtos/base-response.dto.ts +11 -0
- package/src/response/dtos/error-response.dto.ts +36 -0
- package/src/response/dtos/index.ts +3 -0
- package/src/response/dtos/success-response.dto.ts +34 -0
- package/src/response/http-exception-filter.spec.ts +179 -0
- package/src/response/http-exception-filter.ts +21 -0
- package/src/response/index.ts +4 -0
- package/src/response/response.module.ts +9 -0
- package/src/response/response.service.spec.ts +72 -0
- package/src/response/response.service.ts +20 -0
- package/src/testing/factories.ts +60 -0
- package/src/testing/index.ts +1 -0
- package/src/utils/date/date.service.spec.ts +104 -0
- package/src/utils/date/date.service.ts +19 -0
- package/src/utils/date/index.ts +1 -0
- package/src/utils/dismissible.helper.ts +9 -0
- package/src/utils/index.ts +3 -0
- package/src/validation/dismissible-input.dto.ts +47 -0
- package/src/validation/index.ts +1 -0
- package/tsconfig.json +16 -0
- package/tsconfig.lib.json +14 -0
- package/tsconfig.spec.json +12 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Module, DynamicModule, Type, Provider } from '@nestjs/common';
|
|
2
|
+
import { EventEmitterModule } from '@nestjs/event-emitter';
|
|
3
|
+
import { GetOrCreateController } from './api/use-cases/get-or-create/get-or-create.controller';
|
|
4
|
+
import { DismissController } from './api/use-cases/dismiss/dismiss.controller';
|
|
5
|
+
import { RestoreController } from './api/use-cases/restore/restore.controller';
|
|
6
|
+
import { DismissibleService } from './core/dismissible.service';
|
|
7
|
+
import { DismissibleCoreService } from './core/dismissible-core.service';
|
|
8
|
+
import { HookRunner } from './core/hook-runner.service';
|
|
9
|
+
import { DismissibleItemMapper } from './api/dismissible-item.mapper';
|
|
10
|
+
import { DateService } from './utils/date/date.service';
|
|
11
|
+
import {
|
|
12
|
+
DISMISSIBLE_HOOKS,
|
|
13
|
+
IDismissibleLifecycleHook,
|
|
14
|
+
} from '@dismissible/nestjs-dismissible-hooks';
|
|
15
|
+
import { LoggerModule, IDismissibleLoggerModuleOptions } from '@dismissible/nestjs-logger';
|
|
16
|
+
import { ResponseService, ResponseModule } from './response';
|
|
17
|
+
import { ValidationModule } from '@dismissible/nestjs-validation';
|
|
18
|
+
import { IDismissibleStorageModuleOptions, StorageModule } from '@dismissible/nestjs-storage';
|
|
19
|
+
import { DismissibleHelper } from './utils/dismissible.helper';
|
|
20
|
+
import { DismissibleItemModule } from '@dismissible/nestjs-dismissible-item';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Module configuration options.
|
|
24
|
+
*/
|
|
25
|
+
export type IDismissibleModuleOptions = IDismissibleLoggerModuleOptions &
|
|
26
|
+
IDismissibleStorageModuleOptions & {
|
|
27
|
+
hooks?: Type<IDismissibleLifecycleHook>[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
@Module({})
|
|
31
|
+
export class DismissibleModule {
|
|
32
|
+
static forRoot(options: IDismissibleModuleOptions): DynamicModule {
|
|
33
|
+
const providers: Provider[] = [
|
|
34
|
+
DateService,
|
|
35
|
+
ResponseService,
|
|
36
|
+
DismissibleCoreService,
|
|
37
|
+
HookRunner,
|
|
38
|
+
DismissibleService,
|
|
39
|
+
DismissibleItemMapper,
|
|
40
|
+
DismissibleHelper,
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
if (options.hooks && options.hooks.length > 0) {
|
|
44
|
+
for (const hook of options.hooks) {
|
|
45
|
+
providers.push(hook);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
providers.push({
|
|
49
|
+
provide: DISMISSIBLE_HOOKS,
|
|
50
|
+
useFactory: (...hooks: IDismissibleLifecycleHook[]) => hooks,
|
|
51
|
+
inject: options.hooks,
|
|
52
|
+
});
|
|
53
|
+
} else {
|
|
54
|
+
providers.push({
|
|
55
|
+
provide: DISMISSIBLE_HOOKS,
|
|
56
|
+
useValue: [],
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
module: DismissibleModule,
|
|
62
|
+
imports: [
|
|
63
|
+
EventEmitterModule.forRoot(),
|
|
64
|
+
LoggerModule.forRoot(options),
|
|
65
|
+
ValidationModule,
|
|
66
|
+
ResponseModule,
|
|
67
|
+
options.storage ?? StorageModule,
|
|
68
|
+
DismissibleItemModule,
|
|
69
|
+
],
|
|
70
|
+
controllers: [GetOrCreateController, DismissController, RestoreController],
|
|
71
|
+
providers,
|
|
72
|
+
exports: [
|
|
73
|
+
DismissibleService,
|
|
74
|
+
DismissibleCoreService,
|
|
75
|
+
DismissibleItemMapper,
|
|
76
|
+
DateService,
|
|
77
|
+
ResponseService,
|
|
78
|
+
DISMISSIBLE_HOOKS,
|
|
79
|
+
],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { DismissibleItemDto } from '@dismissible/nestjs-dismissible-item';
|
|
2
|
+
import { IRequestContext } from '@dismissible/nestjs-dismissible-request';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Base class for all dismissible events.
|
|
6
|
+
*/
|
|
7
|
+
abstract class BaseDismissibleEvent {
|
|
8
|
+
/** The item identifier */
|
|
9
|
+
readonly id: string;
|
|
10
|
+
|
|
11
|
+
/** The current state of the item */
|
|
12
|
+
readonly item: DismissibleItemDto;
|
|
13
|
+
|
|
14
|
+
/** The user identifier */
|
|
15
|
+
readonly userId: string;
|
|
16
|
+
|
|
17
|
+
/** The request context (optional) */
|
|
18
|
+
readonly context?: IRequestContext;
|
|
19
|
+
|
|
20
|
+
constructor(itemId: string, item: DismissibleItemDto, userId: string, context?: IRequestContext) {
|
|
21
|
+
this.id = itemId;
|
|
22
|
+
this.item = item;
|
|
23
|
+
this.userId = userId;
|
|
24
|
+
this.context = context;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Event emitted when an existing item is retrieved.
|
|
30
|
+
*/
|
|
31
|
+
export class ItemRetrievedEvent extends BaseDismissibleEvent {
|
|
32
|
+
constructor(itemId: string, item: DismissibleItemDto, userId: string, context?: IRequestContext) {
|
|
33
|
+
super(itemId, item, userId, context);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Event emitted when a new item is created.
|
|
39
|
+
*/
|
|
40
|
+
export class ItemCreatedEvent extends BaseDismissibleEvent {
|
|
41
|
+
constructor(itemId: string, item: DismissibleItemDto, userId: string, context?: IRequestContext) {
|
|
42
|
+
super(itemId, item, userId, context);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Event emitted when an item is dismissed.
|
|
48
|
+
*/
|
|
49
|
+
export class ItemDismissedEvent extends BaseDismissibleEvent {
|
|
50
|
+
/** The item state before dismissal */
|
|
51
|
+
readonly previousItem: DismissibleItemDto;
|
|
52
|
+
|
|
53
|
+
constructor(
|
|
54
|
+
itemId: string,
|
|
55
|
+
item: DismissibleItemDto,
|
|
56
|
+
previousItem: DismissibleItemDto,
|
|
57
|
+
userId: string,
|
|
58
|
+
context?: IRequestContext,
|
|
59
|
+
) {
|
|
60
|
+
super(itemId, item, userId, context);
|
|
61
|
+
this.previousItem = previousItem;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Event emitted when a dismissed item is restored.
|
|
67
|
+
*/
|
|
68
|
+
export class ItemRestoredEvent extends BaseDismissibleEvent {
|
|
69
|
+
/** The item state before restoration */
|
|
70
|
+
readonly previousItem: DismissibleItemDto;
|
|
71
|
+
|
|
72
|
+
constructor(
|
|
73
|
+
itemId: string,
|
|
74
|
+
item: DismissibleItemDto,
|
|
75
|
+
previousItem: DismissibleItemDto,
|
|
76
|
+
userId: string,
|
|
77
|
+
context?: IRequestContext,
|
|
78
|
+
) {
|
|
79
|
+
super(itemId, item, userId, context);
|
|
80
|
+
this.previousItem = previousItem;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event names for dismissible operations.
|
|
3
|
+
*/
|
|
4
|
+
export const DismissibleEvents = {
|
|
5
|
+
/** Emitted when an existing item is retrieved */
|
|
6
|
+
ITEM_RETRIEVED: 'dismissible.item.retrieved',
|
|
7
|
+
|
|
8
|
+
/** Emitted when a new item is created */
|
|
9
|
+
ITEM_CREATED: 'dismissible.item.created',
|
|
10
|
+
|
|
11
|
+
/** Emitted when an item is dismissed */
|
|
12
|
+
ITEM_DISMISSED: 'dismissible.item.dismissed',
|
|
13
|
+
|
|
14
|
+
/** Emitted when a dismissed item is restored */
|
|
15
|
+
ITEM_RESTORED: 'dismissible.item.restored',
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Type representing all dismissible event names.
|
|
20
|
+
*/
|
|
21
|
+
export type DismissibleEventType = (typeof DismissibleEvents)[keyof typeof DismissibleEvents];
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { HttpStatus } from '@nestjs/common';
|
|
2
|
+
import {
|
|
3
|
+
ItemNotFoundException,
|
|
4
|
+
ItemAlreadyDismissedException,
|
|
5
|
+
ItemNotDismissedException,
|
|
6
|
+
} from './dismissible.exceptions';
|
|
7
|
+
|
|
8
|
+
describe('Dismissible Exceptions', () => {
|
|
9
|
+
describe('ItemNotFoundException', () => {
|
|
10
|
+
it('should create exception with correct structure', () => {
|
|
11
|
+
const exception = new ItemNotFoundException('test-item');
|
|
12
|
+
|
|
13
|
+
expect(exception.getStatus()).toBe(HttpStatus.BAD_REQUEST);
|
|
14
|
+
|
|
15
|
+
const response = exception.getResponse() as any;
|
|
16
|
+
expect(response.statusCode).toBe(HttpStatus.BAD_REQUEST);
|
|
17
|
+
expect(response.code).toBe('ITEM_NOT_FOUND');
|
|
18
|
+
expect(response.message).toContain('test-item');
|
|
19
|
+
expect(response.itemId).toBe('test-item');
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('ItemAlreadyDismissedException', () => {
|
|
24
|
+
it('should create exception with correct structure', () => {
|
|
25
|
+
const exception = new ItemAlreadyDismissedException('test-item');
|
|
26
|
+
|
|
27
|
+
expect(exception.getStatus()).toBe(HttpStatus.BAD_REQUEST);
|
|
28
|
+
|
|
29
|
+
const response = exception.getResponse() as any;
|
|
30
|
+
expect(response.statusCode).toBe(HttpStatus.BAD_REQUEST);
|
|
31
|
+
expect(response.code).toBe('ITEM_ALREADY_DISMISSED');
|
|
32
|
+
expect(response.message).toContain('test-item');
|
|
33
|
+
expect(response.itemId).toBe('test-item');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('ItemNotDismissedException', () => {
|
|
38
|
+
it('should create exception with correct structure', () => {
|
|
39
|
+
const exception = new ItemNotDismissedException('test-item');
|
|
40
|
+
|
|
41
|
+
expect(exception.getStatus()).toBe(HttpStatus.BAD_REQUEST);
|
|
42
|
+
|
|
43
|
+
const response = exception.getResponse() as any;
|
|
44
|
+
expect(response.statusCode).toBe(HttpStatus.BAD_REQUEST);
|
|
45
|
+
expect(response.code).toBe('ITEM_NOT_DISMISSED');
|
|
46
|
+
expect(response.message).toContain('test-item');
|
|
47
|
+
expect(response.itemId).toBe('test-item');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { HttpException, HttpStatus } from '@nestjs/common';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Error response structure for dismissible exceptions.
|
|
5
|
+
*/
|
|
6
|
+
interface IDismissibleErrorResponse {
|
|
7
|
+
statusCode: number;
|
|
8
|
+
error: string;
|
|
9
|
+
code: string;
|
|
10
|
+
message: string;
|
|
11
|
+
itemId?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Base exception class for dismissible errors.
|
|
16
|
+
*/
|
|
17
|
+
abstract class DismissibleException extends HttpException {
|
|
18
|
+
constructor(message: string, code: string, statusCode: HttpStatus, itemId?: string) {
|
|
19
|
+
const response: IDismissibleErrorResponse = {
|
|
20
|
+
statusCode,
|
|
21
|
+
error: HttpStatus[statusCode].replace(/_/g, ' '),
|
|
22
|
+
code,
|
|
23
|
+
message,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
if (itemId) {
|
|
27
|
+
response.itemId = itemId;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
super(response, statusCode);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Exception thrown when an item is not found.
|
|
36
|
+
*/
|
|
37
|
+
export class ItemNotFoundException extends DismissibleException {
|
|
38
|
+
constructor(itemId: string) {
|
|
39
|
+
super(`Item with id "${itemId}" not found`, 'ITEM_NOT_FOUND', HttpStatus.BAD_REQUEST, itemId);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Exception thrown when trying to dismiss an already dismissed item.
|
|
45
|
+
*/
|
|
46
|
+
export class ItemAlreadyDismissedException extends DismissibleException {
|
|
47
|
+
constructor(itemId: string) {
|
|
48
|
+
super(
|
|
49
|
+
`Item with id "${itemId}" is already dismissed`,
|
|
50
|
+
'ITEM_ALREADY_DISMISSED',
|
|
51
|
+
HttpStatus.BAD_REQUEST,
|
|
52
|
+
itemId,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Exception thrown when trying to restore an item that is not dismissed.
|
|
59
|
+
*/
|
|
60
|
+
export class ItemNotDismissedException extends DismissibleException {
|
|
61
|
+
constructor(itemId: string) {
|
|
62
|
+
super(
|
|
63
|
+
`Item with id "${itemId}" is not dismissed`,
|
|
64
|
+
'ITEM_NOT_DISMISSED',
|
|
65
|
+
HttpStatus.BAD_REQUEST,
|
|
66
|
+
itemId,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './dismissible.exceptions';
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ApiProperty } from '@nestjs/swagger';
|
|
2
|
+
|
|
3
|
+
export type IErrorResponseDto = {
|
|
4
|
+
error: {
|
|
5
|
+
message: string;
|
|
6
|
+
code: number;
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* DTO representing error details in API responses
|
|
12
|
+
*/
|
|
13
|
+
export class ErrorDetailsDto {
|
|
14
|
+
@ApiProperty({
|
|
15
|
+
description: 'Error message',
|
|
16
|
+
example: 'Item not found',
|
|
17
|
+
})
|
|
18
|
+
message: string;
|
|
19
|
+
|
|
20
|
+
@ApiProperty({
|
|
21
|
+
description: 'HTTP status code',
|
|
22
|
+
example: 404,
|
|
23
|
+
})
|
|
24
|
+
code: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* DTO for error responses
|
|
29
|
+
*/
|
|
30
|
+
export class ErrorResponseDto implements IErrorResponseDto {
|
|
31
|
+
@ApiProperty({
|
|
32
|
+
description: 'Error details',
|
|
33
|
+
type: ErrorDetailsDto,
|
|
34
|
+
})
|
|
35
|
+
error: ErrorDetailsDto;
|
|
36
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Type } from '@nestjs/common';
|
|
2
|
+
import { ApiProperty } from '@nestjs/swagger';
|
|
3
|
+
|
|
4
|
+
export type ISuccessResponseDto<T> = {
|
|
5
|
+
data: T;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Factory function to create a success response DTO wrapper.
|
|
10
|
+
*
|
|
11
|
+
* This allows wrapping any data class in a consistent `{ data: T }` structure
|
|
12
|
+
* with proper Swagger documentation.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* class UserDto {
|
|
17
|
+
* @ApiProperty()
|
|
18
|
+
* id: string;
|
|
19
|
+
*
|
|
20
|
+
* @ApiProperty()
|
|
21
|
+
* name: string;
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* export class GetUserResponseDto extends SuccessResponseDto(UserDto) {}
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function SuccessResponseDto<T extends Type>(dataClass: T) {
|
|
28
|
+
class SuccessResponse implements ISuccessResponseDto<T> {
|
|
29
|
+
@ApiProperty({ type: dataClass })
|
|
30
|
+
data!: InstanceType<T>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return SuccessResponse;
|
|
34
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { mock } from 'ts-jest-mocker';
|
|
2
|
+
import { HttpException, HttpStatus, ArgumentsHost } from '@nestjs/common';
|
|
3
|
+
import { FastifyReply } from 'fastify';
|
|
4
|
+
import { HttpExceptionFilter } from './http-exception-filter';
|
|
5
|
+
|
|
6
|
+
describe('HttpExceptionFilter', () => {
|
|
7
|
+
let filter: HttpExceptionFilter;
|
|
8
|
+
let mockArgumentsHost: jest.Mocked<ArgumentsHost>;
|
|
9
|
+
let mockHttpArgumentsHost: {
|
|
10
|
+
getResponse: jest.Mock;
|
|
11
|
+
getRequest: jest.Mock;
|
|
12
|
+
getNext: jest.Mock;
|
|
13
|
+
};
|
|
14
|
+
let mockResponse: jest.Mocked<FastifyReply>;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
filter = new HttpExceptionFilter();
|
|
18
|
+
|
|
19
|
+
mockResponse = mock<FastifyReply>({ failIfMockNotProvided: false });
|
|
20
|
+
mockResponse.status = jest.fn().mockReturnThis();
|
|
21
|
+
mockResponse.send = jest.fn().mockReturnThis();
|
|
22
|
+
|
|
23
|
+
mockHttpArgumentsHost = {
|
|
24
|
+
getResponse: jest.fn().mockReturnValue(mockResponse),
|
|
25
|
+
getRequest: jest.fn(),
|
|
26
|
+
getNext: jest.fn(),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
mockArgumentsHost = mock<ArgumentsHost>({ failIfMockNotProvided: false });
|
|
30
|
+
mockArgumentsHost.switchToHttp = jest.fn().mockReturnValue(mockHttpArgumentsHost);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
jest.clearAllMocks();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('catch', () => {
|
|
38
|
+
it('should handle HttpException with 404 status', () => {
|
|
39
|
+
const exception = new HttpException('Not found', HttpStatus.NOT_FOUND);
|
|
40
|
+
|
|
41
|
+
filter.catch(exception, mockArgumentsHost);
|
|
42
|
+
|
|
43
|
+
expect(mockArgumentsHost.switchToHttp).toHaveBeenCalled();
|
|
44
|
+
expect(mockHttpArgumentsHost.getResponse).toHaveBeenCalled();
|
|
45
|
+
expect(mockResponse.status).toHaveBeenCalledWith(404);
|
|
46
|
+
expect(mockResponse.send).toHaveBeenCalledWith({
|
|
47
|
+
error: {
|
|
48
|
+
message: 'Not found',
|
|
49
|
+
code: 404,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should handle HttpException with 400 status', () => {
|
|
55
|
+
const exception = new HttpException('Bad request', HttpStatus.BAD_REQUEST);
|
|
56
|
+
|
|
57
|
+
filter.catch(exception, mockArgumentsHost);
|
|
58
|
+
|
|
59
|
+
expect(mockResponse.status).toHaveBeenCalledWith(400);
|
|
60
|
+
expect(mockResponse.send).toHaveBeenCalledWith({
|
|
61
|
+
error: {
|
|
62
|
+
message: 'Bad request',
|
|
63
|
+
code: 400,
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should handle HttpException with 401 status', () => {
|
|
69
|
+
const exception = new HttpException('Unauthorized', HttpStatus.UNAUTHORIZED);
|
|
70
|
+
|
|
71
|
+
filter.catch(exception, mockArgumentsHost);
|
|
72
|
+
|
|
73
|
+
expect(mockResponse.status).toHaveBeenCalledWith(401);
|
|
74
|
+
expect(mockResponse.send).toHaveBeenCalledWith({
|
|
75
|
+
error: {
|
|
76
|
+
message: 'Unauthorized',
|
|
77
|
+
code: 401,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should handle HttpException with 403 status', () => {
|
|
83
|
+
const exception = new HttpException('Forbidden', HttpStatus.FORBIDDEN);
|
|
84
|
+
|
|
85
|
+
filter.catch(exception, mockArgumentsHost);
|
|
86
|
+
|
|
87
|
+
expect(mockResponse.status).toHaveBeenCalledWith(403);
|
|
88
|
+
expect(mockResponse.send).toHaveBeenCalledWith({
|
|
89
|
+
error: {
|
|
90
|
+
message: 'Forbidden',
|
|
91
|
+
code: 403,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should handle HttpException with 500 status', () => {
|
|
97
|
+
const exception = new HttpException(
|
|
98
|
+
'Internal server error',
|
|
99
|
+
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
filter.catch(exception, mockArgumentsHost);
|
|
103
|
+
|
|
104
|
+
expect(mockResponse.status).toHaveBeenCalledWith(500);
|
|
105
|
+
expect(mockResponse.send).toHaveBeenCalledWith({
|
|
106
|
+
error: {
|
|
107
|
+
message: 'Internal server error',
|
|
108
|
+
code: 500,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should handle HttpException with custom status code', () => {
|
|
114
|
+
const exception = new HttpException('Custom error', 418);
|
|
115
|
+
|
|
116
|
+
filter.catch(exception, mockArgumentsHost);
|
|
117
|
+
|
|
118
|
+
expect(mockResponse.status).toHaveBeenCalledWith(418);
|
|
119
|
+
expect(mockResponse.send).toHaveBeenCalledWith({
|
|
120
|
+
error: {
|
|
121
|
+
message: 'Custom error',
|
|
122
|
+
code: 418,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should handle HttpException with empty message', () => {
|
|
128
|
+
const exception = new HttpException('', HttpStatus.BAD_REQUEST);
|
|
129
|
+
|
|
130
|
+
filter.catch(exception, mockArgumentsHost);
|
|
131
|
+
|
|
132
|
+
expect(mockResponse.status).toHaveBeenCalledWith(400);
|
|
133
|
+
expect(mockResponse.send).toHaveBeenCalledWith({
|
|
134
|
+
error: {
|
|
135
|
+
message: '',
|
|
136
|
+
code: 400,
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should handle HttpException with long message', () => {
|
|
142
|
+
const longMessage =
|
|
143
|
+
'This is a very long error message that contains multiple words and should be handled correctly by the filter';
|
|
144
|
+
const exception = new HttpException(longMessage, HttpStatus.BAD_REQUEST);
|
|
145
|
+
|
|
146
|
+
filter.catch(exception, mockArgumentsHost);
|
|
147
|
+
|
|
148
|
+
expect(mockResponse.status).toHaveBeenCalledWith(400);
|
|
149
|
+
expect(mockResponse.send).toHaveBeenCalledWith({
|
|
150
|
+
error: {
|
|
151
|
+
message: longMessage,
|
|
152
|
+
code: 400,
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should chain status and send methods correctly', () => {
|
|
158
|
+
const exception = new HttpException('Test error', HttpStatus.NOT_FOUND);
|
|
159
|
+
|
|
160
|
+
filter.catch(exception, mockArgumentsHost);
|
|
161
|
+
|
|
162
|
+
expect(mockResponse.status).toHaveBeenCalled();
|
|
163
|
+
expect(mockResponse.send).toHaveBeenCalled();
|
|
164
|
+
expect(mockResponse.status).toHaveReturnedWith(mockResponse);
|
|
165
|
+
const statusCallOrder = (mockResponse.status as jest.Mock).mock.invocationCallOrder[0];
|
|
166
|
+
const sendCallOrder = (mockResponse.send as jest.Mock).mock.invocationCallOrder[0];
|
|
167
|
+
expect(statusCallOrder).toBeLessThan(sendCallOrder);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should extract response from HTTP context correctly', () => {
|
|
171
|
+
const exception = new HttpException('Test', HttpStatus.BAD_REQUEST);
|
|
172
|
+
|
|
173
|
+
filter.catch(exception, mockArgumentsHost);
|
|
174
|
+
|
|
175
|
+
expect(mockArgumentsHost.switchToHttp).toHaveBeenCalledTimes(1);
|
|
176
|
+
expect(mockHttpArgumentsHost.getResponse).toHaveBeenCalledTimes(1);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
|
|
2
|
+
import { FastifyReply } from 'fastify';
|
|
3
|
+
import { IErrorResponseDto } from './dtos';
|
|
4
|
+
|
|
5
|
+
@Catch(HttpException)
|
|
6
|
+
export class HttpExceptionFilter implements ExceptionFilter {
|
|
7
|
+
catch(exception: HttpException, host: ArgumentsHost) {
|
|
8
|
+
const ctx = host.switchToHttp();
|
|
9
|
+
const response = ctx.getResponse<FastifyReply>();
|
|
10
|
+
const status = exception.getStatus();
|
|
11
|
+
|
|
12
|
+
const errorResponse: IErrorResponseDto = {
|
|
13
|
+
error: {
|
|
14
|
+
message: exception.message,
|
|
15
|
+
code: status,
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
response.status(status).send(errorResponse);
|
|
20
|
+
}
|
|
21
|
+
}
|