@dismissible/nestjs-dismissible 0.0.2-canary.738340d.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 (62) hide show
  1. package/README.md +506 -0
  2. package/jest.config.ts +29 -0
  3. package/package.json +63 -0
  4. package/project.json +42 -0
  5. package/src/api/dismissible-item-response.dto.ts +38 -0
  6. package/src/api/dismissible-item.mapper.spec.ts +63 -0
  7. package/src/api/dismissible-item.mapper.ts +33 -0
  8. package/src/api/index.ts +7 -0
  9. package/src/api/use-cases/api-tags.constants.ts +4 -0
  10. package/src/api/use-cases/dismiss/dismiss.controller.spec.ts +42 -0
  11. package/src/api/use-cases/dismiss/dismiss.controller.ts +63 -0
  12. package/src/api/use-cases/dismiss/dismiss.response.dto.ts +7 -0
  13. package/src/api/use-cases/dismiss/index.ts +2 -0
  14. package/src/api/use-cases/get-or-create/get-or-create.controller.spec.ts +76 -0
  15. package/src/api/use-cases/get-or-create/get-or-create.controller.ts +106 -0
  16. package/src/api/use-cases/get-or-create/get-or-create.request.dto.ts +17 -0
  17. package/src/api/use-cases/get-or-create/get-or-create.response.dto.ts +7 -0
  18. package/src/api/use-cases/get-or-create/index.ts +3 -0
  19. package/src/api/use-cases/index.ts +3 -0
  20. package/src/api/use-cases/restore/index.ts +2 -0
  21. package/src/api/use-cases/restore/restore.controller.spec.ts +42 -0
  22. package/src/api/use-cases/restore/restore.controller.ts +63 -0
  23. package/src/api/use-cases/restore/restore.response.dto.ts +7 -0
  24. package/src/core/create-options.ts +9 -0
  25. package/src/core/dismissible-core.service.spec.ts +357 -0
  26. package/src/core/dismissible-core.service.ts +161 -0
  27. package/src/core/dismissible.service.spec.ts +144 -0
  28. package/src/core/dismissible.service.ts +188 -0
  29. package/src/core/hook-runner.service.spec.ts +304 -0
  30. package/src/core/hook-runner.service.ts +267 -0
  31. package/src/core/index.ts +6 -0
  32. package/src/core/lifecycle-hook.interface.ts +122 -0
  33. package/src/core/service-responses.interface.ts +34 -0
  34. package/src/dismissible.module.ts +83 -0
  35. package/src/events/dismissible.events.ts +105 -0
  36. package/src/events/events.constants.ts +21 -0
  37. package/src/events/index.ts +2 -0
  38. package/src/exceptions/dismissible.exceptions.spec.ts +50 -0
  39. package/src/exceptions/dismissible.exceptions.ts +69 -0
  40. package/src/exceptions/index.ts +1 -0
  41. package/src/index.ts +8 -0
  42. package/src/request/index.ts +2 -0
  43. package/src/request/request-context.decorator.ts +14 -0
  44. package/src/request/request-context.interface.ts +6 -0
  45. package/src/response/dtos/base-response.dto.ts +11 -0
  46. package/src/response/dtos/error-response.dto.ts +36 -0
  47. package/src/response/dtos/index.ts +3 -0
  48. package/src/response/dtos/success-response.dto.ts +34 -0
  49. package/src/response/http-exception-filter.ts +21 -0
  50. package/src/response/index.ts +4 -0
  51. package/src/response/response.module.ts +9 -0
  52. package/src/response/response.service.spec.ts +86 -0
  53. package/src/response/response.service.ts +20 -0
  54. package/src/testing/factories.ts +45 -0
  55. package/src/testing/index.ts +1 -0
  56. package/src/utils/date/date.service.spec.ts +104 -0
  57. package/src/utils/date/date.service.ts +19 -0
  58. package/src/utils/date/index.ts +1 -0
  59. package/src/utils/dismissible.helper.ts +9 -0
  60. package/src/utils/index.ts +3 -0
  61. package/tsconfig.json +13 -0
  62. package/tsconfig.lib.json +14 -0
@@ -0,0 +1,267 @@
1
+ import { Injectable, Inject, Optional, ForbiddenException } from '@nestjs/common';
2
+ import {
3
+ DISMISSIBLE_HOOKS,
4
+ IDismissibleLifecycleHook,
5
+ IHookResult,
6
+ } from './lifecycle-hook.interface';
7
+ import { DISMISSIBLE_LOGGER, IDismissibleLogger } from '@dismissible/nestjs-logger';
8
+ import { BaseMetadata, DismissibleItemDto } from '@dismissible/nestjs-dismissible-item';
9
+ import { IRequestContext } from '../request/request-context.interface';
10
+
11
+ /**
12
+ * Result from running pre-hooks.
13
+ */
14
+ export interface IHookRunResult {
15
+ /** Whether the operation should proceed */
16
+ proceed: boolean;
17
+
18
+ /** The (potentially mutated) item ID */
19
+ id: string;
20
+
21
+ /** The (potentially mutated) user ID */
22
+ userId: string;
23
+
24
+ /** The (potentially mutated) request context */
25
+ context?: IRequestContext;
26
+
27
+ /** Reason for blocking (if proceed is false) */
28
+ reason?: string;
29
+ }
30
+
31
+ /**
32
+ * Service responsible for running lifecycle hooks.
33
+ */
34
+ @Injectable()
35
+ export class HookRunner<TMetadata extends BaseMetadata = BaseMetadata> {
36
+ private readonly sortedHooks: IDismissibleLifecycleHook<TMetadata>[];
37
+
38
+ constructor(
39
+ @Optional()
40
+ @Inject(DISMISSIBLE_HOOKS)
41
+ hooks: IDismissibleLifecycleHook<TMetadata>[] = [],
42
+ @Inject(DISMISSIBLE_LOGGER)
43
+ private readonly logger: IDismissibleLogger,
44
+ ) {
45
+ // Sort hooks by priority (lower numbers first)
46
+ this.sortedHooks = [...hooks].sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
47
+ }
48
+
49
+ /**
50
+ * Run pre-getOrCreate hooks.
51
+ */
52
+ async runPreGetOrCreate(
53
+ itemId: string,
54
+ userId: string,
55
+ context?: IRequestContext,
56
+ ): Promise<IHookRunResult> {
57
+ return this.runPreHooks('onBeforeGetOrCreate', itemId, userId, context);
58
+ }
59
+
60
+ /**
61
+ * Run post-getOrCreate hooks.
62
+ */
63
+ async runPostGetOrCreate(
64
+ itemId: string,
65
+ item: DismissibleItemDto<TMetadata>,
66
+ userId: string,
67
+ context?: IRequestContext,
68
+ ): Promise<void> {
69
+ await this.runPostHooks('onAfterGetOrCreate', itemId, item, userId, context);
70
+ }
71
+
72
+ /**
73
+ * Run pre-create hooks.
74
+ */
75
+ async runPreCreate(
76
+ itemId: string,
77
+ userId: string,
78
+ context?: IRequestContext,
79
+ ): Promise<IHookRunResult> {
80
+ return this.runPreHooks('onBeforeCreate', itemId, userId, context);
81
+ }
82
+
83
+ /**
84
+ * Run post-create hooks.
85
+ */
86
+ async runPostCreate(
87
+ itemId: string,
88
+ item: DismissibleItemDto<TMetadata>,
89
+ userId: string,
90
+ context?: IRequestContext,
91
+ ): Promise<void> {
92
+ await this.runPostHooks('onAfterCreate', itemId, item, userId, context);
93
+ }
94
+
95
+ /**
96
+ * Run pre-dismiss hooks.
97
+ */
98
+ async runPreDismiss(
99
+ itemId: string,
100
+ userId: string,
101
+ context?: IRequestContext,
102
+ ): Promise<IHookRunResult> {
103
+ return this.runPreHooks('onBeforeDismiss', itemId, userId, context);
104
+ }
105
+
106
+ /**
107
+ * Run post-dismiss hooks.
108
+ */
109
+ async runPostDismiss(
110
+ itemId: string,
111
+ item: DismissibleItemDto<TMetadata>,
112
+ userId: string,
113
+ context?: IRequestContext,
114
+ ): Promise<void> {
115
+ await this.runPostHooks('onAfterDismiss', itemId, item, userId, context);
116
+ }
117
+
118
+ /**
119
+ * Run pre-restore hooks.
120
+ */
121
+ async runPreRestore(
122
+ itemId: string,
123
+ userId: string,
124
+ context?: IRequestContext,
125
+ ): Promise<IHookRunResult> {
126
+ return this.runPreHooks('onBeforeRestore', itemId, userId, context);
127
+ }
128
+
129
+ /**
130
+ * Run post-restore hooks.
131
+ */
132
+ async runPostRestore(
133
+ itemId: string,
134
+ item: DismissibleItemDto<TMetadata>,
135
+ userId: string,
136
+ context?: IRequestContext,
137
+ ): Promise<void> {
138
+ await this.runPostHooks('onAfterRestore', itemId, item, userId, context);
139
+ }
140
+
141
+ /**
142
+ * Internal method to run pre-hooks.
143
+ */
144
+ private async runPreHooks(
145
+ hookName: keyof IDismissibleLifecycleHook<TMetadata>,
146
+ itemId: string,
147
+ userId: string,
148
+ context?: IRequestContext,
149
+ ): Promise<IHookRunResult> {
150
+ let currentId = itemId;
151
+ let currentUserId = userId;
152
+ let currentContext = context ? { ...context } : undefined;
153
+
154
+ for (const hook of this.sortedHooks) {
155
+ const hookFn = hook[hookName] as
156
+ | ((
157
+ itemId: string,
158
+ userId: string,
159
+ context?: IRequestContext,
160
+ ) => Promise<IHookResult> | IHookResult)
161
+ | undefined;
162
+
163
+ if (hookFn) {
164
+ try {
165
+ const result = await hookFn.call(hook, currentId, currentUserId, currentContext);
166
+
167
+ if (!result.proceed) {
168
+ this.logger.debug(`Hook ${hook.constructor.name}.${hookName} blocked operation`, {
169
+ itemId: currentId,
170
+ userId: currentUserId,
171
+ reason: result.reason,
172
+ });
173
+
174
+ return {
175
+ proceed: false,
176
+ id: currentId,
177
+ userId: currentUserId,
178
+ context: currentContext,
179
+ reason: result.reason,
180
+ };
181
+ }
182
+
183
+ // Apply mutations if present
184
+ if (result.mutations) {
185
+ if (result.mutations.id !== undefined) {
186
+ currentId = result.mutations.id;
187
+ }
188
+ if (result.mutations.userId !== undefined) {
189
+ currentUserId = result.mutations.userId;
190
+ }
191
+ if (result.mutations.context && currentContext) {
192
+ currentContext = { ...currentContext, ...result.mutations.context };
193
+ }
194
+ }
195
+ } catch (error) {
196
+ this.logger.error(
197
+ `Error in hook ${hook.constructor.name}.${hookName}`,
198
+ error instanceof Error ? error : new Error(String(error)),
199
+ {
200
+ itemId: currentId,
201
+ userId: currentUserId,
202
+ },
203
+ );
204
+ throw error;
205
+ }
206
+ }
207
+ }
208
+
209
+ return {
210
+ proceed: true,
211
+ id: currentId,
212
+ userId: currentUserId,
213
+ context: currentContext,
214
+ };
215
+ }
216
+
217
+ /**
218
+ * Internal method to run post-hooks.
219
+ * Post-hooks run in reverse priority order.
220
+ */
221
+ private async runPostHooks(
222
+ hookName: keyof IDismissibleLifecycleHook<TMetadata>,
223
+ itemId: string,
224
+ item: DismissibleItemDto<TMetadata>,
225
+ userId: string,
226
+ context?: IRequestContext,
227
+ ): Promise<void> {
228
+ // Run in reverse order for post-hooks
229
+ const reversedHooks = [...this.sortedHooks].reverse();
230
+
231
+ for (const hook of reversedHooks) {
232
+ const hookFn = hook[hookName] as
233
+ | ((
234
+ itemId: string,
235
+ item: DismissibleItemDto<TMetadata>,
236
+ userId: string,
237
+ context?: IRequestContext,
238
+ ) => Promise<void> | void)
239
+ | undefined;
240
+
241
+ if (hookFn) {
242
+ try {
243
+ await hookFn.call(hook, itemId, item, userId, context);
244
+ } catch (error) {
245
+ this.logger.error(
246
+ `Error in hook ${hook.constructor.name}.${hookName}`,
247
+ error instanceof Error ? error : new Error(String(error)),
248
+ {
249
+ itemId,
250
+ userId,
251
+ },
252
+ );
253
+ // Post-hooks errors are logged but don't block the operation
254
+ }
255
+ }
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Throw ForbiddenException if the hook result indicates the operation was blocked.
261
+ */
262
+ static throwIfBlocked(result: IHookRunResult): void {
263
+ if (!result.proceed) {
264
+ throw new ForbiddenException(result.reason ?? 'Operation blocked by lifecycle hook');
265
+ }
266
+ }
267
+ }
@@ -0,0 +1,6 @@
1
+ export * from './lifecycle-hook.interface';
2
+ export * from './service-responses.interface';
3
+ export * from './dismissible-core.service';
4
+ export * from './hook-runner.service';
5
+ export * from './dismissible.service';
6
+ export * from './create-options';
@@ -0,0 +1,122 @@
1
+ import { BaseMetadata, DismissibleItemDto } from '@dismissible/nestjs-dismissible-item';
2
+ import { IRequestContext } from '../request/request-context.interface';
3
+
4
+ /**
5
+ * Injection token for lifecycle hooks.
6
+ */
7
+ export const DISMISSIBLE_HOOKS = Symbol('DISMISSIBLE_HOOKS');
8
+
9
+ /**
10
+ * Mutations that can be applied by pre-hooks.
11
+ */
12
+ export interface IHookMutations {
13
+ /** Mutated item ID */
14
+ id?: string;
15
+
16
+ /** Mutated user ID */
17
+ userId?: string;
18
+
19
+ /** Mutated request context */
20
+ context?: Partial<IRequestContext>;
21
+ }
22
+
23
+ /**
24
+ * Result returned by pre-hooks.
25
+ */
26
+ export interface IHookResult {
27
+ /** Whether the operation should proceed */
28
+ proceed: boolean;
29
+
30
+ /** Optional reason if the operation is blocked */
31
+ reason?: string;
32
+
33
+ /** Optional mutations to apply */
34
+ mutations?: IHookMutations;
35
+ }
36
+
37
+ /**
38
+ * Interface for lifecycle hooks that can intercept dismissible operations.
39
+ */
40
+ export interface IDismissibleLifecycleHook<TMetadata extends BaseMetadata = BaseMetadata> {
41
+ /**
42
+ * Priority for hook execution (lower numbers run first).
43
+ * Default is 0.
44
+ */
45
+ readonly priority?: number;
46
+
47
+ /**
48
+ * Called before getOrCreate operation.
49
+ */
50
+ onBeforeGetOrCreate?(
51
+ itemId: string,
52
+ userId: string,
53
+ context?: IRequestContext,
54
+ ): Promise<IHookResult> | IHookResult;
55
+
56
+ /**
57
+ * Called after getOrCreate operation.
58
+ */
59
+ onAfterGetOrCreate?(
60
+ itemId: string,
61
+ item: DismissibleItemDto<TMetadata>,
62
+ userId: string,
63
+ context?: IRequestContext,
64
+ ): Promise<void> | void;
65
+
66
+ /**
67
+ * Called before creating a new item.
68
+ */
69
+ onBeforeCreate?(
70
+ itemId: string,
71
+ userId: string,
72
+ context?: IRequestContext,
73
+ ): Promise<IHookResult> | IHookResult;
74
+
75
+ /**
76
+ * Called after creating a new item.
77
+ */
78
+ onAfterCreate?(
79
+ itemId: string,
80
+ item: DismissibleItemDto<TMetadata>,
81
+ userId: string,
82
+ context?: IRequestContext,
83
+ ): Promise<void> | void;
84
+
85
+ /**
86
+ * Called before dismissing an item.
87
+ */
88
+ onBeforeDismiss?(
89
+ itemId: string,
90
+ userId: string,
91
+ context?: IRequestContext,
92
+ ): Promise<IHookResult> | IHookResult;
93
+
94
+ /**
95
+ * Called after dismissing an item.
96
+ */
97
+ onAfterDismiss?(
98
+ itemId: string,
99
+ item: DismissibleItemDto<TMetadata>,
100
+ userId: string,
101
+ context?: IRequestContext,
102
+ ): Promise<void> | void;
103
+
104
+ /**
105
+ * Called before restoring an item.
106
+ */
107
+ onBeforeRestore?(
108
+ itemId: string,
109
+ userId: string,
110
+ context?: IRequestContext,
111
+ ): Promise<IHookResult> | IHookResult;
112
+
113
+ /**
114
+ * Called after restoring an item.
115
+ */
116
+ onAfterRestore?(
117
+ itemId: string,
118
+ item: DismissibleItemDto<TMetadata>,
119
+ userId: string,
120
+ context?: IRequestContext,
121
+ ): Promise<void> | void;
122
+ }
@@ -0,0 +1,34 @@
1
+ import { BaseMetadata, DismissibleItemDto } from '@dismissible/nestjs-dismissible-item';
2
+
3
+ /**
4
+ * Response from getOrCreate operation.
5
+ */
6
+ export interface IGetOrCreateServiceResponse<TMetadata extends BaseMetadata = BaseMetadata> {
7
+ /** The item (either retrieved or created) */
8
+ item: DismissibleItemDto<TMetadata>;
9
+
10
+ /** Whether the item was newly created */
11
+ created: boolean;
12
+ }
13
+
14
+ /**
15
+ * Response from dismiss operation.
16
+ */
17
+ export interface IDismissServiceResponse<TMetadata extends BaseMetadata = BaseMetadata> {
18
+ /** The dismissed item */
19
+ item: DismissibleItemDto<TMetadata>;
20
+
21
+ /** The item state before dismissal */
22
+ previousItem: DismissibleItemDto<TMetadata>;
23
+ }
24
+
25
+ /**
26
+ * Response from restore operation.
27
+ */
28
+ export interface IRestoreServiceResponse<TMetadata extends BaseMetadata = BaseMetadata> {
29
+ /** The restored item */
30
+ item: DismissibleItemDto<TMetadata>;
31
+
32
+ /** The item state before restoration */
33
+ previousItem: DismissibleItemDto<TMetadata>;
34
+ }
@@ -0,0 +1,83 @@
1
+ import { Module, DynamicModule, Type, Provider } from '@nestjs/common';
2
+ import { EventEmitterModule } from '@nestjs/event-emitter';
3
+ import { GetOrCreateController } from './api/use-cases/get-or-create/get-or-create.controller';
4
+ import { DismissController } from './api/use-cases/dismiss/dismiss.controller';
5
+ import { RestoreController } from './api/use-cases/restore/restore.controller';
6
+ import { DismissibleService } from './core/dismissible.service';
7
+ import { DismissibleCoreService } from './core/dismissible-core.service';
8
+ import { HookRunner } from './core/hook-runner.service';
9
+ import { DismissibleItemMapper } from './api/dismissible-item.mapper';
10
+ import { DateService } from './utils/date/date.service';
11
+ import { DISMISSIBLE_HOOKS, IDismissibleLifecycleHook } from './core/lifecycle-hook.interface';
12
+ import { LoggerModule, IDismissibleLoggerModuleOptions } from '@dismissible/nestjs-logger';
13
+ import { BaseMetadata } from '@dismissible/nestjs-dismissible-item';
14
+ import { ResponseService, ResponseModule } from './response';
15
+ import { ValidationModule } from '@dismissible/nestjs-validation';
16
+ import { IDismissibleStorageModuleOptions, StorageModule } from '@dismissible/nestjs-storage';
17
+ import { DismissibleHelper } from './utils/dismissible.helper';
18
+ import { DismissibleItemModule } from '@dismissible/nestjs-dismissible-item';
19
+
20
+ /**
21
+ * Module configuration options.
22
+ */
23
+ export type IDismissibleModuleOptions<TMetadata extends BaseMetadata = BaseMetadata> =
24
+ IDismissibleLoggerModuleOptions &
25
+ IDismissibleStorageModuleOptions & {
26
+ hooks?: Type<IDismissibleLifecycleHook<TMetadata>>[];
27
+ };
28
+
29
+ @Module({})
30
+ export class DismissibleModule {
31
+ static forRoot<TMetadata extends BaseMetadata = BaseMetadata>(
32
+ options: IDismissibleModuleOptions<TMetadata>,
33
+ ): DynamicModule {
34
+ const providers: Provider[] = [
35
+ DateService,
36
+ ResponseService,
37
+ DismissibleCoreService,
38
+ HookRunner,
39
+ DismissibleService,
40
+ DismissibleItemMapper,
41
+ DismissibleHelper,
42
+ ];
43
+
44
+ if (options.hooks && options.hooks.length > 0) {
45
+ for (const hook of options.hooks) {
46
+ providers.push(hook);
47
+ }
48
+
49
+ providers.push({
50
+ provide: DISMISSIBLE_HOOKS,
51
+ useFactory: (...hooks: IDismissibleLifecycleHook<TMetadata>[]) => hooks,
52
+ inject: options.hooks,
53
+ });
54
+ } else {
55
+ providers.push({
56
+ provide: DISMISSIBLE_HOOKS,
57
+ useValue: [],
58
+ });
59
+ }
60
+
61
+ return {
62
+ module: DismissibleModule,
63
+ imports: [
64
+ EventEmitterModule.forRoot(),
65
+ LoggerModule.forRoot(options),
66
+ ValidationModule,
67
+ ResponseModule,
68
+ options.storage ?? StorageModule,
69
+ DismissibleItemModule,
70
+ ],
71
+ controllers: [GetOrCreateController, DismissController, RestoreController],
72
+ providers,
73
+ exports: [
74
+ DismissibleService,
75
+ DismissibleCoreService,
76
+ DismissibleItemMapper,
77
+ DateService,
78
+ ResponseService,
79
+ DISMISSIBLE_HOOKS,
80
+ ],
81
+ };
82
+ }
83
+ }
@@ -0,0 +1,105 @@
1
+ import { BaseMetadata, DismissibleItemDto } from '@dismissible/nestjs-dismissible-item';
2
+ import { IRequestContext } from '../request/request-context.interface';
3
+
4
+ /**
5
+ * Base class for all dismissible events.
6
+ */
7
+ abstract class BaseDismissibleEvent<TMetadata extends BaseMetadata = BaseMetadata> {
8
+ /** The item identifier */
9
+ readonly id: string;
10
+
11
+ /** The current state of the item */
12
+ readonly item: DismissibleItemDto<TMetadata>;
13
+
14
+ /** The user identifier */
15
+ readonly userId: string;
16
+
17
+ /** The request context (optional) */
18
+ readonly context?: IRequestContext;
19
+
20
+ constructor(
21
+ itemId: string,
22
+ item: DismissibleItemDto<TMetadata>,
23
+ userId: string,
24
+ context?: IRequestContext,
25
+ ) {
26
+ this.id = itemId;
27
+ this.item = item;
28
+ this.userId = userId;
29
+ this.context = context;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Event emitted when an existing item is retrieved.
35
+ */
36
+ export class ItemRetrievedEvent<
37
+ TMetadata extends BaseMetadata = BaseMetadata,
38
+ > extends BaseDismissibleEvent<TMetadata> {
39
+ constructor(
40
+ itemId: string,
41
+ item: DismissibleItemDto<TMetadata>,
42
+ userId: string,
43
+ context?: IRequestContext,
44
+ ) {
45
+ super(itemId, item, userId, context);
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Event emitted when a new item is created.
51
+ */
52
+ export class ItemCreatedEvent<
53
+ TMetadata extends BaseMetadata = BaseMetadata,
54
+ > extends BaseDismissibleEvent<TMetadata> {
55
+ constructor(
56
+ itemId: string,
57
+ item: DismissibleItemDto<TMetadata>,
58
+ userId: string,
59
+ context?: IRequestContext,
60
+ ) {
61
+ super(itemId, item, userId, context);
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Event emitted when an item is dismissed.
67
+ */
68
+ export class ItemDismissedEvent<
69
+ TMetadata extends BaseMetadata = BaseMetadata,
70
+ > extends BaseDismissibleEvent<TMetadata> {
71
+ /** The item state before dismissal */
72
+ readonly previousItem: DismissibleItemDto<TMetadata>;
73
+
74
+ constructor(
75
+ itemId: string,
76
+ item: DismissibleItemDto<TMetadata>,
77
+ previousItem: DismissibleItemDto<TMetadata>,
78
+ userId: string,
79
+ context?: IRequestContext,
80
+ ) {
81
+ super(itemId, item, userId, context);
82
+ this.previousItem = previousItem;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Event emitted when a dismissed item is restored.
88
+ */
89
+ export class ItemRestoredEvent<
90
+ TMetadata extends BaseMetadata = BaseMetadata,
91
+ > extends BaseDismissibleEvent<TMetadata> {
92
+ /** The item state before restoration */
93
+ readonly previousItem: DismissibleItemDto<TMetadata>;
94
+
95
+ constructor(
96
+ itemId: string,
97
+ item: DismissibleItemDto<TMetadata>,
98
+ previousItem: DismissibleItemDto<TMetadata>,
99
+ userId: string,
100
+ context?: IRequestContext,
101
+ ) {
102
+ super(itemId, item, userId, context);
103
+ this.previousItem = previousItem;
104
+ }
105
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Event names for dismissible operations.
3
+ */
4
+ export const DismissibleEvents = {
5
+ /** Emitted when an existing item is retrieved */
6
+ ITEM_RETRIEVED: 'dismissible.item.retrieved',
7
+
8
+ /** Emitted when a new item is created */
9
+ ITEM_CREATED: 'dismissible.item.created',
10
+
11
+ /** Emitted when an item is dismissed */
12
+ ITEM_DISMISSED: 'dismissible.item.dismissed',
13
+
14
+ /** Emitted when a dismissed item is restored */
15
+ ITEM_RESTORED: 'dismissible.item.restored',
16
+ } as const;
17
+
18
+ /**
19
+ * Type representing all dismissible event names.
20
+ */
21
+ export type DismissibleEventType = (typeof DismissibleEvents)[keyof typeof DismissibleEvents];
@@ -0,0 +1,2 @@
1
+ export * from './events.constants';
2
+ export * from './dismissible.events';
@@ -0,0 +1,50 @@
1
+ import { HttpStatus } from '@nestjs/common';
2
+ import {
3
+ ItemNotFoundException,
4
+ ItemAlreadyDismissedException,
5
+ ItemNotDismissedException,
6
+ } from './dismissible.exceptions';
7
+
8
+ describe('Dismissible Exceptions', () => {
9
+ describe('ItemNotFoundException', () => {
10
+ it('should create exception with correct structure', () => {
11
+ const exception = new ItemNotFoundException('test-item');
12
+
13
+ expect(exception.getStatus()).toBe(HttpStatus.BAD_REQUEST);
14
+
15
+ const response = exception.getResponse() as any;
16
+ expect(response.statusCode).toBe(HttpStatus.BAD_REQUEST);
17
+ expect(response.code).toBe('ITEM_NOT_FOUND');
18
+ expect(response.message).toContain('test-item');
19
+ expect(response.itemId).toBe('test-item');
20
+ });
21
+ });
22
+
23
+ describe('ItemAlreadyDismissedException', () => {
24
+ it('should create exception with correct structure', () => {
25
+ const exception = new ItemAlreadyDismissedException('test-item');
26
+
27
+ expect(exception.getStatus()).toBe(HttpStatus.BAD_REQUEST);
28
+
29
+ const response = exception.getResponse() as any;
30
+ expect(response.statusCode).toBe(HttpStatus.BAD_REQUEST);
31
+ expect(response.code).toBe('ITEM_ALREADY_DISMISSED');
32
+ expect(response.message).toContain('test-item');
33
+ expect(response.itemId).toBe('test-item');
34
+ });
35
+ });
36
+
37
+ describe('ItemNotDismissedException', () => {
38
+ it('should create exception with correct structure', () => {
39
+ const exception = new ItemNotDismissedException('test-item');
40
+
41
+ expect(exception.getStatus()).toBe(HttpStatus.BAD_REQUEST);
42
+
43
+ const response = exception.getResponse() as any;
44
+ expect(response.statusCode).toBe(HttpStatus.BAD_REQUEST);
45
+ expect(response.code).toBe('ITEM_NOT_DISMISSED');
46
+ expect(response.message).toContain('test-item');
47
+ expect(response.itemId).toBe('test-item');
48
+ });
49
+ });
50
+ });