@dismissible/nestjs-dismissible 0.0.2-canary.738340d.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 (62) hide show
  1. package/README.md +506 -0
  2. package/jest.config.ts +29 -0
  3. package/package.json +63 -0
  4. package/project.json +42 -0
  5. package/src/api/dismissible-item-response.dto.ts +38 -0
  6. package/src/api/dismissible-item.mapper.spec.ts +63 -0
  7. package/src/api/dismissible-item.mapper.ts +33 -0
  8. package/src/api/index.ts +7 -0
  9. package/src/api/use-cases/api-tags.constants.ts +4 -0
  10. package/src/api/use-cases/dismiss/dismiss.controller.spec.ts +42 -0
  11. package/src/api/use-cases/dismiss/dismiss.controller.ts +63 -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 +76 -0
  15. package/src/api/use-cases/get-or-create/get-or-create.controller.ts +106 -0
  16. package/src/api/use-cases/get-or-create/get-or-create.request.dto.ts +17 -0
  17. package/src/api/use-cases/get-or-create/get-or-create.response.dto.ts +7 -0
  18. package/src/api/use-cases/get-or-create/index.ts +3 -0
  19. package/src/api/use-cases/index.ts +3 -0
  20. package/src/api/use-cases/restore/index.ts +2 -0
  21. package/src/api/use-cases/restore/restore.controller.spec.ts +42 -0
  22. package/src/api/use-cases/restore/restore.controller.ts +63 -0
  23. package/src/api/use-cases/restore/restore.response.dto.ts +7 -0
  24. package/src/core/create-options.ts +9 -0
  25. package/src/core/dismissible-core.service.spec.ts +357 -0
  26. package/src/core/dismissible-core.service.ts +161 -0
  27. package/src/core/dismissible.service.spec.ts +144 -0
  28. package/src/core/dismissible.service.ts +188 -0
  29. package/src/core/hook-runner.service.spec.ts +304 -0
  30. package/src/core/hook-runner.service.ts +267 -0
  31. package/src/core/index.ts +6 -0
  32. package/src/core/lifecycle-hook.interface.ts +122 -0
  33. package/src/core/service-responses.interface.ts +34 -0
  34. package/src/dismissible.module.ts +83 -0
  35. package/src/events/dismissible.events.ts +105 -0
  36. package/src/events/events.constants.ts +21 -0
  37. package/src/events/index.ts +2 -0
  38. package/src/exceptions/dismissible.exceptions.spec.ts +50 -0
  39. package/src/exceptions/dismissible.exceptions.ts +69 -0
  40. package/src/exceptions/index.ts +1 -0
  41. package/src/index.ts +8 -0
  42. package/src/request/index.ts +2 -0
  43. package/src/request/request-context.decorator.ts +14 -0
  44. package/src/request/request-context.interface.ts +6 -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.ts +21 -0
  50. package/src/response/index.ts +4 -0
  51. package/src/response/response.module.ts +9 -0
  52. package/src/response/response.service.spec.ts +86 -0
  53. package/src/response/response.service.ts +20 -0
  54. package/src/testing/factories.ts +45 -0
  55. package/src/testing/index.ts +1 -0
  56. package/src/utils/date/date.service.spec.ts +104 -0
  57. package/src/utils/date/date.service.ts +19 -0
  58. package/src/utils/date/index.ts +1 -0
  59. package/src/utils/dismissible.helper.ts +9 -0
  60. package/src/utils/index.ts +3 -0
  61. package/tsconfig.json +13 -0
  62. package/tsconfig.lib.json +14 -0
@@ -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,8 @@
1
+ export * from './dismissible.module';
2
+ export * from './request';
3
+ export * from './core';
4
+ export * from './api';
5
+ export * from './utils';
6
+ export * from './events';
7
+ export * from './exceptions';
8
+ export * from './testing';
@@ -0,0 +1,2 @@
1
+ export * from './request-context.decorator';
2
+ export * from './request-context.interface';
@@ -0,0 +1,14 @@
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
+ );
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Request context passed through the dismissible operations.
3
+ */
4
+ export interface IRequestContext {
5
+ requestId: string;
6
+ }
@@ -0,0 +1,11 @@
1
+ import { ApiProperty } from '@nestjs/swagger';
2
+
3
+ /**
4
+ * Base response DTO that wraps all successful API responses
5
+ */
6
+ export class BaseResponseDto<T> {
7
+ @ApiProperty({
8
+ description: 'Response data',
9
+ })
10
+ data: T;
11
+ }
@@ -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,3 @@
1
+ export * from './base-response.dto';
2
+ export * from './error-response.dto';
3
+ export * from './success-response.dto';
@@ -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,21 @@
1
+ import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
2
+ import { Response } from 'express';
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<Response>();
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).json(errorResponse);
20
+ }
21
+ }
@@ -0,0 +1,4 @@
1
+ export * from './response.module';
2
+ export * from './response.service';
3
+ export * from './http-exception-filter';
4
+ export * from './dtos';
@@ -0,0 +1,9 @@
1
+ import { Global, Module } from '@nestjs/common';
2
+ import { ResponseService } from './response.service';
3
+
4
+ @Global()
5
+ @Module({
6
+ providers: [ResponseService],
7
+ exports: [ResponseService],
8
+ })
9
+ export class ResponseModule {}
@@ -0,0 +1,86 @@
1
+ import { ResponseService } from './response.service';
2
+ import { NotFoundException } from '@nestjs/common';
3
+
4
+ describe('ResponseService', () => {
5
+ let service: ResponseService;
6
+
7
+ beforeEach(() => {
8
+ // Manually create the service with no dependencies
9
+ service = new ResponseService();
10
+ });
11
+
12
+ describe('success', () => {
13
+ it('should return a success response with the provided data', () => {
14
+ // Arrange
15
+ const testData = { id: '123', name: 'Test Item' };
16
+
17
+ // Act
18
+ const result = service.success(testData);
19
+
20
+ // Assert
21
+ expect(result).toEqual({
22
+ data: testData,
23
+ });
24
+ });
25
+
26
+ it('should work with primitive data types', () => {
27
+ // Arrange
28
+ const testString = 'Test String';
29
+
30
+ // Act
31
+ const result = service.success(testString);
32
+
33
+ // Assert
34
+ expect(result).toEqual({
35
+ data: testString,
36
+ });
37
+ });
38
+
39
+ it('should work with array data', () => {
40
+ // Arrange
41
+ const testArray = [1, 2, 3];
42
+
43
+ // Act
44
+ const result = service.success(testArray);
45
+
46
+ // Assert
47
+ expect(result).toEqual({
48
+ data: testArray,
49
+ });
50
+ });
51
+
52
+ it('should handle null data', () => {
53
+ // Act
54
+ const result = service.success(null);
55
+
56
+ // Assert
57
+ expect(result).toEqual({
58
+ data: null,
59
+ });
60
+ });
61
+
62
+ it('should handle undefined data', () => {
63
+ // Act
64
+ const result = service.success(undefined);
65
+
66
+ // Assert
67
+ expect(result).toEqual({
68
+ data: undefined,
69
+ });
70
+ });
71
+ });
72
+
73
+ describe('error', () => {
74
+ it('should return an error response with the provided message', () => {
75
+ const errorMessage = new NotFoundException('Not found');
76
+ const result = service.error(errorMessage);
77
+
78
+ expect(result).toEqual({
79
+ error: {
80
+ message: 'Not found',
81
+ code: 404,
82
+ },
83
+ });
84
+ });
85
+ });
86
+ });
@@ -0,0 +1,20 @@
1
+ import { HttpException, Injectable } from '@nestjs/common';
2
+ import { IErrorResponseDto, ISuccessResponseDto } from './dtos';
3
+
4
+ @Injectable()
5
+ export class ResponseService {
6
+ success<T>(data: T): ISuccessResponseDto<T> {
7
+ return {
8
+ data,
9
+ };
10
+ }
11
+
12
+ error(error: HttpException): IErrorResponseDto {
13
+ return {
14
+ error: {
15
+ message: error.message,
16
+ code: error.getStatus(),
17
+ },
18
+ };
19
+ }
20
+ }
@@ -0,0 +1,45 @@
1
+ import { BaseMetadata, DismissibleItemDto } from '@dismissible/nestjs-dismissible-item';
2
+ import { DismissibleItemFactory } from '@dismissible/nestjs-dismissible-item';
3
+ import { IRequestContext } from '../request/request-context.interface';
4
+
5
+ /**
6
+ * Shared factory instance for test helpers.
7
+ */
8
+ const testItemFactory = new DismissibleItemFactory();
9
+
10
+ /**
11
+ * Create a test dismissible item.
12
+ */
13
+ export function createTestItem<TMetadata extends BaseMetadata = BaseMetadata>(
14
+ overrides: Partial<DismissibleItemDto<TMetadata>> = {},
15
+ ): DismissibleItemDto<TMetadata> {
16
+ return testItemFactory.create({
17
+ id: overrides.id ?? 'test-item-id',
18
+ createdAt: overrides.createdAt ?? new Date('2024-01-15T10:00:00.000Z'),
19
+ userId: overrides.userId ?? 'test-user-id',
20
+ metadata: overrides.metadata,
21
+ dismissedAt: overrides.dismissedAt,
22
+ });
23
+ }
24
+
25
+ /**
26
+ * Create a test request context.
27
+ */
28
+ export function createTestContext(overrides: Partial<IRequestContext> = {}): IRequestContext {
29
+ return {
30
+ requestId: 'test-request-id',
31
+ ...overrides,
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Create a dismissed test item.
37
+ */
38
+ export function createDismissedTestItem<TMetadata extends BaseMetadata = BaseMetadata>(
39
+ overrides: Partial<DismissibleItemDto<TMetadata>> = {},
40
+ ): DismissibleItemDto<TMetadata> {
41
+ return createTestItem({
42
+ dismissedAt: new Date('2024-01-15T12:00:00.000Z'),
43
+ ...overrides,
44
+ });
45
+ }
@@ -0,0 +1 @@
1
+ export * from './factories';
@@ -0,0 +1,104 @@
1
+ import { DateService } from './date.service';
2
+
3
+ describe('DateService', () => {
4
+ let service: DateService;
5
+
6
+ beforeEach(() => {
7
+ service = new DateService();
8
+ });
9
+
10
+ describe('getNow', () => {
11
+ it('should return current date', () => {
12
+ const before = new Date();
13
+ const result = service.getNow();
14
+ const after = new Date();
15
+
16
+ expect(result).toBeInstanceOf(Date);
17
+ expect(result.getTime()).toBeGreaterThanOrEqual(before.getTime());
18
+ expect(result.getTime()).toBeLessThanOrEqual(after.getTime());
19
+ });
20
+
21
+ it('should return a new date instance each time', () => {
22
+ const date1 = service.getNow();
23
+ const date2 = service.getNow();
24
+
25
+ expect(date1).not.toBe(date2);
26
+ });
27
+ });
28
+
29
+ describe('parseIso', () => {
30
+ it('should parse ISO 8601 string to Date', () => {
31
+ const isoString = '2024-01-15T10:30:00.000Z';
32
+ const result = service.parseIso(isoString);
33
+
34
+ expect(result).toBeInstanceOf(Date);
35
+ expect(result.toISOString()).toBe(isoString);
36
+ });
37
+
38
+ it('should parse date-only ISO string', () => {
39
+ const isoString = '2024-01-15';
40
+ const result = service.parseIso(isoString);
41
+
42
+ expect(result).toBeInstanceOf(Date);
43
+ expect(result.getFullYear()).toBe(2024);
44
+ expect(result.getMonth()).toBe(0); // January is 0
45
+ expect(result.getDate()).toBe(15);
46
+ });
47
+
48
+ it('should parse ISO string with timezone offset', () => {
49
+ const isoString = '2024-01-15T10:30:00+05:00';
50
+ const result = service.parseIso(isoString);
51
+
52
+ expect(result).toBeInstanceOf(Date);
53
+ });
54
+
55
+ it('should handle various ISO formats', () => {
56
+ const testCases = ['2024-01-15T10:30:00.000Z', '2024-01-15T10:30:00Z', '2024-01-15'];
57
+
58
+ testCases.forEach((isoString) => {
59
+ const result = service.parseIso(isoString);
60
+ expect(result).toBeInstanceOf(Date);
61
+ expect(isNaN(result.getTime())).toBe(false);
62
+ });
63
+ });
64
+ });
65
+
66
+ describe('toIso', () => {
67
+ it('should convert Date to ISO 8601 string', () => {
68
+ const date = new Date('2024-01-15T10:30:00.000Z');
69
+ const result = service.toIso(date);
70
+
71
+ expect(result).toBe('2024-01-15T10:30:00.000Z');
72
+ });
73
+
74
+ it('should produce valid ISO string format', () => {
75
+ const date = new Date('2024-12-25T23:59:59.999Z');
76
+ const result = service.toIso(date);
77
+
78
+ expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
79
+ expect(result).toBe('2024-12-25T23:59:59.999Z');
80
+ });
81
+
82
+ it('should handle dates at start of epoch', () => {
83
+ const date = new Date('1970-01-01T00:00:00.000Z');
84
+ const result = service.toIso(date);
85
+
86
+ expect(result).toBe('1970-01-01T00:00:00.000Z');
87
+ });
88
+
89
+ it('should handle dates in the future', () => {
90
+ const date = new Date('2099-12-31T23:59:59.999Z');
91
+ const result = service.toIso(date);
92
+
93
+ expect(result).toBe('2099-12-31T23:59:59.999Z');
94
+ });
95
+
96
+ it('should round-trip through parseIso and toIso', () => {
97
+ const originalIso = '2024-01-15T10:30:00.000Z';
98
+ const date = service.parseIso(originalIso);
99
+ const result = service.toIso(date);
100
+
101
+ expect(result).toBe(originalIso);
102
+ });
103
+ });
104
+ });
@@ -0,0 +1,19 @@
1
+ import { Injectable } from '@nestjs/common';
2
+
3
+ /**
4
+ * Service for date operations.
5
+ */
6
+ @Injectable()
7
+ export class DateService {
8
+ getNow(): Date {
9
+ return new Date();
10
+ }
11
+
12
+ parseIso(isoString: string): Date {
13
+ return new Date(isoString);
14
+ }
15
+
16
+ toIso(date: Date): string {
17
+ return date.toISOString();
18
+ }
19
+ }
@@ -0,0 +1 @@
1
+ export * from './date.service';
@@ -0,0 +1,9 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { BaseMetadata, DismissibleItemDto } from '@dismissible/nestjs-dismissible-item';
3
+
4
+ @Injectable()
5
+ export class DismissibleHelper {
6
+ isDismissed<TMetadata extends BaseMetadata>(item: DismissibleItemDto<TMetadata>): boolean {
7
+ return item.dismissedAt !== undefined && item.dismissedAt !== null;
8
+ }
9
+ }
@@ -0,0 +1,3 @@
1
+ export * from './date';
2
+ export * from '../response';
3
+ export * from './dismissible.helper';
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "files": [],
4
+ "include": [],
5
+ "references": [
6
+ {
7
+ "path": "./tsconfig.lib.json"
8
+ }
9
+ ],
10
+ "compilerOptions": {
11
+ "esModuleInterop": true
12
+ }
13
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "../../dist/out-tsc",
5
+ "declaration": true,
6
+ "module": "commonjs",
7
+ "types": ["node", "jest"],
8
+ "emitDecoratorMetadata": true,
9
+ "experimentalDecorators": true,
10
+ "target": "ES2021"
11
+ },
12
+ "exclude": ["node_modules"],
13
+ "include": ["src/**/*.ts"]
14
+ }