@dismissible/nestjs-dismissible 0.0.2-canary.8976e84.0 → 0.0.2-canary.d2f56d7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +51 -67
  2. package/package.json +4 -4
  3. package/src/api/dismissible-item-response.dto.ts +0 -8
  4. package/src/api/dismissible-item.mapper.spec.ts +0 -12
  5. package/src/api/dismissible-item.mapper.ts +2 -8
  6. package/src/api/index.ts +3 -0
  7. package/src/api/use-cases/dismiss/dismiss.controller.spec.ts +1 -2
  8. package/src/api/use-cases/dismiss/dismiss.controller.ts +8 -8
  9. package/src/api/use-cases/get-or-create/get-or-create.controller.spec.ts +2 -42
  10. package/src/api/use-cases/get-or-create/get-or-create.controller.ts +10 -56
  11. package/src/api/use-cases/get-or-create/index.ts +0 -1
  12. package/src/api/use-cases/restore/restore.controller.spec.ts +1 -2
  13. package/src/api/use-cases/restore/restore.controller.ts +8 -8
  14. package/src/api/validation/index.ts +2 -0
  15. package/src/api/validation/param-validation.pipe.spec.ts +317 -0
  16. package/src/api/validation/param-validation.pipe.ts +42 -0
  17. package/src/api/validation/param.decorators.ts +32 -0
  18. package/src/core/dismissible-core.service.spec.ts +75 -29
  19. package/src/core/dismissible-core.service.ts +40 -24
  20. package/src/core/dismissible.service.spec.ts +111 -25
  21. package/src/core/dismissible.service.ts +115 -49
  22. package/src/core/hook-runner.service.spec.ts +486 -53
  23. package/src/core/hook-runner.service.ts +144 -18
  24. package/src/core/index.ts +0 -1
  25. package/src/core/lifecycle-hook.interface.ts +56 -10
  26. package/src/core/service-responses.interface.ts +9 -9
  27. package/src/dismissible.module.integration.spec.ts +685 -0
  28. package/src/dismissible.module.ts +6 -10
  29. package/src/events/dismissible.events.ts +16 -39
  30. package/src/index.ts +1 -0
  31. package/src/request/request-context.decorator.ts +1 -0
  32. package/src/request/request-context.interface.ts +6 -0
  33. package/src/response/http-exception-filter.spec.ts +213 -0
  34. package/src/response/http-exception-filter.ts +3 -3
  35. package/src/testing/factories.ts +5 -8
  36. package/src/utils/dismissible.helper.ts +2 -2
  37. package/src/validation/dismissible-input.dto.ts +47 -0
  38. package/src/validation/index.ts +1 -0
  39. package/tsconfig.json +3 -0
  40. package/tsconfig.spec.json +12 -0
  41. package/src/api/use-cases/get-or-create/get-or-create.request.dto.ts +0 -17
  42. package/src/core/create-options.ts +0 -9
@@ -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<BaseMetadata>;
13
- let mockCoreService: jest.Mocked<DismissibleCoreService<BaseMetadata>>;
14
- let mockHookRunner: jest.Mocked<HookRunner<BaseMetadata>>;
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,43 @@ describe('DismissibleService', () => {
29
30
  mockHookRunner = mock(HookRunner, { failIfMockNotProvided: false });
30
31
  mockEventEmitter = mock(EventEmitter2, { failIfMockNotProvided: false });
31
32
  mockLogger = mock<IDismissibleLogger>({ failIfMockNotProvided: false });
32
-
33
- service = new DismissibleService(mockCoreService, mockHookRunner, mockEventEmitter, mockLogger);
33
+ mockValidationService = mock(ValidationService, { failIfMockNotProvided: false });
34
+
35
+ // Mock validateDto to resolve successfully by default
36
+ mockValidationService.validateDto.mockResolvedValue({} as never);
37
+
38
+ service = new DismissibleService(
39
+ mockCoreService,
40
+ mockHookRunner,
41
+ mockEventEmitter,
42
+ mockLogger,
43
+ mockValidationService,
44
+ );
34
45
  });
35
46
 
36
47
  describe('getOrCreate', () => {
37
- it('should run hooks, call core service, and emit ITEM_RETRIEVED event for existing item', async () => {
48
+ it('should run request and get hooks for existing item', async () => {
38
49
  const item = createTestItem({ id: 'existing-item' });
39
50
  const context = createTestContext();
40
51
 
41
- mockHookRunner.runPreGetOrCreate.mockResolvedValue(createHookResult('existing-item'));
42
- mockCoreService.getOrCreate.mockResolvedValue({ item, created: false });
52
+ mockHookRunner.runPreRequest.mockResolvedValue(createHookResult('existing-item'));
53
+ mockHookRunner.runPreGet.mockResolvedValue(createHookResult('existing-item'));
54
+ mockCoreService.get.mockResolvedValue(item);
43
55
 
44
- const result = await service.getOrCreate('existing-item', testUserId, undefined, context);
56
+ const result = await service.getOrCreate('existing-item', testUserId, context);
45
57
 
46
- expect(mockHookRunner.runPreGetOrCreate).toHaveBeenCalled();
47
- expect(mockCoreService.getOrCreate).toHaveBeenCalledWith(
58
+ expect(mockHookRunner.runPreRequest).toHaveBeenCalled();
59
+ expect(mockCoreService.get).toHaveBeenCalledWith('existing-item', testUserId);
60
+ expect(mockHookRunner.runPreGet).toHaveBeenCalledWith(
48
61
  'existing-item',
62
+ item,
49
63
  testUserId,
50
- undefined,
64
+ expect.anything(),
51
65
  );
52
- expect(mockHookRunner.runPostGetOrCreate).toHaveBeenCalled();
66
+ expect(mockHookRunner.runPostGet).toHaveBeenCalled();
67
+ expect(mockHookRunner.runPostRequest).toHaveBeenCalled();
68
+ expect(mockCoreService.create).not.toHaveBeenCalled();
69
+ expect(mockHookRunner.runPreCreate).not.toHaveBeenCalled();
53
70
  expect(mockEventEmitter.emit).toHaveBeenCalledWith(
54
71
  DismissibleEvents.ITEM_RETRIEVED,
55
72
  expect.anything(),
@@ -57,24 +74,86 @@ describe('DismissibleService', () => {
57
74
  expect(result.created).toBe(false);
58
75
  });
59
76
 
60
- it('should run create hooks and emit ITEM_CREATED event for new item', async () => {
77
+ it('should run pre-create hooks BEFORE creating for new item', async () => {
61
78
  const item = createTestItem({ id: 'new-item' });
62
79
  const context = createTestContext();
63
-
64
- mockHookRunner.runPreGetOrCreate.mockResolvedValue(createHookResult('new-item'));
65
- mockHookRunner.runPreCreate.mockResolvedValue(createHookResult('new-item'));
66
- mockCoreService.getOrCreate.mockResolvedValue({ item, created: true });
67
-
68
- const result = await service.getOrCreate('new-item', testUserId, undefined, context);
69
-
80
+ const callOrder: string[] = [];
81
+
82
+ mockHookRunner.runPreRequest.mockResolvedValue(createHookResult('new-item'));
83
+ mockCoreService.get.mockResolvedValue(null);
84
+ mockHookRunner.runPreCreate.mockImplementation(async () => {
85
+ callOrder.push('runPreCreate');
86
+ return createHookResult('new-item');
87
+ });
88
+ mockCoreService.create.mockImplementation(async () => {
89
+ callOrder.push('create');
90
+ return item;
91
+ });
92
+
93
+ const result = await service.getOrCreate('new-item', testUserId, context);
94
+
95
+ // Verify pre-create hooks run BEFORE create
96
+ expect(callOrder).toEqual(['runPreCreate', 'create']);
97
+ expect(mockHookRunner.runPreRequest).toHaveBeenCalled();
70
98
  expect(mockHookRunner.runPreCreate).toHaveBeenCalled();
99
+ expect(mockCoreService.create).toHaveBeenCalledWith('new-item', testUserId);
71
100
  expect(mockHookRunner.runPostCreate).toHaveBeenCalled();
101
+ expect(mockHookRunner.runPostRequest).toHaveBeenCalled();
72
102
  expect(mockEventEmitter.emit).toHaveBeenCalledWith(
73
103
  DismissibleEvents.ITEM_CREATED,
74
104
  expect.anything(),
75
105
  );
76
106
  expect(result.created).toBe(true);
77
107
  });
108
+
109
+ it('should NOT create item when pre-create hook blocks the operation', async () => {
110
+ const context = createTestContext();
111
+
112
+ mockHookRunner.runPreRequest.mockResolvedValue(createHookResult('new-item'));
113
+ mockCoreService.get.mockResolvedValue(null);
114
+ mockHookRunner.runPreCreate.mockResolvedValue({
115
+ proceed: false,
116
+ reason: 'Plan limit reached',
117
+ id: 'new-item',
118
+ userId: testUserId,
119
+ context: createTestContext(),
120
+ });
121
+
122
+ await expect(service.getOrCreate('new-item', testUserId, context)).rejects.toThrow();
123
+
124
+ // Verify create was NOT called because pre-create hook blocked it
125
+ expect(mockCoreService.create).not.toHaveBeenCalled();
126
+ expect(mockHookRunner.runPostCreate).not.toHaveBeenCalled();
127
+ expect(mockEventEmitter.emit).not.toHaveBeenCalledWith(
128
+ DismissibleEvents.ITEM_CREATED,
129
+ expect.anything(),
130
+ );
131
+ });
132
+
133
+ it('should NOT return existing item when pre-get hook blocks the operation', async () => {
134
+ const item = createTestItem({ id: 'existing-item' });
135
+ const context = createTestContext();
136
+
137
+ mockHookRunner.runPreRequest.mockResolvedValue(createHookResult('existing-item'));
138
+ mockCoreService.get.mockResolvedValue(item);
139
+ mockHookRunner.runPreGet.mockResolvedValue({
140
+ proceed: false,
141
+ reason: 'Item access denied',
142
+ id: 'existing-item',
143
+ userId: testUserId,
144
+ context: createTestContext(),
145
+ });
146
+
147
+ await expect(service.getOrCreate('existing-item', testUserId, context)).rejects.toThrow();
148
+
149
+ // Verify post-get hooks were NOT called because pre-get hook blocked
150
+ expect(mockHookRunner.runPostGet).not.toHaveBeenCalled();
151
+ expect(mockHookRunner.runPostRequest).not.toHaveBeenCalled();
152
+ expect(mockEventEmitter.emit).not.toHaveBeenCalledWith(
153
+ DismissibleEvents.ITEM_RETRIEVED,
154
+ expect.anything(),
155
+ );
156
+ });
78
157
  });
79
158
 
80
159
  describe('dismiss', () => {
@@ -83,14 +162,17 @@ describe('DismissibleService', () => {
83
162
  const previousItem = createTestItem({ id: 'test-item' });
84
163
  const context = createTestContext();
85
164
 
165
+ mockHookRunner.runPreRequest.mockResolvedValue(createHookResult('test-item'));
86
166
  mockHookRunner.runPreDismiss.mockResolvedValue(createHookResult('test-item'));
87
167
  mockCoreService.dismiss.mockResolvedValue({ item, previousItem });
88
168
 
89
169
  const result = await service.dismiss('test-item', testUserId, context);
90
170
 
171
+ expect(mockHookRunner.runPreRequest).toHaveBeenCalled();
91
172
  expect(mockHookRunner.runPreDismiss).toHaveBeenCalled();
92
173
  expect(mockCoreService.dismiss).toHaveBeenCalledWith('test-item', testUserId);
93
174
  expect(mockHookRunner.runPostDismiss).toHaveBeenCalled();
175
+ expect(mockHookRunner.runPostRequest).toHaveBeenCalled();
94
176
  expect(mockEventEmitter.emit).toHaveBeenCalledWith(
95
177
  DismissibleEvents.ITEM_DISMISSED,
96
178
  expect.anything(),
@@ -105,14 +187,17 @@ describe('DismissibleService', () => {
105
187
  const previousItem = createTestItem({ id: 'test-item' });
106
188
  const context = createTestContext();
107
189
 
190
+ mockHookRunner.runPreRequest.mockResolvedValue(createHookResult('test-item'));
108
191
  mockHookRunner.runPreRestore.mockResolvedValue(createHookResult('test-item'));
109
192
  mockCoreService.restore.mockResolvedValue({ item, previousItem });
110
193
 
111
194
  const result = await service.restore('test-item', testUserId, context);
112
195
 
196
+ expect(mockHookRunner.runPreRequest).toHaveBeenCalled();
113
197
  expect(mockHookRunner.runPreRestore).toHaveBeenCalled();
114
198
  expect(mockCoreService.restore).toHaveBeenCalledWith('test-item', testUserId);
115
199
  expect(mockHookRunner.runPostRestore).toHaveBeenCalled();
200
+ expect(mockHookRunner.runPostRequest).toHaveBeenCalled();
116
201
  expect(mockEventEmitter.emit).toHaveBeenCalledWith(
117
202
  DismissibleEvents.ITEM_RESTORED,
118
203
  expect.anything(),
@@ -126,10 +211,11 @@ describe('DismissibleService', () => {
126
211
  const item = createTestItem({ id: 'test-item' });
127
212
  const context = createTestContext();
128
213
 
129
- mockHookRunner.runPreGetOrCreate.mockResolvedValue(createHookResult('test-item'));
130
- mockCoreService.getOrCreate.mockResolvedValue({ item, created: false });
214
+ mockHookRunner.runPreRequest.mockResolvedValue(createHookResult('test-item'));
215
+ mockHookRunner.runPreGet.mockResolvedValue(createHookResult('test-item'));
216
+ mockCoreService.get.mockResolvedValue(item);
131
217
 
132
- await service.getOrCreate('test-item', testUserId, undefined, context);
218
+ await service.getOrCreate('test-item', testUserId, context);
133
219
 
134
220
  expect(mockLogger.debug).toHaveBeenCalledWith(
135
221
  expect.stringContaining('getOrCreate called'),
@@ -8,8 +8,6 @@ import {
8
8
  IDismissServiceResponse,
9
9
  IRestoreServiceResponse,
10
10
  } from './service-responses.interface';
11
- import { BaseMetadata } from '@dismissible/nestjs-dismissible-item';
12
- import { ICreateItemOptions } from './create-options';
13
11
  import { IRequestContext } from '../request/request-context.interface';
14
12
  import { DismissibleEvents } from '../events';
15
13
  import {
@@ -18,84 +16,124 @@ 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<TMetadata extends BaseMetadata = BaseMetadata> {
27
+ export class DismissibleService {
28
28
  constructor(
29
- private readonly coreService: DismissibleCoreService<TMetadata>,
30
- private readonly hookRunner: HookRunner<TMetadata>,
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
+ // Validate itemId and userId
43
+ await this.validationService.validateDto(DismissibleInputDto, { itemId, userId });
44
+ }
45
+
36
46
  /**
37
47
  * Get an existing item or create a new one.
38
48
  * @param itemId The item identifier
39
49
  * @param userId The user identifier (required)
40
- * @param options Optional creation options (metadata)
41
50
  * @param context Optional request context for tracing
42
51
  */
43
52
  async getOrCreate(
44
53
  itemId: string,
45
54
  userId: string,
46
- options?: ICreateItemOptions<TMetadata>,
47
55
  context?: IRequestContext,
48
- ): Promise<IGetOrCreateServiceResponse<TMetadata>> {
56
+ ): Promise<IGetOrCreateServiceResponse> {
49
57
  this.logger.debug(`getOrCreate called`, { itemId, userId });
50
58
 
51
- // Run pre-getOrCreate hooks
52
- const preResult = await this.hookRunner.runPreGetOrCreate(itemId, userId, context);
59
+ // Validate input parameters
60
+ await this.validateInput(itemId, userId);
61
+
62
+ // Run global pre-request hooks (auth, rate limiting, validation)
63
+ const preResult = await this.hookRunner.runPreRequest(itemId, userId, context);
53
64
  HookRunner.throwIfBlocked(preResult);
54
65
 
55
66
  const resolvedId = preResult.id;
56
67
  const resolvedUserId = preResult.userId;
57
68
  const resolvedContext = preResult.context;
58
69
 
59
- // Check if we need to run create hooks (for potential new item)
60
- const result = await this.coreService.getOrCreate(resolvedId, resolvedUserId, options);
70
+ // Check if item already exists
71
+ const existingItem = await this.coreService.get(resolvedId, resolvedUserId);
61
72
 
62
- // If item was created, run create-specific hooks
63
- if (result.created) {
64
- // Run pre-create hooks
65
- const preCreateResult = await this.hookRunner.runPreCreate(
73
+ if (existingItem) {
74
+ // Item exists - run pre-get hooks (access control based on item state)
75
+ const preGetResult = await this.hookRunner.runPreGet(
66
76
  resolvedId,
77
+ existingItem,
67
78
  resolvedUserId,
68
79
  resolvedContext,
69
80
  );
70
- HookRunner.throwIfBlocked(preCreateResult);
71
-
72
- // Run post-create hooks
73
- await this.hookRunner.runPostCreate(resolvedId, result.item, resolvedUserId, resolvedContext);
81
+ HookRunner.throwIfBlocked(preGetResult);
74
82
 
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)
83
+ // Emit retrieved event
82
84
  this.eventEmitter.emit(
83
85
  DismissibleEvents.ITEM_RETRIEVED,
84
- new ItemRetrievedEvent(resolvedId, result.item, resolvedUserId, resolvedContext),
86
+ new ItemRetrievedEvent(resolvedId, existingItem, resolvedUserId, resolvedContext),
85
87
  );
88
+
89
+ // Run post-get hooks
90
+ await this.hookRunner.runPostGet(resolvedId, existingItem, resolvedUserId, resolvedContext);
91
+
92
+ // Run global post-request hooks
93
+ await this.hookRunner.runPostRequest(
94
+ resolvedId,
95
+ existingItem,
96
+ resolvedUserId,
97
+ resolvedContext,
98
+ );
99
+
100
+ this.logger.debug(`getOrCreate completed`, { itemId, created: false });
101
+
102
+ return {
103
+ item: existingItem,
104
+ created: false,
105
+ };
86
106
  }
87
107
 
88
- // Run post-getOrCreate hooks
89
- await this.hookRunner.runPostGetOrCreate(
108
+ // Item doesn't exist - run pre-create hooks BEFORE creating
109
+ const preCreateResult = await this.hookRunner.runPreCreate(
90
110
  resolvedId,
91
- result.item,
92
111
  resolvedUserId,
93
112
  resolvedContext,
94
113
  );
114
+ HookRunner.throwIfBlocked(preCreateResult);
95
115
 
96
- this.logger.debug(`getOrCreate completed`, { itemId, created: result.created });
116
+ // Now create the item
117
+ const createdItem = await this.coreService.create(resolvedId, resolvedUserId);
97
118
 
98
- return result;
119
+ // Run post-create hooks
120
+ await this.hookRunner.runPostCreate(resolvedId, createdItem, resolvedUserId, resolvedContext);
121
+
122
+ // Emit created event
123
+ this.eventEmitter.emit(
124
+ DismissibleEvents.ITEM_CREATED,
125
+ new ItemCreatedEvent(resolvedId, createdItem, resolvedUserId, resolvedContext),
126
+ );
127
+
128
+ // Run global post-request hooks
129
+ await this.hookRunner.runPostRequest(resolvedId, createdItem, resolvedUserId, resolvedContext);
130
+
131
+ this.logger.debug(`getOrCreate completed`, { itemId, created: true });
132
+
133
+ return {
134
+ item: createdItem,
135
+ created: true,
136
+ };
99
137
  }
100
138
 
101
139
  /**
@@ -108,16 +146,27 @@ export class DismissibleService<TMetadata extends BaseMetadata = BaseMetadata> {
108
146
  itemId: string,
109
147
  userId: string,
110
148
  context?: IRequestContext,
111
- ): Promise<IDismissServiceResponse<TMetadata>> {
149
+ ): Promise<IDismissServiceResponse> {
112
150
  this.logger.debug(`dismiss called`, { itemId, userId });
113
151
 
114
- // Run pre-dismiss hooks
115
- const preResult = await this.hookRunner.runPreDismiss(itemId, userId, context);
116
- HookRunner.throwIfBlocked(preResult);
152
+ // Validate input parameters
153
+ await this.validateInput(itemId, userId);
117
154
 
118
- const resolvedId = preResult.id;
119
- const resolvedUserId = preResult.userId;
120
- const resolvedContext = preResult.context;
155
+ // Run global pre-request hooks (auth, rate limiting, validation)
156
+ const preRequestResult = await this.hookRunner.runPreRequest(itemId, userId, context);
157
+ HookRunner.throwIfBlocked(preRequestResult);
158
+
159
+ const resolvedId = preRequestResult.id;
160
+ const resolvedUserId = preRequestResult.userId;
161
+ const resolvedContext = preRequestResult.context;
162
+
163
+ // Run pre-dismiss hooks
164
+ const preDismissResult = await this.hookRunner.runPreDismiss(
165
+ resolvedId,
166
+ resolvedUserId,
167
+ resolvedContext,
168
+ );
169
+ HookRunner.throwIfBlocked(preDismissResult);
121
170
 
122
171
  // Execute core dismiss operation
123
172
  const result = await this.coreService.dismiss(resolvedId, resolvedUserId);
@@ -125,7 +174,7 @@ export class DismissibleService<TMetadata extends BaseMetadata = BaseMetadata> {
125
174
  // Run post-dismiss hooks
126
175
  await this.hookRunner.runPostDismiss(resolvedId, result.item, resolvedUserId, resolvedContext);
127
176
 
128
- // Emit dismissed event (async)
177
+ // Emit dismissed event
129
178
  this.eventEmitter.emit(
130
179
  DismissibleEvents.ITEM_DISMISSED,
131
180
  new ItemDismissedEvent(
@@ -137,6 +186,9 @@ export class DismissibleService<TMetadata extends BaseMetadata = BaseMetadata> {
137
186
  ),
138
187
  );
139
188
 
189
+ // Run global post-request hooks
190
+ await this.hookRunner.runPostRequest(resolvedId, result.item, resolvedUserId, resolvedContext);
191
+
140
192
  this.logger.debug(`dismiss completed`, { itemId });
141
193
 
142
194
  return result;
@@ -152,16 +204,27 @@ export class DismissibleService<TMetadata extends BaseMetadata = BaseMetadata> {
152
204
  itemId: string,
153
205
  userId: string,
154
206
  context?: IRequestContext,
155
- ): Promise<IRestoreServiceResponse<TMetadata>> {
207
+ ): Promise<IRestoreServiceResponse> {
156
208
  this.logger.debug(`restore called`, { itemId, userId });
157
209
 
158
- // Run pre-restore hooks
159
- const preResult = await this.hookRunner.runPreRestore(itemId, userId, context);
160
- HookRunner.throwIfBlocked(preResult);
210
+ // Validate input parameters
211
+ await this.validateInput(itemId, userId);
161
212
 
162
- const resolvedId = preResult.id;
163
- const resolvedUserId = preResult.userId;
164
- const resolvedContext = preResult.context;
213
+ // Run global pre-request hooks (auth, rate limiting, validation)
214
+ const preRequestResult = await this.hookRunner.runPreRequest(itemId, userId, context);
215
+ HookRunner.throwIfBlocked(preRequestResult);
216
+
217
+ const resolvedId = preRequestResult.id;
218
+ const resolvedUserId = preRequestResult.userId;
219
+ const resolvedContext = preRequestResult.context;
220
+
221
+ // Run pre-restore hooks
222
+ const preRestoreResult = await this.hookRunner.runPreRestore(
223
+ resolvedId,
224
+ resolvedUserId,
225
+ resolvedContext,
226
+ );
227
+ HookRunner.throwIfBlocked(preRestoreResult);
165
228
 
166
229
  // Execute core restore operation
167
230
  const result = await this.coreService.restore(resolvedId, resolvedUserId);
@@ -169,7 +232,7 @@ export class DismissibleService<TMetadata extends BaseMetadata = BaseMetadata> {
169
232
  // Run post-restore hooks
170
233
  await this.hookRunner.runPostRestore(resolvedId, result.item, resolvedUserId, resolvedContext);
171
234
 
172
- // Emit restored event (async)
235
+ // Emit restored event
173
236
  this.eventEmitter.emit(
174
237
  DismissibleEvents.ITEM_RESTORED,
175
238
  new ItemRestoredEvent(
@@ -181,6 +244,9 @@ export class DismissibleService<TMetadata extends BaseMetadata = BaseMetadata> {
181
244
  ),
182
245
  );
183
246
 
247
+ // Run global post-request hooks
248
+ await this.hookRunner.runPostRequest(resolvedId, result.item, resolvedUserId, resolvedContext);
249
+
184
250
  this.logger.debug(`restore completed`, { itemId });
185
251
 
186
252
  return result;