@dismissible/nestjs-dismissible 0.0.2-canary.6b97aa7.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 (69) hide show
  1. package/README.md +490 -0
  2. package/jest.config.ts +29 -0
  3. package/package.json +66 -0
  4. package/project.json +42 -0
  5. package/src/api/dismissible-item-response.dto.ts +30 -0
  6. package/src/api/dismissible-item.mapper.spec.ts +51 -0
  7. package/src/api/dismissible-item.mapper.ts +27 -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 +41 -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 +36 -0
  15. package/src/api/use-cases/get-or-create/get-or-create.controller.ts +60 -0
  16. package/src/api/use-cases/get-or-create/get-or-create.response.dto.ts +7 -0
  17. package/src/api/use-cases/get-or-create/index.ts +2 -0
  18. package/src/api/use-cases/index.ts +3 -0
  19. package/src/api/use-cases/restore/index.ts +2 -0
  20. package/src/api/use-cases/restore/restore.controller.spec.ts +41 -0
  21. package/src/api/use-cases/restore/restore.controller.ts +63 -0
  22. package/src/api/use-cases/restore/restore.response.dto.ts +7 -0
  23. package/src/api/validation/index.ts +2 -0
  24. package/src/api/validation/param-validation.pipe.spec.ts +313 -0
  25. package/src/api/validation/param-validation.pipe.ts +38 -0
  26. package/src/api/validation/param.decorators.ts +32 -0
  27. package/src/core/dismissible-core.service.spec.ts +403 -0
  28. package/src/core/dismissible-core.service.ts +173 -0
  29. package/src/core/dismissible.service.spec.ts +226 -0
  30. package/src/core/dismissible.service.ts +227 -0
  31. package/src/core/hook-runner.service.spec.ts +736 -0
  32. package/src/core/hook-runner.service.ts +368 -0
  33. package/src/core/index.ts +5 -0
  34. package/src/core/lifecycle-hook.interface.ts +148 -0
  35. package/src/core/service-responses.interface.ts +34 -0
  36. package/src/dismissible.module.integration.spec.ts +687 -0
  37. package/src/dismissible.module.ts +79 -0
  38. package/src/events/dismissible.events.ts +82 -0
  39. package/src/events/events.constants.ts +21 -0
  40. package/src/events/index.ts +2 -0
  41. package/src/exceptions/dismissible.exceptions.spec.ts +50 -0
  42. package/src/exceptions/dismissible.exceptions.ts +69 -0
  43. package/src/exceptions/index.ts +1 -0
  44. package/src/index.ts +9 -0
  45. package/src/request/index.ts +2 -0
  46. package/src/request/request-context.decorator.ts +15 -0
  47. package/src/request/request-context.interface.ts +12 -0
  48. package/src/response/dtos/base-response.dto.ts +11 -0
  49. package/src/response/dtos/error-response.dto.ts +36 -0
  50. package/src/response/dtos/index.ts +3 -0
  51. package/src/response/dtos/success-response.dto.ts +34 -0
  52. package/src/response/http-exception-filter.spec.ts +179 -0
  53. package/src/response/http-exception-filter.ts +21 -0
  54. package/src/response/index.ts +4 -0
  55. package/src/response/response.module.ts +9 -0
  56. package/src/response/response.service.spec.ts +72 -0
  57. package/src/response/response.service.ts +20 -0
  58. package/src/testing/factories.ts +42 -0
  59. package/src/testing/index.ts +1 -0
  60. package/src/utils/date/date.service.spec.ts +104 -0
  61. package/src/utils/date/date.service.ts +19 -0
  62. package/src/utils/date/index.ts +1 -0
  63. package/src/utils/dismissible.helper.ts +9 -0
  64. package/src/utils/index.ts +3 -0
  65. package/src/validation/dismissible-input.dto.ts +47 -0
  66. package/src/validation/index.ts +1 -0
  67. package/tsconfig.json +16 -0
  68. package/tsconfig.lib.json +14 -0
  69. package/tsconfig.spec.json +12 -0
@@ -0,0 +1,368 @@
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 { 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 {
36
+ private readonly sortedHooks: IDismissibleLifecycleHook[];
37
+
38
+ constructor(
39
+ @Optional()
40
+ @Inject(DISMISSIBLE_HOOKS)
41
+ hooks: IDismissibleLifecycleHook[] = [],
42
+ @Inject(DISMISSIBLE_LOGGER)
43
+ private readonly logger: IDismissibleLogger,
44
+ ) {
45
+ this.sortedHooks = [...hooks].sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
46
+ }
47
+
48
+ /**
49
+ * Run pre-request hooks (global - runs at start of any operation).
50
+ * Use for authentication, rate limiting, request validation.
51
+ */
52
+ async runPreRequest(
53
+ itemId: string,
54
+ userId: string,
55
+ context?: IRequestContext,
56
+ ): Promise<IHookRunResult> {
57
+ return this.runPreHooks('onBeforeRequest', itemId, userId, context);
58
+ }
59
+
60
+ /**
61
+ * Run post-request hooks (global - runs at end of any operation).
62
+ * Use for audit logging, metrics, cleanup.
63
+ */
64
+ async runPostRequest(
65
+ itemId: string,
66
+ item: DismissibleItemDto,
67
+ userId: string,
68
+ context?: IRequestContext,
69
+ ): Promise<void> {
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);
96
+ }
97
+
98
+ /**
99
+ * Run pre-create hooks.
100
+ */
101
+ async runPreCreate(
102
+ itemId: string,
103
+ userId: string,
104
+ context?: IRequestContext,
105
+ ): Promise<IHookRunResult> {
106
+ return this.runPreHooks('onBeforeCreate', itemId, userId, context);
107
+ }
108
+
109
+ /**
110
+ * Run post-create hooks.
111
+ */
112
+ async runPostCreate(
113
+ itemId: string,
114
+ item: DismissibleItemDto,
115
+ userId: string,
116
+ context?: IRequestContext,
117
+ ): Promise<void> {
118
+ await this.runPostHooks('onAfterCreate', itemId, item, userId, context);
119
+ }
120
+
121
+ /**
122
+ * Run pre-dismiss hooks.
123
+ */
124
+ async runPreDismiss(
125
+ itemId: string,
126
+ userId: string,
127
+ context?: IRequestContext,
128
+ ): Promise<IHookRunResult> {
129
+ return this.runPreHooks('onBeforeDismiss', itemId, userId, context);
130
+ }
131
+
132
+ /**
133
+ * Run post-dismiss hooks.
134
+ */
135
+ async runPostDismiss(
136
+ itemId: string,
137
+ item: DismissibleItemDto,
138
+ userId: string,
139
+ context?: IRequestContext,
140
+ ): Promise<void> {
141
+ await this.runPostHooks('onAfterDismiss', itemId, item, userId, context);
142
+ }
143
+
144
+ /**
145
+ * Run pre-restore hooks.
146
+ */
147
+ async runPreRestore(
148
+ itemId: string,
149
+ userId: string,
150
+ context?: IRequestContext,
151
+ ): Promise<IHookRunResult> {
152
+ return this.runPreHooks('onBeforeRestore', itemId, userId, context);
153
+ }
154
+
155
+ /**
156
+ * Run post-restore hooks.
157
+ */
158
+ async runPostRestore(
159
+ itemId: string,
160
+ item: DismissibleItemDto,
161
+ userId: string,
162
+ context?: IRequestContext,
163
+ ): Promise<void> {
164
+ await this.runPostHooks('onAfterRestore', itemId, item, userId, context);
165
+ }
166
+
167
+ /**
168
+ * Internal method to run pre-hooks.
169
+ */
170
+ private async runPreHooks(
171
+ hookName: keyof IDismissibleLifecycleHook,
172
+ itemId: string,
173
+ userId: string,
174
+ context?: IRequestContext,
175
+ ): Promise<IHookRunResult> {
176
+ let currentId = itemId;
177
+ let currentUserId = userId;
178
+ let currentContext = context ? { ...context } : undefined;
179
+
180
+ for (const hook of this.sortedHooks) {
181
+ const hookFn = hook[hookName] as
182
+ | ((
183
+ itemId: string,
184
+ userId: string,
185
+ context?: IRequestContext,
186
+ ) => Promise<IHookResult> | IHookResult)
187
+ | undefined;
188
+
189
+ if (hookFn) {
190
+ try {
191
+ const result = await hookFn.call(hook, currentId, currentUserId, currentContext);
192
+
193
+ if (!result.proceed) {
194
+ this.logger.debug(`Hook ${hook.constructor.name}.${hookName} blocked operation`, {
195
+ itemId: currentId,
196
+ userId: currentUserId,
197
+ reason: result.reason,
198
+ });
199
+
200
+ return {
201
+ proceed: false,
202
+ id: currentId,
203
+ userId: currentUserId,
204
+ context: currentContext,
205
+ reason: result.reason,
206
+ };
207
+ }
208
+
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
+
287
+ if (result.mutations) {
288
+ if (result.mutations.id !== undefined) {
289
+ currentId = result.mutations.id;
290
+ }
291
+ if (result.mutations.userId !== undefined) {
292
+ currentUserId = result.mutations.userId;
293
+ }
294
+ if (result.mutations.context && currentContext) {
295
+ currentContext = { ...currentContext, ...result.mutations.context };
296
+ }
297
+ }
298
+ } catch (error) {
299
+ this.logger.error(
300
+ `Error in hook ${hook.constructor.name}.${hookName}`,
301
+ error instanceof Error ? error : new Error(String(error)),
302
+ {
303
+ itemId: currentId,
304
+ userId: currentUserId,
305
+ },
306
+ );
307
+ throw error;
308
+ }
309
+ }
310
+ }
311
+
312
+ return {
313
+ proceed: true,
314
+ id: currentId,
315
+ userId: currentUserId,
316
+ context: currentContext,
317
+ };
318
+ }
319
+
320
+ /**
321
+ * Internal method to run post-hooks.
322
+ * Post-hooks run in reverse priority order.
323
+ */
324
+ private async runPostHooks(
325
+ hookName: keyof IDismissibleLifecycleHook,
326
+ itemId: string,
327
+ item: DismissibleItemDto,
328
+ userId: string,
329
+ context?: IRequestContext,
330
+ ): Promise<void> {
331
+ const reversedHooks = [...this.sortedHooks].reverse();
332
+
333
+ for (const hook of reversedHooks) {
334
+ const hookFn = hook[hookName] as
335
+ | ((
336
+ itemId: string,
337
+ item: DismissibleItemDto,
338
+ userId: string,
339
+ context?: IRequestContext,
340
+ ) => Promise<void> | void)
341
+ | undefined;
342
+
343
+ if (hookFn) {
344
+ try {
345
+ await hookFn.call(hook, itemId, item, userId, context);
346
+ } catch (error) {
347
+ this.logger.error(
348
+ `Error in hook ${hook.constructor.name}.${hookName}`,
349
+ error instanceof Error ? error : new Error(String(error)),
350
+ {
351
+ itemId,
352
+ userId,
353
+ },
354
+ );
355
+ }
356
+ }
357
+ }
358
+ }
359
+
360
+ /**
361
+ * Throw ForbiddenException if the hook result indicates the operation was blocked.
362
+ */
363
+ static throwIfBlocked(result: IHookRunResult): void {
364
+ if (!result.proceed) {
365
+ throw new ForbiddenException(result.reason ?? 'Operation blocked by lifecycle hook');
366
+ }
367
+ }
368
+ }
@@ -0,0 +1,5 @@
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';
@@ -0,0 +1,148 @@
1
+ import { 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 {
41
+ /**
42
+ * Priority for hook execution (lower numbers run first).
43
+ * Default is 0.
44
+ */
45
+ readonly priority?: number;
46
+
47
+ /**
48
+ * Called at the start of any operation (getOrCreate, dismiss, restore).
49
+ * Use for global concerns like authentication, rate limiting, request validation.
50
+ */
51
+ onBeforeRequest?(
52
+ itemId: string,
53
+ userId: string,
54
+ context?: IRequestContext,
55
+ ): Promise<IHookResult> | IHookResult;
56
+
57
+ /**
58
+ * Called at the end of any operation (getOrCreate, dismiss, restore).
59
+ * Use for global concerns like audit logging, metrics, cleanup.
60
+ */
61
+ onAfterRequest?(
62
+ itemId: string,
63
+ item: DismissibleItemDto,
64
+ userId: string,
65
+ context?: IRequestContext,
66
+ ): Promise<void> | void;
67
+
68
+ /**
69
+ * Called before returning an existing item.
70
+ * Only called when item exists in storage.
71
+ * Use for access control based on item state (e.g., block dismissed items).
72
+ */
73
+ onBeforeGet?(
74
+ itemId: string,
75
+ item: DismissibleItemDto,
76
+ userId: string,
77
+ context?: IRequestContext,
78
+ ): Promise<IHookResult> | IHookResult;
79
+
80
+ /**
81
+ * Called after returning an existing item.
82
+ * Only called when item exists in storage.
83
+ */
84
+ onAfterGet?(
85
+ itemId: string,
86
+ item: DismissibleItemDto,
87
+ userId: string,
88
+ context?: IRequestContext,
89
+ ): Promise<void> | void;
90
+
91
+ /**
92
+ * Called before creating a new item.
93
+ * Use for plan limits, quota checks, etc.
94
+ */
95
+ onBeforeCreate?(
96
+ itemId: string,
97
+ userId: string,
98
+ context?: IRequestContext,
99
+ ): Promise<IHookResult> | IHookResult;
100
+
101
+ /**
102
+ * Called after creating a new item.
103
+ */
104
+ onAfterCreate?(
105
+ itemId: string,
106
+ item: DismissibleItemDto,
107
+ userId: string,
108
+ context?: IRequestContext,
109
+ ): Promise<void> | void;
110
+
111
+ /**
112
+ * Called before dismissing an item.
113
+ */
114
+ onBeforeDismiss?(
115
+ itemId: string,
116
+ userId: string,
117
+ context?: IRequestContext,
118
+ ): Promise<IHookResult> | IHookResult;
119
+
120
+ /**
121
+ * Called after dismissing an item.
122
+ */
123
+ onAfterDismiss?(
124
+ itemId: string,
125
+ item: DismissibleItemDto,
126
+ userId: string,
127
+ context?: IRequestContext,
128
+ ): Promise<void> | void;
129
+
130
+ /**
131
+ * Called before restoring an item.
132
+ */
133
+ onBeforeRestore?(
134
+ itemId: string,
135
+ userId: string,
136
+ context?: IRequestContext,
137
+ ): Promise<IHookResult> | IHookResult;
138
+
139
+ /**
140
+ * Called after restoring an item.
141
+ */
142
+ onAfterRestore?(
143
+ itemId: string,
144
+ item: DismissibleItemDto,
145
+ userId: string,
146
+ context?: IRequestContext,
147
+ ): Promise<void> | void;
148
+ }
@@ -0,0 +1,34 @@
1
+ import { DismissibleItemDto } from '@dismissible/nestjs-dismissible-item';
2
+
3
+ /**
4
+ * Response from getOrCreate operation.
5
+ */
6
+ export interface IGetOrCreateServiceResponse {
7
+ /** The item (either retrieved or created) */
8
+ item: DismissibleItemDto;
9
+
10
+ /** Whether the item was newly created */
11
+ created: boolean;
12
+ }
13
+
14
+ /**
15
+ * Response from dismiss operation.
16
+ */
17
+ export interface IDismissServiceResponse {
18
+ /** The dismissed item */
19
+ item: DismissibleItemDto;
20
+
21
+ /** The item state before dismissal */
22
+ previousItem: DismissibleItemDto;
23
+ }
24
+
25
+ /**
26
+ * Response from restore operation.
27
+ */
28
+ export interface IRestoreServiceResponse {
29
+ /** The restored item */
30
+ item: DismissibleItemDto;
31
+
32
+ /** The item state before restoration */
33
+ previousItem: DismissibleItemDto;
34
+ }