@dismissible/nestjs-dismissible 0.0.2-canary.c91edbc.0 → 0.0.2-canary.d2f56d7.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 +51 -67
- package/package.json +3 -3
- package/src/api/dismissible-item-response.dto.ts +0 -8
- package/src/api/dismissible-item.mapper.spec.ts +0 -12
- package/src/api/dismissible-item.mapper.ts +2 -8
- package/src/api/index.ts +3 -0
- package/src/api/use-cases/dismiss/dismiss.controller.spec.ts +1 -2
- package/src/api/use-cases/dismiss/dismiss.controller.ts +8 -8
- package/src/api/use-cases/get-or-create/get-or-create.controller.spec.ts +2 -42
- package/src/api/use-cases/get-or-create/get-or-create.controller.ts +10 -56
- package/src/api/use-cases/get-or-create/index.ts +0 -1
- package/src/api/use-cases/restore/restore.controller.spec.ts +1 -2
- package/src/api/use-cases/restore/restore.controller.ts +8 -8
- package/src/api/validation/index.ts +2 -0
- package/src/api/validation/param-validation.pipe.spec.ts +317 -0
- package/src/api/validation/param-validation.pipe.ts +42 -0
- package/src/api/validation/param.decorators.ts +32 -0
- package/src/core/dismissible-core.service.spec.ts +3 -45
- package/src/core/dismissible-core.service.ts +10 -27
- package/src/core/dismissible.service.spec.ts +23 -16
- package/src/core/dismissible.service.ts +28 -11
- package/src/core/hook-runner.service.spec.ts +369 -19
- package/src/core/hook-runner.service.ts +17 -17
- package/src/core/index.ts +0 -1
- package/src/core/lifecycle-hook.interface.ts +8 -8
- package/src/core/service-responses.interface.ts +9 -9
- package/src/dismissible.module.integration.spec.ts +685 -0
- package/src/dismissible.module.ts +6 -10
- package/src/events/dismissible.events.ts +16 -39
- package/src/index.ts +1 -0
- package/src/request/request-context.decorator.ts +1 -0
- package/src/request/request-context.interface.ts +6 -0
- package/src/response/http-exception-filter.spec.ts +213 -0
- package/src/testing/factories.ts +5 -8
- package/src/utils/dismissible.helper.ts +2 -2
- package/src/validation/dismissible-input.dto.ts +47 -0
- package/src/validation/index.ts +1 -0
- package/tsconfig.json +3 -0
- package/tsconfig.spec.json +12 -0
- package/src/api/use-cases/get-or-create/get-or-create.request.dto.ts +0 -17
- package/src/core/create-options.ts +0 -9
|
@@ -10,7 +10,6 @@ import { DismissibleItemMapper } from './api/dismissible-item.mapper';
|
|
|
10
10
|
import { DateService } from './utils/date/date.service';
|
|
11
11
|
import { DISMISSIBLE_HOOKS, IDismissibleLifecycleHook } from './core/lifecycle-hook.interface';
|
|
12
12
|
import { LoggerModule, IDismissibleLoggerModuleOptions } from '@dismissible/nestjs-logger';
|
|
13
|
-
import { BaseMetadata } from '@dismissible/nestjs-dismissible-item';
|
|
14
13
|
import { ResponseService, ResponseModule } from './response';
|
|
15
14
|
import { ValidationModule } from '@dismissible/nestjs-validation';
|
|
16
15
|
import { IDismissibleStorageModuleOptions, StorageModule } from '@dismissible/nestjs-storage';
|
|
@@ -20,17 +19,14 @@ import { DismissibleItemModule } from '@dismissible/nestjs-dismissible-item';
|
|
|
20
19
|
/**
|
|
21
20
|
* Module configuration options.
|
|
22
21
|
*/
|
|
23
|
-
export type IDismissibleModuleOptions
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
};
|
|
22
|
+
export type IDismissibleModuleOptions = IDismissibleLoggerModuleOptions &
|
|
23
|
+
IDismissibleStorageModuleOptions & {
|
|
24
|
+
hooks?: Type<IDismissibleLifecycleHook>[];
|
|
25
|
+
};
|
|
28
26
|
|
|
29
27
|
@Module({})
|
|
30
28
|
export class DismissibleModule {
|
|
31
|
-
static forRoot
|
|
32
|
-
options: IDismissibleModuleOptions<TMetadata>,
|
|
33
|
-
): DynamicModule {
|
|
29
|
+
static forRoot(options: IDismissibleModuleOptions): DynamicModule {
|
|
34
30
|
const providers: Provider[] = [
|
|
35
31
|
DateService,
|
|
36
32
|
ResponseService,
|
|
@@ -48,7 +44,7 @@ export class DismissibleModule {
|
|
|
48
44
|
|
|
49
45
|
providers.push({
|
|
50
46
|
provide: DISMISSIBLE_HOOKS,
|
|
51
|
-
useFactory: (...hooks: IDismissibleLifecycleHook
|
|
47
|
+
useFactory: (...hooks: IDismissibleLifecycleHook[]) => hooks,
|
|
52
48
|
inject: options.hooks,
|
|
53
49
|
});
|
|
54
50
|
} else {
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DismissibleItemDto } from '@dismissible/nestjs-dismissible-item';
|
|
2
2
|
import { IRequestContext } from '../request/request-context.interface';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Base class for all dismissible events.
|
|
6
6
|
*/
|
|
7
|
-
abstract class BaseDismissibleEvent
|
|
7
|
+
abstract class BaseDismissibleEvent {
|
|
8
8
|
/** The item identifier */
|
|
9
9
|
readonly id: string;
|
|
10
10
|
|
|
11
11
|
/** The current state of the item */
|
|
12
|
-
readonly item: DismissibleItemDto
|
|
12
|
+
readonly item: DismissibleItemDto;
|
|
13
13
|
|
|
14
14
|
/** The user identifier */
|
|
15
15
|
readonly userId: string;
|
|
@@ -17,12 +17,7 @@ abstract class BaseDismissibleEvent<TMetadata extends BaseMetadata = BaseMetadat
|
|
|
17
17
|
/** The request context (optional) */
|
|
18
18
|
readonly context?: IRequestContext;
|
|
19
19
|
|
|
20
|
-
constructor(
|
|
21
|
-
itemId: string,
|
|
22
|
-
item: DismissibleItemDto<TMetadata>,
|
|
23
|
-
userId: string,
|
|
24
|
-
context?: IRequestContext,
|
|
25
|
-
) {
|
|
20
|
+
constructor(itemId: string, item: DismissibleItemDto, userId: string, context?: IRequestContext) {
|
|
26
21
|
this.id = itemId;
|
|
27
22
|
this.item = item;
|
|
28
23
|
this.userId = userId;
|
|
@@ -33,15 +28,8 @@ abstract class BaseDismissibleEvent<TMetadata extends BaseMetadata = BaseMetadat
|
|
|
33
28
|
/**
|
|
34
29
|
* Event emitted when an existing item is retrieved.
|
|
35
30
|
*/
|
|
36
|
-
export class ItemRetrievedEvent
|
|
37
|
-
|
|
38
|
-
> extends BaseDismissibleEvent<TMetadata> {
|
|
39
|
-
constructor(
|
|
40
|
-
itemId: string,
|
|
41
|
-
item: DismissibleItemDto<TMetadata>,
|
|
42
|
-
userId: string,
|
|
43
|
-
context?: IRequestContext,
|
|
44
|
-
) {
|
|
31
|
+
export class ItemRetrievedEvent extends BaseDismissibleEvent {
|
|
32
|
+
constructor(itemId: string, item: DismissibleItemDto, userId: string, context?: IRequestContext) {
|
|
45
33
|
super(itemId, item, userId, context);
|
|
46
34
|
}
|
|
47
35
|
}
|
|
@@ -49,15 +37,8 @@ export class ItemRetrievedEvent<
|
|
|
49
37
|
/**
|
|
50
38
|
* Event emitted when a new item is created.
|
|
51
39
|
*/
|
|
52
|
-
export class ItemCreatedEvent
|
|
53
|
-
|
|
54
|
-
> extends BaseDismissibleEvent<TMetadata> {
|
|
55
|
-
constructor(
|
|
56
|
-
itemId: string,
|
|
57
|
-
item: DismissibleItemDto<TMetadata>,
|
|
58
|
-
userId: string,
|
|
59
|
-
context?: IRequestContext,
|
|
60
|
-
) {
|
|
40
|
+
export class ItemCreatedEvent extends BaseDismissibleEvent {
|
|
41
|
+
constructor(itemId: string, item: DismissibleItemDto, userId: string, context?: IRequestContext) {
|
|
61
42
|
super(itemId, item, userId, context);
|
|
62
43
|
}
|
|
63
44
|
}
|
|
@@ -65,16 +46,14 @@ export class ItemCreatedEvent<
|
|
|
65
46
|
/**
|
|
66
47
|
* Event emitted when an item is dismissed.
|
|
67
48
|
*/
|
|
68
|
-
export class ItemDismissedEvent
|
|
69
|
-
TMetadata extends BaseMetadata = BaseMetadata,
|
|
70
|
-
> extends BaseDismissibleEvent<TMetadata> {
|
|
49
|
+
export class ItemDismissedEvent extends BaseDismissibleEvent {
|
|
71
50
|
/** The item state before dismissal */
|
|
72
|
-
readonly previousItem: DismissibleItemDto
|
|
51
|
+
readonly previousItem: DismissibleItemDto;
|
|
73
52
|
|
|
74
53
|
constructor(
|
|
75
54
|
itemId: string,
|
|
76
|
-
item: DismissibleItemDto
|
|
77
|
-
previousItem: DismissibleItemDto
|
|
55
|
+
item: DismissibleItemDto,
|
|
56
|
+
previousItem: DismissibleItemDto,
|
|
78
57
|
userId: string,
|
|
79
58
|
context?: IRequestContext,
|
|
80
59
|
) {
|
|
@@ -86,16 +65,14 @@ export class ItemDismissedEvent<
|
|
|
86
65
|
/**
|
|
87
66
|
* Event emitted when a dismissed item is restored.
|
|
88
67
|
*/
|
|
89
|
-
export class ItemRestoredEvent
|
|
90
|
-
TMetadata extends BaseMetadata = BaseMetadata,
|
|
91
|
-
> extends BaseDismissibleEvent<TMetadata> {
|
|
68
|
+
export class ItemRestoredEvent extends BaseDismissibleEvent {
|
|
92
69
|
/** The item state before restoration */
|
|
93
|
-
readonly previousItem: DismissibleItemDto
|
|
70
|
+
readonly previousItem: DismissibleItemDto;
|
|
94
71
|
|
|
95
72
|
constructor(
|
|
96
73
|
itemId: string,
|
|
97
|
-
item: DismissibleItemDto
|
|
98
|
-
previousItem: DismissibleItemDto
|
|
74
|
+
item: DismissibleItemDto,
|
|
75
|
+
previousItem: DismissibleItemDto,
|
|
99
76
|
userId: string,
|
|
100
77
|
context?: IRequestContext,
|
|
101
78
|
) {
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,213 @@
|
|
|
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
|
+
// Mock FastifyReply using ts-jest-mocker
|
|
20
|
+
mockResponse = mock<FastifyReply>({ failIfMockNotProvided: false });
|
|
21
|
+
mockResponse.status = jest.fn().mockReturnThis();
|
|
22
|
+
mockResponse.send = jest.fn().mockReturnThis();
|
|
23
|
+
|
|
24
|
+
// Mock HTTP arguments host
|
|
25
|
+
mockHttpArgumentsHost = {
|
|
26
|
+
getResponse: jest.fn().mockReturnValue(mockResponse),
|
|
27
|
+
getRequest: jest.fn(),
|
|
28
|
+
getNext: jest.fn(),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Mock ArgumentsHost using ts-jest-mocker
|
|
32
|
+
mockArgumentsHost = mock<ArgumentsHost>({ failIfMockNotProvided: false });
|
|
33
|
+
mockArgumentsHost.switchToHttp = jest.fn().mockReturnValue(mockHttpArgumentsHost);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
jest.clearAllMocks();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('catch', () => {
|
|
41
|
+
it('should handle HttpException with 404 status', () => {
|
|
42
|
+
// Arrange
|
|
43
|
+
const exception = new HttpException('Not found', HttpStatus.NOT_FOUND);
|
|
44
|
+
|
|
45
|
+
// Act
|
|
46
|
+
filter.catch(exception, mockArgumentsHost);
|
|
47
|
+
|
|
48
|
+
// Assert
|
|
49
|
+
expect(mockArgumentsHost.switchToHttp).toHaveBeenCalled();
|
|
50
|
+
expect(mockHttpArgumentsHost.getResponse).toHaveBeenCalled();
|
|
51
|
+
expect(mockResponse.status).toHaveBeenCalledWith(404);
|
|
52
|
+
expect(mockResponse.send).toHaveBeenCalledWith({
|
|
53
|
+
error: {
|
|
54
|
+
message: 'Not found',
|
|
55
|
+
code: 404,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should handle HttpException with 400 status', () => {
|
|
61
|
+
// Arrange
|
|
62
|
+
const exception = new HttpException('Bad request', HttpStatus.BAD_REQUEST);
|
|
63
|
+
|
|
64
|
+
// Act
|
|
65
|
+
filter.catch(exception, mockArgumentsHost);
|
|
66
|
+
|
|
67
|
+
// Assert
|
|
68
|
+
expect(mockResponse.status).toHaveBeenCalledWith(400);
|
|
69
|
+
expect(mockResponse.send).toHaveBeenCalledWith({
|
|
70
|
+
error: {
|
|
71
|
+
message: 'Bad request',
|
|
72
|
+
code: 400,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should handle HttpException with 401 status', () => {
|
|
78
|
+
// Arrange
|
|
79
|
+
const exception = new HttpException('Unauthorized', HttpStatus.UNAUTHORIZED);
|
|
80
|
+
|
|
81
|
+
// Act
|
|
82
|
+
filter.catch(exception, mockArgumentsHost);
|
|
83
|
+
|
|
84
|
+
// Assert
|
|
85
|
+
expect(mockResponse.status).toHaveBeenCalledWith(401);
|
|
86
|
+
expect(mockResponse.send).toHaveBeenCalledWith({
|
|
87
|
+
error: {
|
|
88
|
+
message: 'Unauthorized',
|
|
89
|
+
code: 401,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should handle HttpException with 403 status', () => {
|
|
95
|
+
// Arrange
|
|
96
|
+
const exception = new HttpException('Forbidden', HttpStatus.FORBIDDEN);
|
|
97
|
+
|
|
98
|
+
// Act
|
|
99
|
+
filter.catch(exception, mockArgumentsHost);
|
|
100
|
+
|
|
101
|
+
// Assert
|
|
102
|
+
expect(mockResponse.status).toHaveBeenCalledWith(403);
|
|
103
|
+
expect(mockResponse.send).toHaveBeenCalledWith({
|
|
104
|
+
error: {
|
|
105
|
+
message: 'Forbidden',
|
|
106
|
+
code: 403,
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should handle HttpException with 500 status', () => {
|
|
112
|
+
// Arrange
|
|
113
|
+
const exception = new HttpException(
|
|
114
|
+
'Internal server error',
|
|
115
|
+
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
// Act
|
|
119
|
+
filter.catch(exception, mockArgumentsHost);
|
|
120
|
+
|
|
121
|
+
// Assert
|
|
122
|
+
expect(mockResponse.status).toHaveBeenCalledWith(500);
|
|
123
|
+
expect(mockResponse.send).toHaveBeenCalledWith({
|
|
124
|
+
error: {
|
|
125
|
+
message: 'Internal server error',
|
|
126
|
+
code: 500,
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should handle HttpException with custom status code', () => {
|
|
132
|
+
// Arrange
|
|
133
|
+
const exception = new HttpException('Custom error', 418);
|
|
134
|
+
|
|
135
|
+
// Act
|
|
136
|
+
filter.catch(exception, mockArgumentsHost);
|
|
137
|
+
|
|
138
|
+
// Assert
|
|
139
|
+
expect(mockResponse.status).toHaveBeenCalledWith(418);
|
|
140
|
+
expect(mockResponse.send).toHaveBeenCalledWith({
|
|
141
|
+
error: {
|
|
142
|
+
message: 'Custom error',
|
|
143
|
+
code: 418,
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should handle HttpException with empty message', () => {
|
|
149
|
+
// Arrange
|
|
150
|
+
const exception = new HttpException('', HttpStatus.BAD_REQUEST);
|
|
151
|
+
|
|
152
|
+
// Act
|
|
153
|
+
filter.catch(exception, mockArgumentsHost);
|
|
154
|
+
|
|
155
|
+
// Assert
|
|
156
|
+
expect(mockResponse.status).toHaveBeenCalledWith(400);
|
|
157
|
+
expect(mockResponse.send).toHaveBeenCalledWith({
|
|
158
|
+
error: {
|
|
159
|
+
message: '',
|
|
160
|
+
code: 400,
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should handle HttpException with long message', () => {
|
|
166
|
+
// Arrange
|
|
167
|
+
const longMessage =
|
|
168
|
+
'This is a very long error message that contains multiple words and should be handled correctly by the filter';
|
|
169
|
+
const exception = new HttpException(longMessage, HttpStatus.BAD_REQUEST);
|
|
170
|
+
|
|
171
|
+
// Act
|
|
172
|
+
filter.catch(exception, mockArgumentsHost);
|
|
173
|
+
|
|
174
|
+
// Assert
|
|
175
|
+
expect(mockResponse.status).toHaveBeenCalledWith(400);
|
|
176
|
+
expect(mockResponse.send).toHaveBeenCalledWith({
|
|
177
|
+
error: {
|
|
178
|
+
message: longMessage,
|
|
179
|
+
code: 400,
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should chain status and send methods correctly', () => {
|
|
185
|
+
// Arrange
|
|
186
|
+
const exception = new HttpException('Test error', HttpStatus.NOT_FOUND);
|
|
187
|
+
|
|
188
|
+
// Act
|
|
189
|
+
filter.catch(exception, mockArgumentsHost);
|
|
190
|
+
|
|
191
|
+
// Assert
|
|
192
|
+
expect(mockResponse.status).toHaveBeenCalled();
|
|
193
|
+
expect(mockResponse.send).toHaveBeenCalled();
|
|
194
|
+
expect(mockResponse.status).toHaveReturnedWith(mockResponse);
|
|
195
|
+
// Verify status is called before send by checking call order
|
|
196
|
+
const statusCallOrder = (mockResponse.status as jest.Mock).mock.invocationCallOrder[0];
|
|
197
|
+
const sendCallOrder = (mockResponse.send as jest.Mock).mock.invocationCallOrder[0];
|
|
198
|
+
expect(statusCallOrder).toBeLessThan(sendCallOrder);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should extract response from HTTP context correctly', () => {
|
|
202
|
+
// Arrange
|
|
203
|
+
const exception = new HttpException('Test', HttpStatus.BAD_REQUEST);
|
|
204
|
+
|
|
205
|
+
// Act
|
|
206
|
+
filter.catch(exception, mockArgumentsHost);
|
|
207
|
+
|
|
208
|
+
// Assert
|
|
209
|
+
expect(mockArgumentsHost.switchToHttp).toHaveBeenCalledTimes(1);
|
|
210
|
+
expect(mockHttpArgumentsHost.getResponse).toHaveBeenCalledTimes(1);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
});
|
package/src/testing/factories.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DismissibleItemDto } from '@dismissible/nestjs-dismissible-item';
|
|
2
2
|
import { DismissibleItemFactory } from '@dismissible/nestjs-dismissible-item';
|
|
3
3
|
import { IRequestContext } from '../request/request-context.interface';
|
|
4
4
|
|
|
@@ -10,14 +10,11 @@ const testItemFactory = new DismissibleItemFactory();
|
|
|
10
10
|
/**
|
|
11
11
|
* Create a test dismissible item.
|
|
12
12
|
*/
|
|
13
|
-
export function createTestItem<
|
|
14
|
-
overrides: Partial<DismissibleItemDto<TMetadata>> = {},
|
|
15
|
-
): DismissibleItemDto<TMetadata> {
|
|
13
|
+
export function createTestItem(overrides: Partial<DismissibleItemDto> = {}): DismissibleItemDto {
|
|
16
14
|
return testItemFactory.create({
|
|
17
15
|
id: overrides.id ?? 'test-item-id',
|
|
18
16
|
createdAt: overrides.createdAt ?? new Date('2024-01-15T10:00:00.000Z'),
|
|
19
17
|
userId: overrides.userId ?? 'test-user-id',
|
|
20
|
-
metadata: overrides.metadata,
|
|
21
18
|
dismissedAt: overrides.dismissedAt,
|
|
22
19
|
});
|
|
23
20
|
}
|
|
@@ -35,9 +32,9 @@ export function createTestContext(overrides: Partial<IRequestContext> = {}): IRe
|
|
|
35
32
|
/**
|
|
36
33
|
* Create a dismissed test item.
|
|
37
34
|
*/
|
|
38
|
-
export function createDismissedTestItem
|
|
39
|
-
overrides: Partial<DismissibleItemDto
|
|
40
|
-
): DismissibleItemDto
|
|
35
|
+
export function createDismissedTestItem(
|
|
36
|
+
overrides: Partial<DismissibleItemDto> = {},
|
|
37
|
+
): DismissibleItemDto {
|
|
41
38
|
return createTestItem({
|
|
42
39
|
dismissedAt: new Date('2024-01-15T12:00:00.000Z'),
|
|
43
40
|
...overrides,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Injectable } from '@nestjs/common';
|
|
2
|
-
import {
|
|
2
|
+
import { DismissibleItemDto } from '@dismissible/nestjs-dismissible-item';
|
|
3
3
|
|
|
4
4
|
@Injectable()
|
|
5
5
|
export class DismissibleHelper {
|
|
6
|
-
isDismissed
|
|
6
|
+
isDismissed(item: DismissibleItemDto): boolean {
|
|
7
7
|
return item.dismissedAt !== undefined && item.dismissedAt !== null;
|
|
8
8
|
}
|
|
9
9
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { IsNotEmpty, IsString, Matches, MaxLength, MinLength } from 'class-validator';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Validation constants for dismissible input fields.
|
|
5
|
+
*/
|
|
6
|
+
export const VALIDATION_CONSTANTS = {
|
|
7
|
+
/** Maximum length for userId and itemId */
|
|
8
|
+
ID_MAX_LENGTH: 64,
|
|
9
|
+
/** Minimum length for userId and itemId */
|
|
10
|
+
ID_MIN_LENGTH: 1,
|
|
11
|
+
/** Pattern for valid userId and itemId (alphanumeric, dash, underscore) */
|
|
12
|
+
ID_PATTERN: /^[a-zA-Z0-9_-]+$/,
|
|
13
|
+
/** Human-readable description of the ID pattern */
|
|
14
|
+
ID_PATTERN_MESSAGE: 'must contain only alphanumeric characters, dashes, and underscores',
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* DTO for validating dismissible input parameters (userId and itemId).
|
|
19
|
+
* Used at both controller and service layers for defense in depth.
|
|
20
|
+
*/
|
|
21
|
+
export class DismissibleInputDto {
|
|
22
|
+
@IsString()
|
|
23
|
+
@IsNotEmpty({ message: 'itemId is required' })
|
|
24
|
+
@MinLength(VALIDATION_CONSTANTS.ID_MIN_LENGTH, {
|
|
25
|
+
message: `itemId must be at least ${VALIDATION_CONSTANTS.ID_MIN_LENGTH} character`,
|
|
26
|
+
})
|
|
27
|
+
@MaxLength(VALIDATION_CONSTANTS.ID_MAX_LENGTH, {
|
|
28
|
+
message: `itemId must be at most ${VALIDATION_CONSTANTS.ID_MAX_LENGTH} characters`,
|
|
29
|
+
})
|
|
30
|
+
@Matches(VALIDATION_CONSTANTS.ID_PATTERN, {
|
|
31
|
+
message: `itemId ${VALIDATION_CONSTANTS.ID_PATTERN_MESSAGE}`,
|
|
32
|
+
})
|
|
33
|
+
itemId: string;
|
|
34
|
+
|
|
35
|
+
@IsString()
|
|
36
|
+
@IsNotEmpty({ message: 'userId is required' })
|
|
37
|
+
@MinLength(VALIDATION_CONSTANTS.ID_MIN_LENGTH, {
|
|
38
|
+
message: `userId must be at least ${VALIDATION_CONSTANTS.ID_MIN_LENGTH} character`,
|
|
39
|
+
})
|
|
40
|
+
@MaxLength(VALIDATION_CONSTANTS.ID_MAX_LENGTH, {
|
|
41
|
+
message: `userId must be at most ${VALIDATION_CONSTANTS.ID_MAX_LENGTH} characters`,
|
|
42
|
+
})
|
|
43
|
+
@Matches(VALIDATION_CONSTANTS.ID_PATTERN, {
|
|
44
|
+
message: `userId ${VALIDATION_CONSTANTS.ID_PATTERN_MESSAGE}`,
|
|
45
|
+
})
|
|
46
|
+
userId: string;
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './dismissible-input.dto';
|
package/tsconfig.json
CHANGED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "../../dist/out-tsc",
|
|
5
|
+
"module": "commonjs",
|
|
6
|
+
"types": ["node", "jest"],
|
|
7
|
+
"emitDecoratorMetadata": true,
|
|
8
|
+
"experimentalDecorators": true,
|
|
9
|
+
"target": "ES2021"
|
|
10
|
+
},
|
|
11
|
+
"include": ["src/**/*.spec.ts", "src/**/*.test.ts"]
|
|
12
|
+
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { ApiPropertyOptional } from '@nestjs/swagger';
|
|
2
|
-
import { IsOptional, IsObject } from 'class-validator';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Query parameters for creating a dismissible item.
|
|
6
|
-
*/
|
|
7
|
-
export class CreateDismissibleItemQueryDto {
|
|
8
|
-
@ApiPropertyOptional({
|
|
9
|
-
description: 'Optional metadata to attach to the item',
|
|
10
|
-
example: { key: 'value' },
|
|
11
|
-
type: 'object',
|
|
12
|
-
additionalProperties: true,
|
|
13
|
-
})
|
|
14
|
-
@IsOptional()
|
|
15
|
-
@IsObject()
|
|
16
|
-
metadata?: Record<string, string | number>;
|
|
17
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { BaseMetadata } from '@dismissible/nestjs-dismissible-item';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Options for creating a new dismissible item.
|
|
5
|
-
*/
|
|
6
|
-
export interface ICreateItemOptions<TMetadata extends BaseMetadata = BaseMetadata> {
|
|
7
|
-
/** Optional metadata to attach to the item */
|
|
8
|
-
metadata?: TMetadata;
|
|
9
|
-
}
|