@dismissible/nestjs-dismissible 0.0.2-canary.738340d.0 → 0.0.2-canary.b0d8bfe.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +58 -74
  2. package/jest.config.ts +1 -1
  3. package/package.json +12 -12
  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
@@ -3,10 +3,10 @@ import {
3
3
  DISMISSIBLE_HOOKS,
4
4
  IDismissibleLifecycleHook,
5
5
  IHookResult,
6
- } from './lifecycle-hook.interface';
6
+ } from '@dismissible/nestjs-dismissible-hooks';
7
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';
8
+ import { DismissibleItemDto } from '@dismissible/nestjs-dismissible-item';
9
+ import { IRequestContext } from '@dismissible/nestjs-dismissible-request';
10
10
 
11
11
  /**
12
12
  * Result from running pre-hooks.
@@ -32,41 +32,67 @@ 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
  ) {
45
- // Sort hooks by priority (lower numbers first)
46
45
  this.sortedHooks = [...hooks].sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
47
46
  }
48
47
 
49
48
  /**
50
- * Run pre-getOrCreate hooks.
49
+ * Run pre-request hooks (global - runs at start of any operation).
50
+ * Use for authentication, rate limiting, request validation.
51
51
  */
52
- async runPreGetOrCreate(
52
+ async runPreRequest(
53
53
  itemId: string,
54
54
  userId: string,
55
55
  context?: IRequestContext,
56
56
  ): Promise<IHookRunResult> {
57
- return this.runPreHooks('onBeforeGetOrCreate', itemId, userId, context);
57
+ return this.runPreHooks('onBeforeRequest', itemId, userId, context);
58
58
  }
59
59
 
60
60
  /**
61
- * Run post-getOrCreate hooks.
61
+ * Run post-request hooks (global - runs at end of any operation).
62
+ * Use for audit logging, metrics, cleanup.
62
63
  */
63
- async runPostGetOrCreate(
64
+ async runPostRequest(
64
65
  itemId: string,
65
- item: DismissibleItemDto<TMetadata>,
66
+ item: DismissibleItemDto,
66
67
  userId: string,
67
68
  context?: IRequestContext,
68
69
  ): Promise<void> {
69
- await this.runPostHooks('onAfterGetOrCreate', itemId, item, userId, context);
70
+ await this.runPostHooks('onAfterRequest', itemId, item, userId, context);
71
+ }
72
+
73
+ /**
74
+ * Run pre-get hooks (when item exists and is about to be returned).
75
+ * Receives the item for access control based on item state.
76
+ */
77
+ async runPreGet(
78
+ itemId: string,
79
+ item: DismissibleItemDto,
80
+ userId: string,
81
+ context?: IRequestContext,
82
+ ): Promise<IHookRunResult> {
83
+ return this.runPreHooksWithItem('onBeforeGet', itemId, item, userId, context);
84
+ }
85
+
86
+ /**
87
+ * Run post-get hooks (after item is returned).
88
+ */
89
+ async runPostGet(
90
+ itemId: string,
91
+ item: DismissibleItemDto,
92
+ userId: string,
93
+ context?: IRequestContext,
94
+ ): Promise<void> {
95
+ await this.runPostHooks('onAfterGet', itemId, item, userId, context);
70
96
  }
71
97
 
72
98
  /**
@@ -85,7 +111,7 @@ export class HookRunner<TMetadata extends BaseMetadata = BaseMetadata> {
85
111
  */
86
112
  async runPostCreate(
87
113
  itemId: string,
88
- item: DismissibleItemDto<TMetadata>,
114
+ item: DismissibleItemDto,
89
115
  userId: string,
90
116
  context?: IRequestContext,
91
117
  ): Promise<void> {
@@ -108,7 +134,7 @@ export class HookRunner<TMetadata extends BaseMetadata = BaseMetadata> {
108
134
  */
109
135
  async runPostDismiss(
110
136
  itemId: string,
111
- item: DismissibleItemDto<TMetadata>,
137
+ item: DismissibleItemDto,
112
138
  userId: string,
113
139
  context?: IRequestContext,
114
140
  ): Promise<void> {
@@ -131,7 +157,7 @@ export class HookRunner<TMetadata extends BaseMetadata = BaseMetadata> {
131
157
  */
132
158
  async runPostRestore(
133
159
  itemId: string,
134
- item: DismissibleItemDto<TMetadata>,
160
+ item: DismissibleItemDto,
135
161
  userId: string,
136
162
  context?: IRequestContext,
137
163
  ): Promise<void> {
@@ -142,7 +168,7 @@ export class HookRunner<TMetadata extends BaseMetadata = BaseMetadata> {
142
168
  * Internal method to run pre-hooks.
143
169
  */
144
170
  private async runPreHooks(
145
- hookName: keyof IDismissibleLifecycleHook<TMetadata>,
171
+ hookName: keyof IDismissibleLifecycleHook,
146
172
  itemId: string,
147
173
  userId: string,
148
174
  context?: IRequestContext,
@@ -180,7 +206,84 @@ export class HookRunner<TMetadata extends BaseMetadata = BaseMetadata> {
180
206
  };
181
207
  }
182
208
 
183
- // Apply mutations if present
209
+ if (result.mutations) {
210
+ if (result.mutations.id !== undefined) {
211
+ currentId = result.mutations.id;
212
+ }
213
+ if (result.mutations.userId !== undefined) {
214
+ currentUserId = result.mutations.userId;
215
+ }
216
+ if (result.mutations.context && currentContext) {
217
+ currentContext = { ...currentContext, ...result.mutations.context };
218
+ }
219
+ }
220
+ } catch (error) {
221
+ this.logger.error(
222
+ `Error in hook ${hook.constructor.name}.${hookName}`,
223
+ error instanceof Error ? error : new Error(String(error)),
224
+ {
225
+ itemId: currentId,
226
+ userId: currentUserId,
227
+ },
228
+ );
229
+ throw error;
230
+ }
231
+ }
232
+ }
233
+
234
+ return {
235
+ proceed: true,
236
+ id: currentId,
237
+ userId: currentUserId,
238
+ context: currentContext,
239
+ };
240
+ }
241
+
242
+ /**
243
+ * Internal method to run pre-hooks that receive the item (e.g., onBeforeGet).
244
+ * Unlike standard pre-hooks, these receive the item for inspection/access control.
245
+ */
246
+ private async runPreHooksWithItem(
247
+ hookName: keyof IDismissibleLifecycleHook,
248
+ itemId: string,
249
+ item: DismissibleItemDto,
250
+ userId: string,
251
+ context?: IRequestContext,
252
+ ): Promise<IHookRunResult> {
253
+ let currentId = itemId;
254
+ let currentUserId = userId;
255
+ let currentContext = context ? { ...context } : undefined;
256
+
257
+ for (const hook of this.sortedHooks) {
258
+ const hookFn = hook[hookName] as
259
+ | ((
260
+ itemId: string,
261
+ item: DismissibleItemDto,
262
+ userId: string,
263
+ context?: IRequestContext,
264
+ ) => Promise<IHookResult> | IHookResult)
265
+ | undefined;
266
+
267
+ if (hookFn) {
268
+ try {
269
+ const result = await hookFn.call(hook, currentId, item, currentUserId, currentContext);
270
+
271
+ if (!result.proceed) {
272
+ this.logger.debug(`Hook ${hook.constructor.name}.${hookName} blocked operation`, {
273
+ itemId: currentId,
274
+ userId: currentUserId,
275
+ reason: result.reason,
276
+ });
277
+
278
+ return {
279
+ proceed: false,
280
+ id: currentId,
281
+ userId: currentUserId,
282
+ context: currentContext,
283
+ reason: result.reason,
284
+ };
285
+ }
286
+
184
287
  if (result.mutations) {
185
288
  if (result.mutations.id !== undefined) {
186
289
  currentId = result.mutations.id;
@@ -219,20 +322,19 @@ export class HookRunner<TMetadata extends BaseMetadata = BaseMetadata> {
219
322
  * Post-hooks run in reverse priority order.
220
323
  */
221
324
  private async runPostHooks(
222
- hookName: keyof IDismissibleLifecycleHook<TMetadata>,
325
+ hookName: keyof IDismissibleLifecycleHook,
223
326
  itemId: string,
224
- item: DismissibleItemDto<TMetadata>,
327
+ item: DismissibleItemDto,
225
328
  userId: string,
226
329
  context?: IRequestContext,
227
330
  ): Promise<void> {
228
- // Run in reverse order for post-hooks
229
331
  const reversedHooks = [...this.sortedHooks].reverse();
230
332
 
231
333
  for (const hook of reversedHooks) {
232
334
  const hookFn = hook[hookName] as
233
335
  | ((
234
336
  itemId: string,
235
- item: DismissibleItemDto<TMetadata>,
337
+ item: DismissibleItemDto,
236
338
  userId: string,
237
339
  context?: IRequestContext,
238
340
  ) => Promise<void> | void)
@@ -250,7 +352,6 @@ export class HookRunner<TMetadata extends BaseMetadata = BaseMetadata> {
250
352
  userId,
251
353
  },
252
354
  );
253
- // Post-hooks errors are logged but don't block the operation
254
355
  }
255
356
  }
256
357
  }
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,122 +1,7 @@
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
- }
1
+ // Re-export from hooks library for backward compatibility
2
+ export {
3
+ DISMISSIBLE_HOOKS,
4
+ IHookMutations,
5
+ IHookResult,
6
+ IDismissibleLifecycleHook,
7
+ } from '@dismissible/nestjs-dismissible-hooks';
@@ -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
  }