@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
@@ -5,7 +5,7 @@ import {
5
5
  IHookResult,
6
6
  } from './lifecycle-hook.interface';
7
7
  import { DISMISSIBLE_LOGGER, IDismissibleLogger } from '@dismissible/nestjs-logger';
8
- import { BaseMetadata, DismissibleItemDto } from '@dismissible/nestjs-dismissible-item';
8
+ import { DismissibleItemDto } from '@dismissible/nestjs-dismissible-item';
9
9
  import { IRequestContext } from '../request/request-context.interface';
10
10
 
11
11
  /**
@@ -32,13 +32,13 @@ export interface IHookRunResult {
32
32
  * Service responsible for running lifecycle hooks.
33
33
  */
34
34
  @Injectable()
35
- export class HookRunner<TMetadata extends BaseMetadata = BaseMetadata> {
36
- private readonly sortedHooks: IDismissibleLifecycleHook<TMetadata>[];
35
+ export class HookRunner {
36
+ private readonly sortedHooks: IDismissibleLifecycleHook[];
37
37
 
38
38
  constructor(
39
39
  @Optional()
40
40
  @Inject(DISMISSIBLE_HOOKS)
41
- hooks: IDismissibleLifecycleHook<TMetadata>[] = [],
41
+ hooks: IDismissibleLifecycleHook[] = [],
42
42
  @Inject(DISMISSIBLE_LOGGER)
43
43
  private readonly logger: IDismissibleLogger,
44
44
  ) {
@@ -46,29 +46,68 @@ export class HookRunner<TMetadata extends BaseMetadata = BaseMetadata> {
46
46
  this.sortedHooks = [...hooks].sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
47
47
  }
48
48
 
49
+ // ─────────────────────────────────────────────────────────────────
50
+ // Global Request Hooks
51
+ // ─────────────────────────────────────────────────────────────────
52
+
53
+ /**
54
+ * Run pre-request hooks (global - runs at start of any operation).
55
+ * Use for authentication, rate limiting, request validation.
56
+ */
57
+ async runPreRequest(
58
+ itemId: string,
59
+ userId: string,
60
+ context?: IRequestContext,
61
+ ): Promise<IHookRunResult> {
62
+ return this.runPreHooks('onBeforeRequest', itemId, userId, context);
63
+ }
64
+
65
+ /**
66
+ * Run post-request hooks (global - runs at end of any operation).
67
+ * Use for audit logging, metrics, cleanup.
68
+ */
69
+ async runPostRequest(
70
+ itemId: string,
71
+ item: DismissibleItemDto,
72
+ userId: string,
73
+ context?: IRequestContext,
74
+ ): Promise<void> {
75
+ await this.runPostHooks('onAfterRequest', itemId, item, userId, context);
76
+ }
77
+
78
+ // ─────────────────────────────────────────────────────────────────
79
+ // Get Hooks
80
+ // ─────────────────────────────────────────────────────────────────
81
+
49
82
  /**
50
- * Run pre-getOrCreate hooks.
83
+ * Run pre-get hooks (when item exists and is about to be returned).
84
+ * Receives the item for access control based on item state.
51
85
  */
52
- async runPreGetOrCreate(
86
+ async runPreGet(
53
87
  itemId: string,
88
+ item: DismissibleItemDto,
54
89
  userId: string,
55
90
  context?: IRequestContext,
56
91
  ): Promise<IHookRunResult> {
57
- return this.runPreHooks('onBeforeGetOrCreate', itemId, userId, context);
92
+ return this.runPreHooksWithItem('onBeforeGet', itemId, item, userId, context);
58
93
  }
59
94
 
60
95
  /**
61
- * Run post-getOrCreate hooks.
96
+ * Run post-get hooks (after item is returned).
62
97
  */
63
- async runPostGetOrCreate(
98
+ async runPostGet(
64
99
  itemId: string,
65
- item: DismissibleItemDto<TMetadata>,
100
+ item: DismissibleItemDto,
66
101
  userId: string,
67
102
  context?: IRequestContext,
68
103
  ): Promise<void> {
69
- await this.runPostHooks('onAfterGetOrCreate', itemId, item, userId, context);
104
+ await this.runPostHooks('onAfterGet', itemId, item, userId, context);
70
105
  }
71
106
 
107
+ // ─────────────────────────────────────────────────────────────────
108
+ // Create Hooks
109
+ // ─────────────────────────────────────────────────────────────────
110
+
72
111
  /**
73
112
  * Run pre-create hooks.
74
113
  */
@@ -85,13 +124,17 @@ export class HookRunner<TMetadata extends BaseMetadata = BaseMetadata> {
85
124
  */
86
125
  async runPostCreate(
87
126
  itemId: string,
88
- item: DismissibleItemDto<TMetadata>,
127
+ item: DismissibleItemDto,
89
128
  userId: string,
90
129
  context?: IRequestContext,
91
130
  ): Promise<void> {
92
131
  await this.runPostHooks('onAfterCreate', itemId, item, userId, context);
93
132
  }
94
133
 
134
+ // ─────────────────────────────────────────────────────────────────
135
+ // Dismiss Hooks
136
+ // ─────────────────────────────────────────────────────────────────
137
+
95
138
  /**
96
139
  * Run pre-dismiss hooks.
97
140
  */
@@ -108,13 +151,17 @@ export class HookRunner<TMetadata extends BaseMetadata = BaseMetadata> {
108
151
  */
109
152
  async runPostDismiss(
110
153
  itemId: string,
111
- item: DismissibleItemDto<TMetadata>,
154
+ item: DismissibleItemDto,
112
155
  userId: string,
113
156
  context?: IRequestContext,
114
157
  ): Promise<void> {
115
158
  await this.runPostHooks('onAfterDismiss', itemId, item, userId, context);
116
159
  }
117
160
 
161
+ // ─────────────────────────────────────────────────────────────────
162
+ // Restore Hooks
163
+ // ─────────────────────────────────────────────────────────────────
164
+
118
165
  /**
119
166
  * Run pre-restore hooks.
120
167
  */
@@ -131,7 +178,7 @@ export class HookRunner<TMetadata extends BaseMetadata = BaseMetadata> {
131
178
  */
132
179
  async runPostRestore(
133
180
  itemId: string,
134
- item: DismissibleItemDto<TMetadata>,
181
+ item: DismissibleItemDto,
135
182
  userId: string,
136
183
  context?: IRequestContext,
137
184
  ): Promise<void> {
@@ -142,7 +189,7 @@ export class HookRunner<TMetadata extends BaseMetadata = BaseMetadata> {
142
189
  * Internal method to run pre-hooks.
143
190
  */
144
191
  private async runPreHooks(
145
- hookName: keyof IDismissibleLifecycleHook<TMetadata>,
192
+ hookName: keyof IDismissibleLifecycleHook,
146
193
  itemId: string,
147
194
  userId: string,
148
195
  context?: IRequestContext,
@@ -214,14 +261,93 @@ export class HookRunner<TMetadata extends BaseMetadata = BaseMetadata> {
214
261
  };
215
262
  }
216
263
 
264
+ /**
265
+ * Internal method to run pre-hooks that receive the item (e.g., onBeforeGet).
266
+ * Unlike standard pre-hooks, these receive the item for inspection/access control.
267
+ */
268
+ private async runPreHooksWithItem(
269
+ hookName: keyof IDismissibleLifecycleHook,
270
+ itemId: string,
271
+ item: DismissibleItemDto,
272
+ userId: string,
273
+ context?: IRequestContext,
274
+ ): Promise<IHookRunResult> {
275
+ let currentId = itemId;
276
+ let currentUserId = userId;
277
+ let currentContext = context ? { ...context } : undefined;
278
+
279
+ for (const hook of this.sortedHooks) {
280
+ const hookFn = hook[hookName] as
281
+ | ((
282
+ itemId: string,
283
+ item: DismissibleItemDto,
284
+ userId: string,
285
+ context?: IRequestContext,
286
+ ) => Promise<IHookResult> | IHookResult)
287
+ | undefined;
288
+
289
+ if (hookFn) {
290
+ try {
291
+ const result = await hookFn.call(hook, currentId, item, currentUserId, currentContext);
292
+
293
+ if (!result.proceed) {
294
+ this.logger.debug(`Hook ${hook.constructor.name}.${hookName} blocked operation`, {
295
+ itemId: currentId,
296
+ userId: currentUserId,
297
+ reason: result.reason,
298
+ });
299
+
300
+ return {
301
+ proceed: false,
302
+ id: currentId,
303
+ userId: currentUserId,
304
+ context: currentContext,
305
+ reason: result.reason,
306
+ };
307
+ }
308
+
309
+ // Apply mutations if present
310
+ if (result.mutations) {
311
+ if (result.mutations.id !== undefined) {
312
+ currentId = result.mutations.id;
313
+ }
314
+ if (result.mutations.userId !== undefined) {
315
+ currentUserId = result.mutations.userId;
316
+ }
317
+ if (result.mutations.context && currentContext) {
318
+ currentContext = { ...currentContext, ...result.mutations.context };
319
+ }
320
+ }
321
+ } catch (error) {
322
+ this.logger.error(
323
+ `Error in hook ${hook.constructor.name}.${hookName}`,
324
+ error instanceof Error ? error : new Error(String(error)),
325
+ {
326
+ itemId: currentId,
327
+ userId: currentUserId,
328
+ },
329
+ );
330
+ throw error;
331
+ }
332
+ }
333
+ }
334
+
335
+ return {
336
+ proceed: true,
337
+ id: currentId,
338
+ userId: currentUserId,
339
+ context: currentContext,
340
+ };
341
+ }
342
+
217
343
  /**
218
344
  * Internal method to run post-hooks.
219
345
  * Post-hooks run in reverse priority order.
220
346
  */
221
347
  private async runPostHooks(
222
- hookName: keyof IDismissibleLifecycleHook<TMetadata>,
348
+ hookName: keyof IDismissibleLifecycleHook,
223
349
  itemId: string,
224
- item: DismissibleItemDto<TMetadata>,
350
+ item: DismissibleItemDto,
225
351
  userId: string,
226
352
  context?: IRequestContext,
227
353
  ): Promise<void> {
@@ -232,7 +358,7 @@ export class HookRunner<TMetadata extends BaseMetadata = BaseMetadata> {
232
358
  const hookFn = hook[hookName] as
233
359
  | ((
234
360
  itemId: string,
235
- item: DismissibleItemDto<TMetadata>,
361
+ item: DismissibleItemDto,
236
362
  userId: string,
237
363
  context?: IRequestContext,
238
364
  ) => Promise<void> | void)
package/src/core/index.ts CHANGED
@@ -3,4 +3,3 @@ export * from './service-responses.interface';
3
3
  export * from './dismissible-core.service';
4
4
  export * from './hook-runner.service';
5
5
  export * from './dismissible.service';
6
- export * from './create-options';
@@ -1,4 +1,4 @@
1
- import { BaseMetadata, DismissibleItemDto } from '@dismissible/nestjs-dismissible-item';
1
+ import { DismissibleItemDto } from '@dismissible/nestjs-dismissible-item';
2
2
  import { IRequestContext } from '../request/request-context.interface';
3
3
 
4
4
  /**
@@ -37,34 +37,72 @@ export interface IHookResult {
37
37
  /**
38
38
  * Interface for lifecycle hooks that can intercept dismissible operations.
39
39
  */
40
- export interface IDismissibleLifecycleHook<TMetadata extends BaseMetadata = BaseMetadata> {
40
+ export interface IDismissibleLifecycleHook {
41
41
  /**
42
42
  * Priority for hook execution (lower numbers run first).
43
43
  * Default is 0.
44
44
  */
45
45
  readonly priority?: number;
46
46
 
47
+ // ─────────────────────────────────────────────────────────────────
48
+ // Global Request Hooks (run on ALL operations)
49
+ // ─────────────────────────────────────────────────────────────────
50
+
47
51
  /**
48
- * Called before getOrCreate operation.
52
+ * Called at the start of any operation (getOrCreate, dismiss, restore).
53
+ * Use for global concerns like authentication, rate limiting, request validation.
49
54
  */
50
- onBeforeGetOrCreate?(
55
+ onBeforeRequest?(
51
56
  itemId: string,
52
57
  userId: string,
53
58
  context?: IRequestContext,
54
59
  ): Promise<IHookResult> | IHookResult;
55
60
 
56
61
  /**
57
- * Called after getOrCreate operation.
62
+ * Called at the end of any operation (getOrCreate, dismiss, restore).
63
+ * Use for global concerns like audit logging, metrics, cleanup.
58
64
  */
59
- onAfterGetOrCreate?(
65
+ onAfterRequest?(
60
66
  itemId: string,
61
- item: DismissibleItemDto<TMetadata>,
67
+ item: DismissibleItemDto,
62
68
  userId: string,
63
69
  context?: IRequestContext,
64
70
  ): Promise<void> | void;
65
71
 
72
+ // ─────────────────────────────────────────────────────────────────
73
+ // Get Hooks (when retrieving existing item)
74
+ // ─────────────────────────────────────────────────────────────────
75
+
76
+ /**
77
+ * Called before returning an existing item.
78
+ * Only called when item exists in storage.
79
+ * Use for access control based on item state (e.g., block dismissed items).
80
+ */
81
+ onBeforeGet?(
82
+ itemId: string,
83
+ item: DismissibleItemDto,
84
+ userId: string,
85
+ context?: IRequestContext,
86
+ ): Promise<IHookResult> | IHookResult;
87
+
88
+ /**
89
+ * Called after returning an existing item.
90
+ * Only called when item exists in storage.
91
+ */
92
+ onAfterGet?(
93
+ itemId: string,
94
+ item: DismissibleItemDto,
95
+ userId: string,
96
+ context?: IRequestContext,
97
+ ): Promise<void> | void;
98
+
99
+ // ─────────────────────────────────────────────────────────────────
100
+ // Create Hooks (when creating new item)
101
+ // ─────────────────────────────────────────────────────────────────
102
+
66
103
  /**
67
104
  * Called before creating a new item.
105
+ * Use for plan limits, quota checks, etc.
68
106
  */
69
107
  onBeforeCreate?(
70
108
  itemId: string,
@@ -77,11 +115,15 @@ export interface IDismissibleLifecycleHook<TMetadata extends BaseMetadata = Base
77
115
  */
78
116
  onAfterCreate?(
79
117
  itemId: string,
80
- item: DismissibleItemDto<TMetadata>,
118
+ item: DismissibleItemDto,
81
119
  userId: string,
82
120
  context?: IRequestContext,
83
121
  ): Promise<void> | void;
84
122
 
123
+ // ─────────────────────────────────────────────────────────────────
124
+ // Dismiss Hooks
125
+ // ─────────────────────────────────────────────────────────────────
126
+
85
127
  /**
86
128
  * Called before dismissing an item.
87
129
  */
@@ -96,11 +138,15 @@ export interface IDismissibleLifecycleHook<TMetadata extends BaseMetadata = Base
96
138
  */
97
139
  onAfterDismiss?(
98
140
  itemId: string,
99
- item: DismissibleItemDto<TMetadata>,
141
+ item: DismissibleItemDto,
100
142
  userId: string,
101
143
  context?: IRequestContext,
102
144
  ): Promise<void> | void;
103
145
 
146
+ // ─────────────────────────────────────────────────────────────────
147
+ // Restore Hooks
148
+ // ─────────────────────────────────────────────────────────────────
149
+
104
150
  /**
105
151
  * Called before restoring an item.
106
152
  */
@@ -115,7 +161,7 @@ export interface IDismissibleLifecycleHook<TMetadata extends BaseMetadata = Base
115
161
  */
116
162
  onAfterRestore?(
117
163
  itemId: string,
118
- item: DismissibleItemDto<TMetadata>,
164
+ item: DismissibleItemDto,
119
165
  userId: string,
120
166
  context?: IRequestContext,
121
167
  ): Promise<void> | void;
@@ -1,11 +1,11 @@
1
- import { BaseMetadata, DismissibleItemDto } from '@dismissible/nestjs-dismissible-item';
1
+ import { DismissibleItemDto } from '@dismissible/nestjs-dismissible-item';
2
2
 
3
3
  /**
4
4
  * Response from getOrCreate operation.
5
5
  */
6
- export interface IGetOrCreateServiceResponse<TMetadata extends BaseMetadata = BaseMetadata> {
6
+ export interface IGetOrCreateServiceResponse {
7
7
  /** The item (either retrieved or created) */
8
- item: DismissibleItemDto<TMetadata>;
8
+ item: DismissibleItemDto;
9
9
 
10
10
  /** Whether the item was newly created */
11
11
  created: boolean;
@@ -14,21 +14,21 @@ export interface IGetOrCreateServiceResponse<TMetadata extends BaseMetadata = Ba
14
14
  /**
15
15
  * Response from dismiss operation.
16
16
  */
17
- export interface IDismissServiceResponse<TMetadata extends BaseMetadata = BaseMetadata> {
17
+ export interface IDismissServiceResponse {
18
18
  /** The dismissed item */
19
- item: DismissibleItemDto<TMetadata>;
19
+ item: DismissibleItemDto;
20
20
 
21
21
  /** The item state before dismissal */
22
- previousItem: DismissibleItemDto<TMetadata>;
22
+ previousItem: DismissibleItemDto;
23
23
  }
24
24
 
25
25
  /**
26
26
  * Response from restore operation.
27
27
  */
28
- export interface IRestoreServiceResponse<TMetadata extends BaseMetadata = BaseMetadata> {
28
+ export interface IRestoreServiceResponse {
29
29
  /** The restored item */
30
- item: DismissibleItemDto<TMetadata>;
30
+ item: DismissibleItemDto;
31
31
 
32
32
  /** The item state before restoration */
33
- previousItem: DismissibleItemDto<TMetadata>;
33
+ previousItem: DismissibleItemDto;
34
34
  }