@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.
- 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,72 @@
|
|
|
1
|
+
import { ResponseService } from './response.service';
|
|
2
|
+
import { NotFoundException } from '@nestjs/common';
|
|
3
|
+
|
|
4
|
+
describe('ResponseService', () => {
|
|
5
|
+
let service: ResponseService;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
service = new ResponseService();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe('success', () => {
|
|
12
|
+
it('should return a success response with the provided data', () => {
|
|
13
|
+
const testData = { id: '123', name: 'Test Item' };
|
|
14
|
+
|
|
15
|
+
const result = service.success(testData);
|
|
16
|
+
|
|
17
|
+
expect(result).toEqual({
|
|
18
|
+
data: testData,
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should work with primitive data types', () => {
|
|
23
|
+
const testString = 'Test String';
|
|
24
|
+
|
|
25
|
+
const result = service.success(testString);
|
|
26
|
+
|
|
27
|
+
expect(result).toEqual({
|
|
28
|
+
data: testString,
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should work with array data', () => {
|
|
33
|
+
const testArray = [1, 2, 3];
|
|
34
|
+
|
|
35
|
+
const result = service.success(testArray);
|
|
36
|
+
|
|
37
|
+
expect(result).toEqual({
|
|
38
|
+
data: testArray,
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should handle null data', () => {
|
|
43
|
+
const result = service.success(null);
|
|
44
|
+
|
|
45
|
+
expect(result).toEqual({
|
|
46
|
+
data: null,
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should handle undefined data', () => {
|
|
51
|
+
const result = service.success(undefined);
|
|
52
|
+
|
|
53
|
+
expect(result).toEqual({
|
|
54
|
+
data: undefined,
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('error', () => {
|
|
60
|
+
it('should return an error response with the provided message', () => {
|
|
61
|
+
const errorMessage = new NotFoundException('Not found');
|
|
62
|
+
const result = service.error(errorMessage);
|
|
63
|
+
|
|
64
|
+
expect(result).toEqual({
|
|
65
|
+
error: {
|
|
66
|
+
message: 'Not found',
|
|
67
|
+
code: 404,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -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,60 @@
|
|
|
1
|
+
import { DismissibleItemDto } from '@dismissible/nestjs-dismissible-item';
|
|
2
|
+
import { DismissibleItemFactory } from '@dismissible/nestjs-dismissible-item';
|
|
3
|
+
import { IRequestContext } from '@dismissible/nestjs-dismissible-request';
|
|
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(overrides: Partial<DismissibleItemDto> = {}): DismissibleItemDto {
|
|
14
|
+
return testItemFactory.create({
|
|
15
|
+
id: overrides.id ?? 'test-item-id',
|
|
16
|
+
createdAt: overrides.createdAt ?? new Date('2024-01-15T10:00:00.000Z'),
|
|
17
|
+
userId: overrides.userId ?? 'test-user-id',
|
|
18
|
+
dismissedAt: overrides.dismissedAt,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create a test request context.
|
|
24
|
+
*/
|
|
25
|
+
export function createTestContext(overrides: Partial<IRequestContext> = {}): IRequestContext {
|
|
26
|
+
return {
|
|
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',
|
|
46
|
+
...overrides,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create a dismissed test item.
|
|
52
|
+
*/
|
|
53
|
+
export function createDismissedTestItem(
|
|
54
|
+
overrides: Partial<DismissibleItemDto> = {},
|
|
55
|
+
): DismissibleItemDto {
|
|
56
|
+
return createTestItem({
|
|
57
|
+
dismissedAt: new Date('2024-01-15T12:00:00.000Z'),
|
|
58
|
+
...overrides,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -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 { DismissibleItemDto } from '@dismissible/nestjs-dismissible-item';
|
|
3
|
+
|
|
4
|
+
@Injectable()
|
|
5
|
+
export class DismissibleHelper {
|
|
6
|
+
isDismissed(item: DismissibleItemDto): boolean {
|
|
7
|
+
return item.dismissedAt !== undefined && item.dismissedAt !== null;
|
|
8
|
+
}
|
|
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
ADDED
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|