@dismissible/nestjs-dismissible 0.0.2-canary.585db17.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 (66) hide show
  1. package/README.md +490 -0
  2. package/jest.config.ts +29 -0
  3. package/package.json +62 -0
  4. package/project.json +42 -0
  5. package/src/api/dismissible-item-response.dto.ts +30 -0
  6. package/src/api/dismissible-item.mapper.spec.ts +51 -0
  7. package/src/api/dismissible-item.mapper.ts +27 -0
  8. package/src/api/index.ts +6 -0
  9. package/src/api/use-cases/api-tags.constants.ts +4 -0
  10. package/src/api/use-cases/dismiss/dismiss.controller.spec.ts +41 -0
  11. package/src/api/use-cases/dismiss/dismiss.controller.ts +62 -0
  12. package/src/api/use-cases/dismiss/dismiss.response.dto.ts +7 -0
  13. package/src/api/use-cases/dismiss/index.ts +2 -0
  14. package/src/api/use-cases/get-or-create/get-or-create.controller.spec.ts +36 -0
  15. package/src/api/use-cases/get-or-create/get-or-create.controller.ts +59 -0
  16. package/src/api/use-cases/get-or-create/get-or-create.response.dto.ts +7 -0
  17. package/src/api/use-cases/get-or-create/index.ts +2 -0
  18. package/src/api/use-cases/index.ts +3 -0
  19. package/src/api/use-cases/restore/index.ts +2 -0
  20. package/src/api/use-cases/restore/restore.controller.spec.ts +41 -0
  21. package/src/api/use-cases/restore/restore.controller.ts +62 -0
  22. package/src/api/use-cases/restore/restore.response.dto.ts +7 -0
  23. package/src/api/validation/index.ts +2 -0
  24. package/src/api/validation/param-validation.pipe.spec.ts +313 -0
  25. package/src/api/validation/param-validation.pipe.ts +38 -0
  26. package/src/api/validation/param.decorators.ts +32 -0
  27. package/src/core/dismissible-core.service.spec.ts +403 -0
  28. package/src/core/dismissible-core.service.ts +173 -0
  29. package/src/core/dismissible.service.spec.ts +226 -0
  30. package/src/core/dismissible.service.ts +227 -0
  31. package/src/core/hook-runner.service.spec.ts +745 -0
  32. package/src/core/hook-runner.service.ts +368 -0
  33. package/src/core/index.ts +5 -0
  34. package/src/core/lifecycle-hook.interface.ts +7 -0
  35. package/src/core/service-responses.interface.ts +34 -0
  36. package/src/dismissible.module.integration.spec.ts +704 -0
  37. package/src/dismissible.module.ts +82 -0
  38. package/src/events/dismissible.events.ts +82 -0
  39. package/src/events/events.constants.ts +21 -0
  40. package/src/events/index.ts +2 -0
  41. package/src/exceptions/dismissible.exceptions.spec.ts +50 -0
  42. package/src/exceptions/dismissible.exceptions.ts +69 -0
  43. package/src/exceptions/index.ts +1 -0
  44. package/src/index.ts +8 -0
  45. package/src/response/dtos/base-response.dto.ts +11 -0
  46. package/src/response/dtos/error-response.dto.ts +36 -0
  47. package/src/response/dtos/index.ts +3 -0
  48. package/src/response/dtos/success-response.dto.ts +34 -0
  49. package/src/response/http-exception-filter.spec.ts +179 -0
  50. package/src/response/http-exception-filter.ts +21 -0
  51. package/src/response/index.ts +4 -0
  52. package/src/response/response.module.ts +9 -0
  53. package/src/response/response.service.spec.ts +72 -0
  54. package/src/response/response.service.ts +20 -0
  55. package/src/testing/factories.ts +60 -0
  56. package/src/testing/index.ts +1 -0
  57. package/src/utils/date/date.service.spec.ts +104 -0
  58. package/src/utils/date/date.service.ts +19 -0
  59. package/src/utils/date/index.ts +1 -0
  60. package/src/utils/dismissible.helper.ts +9 -0
  61. package/src/utils/index.ts +3 -0
  62. package/src/validation/dismissible-input.dto.ts +47 -0
  63. package/src/validation/index.ts +1 -0
  64. package/tsconfig.json +16 -0
  65. package/tsconfig.lib.json +14 -0
  66. package/tsconfig.spec.json +12 -0
@@ -0,0 +1,27 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { DismissibleItemDto } from '@dismissible/nestjs-dismissible-item';
3
+ import { DismissibleItemResponseDto } from './dismissible-item-response.dto';
4
+
5
+ /**
6
+ * Mapper for converting domain objects to DTOs.
7
+ */
8
+ @Injectable()
9
+ export class DismissibleItemMapper {
10
+ /**
11
+ * Convert a dismissible item to a response DTO.
12
+ * Converts Date objects to ISO 8601 strings.
13
+ */
14
+ toResponseDto(item: DismissibleItemDto): DismissibleItemResponseDto {
15
+ const dto = new DismissibleItemResponseDto();
16
+
17
+ dto.itemId = item.id;
18
+ dto.userId = item.userId;
19
+ dto.createdAt = item.createdAt.toISOString();
20
+
21
+ if (item.dismissedAt) {
22
+ dto.dismissedAt = item.dismissedAt.toISOString();
23
+ }
24
+
25
+ return dto;
26
+ }
27
+ }
@@ -0,0 +1,6 @@
1
+ export * from './use-cases';
2
+
3
+ export * from './validation';
4
+
5
+ export * from './dismissible-item.mapper';
6
+ export * from './dismissible-item-response.dto';
@@ -0,0 +1,4 @@
1
+ /**
2
+ * API tag constants for Swagger/OpenAPI documentation.
3
+ */
4
+ export const API_TAG_DISMISSIBLE = 'Dismissible';
@@ -0,0 +1,41 @@
1
+ import { mock } from 'ts-jest-mocker';
2
+ import { DismissController } from './dismiss.controller';
3
+ import { DismissibleService } from '../../../core/dismissible.service';
4
+ import { DismissibleItemMapper } from '../../dismissible-item.mapper';
5
+ import { createTestItem, createTestContext } from '../../../testing/factories';
6
+ import { ResponseService } from '../../../response';
7
+
8
+ describe('DismissController', () => {
9
+ let controller: DismissController;
10
+ let mockService: jest.Mocked<DismissibleService>;
11
+ let mockResponseService: jest.Mocked<ResponseService>;
12
+ let mapper: DismissibleItemMapper;
13
+
14
+ beforeEach(() => {
15
+ mockService = mock(DismissibleService);
16
+ mockResponseService = mock(ResponseService, { failIfMockNotProvided: false });
17
+ mockResponseService.success.mockImplementation((data) => ({ data }));
18
+ mapper = new DismissibleItemMapper();
19
+
20
+ controller = new DismissController(mockService, mapper, mockResponseService);
21
+ });
22
+
23
+ describe('dismiss', () => {
24
+ it('should return dismissed item wrapped in data', async () => {
25
+ const item = createTestItem({
26
+ id: 'test-item',
27
+ dismissedAt: new Date(),
28
+ });
29
+ const previousItem = createTestItem({ id: 'test-item' });
30
+ const context = createTestContext();
31
+
32
+ mockService.dismiss.mockResolvedValue({ item, previousItem });
33
+
34
+ const result = await controller.dismiss('test-user-id', 'test-item', context);
35
+
36
+ expect(result.data.itemId).toBe('test-item');
37
+ expect(mockService.dismiss).toHaveBeenCalledWith('test-item', 'test-user-id', context);
38
+ expect(mockResponseService.success).toHaveBeenCalled();
39
+ });
40
+ });
41
+ });
@@ -0,0 +1,62 @@
1
+ import { Controller, Delete, UseFilters } from '@nestjs/common';
2
+ import { ApiTags, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger';
3
+ import { DismissibleService } from '../../../core/dismissible.service';
4
+ import { DismissibleItemMapper } from '../../dismissible-item.mapper';
5
+ import { RequestContext, IRequestContext } from '@dismissible/nestjs-dismissible-request';
6
+ import { DismissResponseDto } from './dismiss.response.dto';
7
+ import { ResponseService } from '../../../response/response.service';
8
+ import { HttpExceptionFilter } from '../../../response/http-exception-filter';
9
+ import { API_TAG_DISMISSIBLE } from '../api-tags.constants';
10
+ import { UserId, ItemId } from '../../validation';
11
+
12
+ /**
13
+ * Controller for dismiss dismissible item operations.
14
+ */
15
+ @ApiTags(API_TAG_DISMISSIBLE)
16
+ @Controller('v1/users/:userId/items')
17
+ export class DismissController {
18
+ constructor(
19
+ private readonly dismissibleService: DismissibleService,
20
+ private readonly mapper: DismissibleItemMapper,
21
+ private readonly responseService: ResponseService,
22
+ ) {}
23
+
24
+ @Delete(':itemId')
25
+ @ApiOperation({
26
+ summary: 'Dismiss an item',
27
+ description: 'Marks a dismissible item as dismissed.',
28
+ })
29
+ @ApiParam({
30
+ name: 'userId',
31
+ description: 'User identifier (max length: 32 characters)',
32
+ example: 'user-123',
33
+ })
34
+ @ApiParam({
35
+ name: 'itemId',
36
+ description: 'Unique identifier for the dismissible item (max length: 32 characters)',
37
+ example: 'welcome-banner-v2',
38
+ })
39
+ @ApiResponse({
40
+ status: 200,
41
+ description: 'The dismissed item',
42
+ type: DismissResponseDto,
43
+ })
44
+ @ApiResponse({
45
+ status: 400,
46
+ description: 'Item not found or already dismissed',
47
+ })
48
+ @ApiResponse({
49
+ status: 403,
50
+ description: 'Operation blocked by lifecycle hook',
51
+ })
52
+ @UseFilters(HttpExceptionFilter)
53
+ async dismiss(
54
+ @UserId() userId: string,
55
+ @ItemId() itemId: string,
56
+ @RequestContext() context: IRequestContext,
57
+ ): Promise<DismissResponseDto> {
58
+ const result = await this.dismissibleService.dismiss(itemId, userId, context);
59
+
60
+ return this.responseService.success(this.mapper.toResponseDto(result.item));
61
+ }
62
+ }
@@ -0,0 +1,7 @@
1
+ import { SuccessResponseDto } from '../../../response/dtos/success-response.dto';
2
+ import { DismissibleItemResponseDto } from '../../dismissible-item-response.dto';
3
+
4
+ /**
5
+ * Response DTO for the dismiss operation.
6
+ */
7
+ export class DismissResponseDto extends SuccessResponseDto(DismissibleItemResponseDto) {}
@@ -0,0 +1,2 @@
1
+ export * from './dismiss.response.dto';
2
+ export * from './dismiss.controller';
@@ -0,0 +1,36 @@
1
+ import { mock } from 'ts-jest-mocker';
2
+ import { GetOrCreateController } from './get-or-create.controller';
3
+ import { DismissibleService } from '../../../core/dismissible.service';
4
+ import { DismissibleItemMapper } from '../../dismissible-item.mapper';
5
+ import { createTestItem, createTestContext } from '../../../testing/factories';
6
+ import { ResponseService } from '../../../response';
7
+
8
+ describe('GetOrCreateController', () => {
9
+ let controller: GetOrCreateController;
10
+ let mockService: jest.Mocked<DismissibleService>;
11
+ let mockResponseService: jest.Mocked<ResponseService>;
12
+ let mapper: DismissibleItemMapper;
13
+
14
+ beforeEach(() => {
15
+ mockService = mock(DismissibleService);
16
+ mockResponseService = mock(ResponseService, { failIfMockNotProvided: false });
17
+ mockResponseService.success.mockImplementation((data) => ({ data }));
18
+ mapper = new DismissibleItemMapper();
19
+
20
+ controller = new GetOrCreateController(mockService, mapper, mockResponseService);
21
+ });
22
+
23
+ describe('getOrCreate', () => {
24
+ it('should return item with created flag wrapped in data', async () => {
25
+ const item = createTestItem({ id: 'test-item' });
26
+ const context = createTestContext();
27
+
28
+ mockService.getOrCreate.mockResolvedValue({ item, created: true });
29
+
30
+ const result = await controller.getOrCreate('test-user-id', 'test-item', context);
31
+
32
+ expect(result.data.itemId).toBe('test-item');
33
+ expect(mockResponseService.success).toHaveBeenCalled();
34
+ });
35
+ });
36
+ });
@@ -0,0 +1,59 @@
1
+ import { Controller, Get, UseFilters } from '@nestjs/common';
2
+ import { ApiTags, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger';
3
+ import { DismissibleService } from '../../../core/dismissible.service';
4
+ import { DismissibleItemMapper } from '../../dismissible-item.mapper';
5
+ import { RequestContext, IRequestContext } from '@dismissible/nestjs-dismissible-request';
6
+ import { GetOrCreateResponseDto } from './get-or-create.response.dto';
7
+ import { ResponseService } from '../../../response/response.service';
8
+ import { HttpExceptionFilter } from '../../../response/http-exception-filter';
9
+ import { API_TAG_DISMISSIBLE } from '../api-tags.constants';
10
+ import { UserId, ItemId } from '../../validation';
11
+
12
+ /**
13
+ * Controller for get-or-create dismissible item operations.
14
+ */
15
+ @ApiTags(API_TAG_DISMISSIBLE)
16
+ @Controller('v1/users/:userId/items')
17
+ export class GetOrCreateController {
18
+ constructor(
19
+ private readonly dismissibleService: DismissibleService,
20
+ private readonly mapper: DismissibleItemMapper,
21
+ private readonly responseService: ResponseService,
22
+ ) {}
23
+
24
+ @Get(':itemId')
25
+ @ApiOperation({
26
+ summary: 'Get or create a dismissible item',
27
+ description:
28
+ 'Retrieves an existing dismissible item by ID, or creates a new one if it does not exist.',
29
+ })
30
+ @ApiParam({
31
+ name: 'userId',
32
+ description: 'User identifier (max length: 32 characters)',
33
+ example: 'user-123',
34
+ })
35
+ @ApiParam({
36
+ name: 'itemId',
37
+ description: 'Unique identifier for the dismissible item (max length: 32 characters)',
38
+ example: 'welcome-banner-v2',
39
+ })
40
+ @ApiResponse({
41
+ status: 200,
42
+ description: 'The dismissible item (retrieved or created)',
43
+ type: GetOrCreateResponseDto,
44
+ })
45
+ @ApiResponse({
46
+ status: 403,
47
+ description: 'Operation blocked by lifecycle hook',
48
+ })
49
+ @UseFilters(HttpExceptionFilter)
50
+ async getOrCreate(
51
+ @UserId() userId: string,
52
+ @ItemId() itemId: string,
53
+ @RequestContext() context: IRequestContext,
54
+ ): Promise<GetOrCreateResponseDto> {
55
+ const result = await this.dismissibleService.getOrCreate(itemId, userId, context);
56
+
57
+ return this.responseService.success(this.mapper.toResponseDto(result.item));
58
+ }
59
+ }
@@ -0,0 +1,7 @@
1
+ import { SuccessResponseDto } from '../../../response/dtos/success-response.dto';
2
+ import { DismissibleItemResponseDto } from '../../dismissible-item-response.dto';
3
+
4
+ /**
5
+ * Response DTO for the getOrCreate operation.
6
+ */
7
+ export class GetOrCreateResponseDto extends SuccessResponseDto(DismissibleItemResponseDto) {}
@@ -0,0 +1,2 @@
1
+ export * from './get-or-create.response.dto';
2
+ export * from './get-or-create.controller';
@@ -0,0 +1,3 @@
1
+ export * from './get-or-create';
2
+ export * from './dismiss';
3
+ export * from './restore';
@@ -0,0 +1,2 @@
1
+ export * from './restore.response.dto';
2
+ export * from './restore.controller';
@@ -0,0 +1,41 @@
1
+ import { mock } from 'ts-jest-mocker';
2
+ import { RestoreController } from './restore.controller';
3
+ import { DismissibleService } from '../../../core/dismissible.service';
4
+ import { DismissibleItemMapper } from '../../dismissible-item.mapper';
5
+ import { createTestItem, createTestContext } from '../../../testing/factories';
6
+ import { ResponseService } from '../../../response';
7
+
8
+ describe('RestoreController', () => {
9
+ let controller: RestoreController;
10
+ let mockService: jest.Mocked<DismissibleService>;
11
+ let mockResponseService: jest.Mocked<ResponseService>;
12
+ let mapper: DismissibleItemMapper;
13
+
14
+ beforeEach(() => {
15
+ mockService = mock(DismissibleService);
16
+ mockResponseService = mock(ResponseService, { failIfMockNotProvided: false });
17
+ mockResponseService.success.mockImplementation((data) => ({ data }));
18
+ mapper = new DismissibleItemMapper();
19
+
20
+ controller = new RestoreController(mockService, mapper, mockResponseService);
21
+ });
22
+
23
+ describe('restore', () => {
24
+ it('should return restored item wrapped in data', async () => {
25
+ const item = createTestItem({ id: 'test-item' });
26
+ const previousItem = createTestItem({
27
+ id: 'test-item',
28
+ dismissedAt: new Date(),
29
+ });
30
+ const context = createTestContext();
31
+
32
+ mockService.restore.mockResolvedValue({ item, previousItem });
33
+
34
+ const result = await controller.restore('test-user-id', 'test-item', context);
35
+
36
+ expect(result.data.itemId).toBe('test-item');
37
+ expect(mockService.restore).toHaveBeenCalledWith('test-item', 'test-user-id', context);
38
+ expect(mockResponseService.success).toHaveBeenCalled();
39
+ });
40
+ });
41
+ });
@@ -0,0 +1,62 @@
1
+ import { Controller, Post, UseFilters } from '@nestjs/common';
2
+ import { ApiTags, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger';
3
+ import { DismissibleService } from '../../../core/dismissible.service';
4
+ import { DismissibleItemMapper } from '../../dismissible-item.mapper';
5
+ import { RequestContext, IRequestContext } from '@dismissible/nestjs-dismissible-request';
6
+ import { RestoreResponseDto } from './restore.response.dto';
7
+ import { ResponseService } from '../../../response/response.service';
8
+ import { HttpExceptionFilter } from '../../../response/http-exception-filter';
9
+ import { API_TAG_DISMISSIBLE } from '../api-tags.constants';
10
+ import { UserId, ItemId } from '../../validation';
11
+
12
+ /**
13
+ * Controller for restore dismissible item operations.
14
+ */
15
+ @ApiTags(API_TAG_DISMISSIBLE)
16
+ @Controller('v1/users/:userId/items')
17
+ export class RestoreController {
18
+ constructor(
19
+ private readonly dismissibleService: DismissibleService,
20
+ private readonly mapper: DismissibleItemMapper,
21
+ private readonly responseService: ResponseService,
22
+ ) {}
23
+
24
+ @Post(':itemId')
25
+ @ApiOperation({
26
+ summary: 'Restore a dismissed item',
27
+ description: 'Restores a previously dismissed item.',
28
+ })
29
+ @ApiParam({
30
+ name: 'userId',
31
+ description: 'User identifier (max length: 32 characters)',
32
+ example: 'user-123',
33
+ })
34
+ @ApiParam({
35
+ name: 'itemId',
36
+ description: 'Unique identifier for the dismissible item (max length: 32 characters)',
37
+ example: 'welcome-banner-v2',
38
+ })
39
+ @ApiResponse({
40
+ status: 200,
41
+ description: 'The restored item',
42
+ type: RestoreResponseDto,
43
+ })
44
+ @ApiResponse({
45
+ status: 400,
46
+ description: 'Item not found or not dismissed',
47
+ })
48
+ @ApiResponse({
49
+ status: 403,
50
+ description: 'Operation blocked by lifecycle hook',
51
+ })
52
+ @UseFilters(HttpExceptionFilter)
53
+ async restore(
54
+ @UserId() userId: string,
55
+ @ItemId() itemId: string,
56
+ @RequestContext() context: IRequestContext,
57
+ ): Promise<RestoreResponseDto> {
58
+ const result = await this.dismissibleService.restore(itemId, userId, context);
59
+
60
+ return this.responseService.success(this.mapper.toResponseDto(result.item));
61
+ }
62
+ }
@@ -0,0 +1,7 @@
1
+ import { SuccessResponseDto } from '../../../response/dtos/success-response.dto';
2
+ import { DismissibleItemResponseDto } from '../../dismissible-item-response.dto';
3
+
4
+ /**
5
+ * Response DTO for the restore operation.
6
+ */
7
+ export class RestoreResponseDto extends SuccessResponseDto(DismissibleItemResponseDto) {}
@@ -0,0 +1,2 @@
1
+ export * from './param-validation.pipe';
2
+ export * from './param.decorators';