@dismissible/nestjs-dismissible 0.0.2-canary.738340d.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 +12 -12
- 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
|
@@ -14,17 +14,16 @@ import {
|
|
|
14
14
|
ItemNotDismissedException,
|
|
15
15
|
} from '../exceptions';
|
|
16
16
|
import { ValidationService } from '@dismissible/nestjs-validation';
|
|
17
|
-
import {
|
|
18
|
-
import { ICreateItemOptions } from './create-options';
|
|
17
|
+
import { DismissibleItemDto, DismissibleItemFactory } from '@dismissible/nestjs-dismissible-item';
|
|
19
18
|
|
|
20
19
|
/**
|
|
21
20
|
* Core business logic service for dismissible operations.
|
|
22
21
|
* Handles pure CRUD operations without side effects (hooks, events).
|
|
23
22
|
*/
|
|
24
23
|
@Injectable()
|
|
25
|
-
export class DismissibleCoreService
|
|
24
|
+
export class DismissibleCoreService {
|
|
26
25
|
constructor(
|
|
27
|
-
@Inject(DISMISSIBLE_STORAGE_ADAPTER) private readonly storage: IDismissibleStorage
|
|
26
|
+
@Inject(DISMISSIBLE_STORAGE_ADAPTER) private readonly storage: IDismissibleStorage,
|
|
28
27
|
private readonly dateService: DateService,
|
|
29
28
|
@Inject(DISMISSIBLE_LOGGER) private readonly logger: IDismissibleLogger,
|
|
30
29
|
private readonly itemFactory: DismissibleItemFactory,
|
|
@@ -33,50 +32,65 @@ export class DismissibleCoreService<TMetadata extends BaseMetadata = BaseMetadat
|
|
|
33
32
|
) {}
|
|
34
33
|
|
|
35
34
|
/**
|
|
36
|
-
* Get an existing item
|
|
35
|
+
* Get an existing item by user ID and item ID.
|
|
37
36
|
* @param itemId The item identifier
|
|
38
37
|
* @param userId The user identifier (required)
|
|
39
|
-
* @
|
|
38
|
+
* @returns The item or null if not found
|
|
40
39
|
*/
|
|
41
|
-
async
|
|
42
|
-
itemId: string,
|
|
43
|
-
userId: string,
|
|
44
|
-
options?: ICreateItemOptions<TMetadata>,
|
|
45
|
-
): Promise<IGetOrCreateServiceResponse<TMetadata>> {
|
|
40
|
+
async get(itemId: string, userId: string): Promise<DismissibleItemDto | null> {
|
|
46
41
|
this.logger.debug(`Looking up item in storage`, { itemId, userId });
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (existingItem) {
|
|
42
|
+
const item = await this.storage.get(userId, itemId);
|
|
43
|
+
if (item) {
|
|
51
44
|
this.logger.debug(`Found existing item`, { itemId, userId });
|
|
52
|
-
return {
|
|
53
|
-
item: existingItem,
|
|
54
|
-
created: false,
|
|
55
|
-
};
|
|
56
45
|
}
|
|
46
|
+
return item;
|
|
47
|
+
}
|
|
57
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Create a new item.
|
|
51
|
+
* @param itemId The item identifier
|
|
52
|
+
* @param userId The user identifier (required)
|
|
53
|
+
* @returns The created item
|
|
54
|
+
*/
|
|
55
|
+
async create(itemId: string, userId: string): Promise<DismissibleItemDto> {
|
|
58
56
|
this.logger.debug(`Creating new item`, {
|
|
59
57
|
itemId,
|
|
60
58
|
userId,
|
|
61
|
-
hasMetadata: !!options?.metadata,
|
|
62
59
|
});
|
|
63
60
|
|
|
64
|
-
// Create new item
|
|
65
61
|
const now = this.dateService.getNow();
|
|
66
|
-
const newItem = this.itemFactory.create
|
|
62
|
+
const newItem = this.itemFactory.create({
|
|
67
63
|
id: itemId,
|
|
68
64
|
createdAt: now,
|
|
69
65
|
userId,
|
|
70
|
-
metadata: options?.metadata,
|
|
71
66
|
});
|
|
72
67
|
|
|
73
|
-
// Validate the item before storage
|
|
74
68
|
await this.validationService.validateInstance(newItem);
|
|
75
69
|
|
|
76
70
|
const createdItem = await this.storage.create(newItem);
|
|
77
71
|
|
|
78
72
|
this.logger.info(`Created new dismissible item`, { itemId, userId });
|
|
79
73
|
|
|
74
|
+
return createdItem;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get an existing item or create a new one.
|
|
79
|
+
* @param itemId The item identifier
|
|
80
|
+
* @param userId The user identifier (required)
|
|
81
|
+
*/
|
|
82
|
+
async getOrCreate(itemId: string, userId: string): Promise<IGetOrCreateServiceResponse> {
|
|
83
|
+
const existingItem = await this.get(itemId, userId);
|
|
84
|
+
|
|
85
|
+
if (existingItem) {
|
|
86
|
+
return {
|
|
87
|
+
item: existingItem,
|
|
88
|
+
created: false,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const createdItem = await this.create(itemId, userId);
|
|
93
|
+
|
|
80
94
|
return {
|
|
81
95
|
item: createdItem,
|
|
82
96
|
created: true,
|
|
@@ -90,7 +104,7 @@ export class DismissibleCoreService<TMetadata extends BaseMetadata = BaseMetadat
|
|
|
90
104
|
* @throws ItemNotFoundException if item doesn't exist
|
|
91
105
|
* @throws ItemAlreadyDismissedException if item is already dismissed
|
|
92
106
|
*/
|
|
93
|
-
async dismiss(itemId: string, userId: string): Promise<IDismissServiceResponse
|
|
107
|
+
async dismiss(itemId: string, userId: string): Promise<IDismissServiceResponse> {
|
|
94
108
|
this.logger.debug(`Attempting to dismiss item`, { itemId, userId });
|
|
95
109
|
|
|
96
110
|
const existingItem = await this.storage.get(userId, itemId);
|
|
@@ -108,7 +122,6 @@ export class DismissibleCoreService<TMetadata extends BaseMetadata = BaseMetadat
|
|
|
108
122
|
const previousItem = this.itemFactory.clone(existingItem);
|
|
109
123
|
const dismissedItem = this.itemFactory.createDismissed(existingItem, this.dateService.getNow());
|
|
110
124
|
|
|
111
|
-
// Validate the item before storage
|
|
112
125
|
await this.validationService.validateInstance(dismissedItem);
|
|
113
126
|
|
|
114
127
|
const updatedItem = await this.storage.update(dismissedItem);
|
|
@@ -128,7 +141,7 @@ export class DismissibleCoreService<TMetadata extends BaseMetadata = BaseMetadat
|
|
|
128
141
|
* @throws ItemNotFoundException if item doesn't exist
|
|
129
142
|
* @throws ItemNotDismissedException if item is not dismissed
|
|
130
143
|
*/
|
|
131
|
-
async restore(itemId: string, userId: string): Promise<IRestoreServiceResponse
|
|
144
|
+
async restore(itemId: string, userId: string): Promise<IRestoreServiceResponse> {
|
|
132
145
|
this.logger.debug(`Attempting to restore item`, { itemId, userId });
|
|
133
146
|
|
|
134
147
|
const existingItem = await this.storage.get(userId, itemId);
|
|
@@ -146,7 +159,6 @@ export class DismissibleCoreService<TMetadata extends BaseMetadata = BaseMetadat
|
|
|
146
159
|
const previousItem = this.itemFactory.clone(existingItem);
|
|
147
160
|
const restoredItem = this.itemFactory.createRestored(existingItem);
|
|
148
161
|
|
|
149
|
-
// Validate the item before storage
|
|
150
162
|
await this.validationService.validateInstance(restoredItem);
|
|
151
163
|
|
|
152
164
|
const updatedItem = await this.storage.update(restoredItem);
|
|
@@ -4,16 +4,17 @@ import { DismissibleService } from './dismissible.service';
|
|
|
4
4
|
import { DismissibleCoreService } from './dismissible-core.service';
|
|
5
5
|
import { HookRunner, IHookRunResult } from './hook-runner.service';
|
|
6
6
|
import { IDismissibleLogger } from '@dismissible/nestjs-logger';
|
|
7
|
+
import { ValidationService } from '@dismissible/nestjs-validation';
|
|
7
8
|
import { DismissibleEvents } from '../events';
|
|
8
|
-
import { BaseMetadata } from '@dismissible/nestjs-dismissible-item';
|
|
9
9
|
import { createTestItem, createTestContext } from '../testing/factories';
|
|
10
10
|
|
|
11
11
|
describe('DismissibleService', () => {
|
|
12
|
-
let service: DismissibleService
|
|
13
|
-
let mockCoreService: jest.Mocked<DismissibleCoreService
|
|
14
|
-
let mockHookRunner: jest.Mocked<HookRunner
|
|
12
|
+
let service: DismissibleService;
|
|
13
|
+
let mockCoreService: jest.Mocked<DismissibleCoreService>;
|
|
14
|
+
let mockHookRunner: jest.Mocked<HookRunner>;
|
|
15
15
|
let mockEventEmitter: jest.Mocked<EventEmitter2>;
|
|
16
16
|
let mockLogger: jest.Mocked<IDismissibleLogger>;
|
|
17
|
+
let mockValidationService: jest.Mocked<ValidationService>;
|
|
17
18
|
|
|
18
19
|
const testUserId = 'test-user-id';
|
|
19
20
|
|
|
@@ -29,27 +30,42 @@ describe('DismissibleService', () => {
|
|
|
29
30
|
mockHookRunner = mock(HookRunner, { failIfMockNotProvided: false });
|
|
30
31
|
mockEventEmitter = mock(EventEmitter2, { failIfMockNotProvided: false });
|
|
31
32
|
mockLogger = mock<IDismissibleLogger>({ failIfMockNotProvided: false });
|
|
33
|
+
mockValidationService = mock(ValidationService, { failIfMockNotProvided: false });
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
mockValidationService.validateDto.mockResolvedValue({} as never);
|
|
36
|
+
|
|
37
|
+
service = new DismissibleService(
|
|
38
|
+
mockCoreService,
|
|
39
|
+
mockHookRunner,
|
|
40
|
+
mockEventEmitter,
|
|
41
|
+
mockLogger,
|
|
42
|
+
mockValidationService,
|
|
43
|
+
);
|
|
34
44
|
});
|
|
35
45
|
|
|
36
46
|
describe('getOrCreate', () => {
|
|
37
|
-
it('should run
|
|
47
|
+
it('should run request and get hooks for existing item', async () => {
|
|
38
48
|
const item = createTestItem({ id: 'existing-item' });
|
|
39
49
|
const context = createTestContext();
|
|
40
50
|
|
|
41
|
-
mockHookRunner.
|
|
42
|
-
|
|
51
|
+
mockHookRunner.runPreRequest.mockResolvedValue(createHookResult('existing-item'));
|
|
52
|
+
mockHookRunner.runPreGet.mockResolvedValue(createHookResult('existing-item'));
|
|
53
|
+
mockCoreService.get.mockResolvedValue(item);
|
|
43
54
|
|
|
44
|
-
const result = await service.getOrCreate('existing-item', testUserId,
|
|
55
|
+
const result = await service.getOrCreate('existing-item', testUserId, context);
|
|
45
56
|
|
|
46
|
-
expect(mockHookRunner.
|
|
47
|
-
expect(mockCoreService.
|
|
57
|
+
expect(mockHookRunner.runPreRequest).toHaveBeenCalled();
|
|
58
|
+
expect(mockCoreService.get).toHaveBeenCalledWith('existing-item', testUserId);
|
|
59
|
+
expect(mockHookRunner.runPreGet).toHaveBeenCalledWith(
|
|
48
60
|
'existing-item',
|
|
61
|
+
item,
|
|
49
62
|
testUserId,
|
|
50
|
-
|
|
63
|
+
expect.anything(),
|
|
51
64
|
);
|
|
52
|
-
expect(mockHookRunner.
|
|
65
|
+
expect(mockHookRunner.runPostGet).toHaveBeenCalled();
|
|
66
|
+
expect(mockHookRunner.runPostRequest).toHaveBeenCalled();
|
|
67
|
+
expect(mockCoreService.create).not.toHaveBeenCalled();
|
|
68
|
+
expect(mockHookRunner.runPreCreate).not.toHaveBeenCalled();
|
|
53
69
|
expect(mockEventEmitter.emit).toHaveBeenCalledWith(
|
|
54
70
|
DismissibleEvents.ITEM_RETRIEVED,
|
|
55
71
|
expect.anything(),
|
|
@@ -57,24 +73,83 @@ describe('DismissibleService', () => {
|
|
|
57
73
|
expect(result.created).toBe(false);
|
|
58
74
|
});
|
|
59
75
|
|
|
60
|
-
it('should run create hooks
|
|
76
|
+
it('should run pre-create hooks BEFORE creating for new item', async () => {
|
|
61
77
|
const item = createTestItem({ id: 'new-item' });
|
|
62
78
|
const context = createTestContext();
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
mockHookRunner.
|
|
66
|
-
mockCoreService.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
79
|
+
const callOrder: string[] = [];
|
|
80
|
+
|
|
81
|
+
mockHookRunner.runPreRequest.mockResolvedValue(createHookResult('new-item'));
|
|
82
|
+
mockCoreService.get.mockResolvedValue(null);
|
|
83
|
+
mockHookRunner.runPreCreate.mockImplementation(async () => {
|
|
84
|
+
callOrder.push('runPreCreate');
|
|
85
|
+
return createHookResult('new-item');
|
|
86
|
+
});
|
|
87
|
+
mockCoreService.create.mockImplementation(async () => {
|
|
88
|
+
callOrder.push('create');
|
|
89
|
+
return item;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const result = await service.getOrCreate('new-item', testUserId, context);
|
|
93
|
+
|
|
94
|
+
expect(callOrder).toEqual(['runPreCreate', 'create']);
|
|
95
|
+
expect(mockHookRunner.runPreRequest).toHaveBeenCalled();
|
|
70
96
|
expect(mockHookRunner.runPreCreate).toHaveBeenCalled();
|
|
97
|
+
expect(mockCoreService.create).toHaveBeenCalledWith('new-item', testUserId);
|
|
71
98
|
expect(mockHookRunner.runPostCreate).toHaveBeenCalled();
|
|
99
|
+
expect(mockHookRunner.runPostRequest).toHaveBeenCalled();
|
|
72
100
|
expect(mockEventEmitter.emit).toHaveBeenCalledWith(
|
|
73
101
|
DismissibleEvents.ITEM_CREATED,
|
|
74
102
|
expect.anything(),
|
|
75
103
|
);
|
|
76
104
|
expect(result.created).toBe(true);
|
|
77
105
|
});
|
|
106
|
+
|
|
107
|
+
it('should NOT create item when pre-create hook blocks the operation', async () => {
|
|
108
|
+
const context = createTestContext();
|
|
109
|
+
|
|
110
|
+
mockHookRunner.runPreRequest.mockResolvedValue(createHookResult('new-item'));
|
|
111
|
+
mockCoreService.get.mockResolvedValue(null);
|
|
112
|
+
mockHookRunner.runPreCreate.mockResolvedValue({
|
|
113
|
+
proceed: false,
|
|
114
|
+
reason: 'Plan limit reached',
|
|
115
|
+
id: 'new-item',
|
|
116
|
+
userId: testUserId,
|
|
117
|
+
context: createTestContext(),
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
await expect(service.getOrCreate('new-item', testUserId, context)).rejects.toThrow();
|
|
121
|
+
|
|
122
|
+
expect(mockCoreService.create).not.toHaveBeenCalled();
|
|
123
|
+
expect(mockHookRunner.runPostCreate).not.toHaveBeenCalled();
|
|
124
|
+
expect(mockEventEmitter.emit).not.toHaveBeenCalledWith(
|
|
125
|
+
DismissibleEvents.ITEM_CREATED,
|
|
126
|
+
expect.anything(),
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should NOT return existing item when pre-get hook blocks the operation', async () => {
|
|
131
|
+
const item = createTestItem({ id: 'existing-item' });
|
|
132
|
+
const context = createTestContext();
|
|
133
|
+
|
|
134
|
+
mockHookRunner.runPreRequest.mockResolvedValue(createHookResult('existing-item'));
|
|
135
|
+
mockCoreService.get.mockResolvedValue(item);
|
|
136
|
+
mockHookRunner.runPreGet.mockResolvedValue({
|
|
137
|
+
proceed: false,
|
|
138
|
+
reason: 'Item access denied',
|
|
139
|
+
id: 'existing-item',
|
|
140
|
+
userId: testUserId,
|
|
141
|
+
context: createTestContext(),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
await expect(service.getOrCreate('existing-item', testUserId, context)).rejects.toThrow();
|
|
145
|
+
|
|
146
|
+
expect(mockHookRunner.runPostGet).not.toHaveBeenCalled();
|
|
147
|
+
expect(mockHookRunner.runPostRequest).not.toHaveBeenCalled();
|
|
148
|
+
expect(mockEventEmitter.emit).not.toHaveBeenCalledWith(
|
|
149
|
+
DismissibleEvents.ITEM_RETRIEVED,
|
|
150
|
+
expect.anything(),
|
|
151
|
+
);
|
|
152
|
+
});
|
|
78
153
|
});
|
|
79
154
|
|
|
80
155
|
describe('dismiss', () => {
|
|
@@ -83,14 +158,17 @@ describe('DismissibleService', () => {
|
|
|
83
158
|
const previousItem = createTestItem({ id: 'test-item' });
|
|
84
159
|
const context = createTestContext();
|
|
85
160
|
|
|
161
|
+
mockHookRunner.runPreRequest.mockResolvedValue(createHookResult('test-item'));
|
|
86
162
|
mockHookRunner.runPreDismiss.mockResolvedValue(createHookResult('test-item'));
|
|
87
163
|
mockCoreService.dismiss.mockResolvedValue({ item, previousItem });
|
|
88
164
|
|
|
89
165
|
const result = await service.dismiss('test-item', testUserId, context);
|
|
90
166
|
|
|
167
|
+
expect(mockHookRunner.runPreRequest).toHaveBeenCalled();
|
|
91
168
|
expect(mockHookRunner.runPreDismiss).toHaveBeenCalled();
|
|
92
169
|
expect(mockCoreService.dismiss).toHaveBeenCalledWith('test-item', testUserId);
|
|
93
170
|
expect(mockHookRunner.runPostDismiss).toHaveBeenCalled();
|
|
171
|
+
expect(mockHookRunner.runPostRequest).toHaveBeenCalled();
|
|
94
172
|
expect(mockEventEmitter.emit).toHaveBeenCalledWith(
|
|
95
173
|
DismissibleEvents.ITEM_DISMISSED,
|
|
96
174
|
expect.anything(),
|
|
@@ -105,14 +183,17 @@ describe('DismissibleService', () => {
|
|
|
105
183
|
const previousItem = createTestItem({ id: 'test-item' });
|
|
106
184
|
const context = createTestContext();
|
|
107
185
|
|
|
186
|
+
mockHookRunner.runPreRequest.mockResolvedValue(createHookResult('test-item'));
|
|
108
187
|
mockHookRunner.runPreRestore.mockResolvedValue(createHookResult('test-item'));
|
|
109
188
|
mockCoreService.restore.mockResolvedValue({ item, previousItem });
|
|
110
189
|
|
|
111
190
|
const result = await service.restore('test-item', testUserId, context);
|
|
112
191
|
|
|
192
|
+
expect(mockHookRunner.runPreRequest).toHaveBeenCalled();
|
|
113
193
|
expect(mockHookRunner.runPreRestore).toHaveBeenCalled();
|
|
114
194
|
expect(mockCoreService.restore).toHaveBeenCalledWith('test-item', testUserId);
|
|
115
195
|
expect(mockHookRunner.runPostRestore).toHaveBeenCalled();
|
|
196
|
+
expect(mockHookRunner.runPostRequest).toHaveBeenCalled();
|
|
116
197
|
expect(mockEventEmitter.emit).toHaveBeenCalledWith(
|
|
117
198
|
DismissibleEvents.ITEM_RESTORED,
|
|
118
199
|
expect.anything(),
|
|
@@ -126,10 +207,11 @@ describe('DismissibleService', () => {
|
|
|
126
207
|
const item = createTestItem({ id: 'test-item' });
|
|
127
208
|
const context = createTestContext();
|
|
128
209
|
|
|
129
|
-
mockHookRunner.
|
|
130
|
-
|
|
210
|
+
mockHookRunner.runPreRequest.mockResolvedValue(createHookResult('test-item'));
|
|
211
|
+
mockHookRunner.runPreGet.mockResolvedValue(createHookResult('test-item'));
|
|
212
|
+
mockCoreService.get.mockResolvedValue(item);
|
|
131
213
|
|
|
132
|
-
await service.getOrCreate('test-item', testUserId,
|
|
214
|
+
await service.getOrCreate('test-item', testUserId, context);
|
|
133
215
|
|
|
134
216
|
expect(mockLogger.debug).toHaveBeenCalledWith(
|
|
135
217
|
expect.stringContaining('getOrCreate called'),
|
|
@@ -8,9 +8,7 @@ import {
|
|
|
8
8
|
IDismissServiceResponse,
|
|
9
9
|
IRestoreServiceResponse,
|
|
10
10
|
} from './service-responses.interface';
|
|
11
|
-
import {
|
|
12
|
-
import { ICreateItemOptions } from './create-options';
|
|
13
|
-
import { IRequestContext } from '../request/request-context.interface';
|
|
11
|
+
import { IRequestContext } from '@dismissible/nestjs-dismissible-request';
|
|
14
12
|
import { DismissibleEvents } from '../events';
|
|
15
13
|
import {
|
|
16
14
|
ItemCreatedEvent,
|
|
@@ -18,84 +16,111 @@ import {
|
|
|
18
16
|
ItemDismissedEvent,
|
|
19
17
|
ItemRestoredEvent,
|
|
20
18
|
} from '../events';
|
|
19
|
+
import { ValidationService } from '@dismissible/nestjs-validation';
|
|
20
|
+
import { DismissibleInputDto } from '../validation';
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Main orchestration service for dismissible operations.
|
|
24
24
|
* Coordinates core logic, hooks, and events.
|
|
25
25
|
*/
|
|
26
26
|
@Injectable()
|
|
27
|
-
export class DismissibleService
|
|
27
|
+
export class DismissibleService {
|
|
28
28
|
constructor(
|
|
29
|
-
private readonly coreService: DismissibleCoreService
|
|
30
|
-
private readonly hookRunner: HookRunner
|
|
29
|
+
private readonly coreService: DismissibleCoreService,
|
|
30
|
+
private readonly hookRunner: HookRunner,
|
|
31
31
|
private readonly eventEmitter: EventEmitter2,
|
|
32
32
|
@Inject(DISMISSIBLE_LOGGER)
|
|
33
33
|
private readonly logger: IDismissibleLogger,
|
|
34
|
+
private readonly validationService: ValidationService,
|
|
34
35
|
) {}
|
|
35
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Validates input parameters for all service methods.
|
|
39
|
+
* Provides defense in depth when the service is used directly without controllers.
|
|
40
|
+
*/
|
|
41
|
+
private async validateInput(itemId: string, userId: string): Promise<void> {
|
|
42
|
+
await this.validationService.validateDto(DismissibleInputDto, { itemId, userId });
|
|
43
|
+
}
|
|
44
|
+
|
|
36
45
|
/**
|
|
37
46
|
* Get an existing item or create a new one.
|
|
38
47
|
* @param itemId The item identifier
|
|
39
48
|
* @param userId The user identifier (required)
|
|
40
|
-
* @param options Optional creation options (metadata)
|
|
41
49
|
* @param context Optional request context for tracing
|
|
42
50
|
*/
|
|
43
51
|
async getOrCreate(
|
|
44
52
|
itemId: string,
|
|
45
53
|
userId: string,
|
|
46
|
-
options?: ICreateItemOptions<TMetadata>,
|
|
47
54
|
context?: IRequestContext,
|
|
48
|
-
): Promise<IGetOrCreateServiceResponse
|
|
55
|
+
): Promise<IGetOrCreateServiceResponse> {
|
|
49
56
|
this.logger.debug(`getOrCreate called`, { itemId, userId });
|
|
50
57
|
|
|
51
|
-
|
|
52
|
-
|
|
58
|
+
await this.validateInput(itemId, userId);
|
|
59
|
+
|
|
60
|
+
const preResult = await this.hookRunner.runPreRequest(itemId, userId, context);
|
|
53
61
|
HookRunner.throwIfBlocked(preResult);
|
|
54
62
|
|
|
55
63
|
const resolvedId = preResult.id;
|
|
56
64
|
const resolvedUserId = preResult.userId;
|
|
57
65
|
const resolvedContext = preResult.context;
|
|
58
66
|
|
|
59
|
-
|
|
60
|
-
const result = await this.coreService.getOrCreate(resolvedId, resolvedUserId, options);
|
|
67
|
+
const existingItem = await this.coreService.get(resolvedId, resolvedUserId);
|
|
61
68
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
// Run pre-create hooks
|
|
65
|
-
const preCreateResult = await this.hookRunner.runPreCreate(
|
|
69
|
+
if (existingItem) {
|
|
70
|
+
const preGetResult = await this.hookRunner.runPreGet(
|
|
66
71
|
resolvedId,
|
|
72
|
+
existingItem,
|
|
67
73
|
resolvedUserId,
|
|
68
74
|
resolvedContext,
|
|
69
75
|
);
|
|
70
|
-
HookRunner.throwIfBlocked(
|
|
71
|
-
|
|
72
|
-
// Run post-create hooks
|
|
73
|
-
await this.hookRunner.runPostCreate(resolvedId, result.item, resolvedUserId, resolvedContext);
|
|
76
|
+
HookRunner.throwIfBlocked(preGetResult);
|
|
74
77
|
|
|
75
|
-
// Emit created event (async)
|
|
76
|
-
this.eventEmitter.emit(
|
|
77
|
-
DismissibleEvents.ITEM_CREATED,
|
|
78
|
-
new ItemCreatedEvent(resolvedId, result.item, resolvedUserId, resolvedContext),
|
|
79
|
-
);
|
|
80
|
-
} else {
|
|
81
|
-
// Emit retrieved event (async)
|
|
82
78
|
this.eventEmitter.emit(
|
|
83
79
|
DismissibleEvents.ITEM_RETRIEVED,
|
|
84
|
-
new ItemRetrievedEvent(resolvedId,
|
|
80
|
+
new ItemRetrievedEvent(resolvedId, existingItem, resolvedUserId, resolvedContext),
|
|
85
81
|
);
|
|
82
|
+
|
|
83
|
+
await this.hookRunner.runPostGet(resolvedId, existingItem, resolvedUserId, resolvedContext);
|
|
84
|
+
|
|
85
|
+
await this.hookRunner.runPostRequest(
|
|
86
|
+
resolvedId,
|
|
87
|
+
existingItem,
|
|
88
|
+
resolvedUserId,
|
|
89
|
+
resolvedContext,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
this.logger.debug(`getOrCreate completed`, { itemId, created: false });
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
item: existingItem,
|
|
96
|
+
created: false,
|
|
97
|
+
};
|
|
86
98
|
}
|
|
87
99
|
|
|
88
|
-
|
|
89
|
-
await this.hookRunner.runPostGetOrCreate(
|
|
100
|
+
const preCreateResult = await this.hookRunner.runPreCreate(
|
|
90
101
|
resolvedId,
|
|
91
|
-
result.item,
|
|
92
102
|
resolvedUserId,
|
|
93
103
|
resolvedContext,
|
|
94
104
|
);
|
|
105
|
+
HookRunner.throwIfBlocked(preCreateResult);
|
|
95
106
|
|
|
96
|
-
this.
|
|
107
|
+
const createdItem = await this.coreService.create(resolvedId, resolvedUserId);
|
|
97
108
|
|
|
98
|
-
|
|
109
|
+
await this.hookRunner.runPostCreate(resolvedId, createdItem, resolvedUserId, resolvedContext);
|
|
110
|
+
|
|
111
|
+
this.eventEmitter.emit(
|
|
112
|
+
DismissibleEvents.ITEM_CREATED,
|
|
113
|
+
new ItemCreatedEvent(resolvedId, createdItem, resolvedUserId, resolvedContext),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
await this.hookRunner.runPostRequest(resolvedId, createdItem, resolvedUserId, resolvedContext);
|
|
117
|
+
|
|
118
|
+
this.logger.debug(`getOrCreate completed`, { itemId, created: true });
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
item: createdItem,
|
|
122
|
+
created: true,
|
|
123
|
+
};
|
|
99
124
|
}
|
|
100
125
|
|
|
101
126
|
/**
|
|
@@ -108,24 +133,29 @@ export class DismissibleService<TMetadata extends BaseMetadata = BaseMetadata> {
|
|
|
108
133
|
itemId: string,
|
|
109
134
|
userId: string,
|
|
110
135
|
context?: IRequestContext,
|
|
111
|
-
): Promise<IDismissServiceResponse
|
|
136
|
+
): Promise<IDismissServiceResponse> {
|
|
112
137
|
this.logger.debug(`dismiss called`, { itemId, userId });
|
|
113
138
|
|
|
114
|
-
|
|
115
|
-
const preResult = await this.hookRunner.runPreDismiss(itemId, userId, context);
|
|
116
|
-
HookRunner.throwIfBlocked(preResult);
|
|
139
|
+
await this.validateInput(itemId, userId);
|
|
117
140
|
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
141
|
+
const preRequestResult = await this.hookRunner.runPreRequest(itemId, userId, context);
|
|
142
|
+
HookRunner.throwIfBlocked(preRequestResult);
|
|
143
|
+
|
|
144
|
+
const resolvedId = preRequestResult.id;
|
|
145
|
+
const resolvedUserId = preRequestResult.userId;
|
|
146
|
+
const resolvedContext = preRequestResult.context;
|
|
147
|
+
|
|
148
|
+
const preDismissResult = await this.hookRunner.runPreDismiss(
|
|
149
|
+
resolvedId,
|
|
150
|
+
resolvedUserId,
|
|
151
|
+
resolvedContext,
|
|
152
|
+
);
|
|
153
|
+
HookRunner.throwIfBlocked(preDismissResult);
|
|
121
154
|
|
|
122
|
-
// Execute core dismiss operation
|
|
123
155
|
const result = await this.coreService.dismiss(resolvedId, resolvedUserId);
|
|
124
156
|
|
|
125
|
-
// Run post-dismiss hooks
|
|
126
157
|
await this.hookRunner.runPostDismiss(resolvedId, result.item, resolvedUserId, resolvedContext);
|
|
127
158
|
|
|
128
|
-
// Emit dismissed event (async)
|
|
129
159
|
this.eventEmitter.emit(
|
|
130
160
|
DismissibleEvents.ITEM_DISMISSED,
|
|
131
161
|
new ItemDismissedEvent(
|
|
@@ -137,6 +167,8 @@ export class DismissibleService<TMetadata extends BaseMetadata = BaseMetadata> {
|
|
|
137
167
|
),
|
|
138
168
|
);
|
|
139
169
|
|
|
170
|
+
await this.hookRunner.runPostRequest(resolvedId, result.item, resolvedUserId, resolvedContext);
|
|
171
|
+
|
|
140
172
|
this.logger.debug(`dismiss completed`, { itemId });
|
|
141
173
|
|
|
142
174
|
return result;
|
|
@@ -152,24 +184,29 @@ export class DismissibleService<TMetadata extends BaseMetadata = BaseMetadata> {
|
|
|
152
184
|
itemId: string,
|
|
153
185
|
userId: string,
|
|
154
186
|
context?: IRequestContext,
|
|
155
|
-
): Promise<IRestoreServiceResponse
|
|
187
|
+
): Promise<IRestoreServiceResponse> {
|
|
156
188
|
this.logger.debug(`restore called`, { itemId, userId });
|
|
157
189
|
|
|
158
|
-
|
|
159
|
-
const preResult = await this.hookRunner.runPreRestore(itemId, userId, context);
|
|
160
|
-
HookRunner.throwIfBlocked(preResult);
|
|
190
|
+
await this.validateInput(itemId, userId);
|
|
161
191
|
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
192
|
+
const preRequestResult = await this.hookRunner.runPreRequest(itemId, userId, context);
|
|
193
|
+
HookRunner.throwIfBlocked(preRequestResult);
|
|
194
|
+
|
|
195
|
+
const resolvedId = preRequestResult.id;
|
|
196
|
+
const resolvedUserId = preRequestResult.userId;
|
|
197
|
+
const resolvedContext = preRequestResult.context;
|
|
198
|
+
|
|
199
|
+
const preRestoreResult = await this.hookRunner.runPreRestore(
|
|
200
|
+
resolvedId,
|
|
201
|
+
resolvedUserId,
|
|
202
|
+
resolvedContext,
|
|
203
|
+
);
|
|
204
|
+
HookRunner.throwIfBlocked(preRestoreResult);
|
|
165
205
|
|
|
166
|
-
// Execute core restore operation
|
|
167
206
|
const result = await this.coreService.restore(resolvedId, resolvedUserId);
|
|
168
207
|
|
|
169
|
-
// Run post-restore hooks
|
|
170
208
|
await this.hookRunner.runPostRestore(resolvedId, result.item, resolvedUserId, resolvedContext);
|
|
171
209
|
|
|
172
|
-
// Emit restored event (async)
|
|
173
210
|
this.eventEmitter.emit(
|
|
174
211
|
DismissibleEvents.ITEM_RESTORED,
|
|
175
212
|
new ItemRestoredEvent(
|
|
@@ -181,6 +218,8 @@ export class DismissibleService<TMetadata extends BaseMetadata = BaseMetadata> {
|
|
|
181
218
|
),
|
|
182
219
|
);
|
|
183
220
|
|
|
221
|
+
await this.hookRunner.runPostRequest(resolvedId, result.item, resolvedUserId, resolvedContext);
|
|
222
|
+
|
|
184
223
|
this.logger.debug(`restore completed`, { itemId });
|
|
185
224
|
|
|
186
225
|
return result;
|