@dismissible/nestjs-dismissible 0.0.2-canary.585db17.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 (66) hide show
  1. package/README.md +490 -0
  2. package/jest.config.ts +29 -0
  3. package/package.json +62 -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 +6 -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 +62 -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 +59 -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 +62 -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 +745 -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 +7 -0
  35. package/src/core/service-responses.interface.ts +34 -0
  36. package/src/dismissible.module.integration.spec.ts +704 -0
  37. package/src/dismissible.module.ts +82 -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 +8 -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.spec.ts +179 -0
  50. package/src/response/http-exception-filter.ts +21 -0
  51. package/src/response/index.ts +4 -0
  52. package/src/response/response.module.ts +9 -0
  53. package/src/response/response.service.spec.ts +72 -0
  54. package/src/response/response.service.ts +20 -0
  55. package/src/testing/factories.ts +60 -0
  56. package/src/testing/index.ts +1 -0
  57. package/src/utils/date/date.service.spec.ts +104 -0
  58. package/src/utils/date/date.service.ts +19 -0
  59. package/src/utils/date/index.ts +1 -0
  60. package/src/utils/dismissible.helper.ts +9 -0
  61. package/src/utils/index.ts +3 -0
  62. package/src/validation/dismissible-input.dto.ts +47 -0
  63. package/src/validation/index.ts +1 -0
  64. package/tsconfig.json +16 -0
  65. package/tsconfig.lib.json +14 -0
  66. 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 '@dismissible/nestjs-dismissible-hooks';
7
+ import { DISMISSIBLE_LOGGER, IDismissibleLogger } from '@dismissible/nestjs-logger';
8
+ import { DismissibleItemDto } from '@dismissible/nestjs-dismissible-item';
9
+ import { IRequestContext } from '@dismissible/nestjs-dismissible-request';
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,7 @@
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';
@@ -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
+ }