@dismissible/nestjs-dismissible 0.0.2-canary.8976e84.0 → 0.0.2-canary.b0d8bfe.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.
Files changed (46) hide show
  1. package/README.md +58 -74
  2. package/jest.config.ts +1 -1
  3. package/package.json +8 -11
  4. package/project.json +1 -1
  5. package/src/api/dismissible-item-response.dto.ts +0 -8
  6. package/src/api/dismissible-item.mapper.spec.ts +0 -12
  7. package/src/api/dismissible-item.mapper.ts +2 -8
  8. package/src/api/index.ts +2 -3
  9. package/src/api/use-cases/dismiss/dismiss.controller.spec.ts +1 -2
  10. package/src/api/use-cases/dismiss/dismiss.controller.ts +9 -10
  11. package/src/api/use-cases/get-or-create/get-or-create.controller.spec.ts +2 -42
  12. package/src/api/use-cases/get-or-create/get-or-create.controller.ts +11 -58
  13. package/src/api/use-cases/get-or-create/index.ts +0 -1
  14. package/src/api/use-cases/restore/restore.controller.spec.ts +1 -2
  15. package/src/api/use-cases/restore/restore.controller.ts +9 -10
  16. package/src/api/validation/index.ts +2 -0
  17. package/src/api/validation/param-validation.pipe.spec.ts +313 -0
  18. package/src/api/validation/param-validation.pipe.ts +38 -0
  19. package/src/api/validation/param.decorators.ts +32 -0
  20. package/src/core/dismissible-core.service.spec.ts +75 -29
  21. package/src/core/dismissible-core.service.ts +40 -28
  22. package/src/core/dismissible.service.spec.ts +106 -24
  23. package/src/core/dismissible.service.ts +93 -54
  24. package/src/core/hook-runner.service.spec.ts +495 -54
  25. package/src/core/hook-runner.service.ts +125 -24
  26. package/src/core/index.ts +0 -1
  27. package/src/core/lifecycle-hook.interface.ts +7 -122
  28. package/src/core/service-responses.interface.ts +9 -9
  29. package/src/dismissible.module.integration.spec.ts +704 -0
  30. package/src/dismissible.module.ts +10 -11
  31. package/src/events/dismissible.events.ts +17 -40
  32. package/src/index.ts +1 -1
  33. package/src/response/http-exception-filter.spec.ts +179 -0
  34. package/src/response/http-exception-filter.ts +3 -3
  35. package/src/response/response.service.spec.ts +0 -14
  36. package/src/testing/factories.ts +24 -9
  37. package/src/utils/dismissible.helper.ts +2 -2
  38. package/src/validation/dismissible-input.dto.ts +47 -0
  39. package/src/validation/index.ts +1 -0
  40. package/tsconfig.json +3 -0
  41. package/tsconfig.spec.json +12 -0
  42. package/src/api/use-cases/get-or-create/get-or-create.request.dto.ts +0 -17
  43. package/src/core/create-options.ts +0 -9
  44. package/src/request/index.ts +0 -2
  45. package/src/request/request-context.decorator.ts +0 -14
  46. package/src/request/request-context.interface.ts +0 -6
@@ -8,9 +8,11 @@ import { DismissibleCoreService } from './core/dismissible-core.service';
8
8
  import { HookRunner } from './core/hook-runner.service';
9
9
  import { DismissibleItemMapper } from './api/dismissible-item.mapper';
10
10
  import { DateService } from './utils/date/date.service';
11
- import { DISMISSIBLE_HOOKS, IDismissibleLifecycleHook } from './core/lifecycle-hook.interface';
11
+ import {
12
+ DISMISSIBLE_HOOKS,
13
+ IDismissibleLifecycleHook,
14
+ } from '@dismissible/nestjs-dismissible-hooks';
12
15
  import { LoggerModule, IDismissibleLoggerModuleOptions } from '@dismissible/nestjs-logger';
13
- import { BaseMetadata } from '@dismissible/nestjs-dismissible-item';
14
16
  import { ResponseService, ResponseModule } from './response';
15
17
  import { ValidationModule } from '@dismissible/nestjs-validation';
16
18
  import { IDismissibleStorageModuleOptions, StorageModule } from '@dismissible/nestjs-storage';
@@ -20,17 +22,14 @@ import { DismissibleItemModule } from '@dismissible/nestjs-dismissible-item';
20
22
  /**
21
23
  * Module configuration options.
22
24
  */
23
- export type IDismissibleModuleOptions<TMetadata extends BaseMetadata = BaseMetadata> =
24
- IDismissibleLoggerModuleOptions &
25
- IDismissibleStorageModuleOptions & {
26
- hooks?: Type<IDismissibleLifecycleHook<TMetadata>>[];
27
- };
25
+ export type IDismissibleModuleOptions = IDismissibleLoggerModuleOptions &
26
+ IDismissibleStorageModuleOptions & {
27
+ hooks?: Type<IDismissibleLifecycleHook>[];
28
+ };
28
29
 
29
30
  @Module({})
30
31
  export class DismissibleModule {
31
- static forRoot<TMetadata extends BaseMetadata = BaseMetadata>(
32
- options: IDismissibleModuleOptions<TMetadata>,
33
- ): DynamicModule {
32
+ static forRoot(options: IDismissibleModuleOptions = {}): DynamicModule {
34
33
  const providers: Provider[] = [
35
34
  DateService,
36
35
  ResponseService,
@@ -48,7 +47,7 @@ export class DismissibleModule {
48
47
 
49
48
  providers.push({
50
49
  provide: DISMISSIBLE_HOOKS,
51
- useFactory: (...hooks: IDismissibleLifecycleHook<TMetadata>[]) => hooks,
50
+ useFactory: (...hooks: IDismissibleLifecycleHook[]) => hooks,
52
51
  inject: options.hooks,
53
52
  });
54
53
  } else {
@@ -1,15 +1,15 @@
1
- import { BaseMetadata, DismissibleItemDto } from '@dismissible/nestjs-dismissible-item';
2
- import { IRequestContext } from '../request/request-context.interface';
1
+ import { DismissibleItemDto } from '@dismissible/nestjs-dismissible-item';
2
+ import { IRequestContext } from '@dismissible/nestjs-dismissible-request';
3
3
 
4
4
  /**
5
5
  * Base class for all dismissible events.
6
6
  */
7
- abstract class BaseDismissibleEvent<TMetadata extends BaseMetadata = BaseMetadata> {
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<TMetadata>;
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
- TMetadata extends BaseMetadata = BaseMetadata,
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
- TMetadata extends BaseMetadata = BaseMetadata,
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<TMetadata>;
51
+ readonly previousItem: DismissibleItemDto;
73
52
 
74
53
  constructor(
75
54
  itemId: string,
76
- item: DismissibleItemDto<TMetadata>,
77
- previousItem: DismissibleItemDto<TMetadata>,
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<TMetadata>;
70
+ readonly previousItem: DismissibleItemDto;
94
71
 
95
72
  constructor(
96
73
  itemId: string,
97
- item: DismissibleItemDto<TMetadata>,
98
- previousItem: DismissibleItemDto<TMetadata>,
74
+ item: DismissibleItemDto,
75
+ previousItem: DismissibleItemDto,
99
76
  userId: string,
100
77
  context?: IRequestContext,
101
78
  ) {
package/src/index.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  export * from './dismissible.module';
2
- export * from './request';
3
2
  export * from './core';
4
3
  export * from './api';
5
4
  export * from './utils';
6
5
  export * from './events';
7
6
  export * from './exceptions';
8
7
  export * from './testing';
8
+ export * from './validation';
@@ -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
+ });
@@ -1,12 +1,12 @@
1
1
  import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
2
- import { Response } from 'express';
2
+ import { FastifyReply } from 'fastify';
3
3
  import { IErrorResponseDto } from './dtos';
4
4
 
5
5
  @Catch(HttpException)
6
6
  export class HttpExceptionFilter implements ExceptionFilter {
7
7
  catch(exception: HttpException, host: ArgumentsHost) {
8
8
  const ctx = host.switchToHttp();
9
- const response = ctx.getResponse<Response>();
9
+ const response = ctx.getResponse<FastifyReply>();
10
10
  const status = exception.getStatus();
11
11
 
12
12
  const errorResponse: IErrorResponseDto = {
@@ -16,6 +16,6 @@ export class HttpExceptionFilter implements ExceptionFilter {
16
16
  },
17
17
  };
18
18
 
19
- response.status(status).json(errorResponse);
19
+ response.status(status).send(errorResponse);
20
20
  }
21
21
  }
@@ -5,65 +5,51 @@ describe('ResponseService', () => {
5
5
  let service: ResponseService;
6
6
 
7
7
  beforeEach(() => {
8
- // Manually create the service with no dependencies
9
8
  service = new ResponseService();
10
9
  });
11
10
 
12
11
  describe('success', () => {
13
12
  it('should return a success response with the provided data', () => {
14
- // Arrange
15
13
  const testData = { id: '123', name: 'Test Item' };
16
14
 
17
- // Act
18
15
  const result = service.success(testData);
19
16
 
20
- // Assert
21
17
  expect(result).toEqual({
22
18
  data: testData,
23
19
  });
24
20
  });
25
21
 
26
22
  it('should work with primitive data types', () => {
27
- // Arrange
28
23
  const testString = 'Test String';
29
24
 
30
- // Act
31
25
  const result = service.success(testString);
32
26
 
33
- // Assert
34
27
  expect(result).toEqual({
35
28
  data: testString,
36
29
  });
37
30
  });
38
31
 
39
32
  it('should work with array data', () => {
40
- // Arrange
41
33
  const testArray = [1, 2, 3];
42
34
 
43
- // Act
44
35
  const result = service.success(testArray);
45
36
 
46
- // Assert
47
37
  expect(result).toEqual({
48
38
  data: testArray,
49
39
  });
50
40
  });
51
41
 
52
42
  it('should handle null data', () => {
53
- // Act
54
43
  const result = service.success(null);
55
44
 
56
- // Assert
57
45
  expect(result).toEqual({
58
46
  data: null,
59
47
  });
60
48
  });
61
49
 
62
50
  it('should handle undefined data', () => {
63
- // Act
64
51
  const result = service.success(undefined);
65
52
 
66
- // Assert
67
53
  expect(result).toEqual({
68
54
  data: undefined,
69
55
  });
@@ -1,6 +1,6 @@
1
- import { BaseMetadata, DismissibleItemDto } from '@dismissible/nestjs-dismissible-item';
1
+ import { DismissibleItemDto } from '@dismissible/nestjs-dismissible-item';
2
2
  import { DismissibleItemFactory } from '@dismissible/nestjs-dismissible-item';
3
- import { IRequestContext } from '../request/request-context.interface';
3
+ import { IRequestContext } from '@dismissible/nestjs-dismissible-request';
4
4
 
5
5
  /**
6
6
  * Shared factory instance for test helpers.
@@ -10,14 +10,11 @@ const testItemFactory = new DismissibleItemFactory();
10
10
  /**
11
11
  * Create a test dismissible item.
12
12
  */
13
- export function createTestItem<TMetadata extends BaseMetadata = BaseMetadata>(
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
  }
@@ -28,6 +25,24 @@ export function createTestItem<TMetadata extends BaseMetadata = BaseMetadata>(
28
25
  export function createTestContext(overrides: Partial<IRequestContext> = {}): IRequestContext {
29
26
  return {
30
27
  requestId: 'test-request-id',
28
+ headers: {},
29
+ query: {},
30
+ params: {},
31
+ body: {},
32
+ user: {},
33
+ ip: '127.0.0.1',
34
+ method: 'GET',
35
+ url: '/test',
36
+ protocol: 'http',
37
+ secure: false,
38
+ hostname: 'localhost',
39
+ port: 3000,
40
+ path: '/test',
41
+ search: '',
42
+ searchParams: {},
43
+ origin: 'http://localhost:3000',
44
+ referer: '',
45
+ userAgent: 'test-agent',
31
46
  ...overrides,
32
47
  };
33
48
  }
@@ -35,9 +50,9 @@ export function createTestContext(overrides: Partial<IRequestContext> = {}): IRe
35
50
  /**
36
51
  * Create a dismissed test item.
37
52
  */
38
- export function createDismissedTestItem<TMetadata extends BaseMetadata = BaseMetadata>(
39
- overrides: Partial<DismissibleItemDto<TMetadata>> = {},
40
- ): DismissibleItemDto<TMetadata> {
53
+ export function createDismissedTestItem(
54
+ overrides: Partial<DismissibleItemDto> = {},
55
+ ): DismissibleItemDto {
41
56
  return createTestItem({
42
57
  dismissedAt: new Date('2024-01-15T12:00:00.000Z'),
43
58
  ...overrides,
@@ -1,9 +1,9 @@
1
1
  import { Injectable } from '@nestjs/common';
2
- import { BaseMetadata, DismissibleItemDto } from '@dismissible/nestjs-dismissible-item';
2
+ import { DismissibleItemDto } from '@dismissible/nestjs-dismissible-item';
3
3
 
4
4
  @Injectable()
5
5
  export class DismissibleHelper {
6
- isDismissed<TMetadata extends BaseMetadata>(item: DismissibleItemDto<TMetadata>): boolean {
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
@@ -5,6 +5,9 @@
5
5
  "references": [
6
6
  {
7
7
  "path": "./tsconfig.lib.json"
8
+ },
9
+ {
10
+ "path": "./tsconfig.spec.json"
8
11
  }
9
12
  ],
10
13
  "compilerOptions": {
@@ -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
- }
@@ -1,2 +0,0 @@
1
- export * from './request-context.decorator';
2
- export * from './request-context.interface';
@@ -1,14 +0,0 @@
1
- import { createParamDecorator, ExecutionContext } from '@nestjs/common';
2
- import { v4 as uuidv4 } from 'uuid';
3
- import { IRequestContext } from './request-context.interface';
4
-
5
- export const RequestContext = createParamDecorator(
6
- (_data: unknown, ctx: ExecutionContext): IRequestContext => {
7
- const request = ctx.switchToHttp().getRequest();
8
- const headers = request.headers;
9
-
10
- return {
11
- requestId: headers['x-request-id'] ?? uuidv4(),
12
- };
13
- },
14
- );
@@ -1,6 +0,0 @@
1
- /**
2
- * Request context passed through the dismissible operations.
3
- */
4
- export interface IRequestContext {
5
- requestId: string;
6
- }