@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.
Files changed (46) hide show
  1. package/README.md +58 -74
  2. package/jest.config.ts +1 -1
  3. package/package.json +8 -11
  4. package/project.json +1 -1
  5. package/src/api/dismissible-item-response.dto.ts +0 -8
  6. package/src/api/dismissible-item.mapper.spec.ts +0 -12
  7. package/src/api/dismissible-item.mapper.ts +2 -8
  8. package/src/api/index.ts +2 -3
  9. package/src/api/use-cases/dismiss/dismiss.controller.spec.ts +1 -2
  10. package/src/api/use-cases/dismiss/dismiss.controller.ts +9 -10
  11. package/src/api/use-cases/get-or-create/get-or-create.controller.spec.ts +2 -42
  12. package/src/api/use-cases/get-or-create/get-or-create.controller.ts +11 -58
  13. package/src/api/use-cases/get-or-create/index.ts +0 -1
  14. package/src/api/use-cases/restore/restore.controller.spec.ts +1 -2
  15. package/src/api/use-cases/restore/restore.controller.ts +9 -10
  16. package/src/api/validation/index.ts +2 -0
  17. package/src/api/validation/param-validation.pipe.spec.ts +313 -0
  18. package/src/api/validation/param-validation.pipe.ts +38 -0
  19. package/src/api/validation/param.decorators.ts +32 -0
  20. package/src/core/dismissible-core.service.spec.ts +75 -29
  21. package/src/core/dismissible-core.service.ts +40 -28
  22. package/src/core/dismissible.service.spec.ts +106 -24
  23. package/src/core/dismissible.service.ts +93 -54
  24. package/src/core/hook-runner.service.spec.ts +495 -54
  25. package/src/core/hook-runner.service.ts +125 -24
  26. package/src/core/index.ts +0 -1
  27. package/src/core/lifecycle-hook.interface.ts +7 -122
  28. package/src/core/service-responses.interface.ts +9 -9
  29. package/src/dismissible.module.integration.spec.ts +704 -0
  30. package/src/dismissible.module.ts +10 -11
  31. package/src/events/dismissible.events.ts +17 -40
  32. package/src/index.ts +1 -1
  33. package/src/response/http-exception-filter.spec.ts +179 -0
  34. package/src/response/http-exception-filter.ts +3 -3
  35. package/src/response/response.service.spec.ts +0 -14
  36. package/src/testing/factories.ts +24 -9
  37. package/src/utils/dismissible.helper.ts +2 -2
  38. package/src/validation/dismissible-input.dto.ts +47 -0
  39. package/src/validation/index.ts +1 -0
  40. package/tsconfig.json +3 -0
  41. package/tsconfig.spec.json +12 -0
  42. package/src/api/use-cases/get-or-create/get-or-create.request.dto.ts +0 -17
  43. package/src/core/create-options.ts +0 -9
  44. package/src/request/index.ts +0 -2
  45. package/src/request/request-context.decorator.ts +0 -14
  46. 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 { BaseMetadata, DismissibleItemFactory } from '@dismissible/nestjs-dismissible-item';
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<TMetadata extends BaseMetadata = BaseMetadata> {
24
+ export class DismissibleCoreService {
26
25
  constructor(
27
- @Inject(DISMISSIBLE_STORAGE_ADAPTER) private readonly storage: IDismissibleStorage<TMetadata>,
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 or create a new one.
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
- * @param options Optional creation options (metadata)
38
+ * @returns The item or null if not found
40
39
  */
41
- async getOrCreate(
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
- const existingItem = await this.storage.get(userId, itemId);
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<TMetadata>({
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<TMetadata>> {
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<TMetadata>> {
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<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,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
- service = new DismissibleService(mockCoreService, mockHookRunner, mockEventEmitter, mockLogger);
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 hooks, call core service, and emit ITEM_RETRIEVED event for existing item', async () => {
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.runPreGetOrCreate.mockResolvedValue(createHookResult('existing-item'));
42
- mockCoreService.getOrCreate.mockResolvedValue({ item, created: false });
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, undefined, context);
55
+ const result = await service.getOrCreate('existing-item', testUserId, context);
45
56
 
46
- expect(mockHookRunner.runPreGetOrCreate).toHaveBeenCalled();
47
- expect(mockCoreService.getOrCreate).toHaveBeenCalledWith(
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
- undefined,
63
+ expect.anything(),
51
64
  );
52
- expect(mockHookRunner.runPostGetOrCreate).toHaveBeenCalled();
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 and emit ITEM_CREATED event for new item', async () => {
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
- 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
-
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.runPreGetOrCreate.mockResolvedValue(createHookResult('test-item'));
130
- mockCoreService.getOrCreate.mockResolvedValue({ item, created: false });
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, undefined, context);
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 { BaseMetadata } from '@dismissible/nestjs-dismissible-item';
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<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
+ 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<TMetadata>> {
55
+ ): Promise<IGetOrCreateServiceResponse> {
49
56
  this.logger.debug(`getOrCreate called`, { itemId, userId });
50
57
 
51
- // Run pre-getOrCreate hooks
52
- const preResult = await this.hookRunner.runPreGetOrCreate(itemId, userId, context);
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
- // Check if we need to run create hooks (for potential new item)
60
- const result = await this.coreService.getOrCreate(resolvedId, resolvedUserId, options);
67
+ const existingItem = await this.coreService.get(resolvedId, resolvedUserId);
61
68
 
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(
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(preCreateResult);
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, result.item, resolvedUserId, resolvedContext),
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
- // Run post-getOrCreate hooks
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.logger.debug(`getOrCreate completed`, { itemId, created: result.created });
107
+ const createdItem = await this.coreService.create(resolvedId, resolvedUserId);
97
108
 
98
- return result;
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<TMetadata>> {
136
+ ): Promise<IDismissServiceResponse> {
112
137
  this.logger.debug(`dismiss called`, { itemId, userId });
113
138
 
114
- // Run pre-dismiss hooks
115
- const preResult = await this.hookRunner.runPreDismiss(itemId, userId, context);
116
- HookRunner.throwIfBlocked(preResult);
139
+ await this.validateInput(itemId, userId);
117
140
 
118
- const resolvedId = preResult.id;
119
- const resolvedUserId = preResult.userId;
120
- const resolvedContext = preResult.context;
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<TMetadata>> {
187
+ ): Promise<IRestoreServiceResponse> {
156
188
  this.logger.debug(`restore called`, { itemId, userId });
157
189
 
158
- // Run pre-restore hooks
159
- const preResult = await this.hookRunner.runPreRestore(itemId, userId, context);
160
- HookRunner.throwIfBlocked(preResult);
190
+ await this.validateInput(itemId, userId);
161
191
 
162
- const resolvedId = preResult.id;
163
- const resolvedUserId = preResult.userId;
164
- const resolvedContext = preResult.context;
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;