@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
package/README.md
CHANGED
|
@@ -6,14 +6,16 @@ A powerful NestJS library for managing dismissible state in your applications. P
|
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
9
|
+
- **Simple API** - Easy-to-use service methods for get-or-create, dismiss, and restore operations
|
|
10
|
+
- **High Performance** - Built on Fastify for maximum throughput
|
|
11
|
+
- **Flexible Storage** - Default in-memory storage with support for custom storage adapters (PostgreSQL, Redis, etc.)
|
|
12
|
+
- **Lifecycle Hooks** - Intercept and customize operations with pre/post hooks
|
|
13
|
+
- **JWT Authentication** - Optional JWT auth hook for securing endpoints with OIDC providers
|
|
14
|
+
- **Event-Driven** - Built-in event emission for all operations
|
|
15
|
+
- **Type-Safe** - Full TypeScript support
|
|
16
|
+
- **Validation** - Automatic validation of dismissible items
|
|
17
|
+
- **Swagger Integration** - Auto-generated API documentation
|
|
18
|
+
- **React Client** - Works out of the box with [@dismissible/react-client](https://www.npmjs.com/package/@dismissible/react-client)
|
|
17
19
|
|
|
18
20
|
## Installation
|
|
19
21
|
|
|
@@ -41,21 +43,21 @@ export class AppModule {}
|
|
|
41
43
|
|
|
42
44
|
The module automatically registers REST endpoints for all operations:
|
|
43
45
|
|
|
44
|
-
- `GET /v1/
|
|
45
|
-
- `DELETE /v1/
|
|
46
|
-
- `POST /v1/
|
|
46
|
+
- `GET /v1/users/:userId/items/:itemId` - Get or create an item
|
|
47
|
+
- `DELETE /v1/users/:userId/items/:itemId` - Dismiss an item
|
|
48
|
+
- `POST /v1/users/:userId/items/:itemId` - Restore a dismissed item
|
|
47
49
|
|
|
48
50
|
Example request:
|
|
49
51
|
|
|
50
52
|
```bash
|
|
51
53
|
# Get or create an item
|
|
52
|
-
curl http://localhost:3000/v1/
|
|
54
|
+
curl http://localhost:3000/v1/users/user-123/items/welcome-banner
|
|
53
55
|
|
|
54
56
|
# Dismiss an item
|
|
55
|
-
curl -X DELETE http://localhost:3000/v1/
|
|
57
|
+
curl -X DELETE http://localhost:3000/v1/users/user-123/items/welcome-banner
|
|
56
58
|
|
|
57
59
|
# Restore a dismissed item
|
|
58
|
-
curl -X POST http://localhost:3000/v1/
|
|
60
|
+
curl -X POST http://localhost:3000/v1/users/user-123/items/welcome-banner
|
|
59
61
|
```
|
|
60
62
|
|
|
61
63
|
### React Client Integration
|
|
@@ -156,9 +158,6 @@ export class FeaturesController {
|
|
|
156
158
|
const result = await this.dismissibleService.getOrCreate(
|
|
157
159
|
itemId,
|
|
158
160
|
userId,
|
|
159
|
-
{
|
|
160
|
-
metadata: { version: '1.0', category: 'onboarding' },
|
|
161
|
-
},
|
|
162
161
|
undefined, // optional request context
|
|
163
162
|
);
|
|
164
163
|
|
|
@@ -182,6 +181,33 @@ export class FeaturesController {
|
|
|
182
181
|
}
|
|
183
182
|
```
|
|
184
183
|
|
|
184
|
+
### JWT Authentication
|
|
185
|
+
|
|
186
|
+
Secure your API endpoints using the JWT Auth Hook with any OIDC-compliant identity provider:
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
import { Module } from '@nestjs/common';
|
|
190
|
+
import { DismissibleModule } from '@dismissible/nestjs-dismissible';
|
|
191
|
+
import { JwtAuthHookModule, JwtAuthHook } from '@dismissible/nestjs-jwt-auth-hook';
|
|
192
|
+
|
|
193
|
+
@Module({
|
|
194
|
+
imports: [
|
|
195
|
+
JwtAuthHookModule.forRoot({
|
|
196
|
+
enabled: true,
|
|
197
|
+
wellKnownUrl: 'https://auth.example.com/.well-known/openid-configuration',
|
|
198
|
+
issuer: 'https://auth.example.com',
|
|
199
|
+
audience: 'my-api',
|
|
200
|
+
}),
|
|
201
|
+
DismissibleModule.forRoot({
|
|
202
|
+
hooks: [JwtAuthHook],
|
|
203
|
+
}),
|
|
204
|
+
],
|
|
205
|
+
})
|
|
206
|
+
export class AppModule {}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
See the [@dismissible/nestjs-jwt-auth-hook](https://www.npmjs.com/package/@dismissible/nestjs-jwt-auth-hook) package for detailed configuration options.
|
|
210
|
+
|
|
185
211
|
### Custom Lifecycle Hooks
|
|
186
212
|
|
|
187
213
|
Lifecycle hooks allow you to intercept operations and add custom logic, validation, or mutations:
|
|
@@ -189,10 +215,8 @@ Lifecycle hooks allow you to intercept operations and add custom logic, validati
|
|
|
189
215
|
```typescript
|
|
190
216
|
import { Injectable } from '@nestjs/common';
|
|
191
217
|
import { IDismissibleLifecycleHook, IHookResult } from '@dismissible/nestjs-dismissible';
|
|
192
|
-
import { BaseMetadata } from '@dismissible/nestjs-dismissible-item';
|
|
193
|
-
|
|
194
218
|
@Injectable()
|
|
195
|
-
export class AuditHook implements IDismissibleLifecycleHook
|
|
219
|
+
export class AuditHook implements IDismissibleLifecycleHook {
|
|
196
220
|
// Lower priority runs first (default is 0)
|
|
197
221
|
readonly priority = 10;
|
|
198
222
|
|
|
@@ -215,7 +239,7 @@ export class AuditHook implements IDismissibleLifecycleHook<BaseMetadata> {
|
|
|
215
239
|
|
|
216
240
|
async onAfterCreate(
|
|
217
241
|
itemId: string,
|
|
218
|
-
item: DismissibleItemDto
|
|
242
|
+
item: DismissibleItemDto,
|
|
219
243
|
userId: string,
|
|
220
244
|
context?: IRequestContext,
|
|
221
245
|
): Promise<void> {
|
|
@@ -293,42 +317,6 @@ export class AnalyticsService {
|
|
|
293
317
|
}
|
|
294
318
|
```
|
|
295
319
|
|
|
296
|
-
### Custom Metadata Types
|
|
297
|
-
|
|
298
|
-
Define custom metadata types for type-safe item properties:
|
|
299
|
-
|
|
300
|
-
```typescript
|
|
301
|
-
import { BaseMetadata } from '@dismissible/nestjs-dismissible-item';
|
|
302
|
-
|
|
303
|
-
interface OnboardingMetadata extends BaseMetadata {
|
|
304
|
-
step: number;
|
|
305
|
-
completedAt?: string;
|
|
306
|
-
skipped: boolean;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Use in your service
|
|
310
|
-
@Injectable()
|
|
311
|
-
export class OnboardingService {
|
|
312
|
-
constructor(private readonly dismissibleService: DismissibleService<OnboardingMetadata>) {}
|
|
313
|
-
|
|
314
|
-
async trackStep(userId: string, step: number) {
|
|
315
|
-
const result = await this.dismissibleService.getOrCreate(
|
|
316
|
-
`onboarding-step-${step}`,
|
|
317
|
-
userId,
|
|
318
|
-
{
|
|
319
|
-
metadata: {
|
|
320
|
-
step,
|
|
321
|
-
skipped: false,
|
|
322
|
-
},
|
|
323
|
-
},
|
|
324
|
-
undefined,
|
|
325
|
-
);
|
|
326
|
-
|
|
327
|
-
return result.item;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
```
|
|
331
|
-
|
|
332
320
|
### Custom Logger
|
|
333
321
|
|
|
334
322
|
Provide a custom logger implementation:
|
|
@@ -376,17 +364,15 @@ The main service for interacting with dismissible items.
|
|
|
376
364
|
|
|
377
365
|
#### Methods
|
|
378
366
|
|
|
379
|
-
**`getOrCreate(itemId, userId,
|
|
367
|
+
**`getOrCreate(itemId, userId, context?)`**
|
|
380
368
|
|
|
381
369
|
Retrieves an existing item or creates a new one if it doesn't exist.
|
|
382
370
|
|
|
383
371
|
- `itemId: string` - Unique identifier for the item
|
|
384
372
|
- `userId: string` - User identifier (required)
|
|
385
|
-
- `options?: ICreateItemOptions<TMetadata>` - Optional creation options
|
|
386
|
-
- `metadata?: TMetadata` - Custom metadata to attach to the item
|
|
387
373
|
- `context?: IRequestContext` - Optional request context for tracing
|
|
388
374
|
|
|
389
|
-
Returns: `Promise<IGetOrCreateServiceResponse
|
|
375
|
+
Returns: `Promise<IGetOrCreateServiceResponse>`
|
|
390
376
|
|
|
391
377
|
**`dismiss(itemId, userId, context?)`**
|
|
392
378
|
|
|
@@ -396,7 +382,7 @@ Marks an item as dismissed.
|
|
|
396
382
|
- `userId: string` - User identifier
|
|
397
383
|
- `context?: IRequestContext` - Optional request context
|
|
398
384
|
|
|
399
|
-
Returns: `Promise<IDismissServiceResponse
|
|
385
|
+
Returns: `Promise<IDismissServiceResponse>`
|
|
400
386
|
|
|
401
387
|
**`restore(itemId, userId, context?)`**
|
|
402
388
|
|
|
@@ -406,12 +392,12 @@ Restores a previously dismissed item.
|
|
|
406
392
|
- `userId: string` - User identifier
|
|
407
393
|
- `context?: IRequestContext` - Optional request context
|
|
408
394
|
|
|
409
|
-
Returns: `Promise<IRestoreServiceResponse
|
|
395
|
+
Returns: `Promise<IRestoreServiceResponse>`
|
|
410
396
|
|
|
411
397
|
### Module Configuration
|
|
412
398
|
|
|
413
399
|
```typescript
|
|
414
|
-
interface IDismissibleModuleOptions
|
|
400
|
+
interface IDismissibleModuleOptions {
|
|
415
401
|
// Custom storage module (defaults to in-memory storage)
|
|
416
402
|
storage?: DynamicModule | Type<any>;
|
|
417
403
|
|
|
@@ -419,7 +405,7 @@ interface IDismissibleModuleOptions<TMetadata extends BaseMetadata> {
|
|
|
419
405
|
logger?: Type<IDismissibleLogger>;
|
|
420
406
|
|
|
421
407
|
// Lifecycle hooks to register
|
|
422
|
-
hooks?: Type<IDismissibleLifecycleHook
|
|
408
|
+
hooks?: Type<IDismissibleLifecycleHook>[];
|
|
423
409
|
}
|
|
424
410
|
```
|
|
425
411
|
|
|
@@ -435,13 +421,13 @@ The library emits the following events:
|
|
|
435
421
|
All events include:
|
|
436
422
|
|
|
437
423
|
- `id: string` - The item identifier
|
|
438
|
-
- `item: DismissibleItemDto
|
|
424
|
+
- `item: DismissibleItemDto` - The current item state
|
|
439
425
|
- `userId: string` - The user identifier
|
|
440
426
|
- `context?: IRequestContext` - Optional request context
|
|
441
427
|
|
|
442
428
|
Dismiss and restore events also include:
|
|
443
429
|
|
|
444
|
-
- `previousItem: DismissibleItemDto
|
|
430
|
+
- `previousItem: DismissibleItemDto` - The item state before the operation
|
|
445
431
|
|
|
446
432
|
## Lifecycle Hooks
|
|
447
433
|
|
|
@@ -481,21 +467,19 @@ Implement the `IDismissibleStorage` interface to create a custom storage adapter
|
|
|
481
467
|
```typescript
|
|
482
468
|
import { Injectable } from '@nestjs/common';
|
|
483
469
|
import { IDismissibleStorage } from '@dismissible/nestjs-storage';
|
|
484
|
-
import { DismissibleItemDto
|
|
470
|
+
import { DismissibleItemDto } from '@dismissible/nestjs-dismissible-item';
|
|
485
471
|
|
|
486
472
|
@Injectable()
|
|
487
|
-
export class RedisStorageAdapter
|
|
488
|
-
|
|
489
|
-
> implements IDismissibleStorage<TMetadata> {
|
|
490
|
-
async get(userId: string, itemId: string): Promise<DismissibleItemDto<TMetadata> | null> {
|
|
473
|
+
export class RedisStorageAdapter implements IDismissibleStorage {
|
|
474
|
+
async get(userId: string, itemId: string): Promise<DismissibleItemDto | null> {
|
|
491
475
|
// Your implementation
|
|
492
476
|
}
|
|
493
477
|
|
|
494
|
-
async create(userId: string, item: DismissibleItemDto
|
|
478
|
+
async create(userId: string, item: DismissibleItemDto): Promise<void> {
|
|
495
479
|
// Your implementation
|
|
496
480
|
}
|
|
497
481
|
|
|
498
|
-
async update(userId: string, item: DismissibleItemDto
|
|
482
|
+
async update(userId: string, item: DismissibleItemDto): Promise<void> {
|
|
499
483
|
// Your implementation
|
|
500
484
|
}
|
|
501
485
|
}
|
package/jest.config.ts
CHANGED
|
@@ -5,9 +5,9 @@ export default {
|
|
|
5
5
|
transform: {
|
|
6
6
|
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.json' }],
|
|
7
7
|
},
|
|
8
|
+
transformIgnorePatterns: ['node_modules/(?!(nest-typed-config|uuid)/)'],
|
|
8
9
|
moduleFileExtensions: ['ts', 'js', 'html'],
|
|
9
10
|
coverageDirectory: '../../coverage/libs/dismissible',
|
|
10
|
-
transformIgnorePatterns: ['node_modules/(?!(uuid)/)'],
|
|
11
11
|
collectCoverageFrom: [
|
|
12
12
|
'src/**/*.ts',
|
|
13
13
|
'!src/**/*.spec.ts',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dismissible/nestjs-dismissible",
|
|
3
|
-
"version": "0.0.2-canary.
|
|
3
|
+
"version": "0.0.2-canary.b0d8bfe.0",
|
|
4
4
|
"description": "Dismissible state management library for NestJS applications",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"types": "./src/index.d.ts",
|
|
@@ -12,15 +12,18 @@
|
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@nestjs/event-emitter": "^3.0.
|
|
16
|
-
"
|
|
15
|
+
"@nestjs/event-emitter": "^3.0.1",
|
|
16
|
+
"@dismissible/nestjs-dismissible-hooks": "^0.0.2-canary.b0d8bfe.0",
|
|
17
|
+
"@dismissible/nestjs-dismissible-item": "^0.0.2-canary.b0d8bfe.0",
|
|
18
|
+
"@dismissible/nestjs-dismissible-request": "^0.0.2-canary.b0d8bfe.0",
|
|
19
|
+
"@dismissible/nestjs-storage": "^0.0.2-canary.b0d8bfe.0",
|
|
20
|
+
"@dismissible/nestjs-logger": "^0.0.2-canary.b0d8bfe.0",
|
|
21
|
+
"@dismissible/nestjs-validation": "^0.0.2-canary.b0d8bfe.0"
|
|
17
22
|
},
|
|
18
23
|
"peerDependencies": {
|
|
19
24
|
"@nestjs/common": "^11.0.0",
|
|
20
25
|
"@nestjs/core": "^11.0.0",
|
|
21
26
|
"@nestjs/swagger": "^11.0.0",
|
|
22
|
-
"@dismissible/nestjs-dismissible-item": "^0.0.2-canary.8976e84.0",
|
|
23
|
-
"@dismissible/nestjs-storage": "^0.0.2-canary.8976e84.0",
|
|
24
27
|
"class-validator": "^0.14.0",
|
|
25
28
|
"class-transformer": "^0.5.0"
|
|
26
29
|
},
|
|
@@ -34,12 +37,6 @@
|
|
|
34
37
|
"@nestjs/swagger": {
|
|
35
38
|
"optional": false
|
|
36
39
|
},
|
|
37
|
-
"dismissible/nestjs-dismissible-item": {
|
|
38
|
-
"optional": false
|
|
39
|
-
},
|
|
40
|
-
"@dismissible/nestjs-storage": {
|
|
41
|
-
"optional": false
|
|
42
|
-
},
|
|
43
40
|
"class-validator": {
|
|
44
41
|
"optional": false
|
|
45
42
|
},
|
package/project.json
CHANGED
|
@@ -27,12 +27,4 @@ export class DismissibleItemResponseDto {
|
|
|
27
27
|
example: '2024-01-15T12:00:00.000Z',
|
|
28
28
|
})
|
|
29
29
|
dismissedAt?: string;
|
|
30
|
-
|
|
31
|
-
@ApiPropertyOptional({
|
|
32
|
-
description: 'Optional metadata associated with the item',
|
|
33
|
-
example: { version: 2, category: 'promotional' },
|
|
34
|
-
type: 'object',
|
|
35
|
-
additionalProperties: true,
|
|
36
|
-
})
|
|
37
|
-
metadata?: Record<string, unknown>;
|
|
38
30
|
}
|
|
@@ -22,7 +22,6 @@ describe('DismissibleItemMapper', () => {
|
|
|
22
22
|
expect(dto.userId).toBe('test-user-id');
|
|
23
23
|
expect(dto.createdAt).toBe('2024-01-15T10:00:00.000Z');
|
|
24
24
|
expect(dto.dismissedAt).toBeUndefined();
|
|
25
|
-
expect(dto.metadata).toBeUndefined();
|
|
26
25
|
});
|
|
27
26
|
|
|
28
27
|
it('should convert dates to ISO strings', () => {
|
|
@@ -48,16 +47,5 @@ describe('DismissibleItemMapper', () => {
|
|
|
48
47
|
|
|
49
48
|
expect(dto.userId).toBe('user-123');
|
|
50
49
|
});
|
|
51
|
-
|
|
52
|
-
it('should include metadata when present', () => {
|
|
53
|
-
const item = createTestItem({
|
|
54
|
-
id: 'test-item',
|
|
55
|
-
metadata: { version: 2, category: 'test' },
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
const dto = mapper.toResponseDto(item);
|
|
59
|
-
|
|
60
|
-
expect(dto.metadata).toEqual({ version: 2, category: 'test' });
|
|
61
|
-
});
|
|
62
50
|
});
|
|
63
51
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Injectable } from '@nestjs/common';
|
|
2
|
-
import {
|
|
2
|
+
import { DismissibleItemDto } from '@dismissible/nestjs-dismissible-item';
|
|
3
3
|
import { DismissibleItemResponseDto } from './dismissible-item-response.dto';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -11,9 +11,7 @@ export class DismissibleItemMapper {
|
|
|
11
11
|
* Convert a dismissible item to a response DTO.
|
|
12
12
|
* Converts Date objects to ISO 8601 strings.
|
|
13
13
|
*/
|
|
14
|
-
toResponseDto
|
|
15
|
-
item: DismissibleItemDto<TMetadata>,
|
|
16
|
-
): DismissibleItemResponseDto {
|
|
14
|
+
toResponseDto(item: DismissibleItemDto): DismissibleItemResponseDto {
|
|
17
15
|
const dto = new DismissibleItemResponseDto();
|
|
18
16
|
|
|
19
17
|
dto.itemId = item.id;
|
|
@@ -24,10 +22,6 @@ export class DismissibleItemMapper {
|
|
|
24
22
|
dto.dismissedAt = item.dismissedAt.toISOString();
|
|
25
23
|
}
|
|
26
24
|
|
|
27
|
-
if (item.metadata) {
|
|
28
|
-
dto.metadata = item.metadata as Record<string, unknown>;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
25
|
return dto;
|
|
32
26
|
}
|
|
33
27
|
}
|
package/src/api/index.ts
CHANGED
|
@@ -2,13 +2,12 @@ import { mock } from 'ts-jest-mocker';
|
|
|
2
2
|
import { DismissController } from './dismiss.controller';
|
|
3
3
|
import { DismissibleService } from '../../../core/dismissible.service';
|
|
4
4
|
import { DismissibleItemMapper } from '../../dismissible-item.mapper';
|
|
5
|
-
import { BaseMetadata } from '@dismissible/nestjs-dismissible-item';
|
|
6
5
|
import { createTestItem, createTestContext } from '../../../testing/factories';
|
|
7
6
|
import { ResponseService } from '../../../response';
|
|
8
7
|
|
|
9
8
|
describe('DismissController', () => {
|
|
10
9
|
let controller: DismissController;
|
|
11
|
-
let mockService: jest.Mocked<DismissibleService
|
|
10
|
+
let mockService: jest.Mocked<DismissibleService>;
|
|
12
11
|
let mockResponseService: jest.Mocked<ResponseService>;
|
|
13
12
|
let mapper: DismissibleItemMapper;
|
|
14
13
|
|
|
@@ -1,23 +1,22 @@
|
|
|
1
|
-
import { Controller, Delete,
|
|
1
|
+
import { Controller, Delete, 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 { DismissResponseDto } from './dismiss.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 dismiss dismissible item operations.
|
|
15
14
|
*/
|
|
16
15
|
@ApiTags(API_TAG_DISMISSIBLE)
|
|
17
|
-
@Controller('v1/
|
|
16
|
+
@Controller('v1/users/:userId/items')
|
|
18
17
|
export class DismissController {
|
|
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 DismissController {
|
|
|
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 DismissController {
|
|
|
52
51
|
})
|
|
53
52
|
@UseFilters(HttpExceptionFilter)
|
|
54
53
|
async dismiss(
|
|
55
|
-
@
|
|
56
|
-
@
|
|
54
|
+
@UserId() userId: string,
|
|
55
|
+
@ItemId() itemId: string,
|
|
57
56
|
@RequestContext() context: IRequestContext,
|
|
58
57
|
): Promise<DismissResponseDto> {
|
|
59
58
|
const result = await this.dismissibleService.dismiss(itemId, userId, context);
|
|
@@ -2,13 +2,12 @@ import { mock } from 'ts-jest-mocker';
|
|
|
2
2
|
import { GetOrCreateController } from './get-or-create.controller';
|
|
3
3
|
import { DismissibleService } from '../../../core/dismissible.service';
|
|
4
4
|
import { DismissibleItemMapper } from '../../dismissible-item.mapper';
|
|
5
|
-
import { BaseMetadata } from '@dismissible/nestjs-dismissible-item';
|
|
6
5
|
import { createTestItem, createTestContext } from '../../../testing/factories';
|
|
7
6
|
import { ResponseService } from '../../../response';
|
|
8
7
|
|
|
9
8
|
describe('GetOrCreateController', () => {
|
|
10
9
|
let controller: GetOrCreateController;
|
|
11
|
-
let mockService: jest.Mocked<DismissibleService
|
|
10
|
+
let mockService: jest.Mocked<DismissibleService>;
|
|
12
11
|
let mockResponseService: jest.Mocked<ResponseService>;
|
|
13
12
|
let mapper: DismissibleItemMapper;
|
|
14
13
|
|
|
@@ -28,49 +27,10 @@ describe('GetOrCreateController', () => {
|
|
|
28
27
|
|
|
29
28
|
mockService.getOrCreate.mockResolvedValue({ item, created: true });
|
|
30
29
|
|
|
31
|
-
const result = await controller.getOrCreate('test-user-id', 'test-item', context
|
|
30
|
+
const result = await controller.getOrCreate('test-user-id', 'test-item', context);
|
|
32
31
|
|
|
33
32
|
expect(result.data.itemId).toBe('test-item');
|
|
34
33
|
expect(mockResponseService.success).toHaveBeenCalled();
|
|
35
34
|
});
|
|
36
|
-
|
|
37
|
-
it('should parse metadata from query params', async () => {
|
|
38
|
-
const item = createTestItem({ id: 'test-item' });
|
|
39
|
-
const context = createTestContext();
|
|
40
|
-
|
|
41
|
-
mockService.getOrCreate.mockResolvedValue({ item, created: true });
|
|
42
|
-
|
|
43
|
-
await controller.getOrCreate('test-user-id', 'test-item', context, [
|
|
44
|
-
'version:2',
|
|
45
|
-
'category:test',
|
|
46
|
-
]);
|
|
47
|
-
|
|
48
|
-
expect(mockService.getOrCreate).toHaveBeenCalledWith(
|
|
49
|
-
'test-item',
|
|
50
|
-
'test-user-id',
|
|
51
|
-
expect.objectContaining({
|
|
52
|
-
metadata: { version: 2, category: 'test' },
|
|
53
|
-
}),
|
|
54
|
-
context,
|
|
55
|
-
);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('should handle single metadata value', async () => {
|
|
59
|
-
const item = createTestItem({ id: 'test-item' });
|
|
60
|
-
const context = createTestContext();
|
|
61
|
-
|
|
62
|
-
mockService.getOrCreate.mockResolvedValue({ item, created: true });
|
|
63
|
-
|
|
64
|
-
await controller.getOrCreate('test-user-id', 'test-item', context, 'version:2');
|
|
65
|
-
|
|
66
|
-
expect(mockService.getOrCreate).toHaveBeenCalledWith(
|
|
67
|
-
'test-item',
|
|
68
|
-
'test-user-id',
|
|
69
|
-
expect.objectContaining({
|
|
70
|
-
metadata: { version: 2 },
|
|
71
|
-
}),
|
|
72
|
-
context,
|
|
73
|
-
);
|
|
74
|
-
});
|
|
75
35
|
});
|
|
76
36
|
});
|
|
@@ -1,23 +1,22 @@
|
|
|
1
|
-
import { Controller, Get,
|
|
2
|
-
import { ApiTags, ApiOperation, ApiParam, ApiResponse
|
|
1
|
+
import { Controller, Get, UseFilters } from '@nestjs/common';
|
|
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 { GetOrCreateResponseDto } from './get-or-create.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 get-or-create dismissible item operations.
|
|
15
14
|
*/
|
|
16
15
|
@ApiTags(API_TAG_DISMISSIBLE)
|
|
17
|
-
@Controller('v1/
|
|
16
|
+
@Controller('v1/users/:userId/items')
|
|
18
17
|
export class GetOrCreateController {
|
|
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
|
) {}
|
|
@@ -30,22 +29,14 @@ export class GetOrCreateController {
|
|
|
30
29
|
})
|
|
31
30
|
@ApiParam({
|
|
32
31
|
name: 'userId',
|
|
33
|
-
description: 'User identifier',
|
|
32
|
+
description: 'User identifier (max length: 32 characters)',
|
|
34
33
|
example: 'user-123',
|
|
35
34
|
})
|
|
36
35
|
@ApiParam({
|
|
37
36
|
name: 'itemId',
|
|
38
|
-
description: 'Unique identifier for the dismissible item',
|
|
37
|
+
description: 'Unique identifier for the dismissible item (max length: 32 characters)',
|
|
39
38
|
example: 'welcome-banner-v2',
|
|
40
39
|
})
|
|
41
|
-
@ApiQuery({
|
|
42
|
-
name: 'metadata',
|
|
43
|
-
description: 'Optional metadata as key:value pairs (can be repeated)',
|
|
44
|
-
required: false,
|
|
45
|
-
isArray: true,
|
|
46
|
-
type: String,
|
|
47
|
-
example: 'version:2',
|
|
48
|
-
})
|
|
49
40
|
@ApiResponse({
|
|
50
41
|
status: 200,
|
|
51
42
|
description: 'The dismissible item (retrieved or created)',
|
|
@@ -57,50 +48,12 @@ export class GetOrCreateController {
|
|
|
57
48
|
})
|
|
58
49
|
@UseFilters(HttpExceptionFilter)
|
|
59
50
|
async getOrCreate(
|
|
60
|
-
@
|
|
61
|
-
@
|
|
51
|
+
@UserId() userId: string,
|
|
52
|
+
@ItemId() itemId: string,
|
|
62
53
|
@RequestContext() context: IRequestContext,
|
|
63
|
-
@Query('metadata') metadataRaw?: string | string[],
|
|
64
54
|
): Promise<GetOrCreateResponseDto> {
|
|
65
|
-
|
|
66
|
-
const metadata = this.parseMetadata(metadataRaw);
|
|
67
|
-
|
|
68
|
-
const result = await this.dismissibleService.getOrCreate(
|
|
69
|
-
itemId,
|
|
70
|
-
userId,
|
|
71
|
-
{
|
|
72
|
-
metadata: metadata as BaseMetadata,
|
|
73
|
-
},
|
|
74
|
-
context,
|
|
75
|
-
);
|
|
55
|
+
const result = await this.dismissibleService.getOrCreate(itemId, userId, context);
|
|
76
56
|
|
|
77
57
|
return this.responseService.success(this.mapper.toResponseDto(result.item));
|
|
78
58
|
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Parse metadata from query parameter format.
|
|
82
|
-
* Supports: `?metadata=key1:value1&metadata=key2:value2`
|
|
83
|
-
*/
|
|
84
|
-
private parseMetadata(raw?: string | string[]): Record<string, string | number> | undefined {
|
|
85
|
-
if (!raw) {
|
|
86
|
-
return undefined;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const items = Array.isArray(raw) ? raw : [raw];
|
|
90
|
-
const metadata: Record<string, string | number> = {};
|
|
91
|
-
|
|
92
|
-
for (const item of items) {
|
|
93
|
-
const colonIndex = item.indexOf(':');
|
|
94
|
-
if (colonIndex > 0) {
|
|
95
|
-
const key = item.substring(0, colonIndex);
|
|
96
|
-
const value = item.substring(colonIndex + 1);
|
|
97
|
-
|
|
98
|
-
// Try to parse as number if possible
|
|
99
|
-
const numValue = Number(value);
|
|
100
|
-
metadata[key] = isNaN(numValue) ? value : numValue;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return Object.keys(metadata).length > 0 ? metadata : undefined;
|
|
105
|
-
}
|
|
106
59
|
}
|
|
@@ -2,13 +2,12 @@ import { mock } from 'ts-jest-mocker';
|
|
|
2
2
|
import { RestoreController } from './restore.controller';
|
|
3
3
|
import { DismissibleService } from '../../../core/dismissible.service';
|
|
4
4
|
import { DismissibleItemMapper } from '../../dismissible-item.mapper';
|
|
5
|
-
import { BaseMetadata } from '@dismissible/nestjs-dismissible-item';
|
|
6
5
|
import { createTestItem, createTestContext } from '../../../testing/factories';
|
|
7
6
|
import { ResponseService } from '../../../response';
|
|
8
7
|
|
|
9
8
|
describe('RestoreController', () => {
|
|
10
9
|
let controller: RestoreController;
|
|
11
|
-
let mockService: jest.Mocked<DismissibleService
|
|
10
|
+
let mockService: jest.Mocked<DismissibleService>;
|
|
12
11
|
let mockResponseService: jest.Mocked<ResponseService>;
|
|
13
12
|
let mapper: DismissibleItemMapper;
|
|
14
13
|
|