@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.
- package/README.md +58 -74
- package/jest.config.ts +1 -1
- package/package.json +8 -11
- package/project.json +1 -1
- 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 +2 -3
- package/src/api/use-cases/dismiss/dismiss.controller.spec.ts +1 -2
- package/src/api/use-cases/dismiss/dismiss.controller.ts +9 -10
- 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 +11 -58
- 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 +9 -10
- 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 +75 -29
- package/src/core/dismissible-core.service.ts +40 -28
- package/src/core/dismissible.service.spec.ts +106 -24
- package/src/core/dismissible.service.ts +93 -54
- package/src/core/hook-runner.service.spec.ts +495 -54
- package/src/core/hook-runner.service.ts +125 -24
- package/src/core/index.ts +0 -1
- package/src/core/lifecycle-hook.interface.ts +7 -122
- package/src/core/service-responses.interface.ts +9 -9
- package/src/dismissible.module.integration.spec.ts +704 -0
- package/src/dismissible.module.ts +10 -11
- package/src/events/dismissible.events.ts +17 -40
- package/src/index.ts +1 -1
- package/src/response/http-exception-filter.spec.ts +179 -0
- package/src/response/http-exception-filter.ts +3 -3
- package/src/response/response.service.spec.ts +0 -14
- package/src/testing/factories.ts +24 -9
- 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
- package/src/request/index.ts +0 -2
- package/src/request/request-context.decorator.ts +0 -14
- package/src/request/request-context.interface.ts +0 -6
|
@@ -1,23 +1,22 @@
|
|
|
1
|
-
import { Controller, Post,
|
|
1
|
+
import { Controller, Post, UseFilters } from '@nestjs/common';
|
|
2
2
|
import { ApiTags, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger';
|
|
3
3
|
import { DismissibleService } from '../../../core/dismissible.service';
|
|
4
4
|
import { DismissibleItemMapper } from '../../dismissible-item.mapper';
|
|
5
|
-
import { RequestContext } from '
|
|
6
|
-
import { IRequestContext } from '../../../request/request-context.interface';
|
|
7
|
-
import { BaseMetadata } from '@dismissible/nestjs-dismissible-item';
|
|
5
|
+
import { RequestContext, IRequestContext } from '@dismissible/nestjs-dismissible-request';
|
|
8
6
|
import { RestoreResponseDto } from './restore.response.dto';
|
|
9
7
|
import { ResponseService } from '../../../response/response.service';
|
|
10
8
|
import { HttpExceptionFilter } from '../../../response/http-exception-filter';
|
|
11
9
|
import { API_TAG_DISMISSIBLE } from '../api-tags.constants';
|
|
10
|
+
import { UserId, ItemId } from '../../validation';
|
|
12
11
|
|
|
13
12
|
/**
|
|
14
13
|
* Controller for restore dismissible item operations.
|
|
15
14
|
*/
|
|
16
15
|
@ApiTags(API_TAG_DISMISSIBLE)
|
|
17
|
-
@Controller('v1/
|
|
16
|
+
@Controller('v1/users/:userId/items')
|
|
18
17
|
export class RestoreController {
|
|
19
18
|
constructor(
|
|
20
|
-
private readonly dismissibleService: DismissibleService
|
|
19
|
+
private readonly dismissibleService: DismissibleService,
|
|
21
20
|
private readonly mapper: DismissibleItemMapper,
|
|
22
21
|
private readonly responseService: ResponseService,
|
|
23
22
|
) {}
|
|
@@ -29,12 +28,12 @@ export class RestoreController {
|
|
|
29
28
|
})
|
|
30
29
|
@ApiParam({
|
|
31
30
|
name: 'userId',
|
|
32
|
-
description: 'User identifier',
|
|
31
|
+
description: 'User identifier (max length: 32 characters)',
|
|
33
32
|
example: 'user-123',
|
|
34
33
|
})
|
|
35
34
|
@ApiParam({
|
|
36
35
|
name: 'itemId',
|
|
37
|
-
description: 'Unique identifier for the dismissible item',
|
|
36
|
+
description: 'Unique identifier for the dismissible item (max length: 32 characters)',
|
|
38
37
|
example: 'welcome-banner-v2',
|
|
39
38
|
})
|
|
40
39
|
@ApiResponse({
|
|
@@ -52,8 +51,8 @@ export class RestoreController {
|
|
|
52
51
|
})
|
|
53
52
|
@UseFilters(HttpExceptionFilter)
|
|
54
53
|
async restore(
|
|
55
|
-
@
|
|
56
|
-
@
|
|
54
|
+
@UserId() userId: string,
|
|
55
|
+
@ItemId() itemId: string,
|
|
57
56
|
@RequestContext() context: IRequestContext,
|
|
58
57
|
): Promise<RestoreResponseDto> {
|
|
59
58
|
const result = await this.dismissibleService.restore(itemId, userId, context);
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { BadRequestException, ArgumentMetadata } from '@nestjs/common';
|
|
2
|
+
import { ParamValidationPipe } from './param-validation.pipe';
|
|
3
|
+
import { VALIDATION_CONSTANTS } from '../../validation/dismissible-input.dto';
|
|
4
|
+
|
|
5
|
+
describe('ParamValidationPipe', () => {
|
|
6
|
+
let pipe: ParamValidationPipe;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
pipe = new ParamValidationPipe();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe('valid values', () => {
|
|
13
|
+
it('should pass validation for a valid alphanumeric string', () => {
|
|
14
|
+
const metadata: ArgumentMetadata = {
|
|
15
|
+
type: 'param',
|
|
16
|
+
data: 'userId',
|
|
17
|
+
};
|
|
18
|
+
const result = pipe.transform('user123', metadata);
|
|
19
|
+
expect(result).toBe('user123');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should pass validation for a string with dashes', () => {
|
|
23
|
+
const metadata: ArgumentMetadata = {
|
|
24
|
+
type: 'param',
|
|
25
|
+
data: 'itemId',
|
|
26
|
+
};
|
|
27
|
+
const result = pipe.transform('item-123', metadata);
|
|
28
|
+
expect(result).toBe('item-123');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should pass validation for a string with underscores', () => {
|
|
32
|
+
const metadata: ArgumentMetadata = {
|
|
33
|
+
type: 'param',
|
|
34
|
+
data: 'userId',
|
|
35
|
+
};
|
|
36
|
+
const result = pipe.transform('user_123', metadata);
|
|
37
|
+
expect(result).toBe('user_123');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should pass validation for a string with mixed valid characters', () => {
|
|
41
|
+
const metadata: ArgumentMetadata = {
|
|
42
|
+
type: 'param',
|
|
43
|
+
data: 'itemId',
|
|
44
|
+
};
|
|
45
|
+
const result = pipe.transform('item-123_test', metadata);
|
|
46
|
+
expect(result).toBe('item-123_test');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should pass validation for minimum length (1 character)', () => {
|
|
50
|
+
const metadata: ArgumentMetadata = {
|
|
51
|
+
type: 'param',
|
|
52
|
+
data: 'userId',
|
|
53
|
+
};
|
|
54
|
+
const result = pipe.transform('a', metadata);
|
|
55
|
+
expect(result).toBe('a');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should pass validation for maximum length (64 characters)', () => {
|
|
59
|
+
const metadata: ArgumentMetadata = {
|
|
60
|
+
type: 'param',
|
|
61
|
+
data: 'itemId',
|
|
62
|
+
};
|
|
63
|
+
const validMaxLength = 'a'.repeat(VALIDATION_CONSTANTS.ID_MAX_LENGTH);
|
|
64
|
+
const result = pipe.transform(validMaxLength, metadata);
|
|
65
|
+
expect(result).toBe(validMaxLength);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('empty or null values', () => {
|
|
70
|
+
it('should throw BadRequestException for empty string', () => {
|
|
71
|
+
const metadata: ArgumentMetadata = {
|
|
72
|
+
type: 'param',
|
|
73
|
+
data: 'userId',
|
|
74
|
+
};
|
|
75
|
+
expect(() => pipe.transform('', metadata)).toThrow(BadRequestException);
|
|
76
|
+
expect(() => pipe.transform('', metadata)).toThrow('userId is required');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should throw BadRequestException for whitespace-only string', () => {
|
|
80
|
+
const metadata: ArgumentMetadata = {
|
|
81
|
+
type: 'param',
|
|
82
|
+
data: 'itemId',
|
|
83
|
+
};
|
|
84
|
+
expect(() => pipe.transform(' ', metadata)).toThrow(BadRequestException);
|
|
85
|
+
expect(() => pipe.transform(' ', metadata)).toThrow('itemId is required');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should throw BadRequestException for null value', () => {
|
|
89
|
+
const metadata: ArgumentMetadata = {
|
|
90
|
+
type: 'param',
|
|
91
|
+
data: 'userId',
|
|
92
|
+
};
|
|
93
|
+
expect(() => pipe.transform(null as any, metadata)).toThrow(BadRequestException);
|
|
94
|
+
expect(() => pipe.transform(null as any, metadata)).toThrow('userId is required');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should throw BadRequestException for undefined value', () => {
|
|
98
|
+
const metadata: ArgumentMetadata = {
|
|
99
|
+
type: 'param',
|
|
100
|
+
data: 'itemId',
|
|
101
|
+
};
|
|
102
|
+
expect(() => pipe.transform(undefined as any, metadata)).toThrow(BadRequestException);
|
|
103
|
+
expect(() => pipe.transform(undefined as any, metadata)).toThrow('itemId is required');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should use default parameter name when metadata.data is not provided', () => {
|
|
107
|
+
const metadata: ArgumentMetadata = {
|
|
108
|
+
type: 'param',
|
|
109
|
+
};
|
|
110
|
+
expect(() => pipe.transform('', metadata)).toThrow(BadRequestException);
|
|
111
|
+
expect(() => pipe.transform('', metadata)).toThrow('parameter is required');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('length validation', () => {
|
|
116
|
+
it('should throw BadRequestException for value below minimum length', () => {
|
|
117
|
+
const metadata: ArgumentMetadata = {
|
|
118
|
+
type: 'param',
|
|
119
|
+
data: 'userId',
|
|
120
|
+
};
|
|
121
|
+
expect(() => pipe.transform('', metadata)).toThrow(BadRequestException);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should throw BadRequestException for value exceeding maximum length', () => {
|
|
125
|
+
const metadata: ArgumentMetadata = {
|
|
126
|
+
type: 'param',
|
|
127
|
+
data: 'itemId',
|
|
128
|
+
};
|
|
129
|
+
const tooLong = 'a'.repeat(VALIDATION_CONSTANTS.ID_MAX_LENGTH + 1);
|
|
130
|
+
expect(() => pipe.transform(tooLong, metadata)).toThrow(BadRequestException);
|
|
131
|
+
expect(() => pipe.transform(tooLong, metadata)).toThrow(
|
|
132
|
+
`itemId must be at most ${VALIDATION_CONSTANTS.ID_MAX_LENGTH} characters`,
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('pattern validation', () => {
|
|
138
|
+
it('should throw BadRequestException for string with spaces', () => {
|
|
139
|
+
const metadata: ArgumentMetadata = {
|
|
140
|
+
type: 'param',
|
|
141
|
+
data: 'userId',
|
|
142
|
+
};
|
|
143
|
+
expect(() => pipe.transform('user 123', metadata)).toThrow(BadRequestException);
|
|
144
|
+
expect(() => pipe.transform('user 123', metadata)).toThrow(
|
|
145
|
+
`userId ${VALIDATION_CONSTANTS.ID_PATTERN_MESSAGE}`,
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should throw BadRequestException for string with special characters', () => {
|
|
150
|
+
const metadata: ArgumentMetadata = {
|
|
151
|
+
type: 'param',
|
|
152
|
+
data: 'itemId',
|
|
153
|
+
};
|
|
154
|
+
expect(() => pipe.transform('item@123', metadata)).toThrow(BadRequestException);
|
|
155
|
+
expect(() => pipe.transform('item@123', metadata)).toThrow(
|
|
156
|
+
`itemId ${VALIDATION_CONSTANTS.ID_PATTERN_MESSAGE}`,
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should throw BadRequestException for string with dots', () => {
|
|
161
|
+
const metadata: ArgumentMetadata = {
|
|
162
|
+
type: 'param',
|
|
163
|
+
data: 'userId',
|
|
164
|
+
};
|
|
165
|
+
expect(() => pipe.transform('user.123', metadata)).toThrow(BadRequestException);
|
|
166
|
+
expect(() => pipe.transform('user.123', metadata)).toThrow(
|
|
167
|
+
`userId ${VALIDATION_CONSTANTS.ID_PATTERN_MESSAGE}`,
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should throw BadRequestException for string with slashes', () => {
|
|
172
|
+
const metadata: ArgumentMetadata = {
|
|
173
|
+
type: 'param',
|
|
174
|
+
data: 'itemId',
|
|
175
|
+
};
|
|
176
|
+
expect(() => pipe.transform('item/123', metadata)).toThrow(BadRequestException);
|
|
177
|
+
expect(() => pipe.transform('item/123', metadata)).toThrow(
|
|
178
|
+
`itemId ${VALIDATION_CONSTANTS.ID_PATTERN_MESSAGE}`,
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should throw BadRequestException for string with unicode characters', () => {
|
|
183
|
+
const metadata: ArgumentMetadata = {
|
|
184
|
+
type: 'param',
|
|
185
|
+
data: 'userId',
|
|
186
|
+
};
|
|
187
|
+
expect(() => pipe.transform('userñ123', metadata)).toThrow(BadRequestException);
|
|
188
|
+
expect(() => pipe.transform('userñ123', metadata)).toThrow(
|
|
189
|
+
`userId ${VALIDATION_CONSTANTS.ID_PATTERN_MESSAGE}`,
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should throw BadRequestException for string starting with invalid character', () => {
|
|
194
|
+
const metadata: ArgumentMetadata = {
|
|
195
|
+
type: 'param',
|
|
196
|
+
data: 'itemId',
|
|
197
|
+
};
|
|
198
|
+
expect(() => pipe.transform('@item123', metadata)).toThrow(BadRequestException);
|
|
199
|
+
expect(() => pipe.transform('@item123', metadata)).toThrow(
|
|
200
|
+
`itemId ${VALIDATION_CONSTANTS.ID_PATTERN_MESSAGE}`,
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should throw BadRequestException for string ending with invalid character', () => {
|
|
205
|
+
const metadata: ArgumentMetadata = {
|
|
206
|
+
type: 'param',
|
|
207
|
+
data: 'userId',
|
|
208
|
+
};
|
|
209
|
+
expect(() => pipe.transform('user123#', metadata)).toThrow(BadRequestException);
|
|
210
|
+
expect(() => pipe.transform('user123#', metadata)).toThrow(
|
|
211
|
+
`userId ${VALIDATION_CONSTANTS.ID_PATTERN_MESSAGE}`,
|
|
212
|
+
);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe('error messages', () => {
|
|
217
|
+
it('should include parameter name in required error message', () => {
|
|
218
|
+
const metadata: ArgumentMetadata = {
|
|
219
|
+
type: 'param',
|
|
220
|
+
data: 'customParam',
|
|
221
|
+
};
|
|
222
|
+
try {
|
|
223
|
+
pipe.transform('', metadata);
|
|
224
|
+
fail('Should have thrown BadRequestException');
|
|
225
|
+
} catch (error) {
|
|
226
|
+
expect(error).toBeInstanceOf(BadRequestException);
|
|
227
|
+
expect((error as BadRequestException).message).toBe('customParam is required');
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('should include parameter name in length error message', () => {
|
|
232
|
+
const metadata: ArgumentMetadata = {
|
|
233
|
+
type: 'param',
|
|
234
|
+
data: 'testParam',
|
|
235
|
+
};
|
|
236
|
+
const tooLong = 'a'.repeat(VALIDATION_CONSTANTS.ID_MAX_LENGTH + 1);
|
|
237
|
+
try {
|
|
238
|
+
pipe.transform(tooLong, metadata);
|
|
239
|
+
fail('Should have thrown BadRequestException');
|
|
240
|
+
} catch (error) {
|
|
241
|
+
expect(error).toBeInstanceOf(BadRequestException);
|
|
242
|
+
expect((error as BadRequestException).message).toContain('testParam');
|
|
243
|
+
expect((error as BadRequestException).message).toContain(
|
|
244
|
+
`must be at most ${VALIDATION_CONSTANTS.ID_MAX_LENGTH} characters`,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('should include parameter name in pattern error message', () => {
|
|
250
|
+
const metadata: ArgumentMetadata = {
|
|
251
|
+
type: 'param',
|
|
252
|
+
data: 'myParam',
|
|
253
|
+
};
|
|
254
|
+
try {
|
|
255
|
+
pipe.transform('invalid@value', metadata);
|
|
256
|
+
fail('Should have thrown BadRequestException');
|
|
257
|
+
} catch (error) {
|
|
258
|
+
expect(error).toBeInstanceOf(BadRequestException);
|
|
259
|
+
expect((error as BadRequestException).message).toContain('myParam');
|
|
260
|
+
expect((error as BadRequestException).message).toContain(
|
|
261
|
+
VALIDATION_CONSTANTS.ID_PATTERN_MESSAGE,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe('edge cases', () => {
|
|
268
|
+
it('should handle numeric-only strings', () => {
|
|
269
|
+
const metadata: ArgumentMetadata = {
|
|
270
|
+
type: 'param',
|
|
271
|
+
data: 'itemId',
|
|
272
|
+
};
|
|
273
|
+
const result = pipe.transform('123456', metadata);
|
|
274
|
+
expect(result).toBe('123456');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('should handle uppercase letters', () => {
|
|
278
|
+
const metadata: ArgumentMetadata = {
|
|
279
|
+
type: 'param',
|
|
280
|
+
data: 'userId',
|
|
281
|
+
};
|
|
282
|
+
const result = pipe.transform('USER123', metadata);
|
|
283
|
+
expect(result).toBe('USER123');
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('should handle lowercase letters', () => {
|
|
287
|
+
const metadata: ArgumentMetadata = {
|
|
288
|
+
type: 'param',
|
|
289
|
+
data: 'itemId',
|
|
290
|
+
};
|
|
291
|
+
const result = pipe.transform('user123', metadata);
|
|
292
|
+
expect(result).toBe('user123');
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should handle mixed case letters', () => {
|
|
296
|
+
const metadata: ArgumentMetadata = {
|
|
297
|
+
type: 'param',
|
|
298
|
+
data: 'userId',
|
|
299
|
+
};
|
|
300
|
+
const result = pipe.transform('User123Item', metadata);
|
|
301
|
+
expect(result).toBe('User123Item');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('should handle string with only dashes and underscores', () => {
|
|
305
|
+
const metadata: ArgumentMetadata = {
|
|
306
|
+
type: 'param',
|
|
307
|
+
data: 'itemId',
|
|
308
|
+
};
|
|
309
|
+
const result = pipe.transform('_-', metadata);
|
|
310
|
+
expect(result).toBe('_-');
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { PipeTransform, Injectable, BadRequestException, ArgumentMetadata } from '@nestjs/common';
|
|
2
|
+
import { VALIDATION_CONSTANTS } from '../../validation/dismissible-input.dto';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Validation pipe for userId and itemId route parameters.
|
|
6
|
+
* Validates:
|
|
7
|
+
* - Required (non-empty)
|
|
8
|
+
* - Length between 1-64 characters
|
|
9
|
+
* - Contains only alphanumeric characters, dashes, and underscores
|
|
10
|
+
*/
|
|
11
|
+
@Injectable()
|
|
12
|
+
export class ParamValidationPipe implements PipeTransform<string, string> {
|
|
13
|
+
transform(value: string, metadata: ArgumentMetadata): string {
|
|
14
|
+
const paramName = metadata.data || 'parameter';
|
|
15
|
+
|
|
16
|
+
if (!value || value.trim() === '') {
|
|
17
|
+
throw new BadRequestException(`${paramName} is required`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (value.length < VALIDATION_CONSTANTS.ID_MIN_LENGTH) {
|
|
21
|
+
throw new BadRequestException(
|
|
22
|
+
`${paramName} must be at least ${VALIDATION_CONSTANTS.ID_MIN_LENGTH} character`,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (value.length > VALIDATION_CONSTANTS.ID_MAX_LENGTH) {
|
|
27
|
+
throw new BadRequestException(
|
|
28
|
+
`${paramName} must be at most ${VALIDATION_CONSTANTS.ID_MAX_LENGTH} characters`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!VALIDATION_CONSTANTS.ID_PATTERN.test(value)) {
|
|
33
|
+
throw new BadRequestException(`${paramName} ${VALIDATION_CONSTANTS.ID_PATTERN_MESSAGE}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Param } from '@nestjs/common';
|
|
2
|
+
import { ParamValidationPipe } from './param-validation.pipe';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Custom parameter decorator for userId.
|
|
6
|
+
* Combines @Param('userId') with ParamValidationPipe for validation.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* @Get(':itemId')
|
|
11
|
+
* async getOrCreate(
|
|
12
|
+
* @UserId() userId: string,
|
|
13
|
+
* @ItemId() itemId: string,
|
|
14
|
+
* )
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export const UserId = () => Param('userId', ParamValidationPipe);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Custom parameter decorator for itemId.
|
|
21
|
+
* Combines @Param('itemId') with ParamValidationPipe for validation.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* @Get(':itemId')
|
|
26
|
+
* async getOrCreate(
|
|
27
|
+
* @UserId() userId: string,
|
|
28
|
+
* @ItemId() itemId: string,
|
|
29
|
+
* )
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export const ItemId = () => Param('itemId', ParamValidationPipe);
|
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
ItemAlreadyDismissedException,
|
|
8
8
|
ItemNotDismissedException,
|
|
9
9
|
} from '../exceptions';
|
|
10
|
-
import { BaseMetadata } from '@dismissible/nestjs-dismissible-item';
|
|
11
10
|
import { DismissibleItemFactory } from '@dismissible/nestjs-dismissible-item';
|
|
12
11
|
import { IDismissibleLogger } from '@dismissible/nestjs-logger';
|
|
13
12
|
import { ValidationService } from '@dismissible/nestjs-validation';
|
|
@@ -16,8 +15,8 @@ import { DismissibleHelper } from '../utils/dismissible.helper';
|
|
|
16
15
|
import { DateService } from '../utils/date/date.service';
|
|
17
16
|
|
|
18
17
|
describe('DismissibleCoreService', () => {
|
|
19
|
-
let service: DismissibleCoreService
|
|
20
|
-
let storage: Mock<IDismissibleStorage
|
|
18
|
+
let service: DismissibleCoreService;
|
|
19
|
+
let storage: Mock<IDismissibleStorage>;
|
|
21
20
|
let mockDateService: Mock<DateService>;
|
|
22
21
|
let mockLogger: Mock<IDismissibleLogger>;
|
|
23
22
|
let itemFactory: Mock<DismissibleItemFactory>;
|
|
@@ -30,7 +29,7 @@ describe('DismissibleCoreService', () => {
|
|
|
30
29
|
mockLogger = mock<IDismissibleLogger>({
|
|
31
30
|
failIfMockNotProvided: false,
|
|
32
31
|
});
|
|
33
|
-
storage = mock<IDismissibleStorage
|
|
32
|
+
storage = mock<IDismissibleStorage>({
|
|
34
33
|
failIfMockNotProvided: false,
|
|
35
34
|
});
|
|
36
35
|
dismissibleHelper = mock(DismissibleHelper, { failIfMockNotProvided: false });
|
|
@@ -51,6 +50,78 @@ describe('DismissibleCoreService', () => {
|
|
|
51
50
|
jest.clearAllMocks();
|
|
52
51
|
});
|
|
53
52
|
|
|
53
|
+
describe('get', () => {
|
|
54
|
+
it('should return item when it exists', async () => {
|
|
55
|
+
const userId = 'user-123';
|
|
56
|
+
const existingItem = createTestItem({ id: 'existing-item', userId });
|
|
57
|
+
storage.get.mockResolvedValue(existingItem);
|
|
58
|
+
|
|
59
|
+
const result = await service.get('existing-item', userId);
|
|
60
|
+
|
|
61
|
+
expect(result).toEqual(existingItem);
|
|
62
|
+
expect(storage.get).toHaveBeenCalledWith(userId, 'existing-item');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should return null when item does not exist', async () => {
|
|
66
|
+
const userId = 'user-123';
|
|
67
|
+
storage.get.mockResolvedValue(null);
|
|
68
|
+
|
|
69
|
+
const result = await service.get('non-existent', userId);
|
|
70
|
+
|
|
71
|
+
expect(result).toBeNull();
|
|
72
|
+
expect(storage.get).toHaveBeenCalledWith(userId, 'non-existent');
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('create', () => {
|
|
77
|
+
it('should create a new item', async () => {
|
|
78
|
+
const testDate = new Date('2024-01-15T10:00:00.000Z');
|
|
79
|
+
const userId = 'user-123';
|
|
80
|
+
const newItem = createTestItem({ id: 'new-item', userId, createdAt: testDate });
|
|
81
|
+
|
|
82
|
+
storage.create.mockResolvedValue(newItem);
|
|
83
|
+
mockDateService.getNow.mockReturnValue(testDate);
|
|
84
|
+
itemFactory.create.mockReturnValue(newItem);
|
|
85
|
+
|
|
86
|
+
const result = await service.create('new-item', userId);
|
|
87
|
+
|
|
88
|
+
expect(result.id).toBe('new-item');
|
|
89
|
+
expect(result.userId).toBe(userId);
|
|
90
|
+
expect(result.createdAt).toBeInstanceOf(Date);
|
|
91
|
+
expect(result.dismissedAt).toBeUndefined();
|
|
92
|
+
expect(storage.create).toHaveBeenCalledWith(newItem);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should validate item before storage', async () => {
|
|
96
|
+
const userId = 'user-123';
|
|
97
|
+
const testDate = new Date('2024-01-15T10:00:00.000Z');
|
|
98
|
+
const newItem = createTestItem({ id: 'new-item', userId, createdAt: testDate });
|
|
99
|
+
|
|
100
|
+
storage.create.mockResolvedValue(newItem);
|
|
101
|
+
mockDateService.getNow.mockReturnValue(testDate);
|
|
102
|
+
itemFactory.create.mockReturnValue(newItem);
|
|
103
|
+
|
|
104
|
+
await service.create('new-item', userId);
|
|
105
|
+
|
|
106
|
+
expect(validationService.validateInstance).toHaveBeenCalledWith(newItem);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should throw BadRequestException when validation fails', async () => {
|
|
110
|
+
const userId = 'user-123';
|
|
111
|
+
const testDate = new Date('2024-01-15T10:00:00.000Z');
|
|
112
|
+
const newItem = createTestItem({ id: 'new-item', userId, createdAt: testDate });
|
|
113
|
+
|
|
114
|
+
mockDateService.getNow.mockReturnValue(testDate);
|
|
115
|
+
itemFactory.create.mockReturnValue(newItem);
|
|
116
|
+
validationService.validateInstance.mockRejectedValue(
|
|
117
|
+
new BadRequestException('id must be a string'),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
await expect(service.create('new-item', userId)).rejects.toThrow(BadRequestException);
|
|
121
|
+
expect(storage.create).not.toHaveBeenCalled();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
54
125
|
describe('getOrCreate', () => {
|
|
55
126
|
it('should create a new item when it does not exist', async () => {
|
|
56
127
|
const testDate = new Date('2024-01-15T10:00:00.000Z');
|
|
@@ -83,23 +154,6 @@ describe('DismissibleCoreService', () => {
|
|
|
83
154
|
expect(result.item).toEqual(existingItem);
|
|
84
155
|
expect(storage.get).toHaveBeenCalledWith(userId, 'existing-item');
|
|
85
156
|
});
|
|
86
|
-
|
|
87
|
-
it('should create item with metadata when provided', async () => {
|
|
88
|
-
const userId = 'user-123';
|
|
89
|
-
const metadata = { version: 2, category: 'test' };
|
|
90
|
-
const testDate = new Date('2024-01-15T10:00:00.000Z');
|
|
91
|
-
const newItem = createTestItem({ id: 'new-item', userId, metadata, createdAt: testDate });
|
|
92
|
-
|
|
93
|
-
storage.get.mockResolvedValue(null);
|
|
94
|
-
storage.create.mockResolvedValue(newItem);
|
|
95
|
-
mockDateService.getNow.mockReturnValue(testDate);
|
|
96
|
-
itemFactory.create.mockReturnValue(newItem);
|
|
97
|
-
|
|
98
|
-
const result = await service.getOrCreate('new-item', userId, { metadata });
|
|
99
|
-
|
|
100
|
-
expect(result.item.metadata).toEqual(metadata);
|
|
101
|
-
expect(storage.create).toHaveBeenCalledWith(newItem);
|
|
102
|
-
});
|
|
103
157
|
});
|
|
104
158
|
|
|
105
159
|
describe('dismiss', () => {
|
|
@@ -148,17 +202,14 @@ describe('DismissibleCoreService', () => {
|
|
|
148
202
|
const item = createTestItem({
|
|
149
203
|
id: 'test-item',
|
|
150
204
|
userId,
|
|
151
|
-
metadata: { key: 'value' },
|
|
152
205
|
});
|
|
153
206
|
const previousItem = createTestItem({
|
|
154
207
|
id: 'test-item',
|
|
155
208
|
userId,
|
|
156
|
-
metadata: { key: 'value' },
|
|
157
209
|
});
|
|
158
210
|
const dismissedItem = createDismissedTestItem({
|
|
159
211
|
id: 'test-item',
|
|
160
212
|
userId,
|
|
161
|
-
metadata: { key: 'value' },
|
|
162
213
|
});
|
|
163
214
|
const testDate = new Date('2024-01-15T12:00:00.000Z');
|
|
164
215
|
|
|
@@ -173,7 +224,6 @@ describe('DismissibleCoreService', () => {
|
|
|
173
224
|
|
|
174
225
|
expect(result.previousItem.id).toBe(item.id);
|
|
175
226
|
expect(result.previousItem.dismissedAt).toBeUndefined();
|
|
176
|
-
expect(result.previousItem.metadata).toEqual({ key: 'value' });
|
|
177
227
|
});
|
|
178
228
|
});
|
|
179
229
|
|
|
@@ -221,17 +271,14 @@ describe('DismissibleCoreService', () => {
|
|
|
221
271
|
const dismissedItem = createDismissedTestItem({
|
|
222
272
|
id: 'dismissed-item',
|
|
223
273
|
userId,
|
|
224
|
-
metadata: { key: 'value' },
|
|
225
274
|
});
|
|
226
275
|
const previousItem = createDismissedTestItem({
|
|
227
276
|
id: 'dismissed-item',
|
|
228
277
|
userId,
|
|
229
|
-
metadata: { key: 'value' },
|
|
230
278
|
});
|
|
231
279
|
const restoredItem = createTestItem({
|
|
232
280
|
id: 'dismissed-item',
|
|
233
281
|
userId,
|
|
234
|
-
metadata: { key: 'value' },
|
|
235
282
|
});
|
|
236
283
|
|
|
237
284
|
storage.get.mockResolvedValue(dismissedItem);
|
|
@@ -244,7 +291,6 @@ describe('DismissibleCoreService', () => {
|
|
|
244
291
|
|
|
245
292
|
expect(result.previousItem.id).toBe(dismissedItem.id);
|
|
246
293
|
expect(result.previousItem.dismissedAt).toBeDefined();
|
|
247
|
-
expect(result.previousItem.metadata).toEqual({ key: 'value' });
|
|
248
294
|
});
|
|
249
295
|
});
|
|
250
296
|
|