@dismissible/nestjs-core 1.0.3-alpha.064e57a.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 (179) hide show
  1. package/README.md +504 -0
  2. package/package.json +69 -0
  3. package/src/api/dismissible-item-response.dto.d.ts +9 -0
  4. package/src/api/dismissible-item-response.dto.js +40 -0
  5. package/src/api/dismissible-item-response.dto.js.map +1 -0
  6. package/src/api/dismissible-item.mapper.d.ts +12 -0
  7. package/src/api/dismissible-item.mapper.js +30 -0
  8. package/src/api/dismissible-item.mapper.js.map +1 -0
  9. package/src/api/dismissible-item.mapper.spec.d.ts +1 -0
  10. package/src/api/dismissible-item.mapper.spec.js +43 -0
  11. package/src/api/dismissible-item.mapper.spec.js.map +1 -0
  12. package/src/api/index.d.ts +4 -0
  13. package/src/api/index.js +8 -0
  14. package/src/api/index.js.map +1 -0
  15. package/src/api/use-cases/api-tags.constants.d.ts +4 -0
  16. package/src/api/use-cases/api-tags.constants.js +8 -0
  17. package/src/api/use-cases/api-tags.constants.js.map +1 -0
  18. package/src/api/use-cases/dismiss/dismiss.controller.d.ts +15 -0
  19. package/src/api/use-cases/dismiss/dismiss.controller.js +74 -0
  20. package/src/api/use-cases/dismiss/dismiss.controller.js.map +1 -0
  21. package/src/api/use-cases/dismiss/dismiss.controller.spec.d.ts +1 -0
  22. package/src/api/use-cases/dismiss/dismiss.controller.spec.js +37 -0
  23. package/src/api/use-cases/dismiss/dismiss.controller.spec.js.map +1 -0
  24. package/src/api/use-cases/dismiss/dismiss.response.dto.d.ts +12 -0
  25. package/src/api/use-cases/dismiss/dismiss.response.dto.js +12 -0
  26. package/src/api/use-cases/dismiss/dismiss.response.dto.js.map +1 -0
  27. package/src/api/use-cases/dismiss/index.d.ts +2 -0
  28. package/src/api/use-cases/dismiss/index.js +6 -0
  29. package/src/api/use-cases/dismiss/index.js.map +1 -0
  30. package/src/api/use-cases/get-or-create/get-or-create.controller.d.ts +15 -0
  31. package/src/api/use-cases/get-or-create/get-or-create.controller.js +70 -0
  32. package/src/api/use-cases/get-or-create/get-or-create.controller.js.map +1 -0
  33. package/src/api/use-cases/get-or-create/get-or-create.controller.spec.d.ts +1 -0
  34. package/src/api/use-cases/get-or-create/get-or-create.controller.spec.js +32 -0
  35. package/src/api/use-cases/get-or-create/get-or-create.controller.spec.js.map +1 -0
  36. package/src/api/use-cases/get-or-create/get-or-create.response.dto.d.ts +12 -0
  37. package/src/api/use-cases/get-or-create/get-or-create.response.dto.js +12 -0
  38. package/src/api/use-cases/get-or-create/get-or-create.response.dto.js.map +1 -0
  39. package/src/api/use-cases/get-or-create/index.d.ts +2 -0
  40. package/src/api/use-cases/get-or-create/index.js +6 -0
  41. package/src/api/use-cases/get-or-create/index.js.map +1 -0
  42. package/src/api/use-cases/index.d.ts +3 -0
  43. package/src/api/use-cases/index.js +7 -0
  44. package/src/api/use-cases/index.js.map +1 -0
  45. package/src/api/use-cases/restore/index.d.ts +2 -0
  46. package/src/api/use-cases/restore/index.js +6 -0
  47. package/src/api/use-cases/restore/index.js.map +1 -0
  48. package/src/api/use-cases/restore/restore.controller.d.ts +15 -0
  49. package/src/api/use-cases/restore/restore.controller.js +74 -0
  50. package/src/api/use-cases/restore/restore.controller.js.map +1 -0
  51. package/src/api/use-cases/restore/restore.controller.spec.d.ts +1 -0
  52. package/src/api/use-cases/restore/restore.controller.spec.js +37 -0
  53. package/src/api/use-cases/restore/restore.controller.spec.js.map +1 -0
  54. package/src/api/use-cases/restore/restore.response.dto.d.ts +12 -0
  55. package/src/api/use-cases/restore/restore.response.dto.js +12 -0
  56. package/src/api/use-cases/restore/restore.response.dto.js.map +1 -0
  57. package/src/api/validation/index.d.ts +2 -0
  58. package/src/api/validation/index.js +6 -0
  59. package/src/api/validation/index.js.map +1 -0
  60. package/src/api/validation/param-validation.pipe.d.ts +11 -0
  61. package/src/api/validation/param-validation.pipe.js +36 -0
  62. package/src/api/validation/param-validation.pipe.js.map +1 -0
  63. package/src/api/validation/param-validation.pipe.spec.d.ts +1 -0
  64. package/src/api/validation/param-validation.pipe.spec.js +269 -0
  65. package/src/api/validation/param-validation.pipe.spec.js.map +1 -0
  66. package/src/api/validation/param.decorators.d.ts +28 -0
  67. package/src/api/validation/param.decorators.js +36 -0
  68. package/src/api/validation/param.decorators.js.map +1 -0
  69. package/src/core/dismissible-core.service.d.ts +56 -0
  70. package/src/core/dismissible-core.service.js +147 -0
  71. package/src/core/dismissible-core.service.js.map +1 -0
  72. package/src/core/dismissible-core.service.spec.d.ts +1 -0
  73. package/src/core/dismissible-core.service.spec.js +309 -0
  74. package/src/core/dismissible-core.service.spec.js.map +1 -0
  75. package/src/core/dismissible.service.d.ts +45 -0
  76. package/src/core/dismissible.service.js +127 -0
  77. package/src/core/dismissible.service.js.map +1 -0
  78. package/src/core/dismissible.service.spec.d.ts +1 -0
  79. package/src/core/dismissible.service.spec.js +159 -0
  80. package/src/core/dismissible.service.spec.js.map +1 -0
  81. package/src/core/hook-runner.service.d.ts +88 -0
  82. package/src/core/hook-runner.service.js +226 -0
  83. package/src/core/hook-runner.service.js.map +1 -0
  84. package/src/core/hook-runner.service.spec.d.ts +1 -0
  85. package/src/core/hook-runner.service.spec.js +538 -0
  86. package/src/core/hook-runner.service.spec.js.map +1 -0
  87. package/src/core/index.d.ts +5 -0
  88. package/src/core/index.js +9 -0
  89. package/src/core/index.js.map +1 -0
  90. package/src/core/lifecycle-hook.interface.d.ts +1 -0
  91. package/src/core/lifecycle-hook.interface.js +7 -0
  92. package/src/core/lifecycle-hook.interface.js.map +1 -0
  93. package/src/core/service-responses.interface.d.ts +28 -0
  94. package/src/core/service-responses.interface.js +3 -0
  95. package/src/core/service-responses.interface.js.map +1 -0
  96. package/src/dismissible.module.d.ts +13 -0
  97. package/src/dismissible.module.integration.spec.d.ts +1 -0
  98. package/src/dismissible.module.integration.spec.js +529 -0
  99. package/src/dismissible.module.integration.spec.js.map +1 -0
  100. package/src/dismissible.module.js +77 -0
  101. package/src/dismissible.module.js.map +1 -0
  102. package/src/events/dismissible.events.d.ts +45 -0
  103. package/src/events/dismissible.events.js +53 -0
  104. package/src/events/dismissible.events.js.map +1 -0
  105. package/src/events/events.constants.d.ts +17 -0
  106. package/src/events/events.constants.js +17 -0
  107. package/src/events/events.constants.js.map +1 -0
  108. package/src/events/index.d.ts +2 -0
  109. package/src/events/index.js +6 -0
  110. package/src/events/index.js.map +1 -0
  111. package/src/exceptions/dismissible.exceptions.d.ts +26 -0
  112. package/src/exceptions/dismissible.exceptions.js +49 -0
  113. package/src/exceptions/dismissible.exceptions.js.map +1 -0
  114. package/src/exceptions/dismissible.exceptions.spec.d.ts +1 -0
  115. package/src/exceptions/dismissible.exceptions.spec.js +40 -0
  116. package/src/exceptions/dismissible.exceptions.spec.js.map +1 -0
  117. package/src/exceptions/index.d.ts +1 -0
  118. package/src/exceptions/index.js +5 -0
  119. package/src/exceptions/index.js.map +1 -0
  120. package/src/index.d.ts +8 -0
  121. package/src/index.js +12 -0
  122. package/src/index.js.map +1 -0
  123. package/src/response/dtos/base-response.dto.d.ts +6 -0
  124. package/src/response/dtos/base-response.dto.js +18 -0
  125. package/src/response/dtos/base-response.dto.js.map +1 -0
  126. package/src/response/dtos/error-response.dto.d.ts +19 -0
  127. package/src/response/dtos/error-response.dto.js +39 -0
  128. package/src/response/dtos/error-response.dto.js.map +1 -0
  129. package/src/response/dtos/index.d.ts +3 -0
  130. package/src/response/dtos/index.js +7 -0
  131. package/src/response/dtos/index.js.map +1 -0
  132. package/src/response/dtos/success-response.dto.d.ts +28 -0
  133. package/src/response/dtos/success-response.dto.js +34 -0
  134. package/src/response/dtos/success-response.dto.js.map +1 -0
  135. package/src/response/http-exception-filter.d.ts +4 -0
  136. package/src/response/http-exception-filter.js +24 -0
  137. package/src/response/http-exception-filter.js.map +1 -0
  138. package/src/response/http-exception-filter.spec.d.ts +1 -0
  139. package/src/response/http-exception-filter.spec.js +137 -0
  140. package/src/response/http-exception-filter.spec.js.map +1 -0
  141. package/src/response/index.d.ts +4 -0
  142. package/src/response/index.js +8 -0
  143. package/src/response/index.js.map +1 -0
  144. package/src/response/response.module.d.ts +2 -0
  145. package/src/response/response.module.js +17 -0
  146. package/src/response/response.module.js.map +1 -0
  147. package/src/response/response.service.d.ts +6 -0
  148. package/src/response/response.service.js +25 -0
  149. package/src/response/response.service.js.map +1 -0
  150. package/src/response/response.service.spec.d.ts +1 -0
  151. package/src/response/response.service.spec.js +58 -0
  152. package/src/response/response.service.spec.js.map +1 -0
  153. package/src/testing/factories.d.ts +14 -0
  154. package/src/testing/factories.js +58 -0
  155. package/src/testing/factories.js.map +1 -0
  156. package/src/testing/index.d.ts +1 -0
  157. package/src/testing/index.js +5 -0
  158. package/src/testing/index.js.map +1 -0
  159. package/src/utils/date/date.service.d.ts +8 -0
  160. package/src/utils/date/date.service.js +24 -0
  161. package/src/utils/date/date.service.js.map +1 -0
  162. package/src/utils/date/date.service.spec.d.ts +1 -0
  163. package/src/utils/date/date.service.spec.js +83 -0
  164. package/src/utils/date/date.service.spec.js.map +1 -0
  165. package/src/utils/date/index.d.ts +1 -0
  166. package/src/utils/date/index.js +5 -0
  167. package/src/utils/date/index.js.map +1 -0
  168. package/src/utils/dismissible.helper.d.ts +4 -0
  169. package/src/utils/dismissible.helper.js +15 -0
  170. package/src/utils/dismissible.helper.js.map +1 -0
  171. package/src/utils/index.d.ts +3 -0
  172. package/src/utils/index.js +7 -0
  173. package/src/utils/index.js.map +1 -0
  174. package/src/validation/dismissible-input.dto.d.ts +21 -0
  175. package/src/validation/dismissible-input.dto.js +54 -0
  176. package/src/validation/dismissible-input.dto.js.map +1 -0
  177. package/src/validation/index.d.ts +1 -0
  178. package/src/validation/index.js +5 -0
  179. package/src/validation/index.js.map +1 -0
package/README.md ADDED
@@ -0,0 +1,504 @@
1
+ <p align="center">
2
+ <a href="https://dismissible.io" target="_blank"><img src="../../docs/images/dismissible_logo.png" width="240" alt="Dismissible" /></a>
3
+ </p>
4
+
5
+ <p align="center">Never Show The Same Thing Twice!</p>
6
+ <p align="center">
7
+ <a href="https://www.npmjs.com/package/@dismissible/nestjs-core" target="_blank"><img src="https://img.shields.io/npm/v/@dismissible/nestjs-core.svg" alt="NPM Version" /></a>
8
+ <a href="https://github.com/dismissibleio/dismissible-api/blob/main/LICENSE" target="_blank"><img src="https://img.shields.io/npm/l/@dismissible/nestjs-core.svg" alt="Package License" /></a>
9
+ <a href="https://www.npmjs.com/package/@dismissible/nestjs-core" target="_blank"><img src="https://img.shields.io/npm/dm/@dismissible/nestjs-core.svg" alt="NPM Downloads" /></a>
10
+ <a href="https://github.com/dismissibleio/dismissible-api" target="_blank"><img alt="GitHub Actions Workflow Status" src="https://img.shields.io/github/actions/workflow/status/dismissibleio/dismissible-api/release.yml">
11
+ </a>
12
+ <a href="https://paypal.me/joshstuartx" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg"/></a>
13
+ </p>
14
+
15
+ # @dismissible/nestjs-core
16
+
17
+ A powerful NestJS library for managing dismissible state in your applications. Perfect for guided tours, user preferences, onboarding flows, and any scenario where you need to track whether a user has dismissed or interacted with specific items.
18
+
19
+ > **Part of the Dismissible API** - This library is part of the [Dismissible API](https://dismissible.io) ecosystem. Visit [dismissible.io](https://dismissible.io) for more information and documentation.
20
+
21
+ ## Features
22
+
23
+ - **Simple API** - Easy-to-use service methods for get-or-create, dismiss, and restore operations
24
+ - **High Performance** - Built on Fastify for maximum throughput
25
+ - **Flexible Storage** - Default memory storage with support for custom storage adapters (PostgreSQL, Redis, etc.)
26
+ - **Lifecycle Hooks** - Intercept and customize operations with pre/post hooks
27
+ - **JWT Authentication** - Optional JWT auth hook for securing endpoints with OIDC providers
28
+ - **Event-Driven** - Built-in event emission for all operations
29
+ - **Type-Safe** - Full TypeScript support
30
+ - **Validation** - Automatic validation of dismissible items
31
+ - **Swagger Integration** - Auto-generated API documentation
32
+ - **React Client** - Works out of the box with [@dismissible/react-client](https://www.npmjs.com/package/@dismissible/react-client)
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ npm install @dismissible/nestjs-core
38
+ ```
39
+
40
+ ## Getting Started
41
+
42
+ ### Basic Setup
43
+
44
+ The simplest way to get started is with the default configuration, which uses memory storage:
45
+
46
+ ```typescript
47
+ import { Module } from '@nestjs/common';
48
+ import { DismissibleModule } from '@dismissible/nestjs-core';
49
+
50
+ @Module({
51
+ imports: [DismissibleModule.forRoot({})],
52
+ })
53
+ export class AppModule {}
54
+ ```
55
+
56
+ ### Built-in REST API
57
+
58
+ The module automatically registers REST endpoints for all operations:
59
+
60
+ - `GET /v1/users/:userId/items/:itemId` - Get or create an item
61
+ - `DELETE /v1/users/:userId/items/:itemId` - Dismiss an item
62
+ - `POST /v1/users/:userId/items/:itemId` - Restore a dismissed item
63
+
64
+ Example request:
65
+
66
+ ```bash
67
+ # Get or create an item
68
+ curl http://localhost:3000/v1/users/user-123/items/welcome-banner
69
+
70
+ # Dismiss an item
71
+ curl -X DELETE http://localhost:3000/v1/users/user-123/items/welcome-banner
72
+
73
+ # Restore a dismissed item
74
+ curl -X POST http://localhost:3000/v1/users/user-123/items/welcome-banner
75
+ ```
76
+
77
+ ### React Client Integration
78
+
79
+ This library works seamlessly with the [@dismissible/react-client](https://www.npmjs.com/package/@dismissible/react-client) package. Once your NestJS backend is set up with the built-in REST API endpoints, you can use the React client in your frontend:
80
+
81
+ ```bash
82
+ npm install @dismissible/react-client
83
+ ```
84
+
85
+ ```typescript
86
+ import { DismissibleProvider, useDismissible } from '@dismissible/react-client';
87
+
88
+ function App() {
89
+ return (
90
+ <DismissibleProvider
91
+ apiUrl="http://localhost:3000"
92
+ userId="user-123"
93
+ >
94
+ <WelcomeBanner />
95
+ </DismissibleProvider>
96
+ );
97
+ }
98
+
99
+ function WelcomeBanner() {
100
+ const { item, dismiss, isLoading } = useDismissible('welcome-banner');
101
+
102
+ if (isLoading) return <div>Loading...</div>;
103
+ if (item?.dismissedAt) return null;
104
+
105
+ return (
106
+ <div>
107
+ <h2>Welcome!</h2>
108
+ <button onClick={() => dismiss()}>Dismiss</button>
109
+ </div>
110
+ );
111
+ }
112
+ ```
113
+
114
+ The React client automatically uses the built-in REST API endpoints, so no additional configuration is needed on the backend.
115
+
116
+ ## Advanced Usage
117
+
118
+ ### Using PostgreSQL Storage
119
+
120
+ To persist dismissible items in a PostgreSQL database, use the `PostgresStorageModule`:
121
+
122
+ ```typescript
123
+ import { Module } from '@nestjs/common';
124
+ import { DismissibleModule } from '@dismissible/nestjs-core';
125
+ import { PostgresStorageModule } from '@dismissible/nestjs-postgres-storage';
126
+
127
+ @Module({
128
+ imports: [
129
+ DismissibleModule.forRoot({
130
+ storage: PostgresStorageModule,
131
+ }),
132
+ ],
133
+ })
134
+ export class AppModule {}
135
+ ```
136
+
137
+ **Prerequisites:**
138
+
139
+ 1. Install the PostgreSQL storage package:
140
+
141
+ ```bash
142
+ npm install @dismissible/nestjs-postgres-storage
143
+ ```
144
+
145
+ 2. Set up your database connection string:
146
+
147
+ ```env
148
+ DISMISSIBLE_STORAGE_POSTGRES_CONNECTION_STRING=postgresql://user:password@localhost:5432/dismissible
149
+ ```
150
+
151
+ 3. Run Prisma migrations (if using Prisma):
152
+ ```bash
153
+ npx prisma migrate dev
154
+ ```
155
+
156
+ The PostgreSQL adapter uses Prisma and automatically handles schema migrations. The storage persists all dismissible items across application restarts.
157
+
158
+ ### Using the Service
159
+
160
+ Instead of using the built-in REST API endpoints, you can inject `DismissibleService` directly into your controllers or other services for more control:
161
+
162
+ ```typescript
163
+ import { Controller, Get, Param, Delete, Post } from '@nestjs/common';
164
+ import { DismissibleService } from '@dismissible/nestjs-core';
165
+
166
+ @Controller('features')
167
+ export class FeaturesController {
168
+ constructor(private readonly dismissibleService: DismissibleService) {}
169
+
170
+ @Get(':userId/items/:itemId')
171
+ async getOrCreateItem(@Param('userId') userId: string, @Param('itemId') itemId: string) {
172
+ const result = await this.dismissibleService.getOrCreate(
173
+ itemId,
174
+ userId,
175
+ undefined, // optional request context
176
+ );
177
+
178
+ return {
179
+ item: result.item,
180
+ wasCreated: result.created,
181
+ };
182
+ }
183
+
184
+ @Delete(':userId/items/:itemId')
185
+ async dismissItem(@Param('userId') userId: string, @Param('itemId') itemId: string) {
186
+ const result = await this.dismissibleService.dismiss(itemId, userId);
187
+ return { item: result.item };
188
+ }
189
+
190
+ @Post(':userId/items/:itemId/restore')
191
+ async restoreItem(@Param('userId') userId: string, @Param('itemId') itemId: string) {
192
+ const result = await this.dismissibleService.restore(itemId, userId);
193
+ return { item: result.item };
194
+ }
195
+ }
196
+ ```
197
+
198
+ ### JWT Authentication
199
+
200
+ Secure your API endpoints using the JWT Auth Hook with any OIDC-compliant identity provider:
201
+
202
+ ```typescript
203
+ import { Module } from '@nestjs/common';
204
+ import { DismissibleModule } from '@dismissible/nestjs-core';
205
+ import { JwtAuthHookModule, JwtAuthHook } from '@dismissible/nestjs-jwt-auth-hook';
206
+
207
+ @Module({
208
+ imports: [
209
+ JwtAuthHookModule.forRoot({
210
+ enabled: true,
211
+ wellKnownUrl: 'https://auth.example.com/.well-known/openid-configuration',
212
+ issuer: 'https://auth.example.com',
213
+ audience: 'my-api',
214
+ }),
215
+ DismissibleModule.forRoot({
216
+ hooks: [JwtAuthHook],
217
+ }),
218
+ ],
219
+ })
220
+ export class AppModule {}
221
+ ```
222
+
223
+ See the [@dismissible/nestjs-jwt-auth-hook](https://www.npmjs.com/package/@dismissible/nestjs-jwt-auth-hook) package for detailed configuration options.
224
+
225
+ ### Custom Lifecycle Hooks
226
+
227
+ Lifecycle hooks allow you to intercept operations and add custom logic, validation, or mutations:
228
+
229
+ ```typescript
230
+ import { Injectable } from '@nestjs/common';
231
+ import { IDismissibleLifecycleHook, IHookResult } from '@dismissible/nestjs-core';
232
+ @Injectable()
233
+ export class AuditHook implements IDismissibleLifecycleHook {
234
+ // Lower priority runs first (default is 0)
235
+ readonly priority = 10;
236
+
237
+ async onBeforeDismiss(
238
+ itemId: string,
239
+ userId: string,
240
+ context?: IRequestContext,
241
+ ): Promise<IHookResult> {
242
+ // Block dismissal of critical items
243
+ if (itemId.startsWith('critical-')) {
244
+ return {
245
+ proceed: false,
246
+ reason: 'Cannot dismiss critical items',
247
+ };
248
+ }
249
+
250
+ // Allow the operation to proceed
251
+ return { proceed: true };
252
+ }
253
+
254
+ async onAfterCreate(
255
+ itemId: string,
256
+ item: DismissibleItemDto,
257
+ userId: string,
258
+ context?: IRequestContext,
259
+ ): Promise<void> {
260
+ // Log item creation for analytics
261
+ console.log(`Item created: ${itemId} for user ${userId}`);
262
+ }
263
+
264
+ // Mutate item ID before operation
265
+ async onBeforeGetOrCreate(
266
+ itemId: string,
267
+ userId: string,
268
+ context?: IRequestContext,
269
+ ): Promise<IHookResult> {
270
+ // Normalize item IDs (e.g., lowercase)
271
+ return {
272
+ proceed: true,
273
+ mutations: {
274
+ id: itemId.toLowerCase(),
275
+ },
276
+ };
277
+ }
278
+ }
279
+ ```
280
+
281
+ Register hooks in your module:
282
+
283
+ ```typescript
284
+ import { DismissibleModule } from '@dismissible/nestjs-core';
285
+ import { AuditHook } from './hooks/audit.hook';
286
+
287
+ @Module({
288
+ imports: [
289
+ DismissibleModule.forRoot({
290
+ hooks: [AuditHook],
291
+ }),
292
+ ],
293
+ })
294
+ export class AppModule {}
295
+ ```
296
+
297
+ ### Listening to Events
298
+
299
+ The library emits events for all operations. Listen to them using NestJS's `EventEmitter2`:
300
+
301
+ ```typescript
302
+ import { Injectable } from '@nestjs/common';
303
+ import { OnEvent } from '@nestjs/event-emitter';
304
+ import {
305
+ ItemCreatedEvent,
306
+ ItemDismissedEvent,
307
+ ItemRestoredEvent,
308
+ ItemRetrievedEvent,
309
+ DismissibleEvents,
310
+ } from '@dismissible/nestjs-core';
311
+
312
+ @Injectable()
313
+ export class AnalyticsService {
314
+ @OnEvent(DismissibleEvents.ITEM_CREATED)
315
+ handleItemCreated(event: ItemCreatedEvent) {
316
+ // Track item creation in analytics
317
+ console.log(`Analytics: Item ${event.id} created for user ${event.userId}`);
318
+ }
319
+
320
+ @OnEvent(DismissibleEvents.ITEM_DISMISSED)
321
+ handleItemDismissed(event: ItemDismissedEvent) {
322
+ // Track dismissals
323
+ console.log(`Analytics: Item ${event.id} dismissed by user ${event.userId}`);
324
+ }
325
+
326
+ @OnEvent(DismissibleEvents.ITEM_RESTORED)
327
+ handleItemRestored(event: ItemRestoredEvent) {
328
+ // Track restorations
329
+ console.log(`Analytics: Item ${event.id} restored by user ${event.userId}`);
330
+ }
331
+ }
332
+ ```
333
+
334
+ ### Custom Logger
335
+
336
+ Provide a custom logger implementation:
337
+
338
+ ```typescript
339
+ import { Injectable } from '@nestjs/common';
340
+ import { IDismissibleLogger } from '@dismissible/nestjs-logger';
341
+ import { DismissibleModule } from '@dismissible/nestjs-core';
342
+
343
+ @Injectable()
344
+ export class CustomLogger implements IDismissibleLogger {
345
+ debug(message: string, context?: any) {
346
+ // Your custom logging logic
347
+ console.log(`[DEBUG] ${message}`, context);
348
+ }
349
+
350
+ info(message: string, context?: any) {
351
+ console.log(`[INFO] ${message}`, context);
352
+ }
353
+
354
+ warn(message: string, context?: any) {
355
+ console.warn(`[WARN] ${message}`, context);
356
+ }
357
+
358
+ error(message: string, context?: any) {
359
+ console.error(`[ERROR] ${message}`, context);
360
+ }
361
+ }
362
+
363
+ @Module({
364
+ imports: [
365
+ DismissibleModule.forRoot({
366
+ logger: CustomLogger,
367
+ }),
368
+ ],
369
+ })
370
+ export class AppModule {}
371
+ ```
372
+
373
+ ## API Reference
374
+
375
+ ### DismissibleService
376
+
377
+ The main service for interacting with dismissible items.
378
+
379
+ #### Methods
380
+
381
+ **`getOrCreate(itemId, userId, context?)`**
382
+
383
+ Retrieves an existing item or creates a new one if it doesn't exist.
384
+
385
+ - `itemId: string` - Unique identifier for the item
386
+ - `userId: string` - User identifier (required)
387
+ - `context?: IRequestContext` - Optional request context for tracing
388
+
389
+ Returns: `Promise<IGetOrCreateServiceResponse>`
390
+
391
+ **`dismiss(itemId, userId, context?)`**
392
+
393
+ Marks an item as dismissed.
394
+
395
+ - `itemId: string` - Item identifier
396
+ - `userId: string` - User identifier
397
+ - `context?: IRequestContext` - Optional request context
398
+
399
+ Returns: `Promise<IDismissServiceResponse>`
400
+
401
+ **`restore(itemId, userId, context?)`**
402
+
403
+ Restores a previously dismissed item.
404
+
405
+ - `itemId: string` - Item identifier
406
+ - `userId: string` - User identifier
407
+ - `context?: IRequestContext` - Optional request context
408
+
409
+ Returns: `Promise<IRestoreServiceResponse>`
410
+
411
+ ### Module Configuration
412
+
413
+ ```typescript
414
+ interface IDismissibleModuleOptions {
415
+ // Custom storage module (defaults to memory storage)
416
+ storage?: DynamicModule | Type<any>;
417
+
418
+ // Custom logger implementation
419
+ logger?: Type<IDismissibleLogger>;
420
+
421
+ // Lifecycle hooks to register
422
+ hooks?: Type<IDismissibleLifecycleHook>[];
423
+ }
424
+ ```
425
+
426
+ ## Events
427
+
428
+ The library emits the following events:
429
+
430
+ - `DismissibleEvents.ITEM_CREATED` - Emitted when a new item is created
431
+ - `DismissibleEvents.ITEM_RETRIEVED` - Emitted when an existing item is retrieved
432
+ - `DismissibleEvents.ITEM_DISMISSED` - Emitted when an item is dismissed
433
+ - `DismissibleEvents.ITEM_RESTORED` - Emitted when an item is restored
434
+
435
+ All events include:
436
+
437
+ - `id: string` - The item identifier
438
+ - `item: DismissibleItemDto` - The current item state
439
+ - `userId: string` - The user identifier
440
+ - `context?: IRequestContext` - Optional request context
441
+
442
+ Dismiss and restore events also include:
443
+
444
+ - `previousItem: DismissibleItemDto` - The item state before the operation
445
+
446
+ ## Lifecycle Hooks
447
+
448
+ Hooks can implement any of the following methods:
449
+
450
+ - `onBeforeGetOrCreate()` - Called before get-or-create operation
451
+ - `onAfterGetOrCreate()` - Called after get-or-create operation
452
+ - `onBeforeCreate()` - Called before creating a new item
453
+ - `onAfterCreate()` - Called after creating a new item
454
+ - `onBeforeDismiss()` - Called before dismissing an item
455
+ - `onAfterDismiss()` - Called after dismissing an item
456
+ - `onBeforeRestore()` - Called before restoring an item
457
+ - `onAfterRestore()` - Called after restoring an item
458
+
459
+ Hooks can:
460
+
461
+ - **Block operations** by returning `{ proceed: false, reason: string }`
462
+ - **Mutate parameters** by returning `{ proceed: true, mutations: { id?, userId?, context? } }`
463
+ - **Perform side effects** in post-hooks (no return value needed)
464
+
465
+ Hooks are executed in priority order (lower numbers first).
466
+
467
+ ## Storage Adapters
468
+
469
+ ### In-Memory Storage (Default)
470
+
471
+ The default storage adapter stores items in memory. Data is lost on application restart.
472
+
473
+ ### PostgreSQL Storage
474
+
475
+ Use `PostgresStorageModule` for persistent storage. See the [Using PostgreSQL Storage](#using-postgresql-storage) section above.
476
+
477
+ ### Custom Storage Adapter
478
+
479
+ Implement the `IDismissibleStorage` interface to create a custom storage adapter:
480
+
481
+ ```typescript
482
+ import { Injectable } from '@nestjs/common';
483
+ import { IDismissibleStorage } from '@dismissible/nestjs-storage';
484
+ import { DismissibleItemDto } from '@dismissible/nestjs-item';
485
+
486
+ @Injectable()
487
+ export class RedisStorageAdapter implements IDismissibleStorage {
488
+ async get(userId: string, itemId: string): Promise<DismissibleItemDto | null> {
489
+ // Your implementation
490
+ }
491
+
492
+ async create(userId: string, item: DismissibleItemDto): Promise<void> {
493
+ // Your implementation
494
+ }
495
+
496
+ async update(userId: string, item: DismissibleItemDto): Promise<void> {
497
+ // Your implementation
498
+ }
499
+ }
500
+ ```
501
+
502
+ ## License
503
+
504
+ MIT
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@dismissible/nestjs-core",
3
+ "version": "1.0.3-alpha.064e57a.0",
4
+ "description": "Core Dismissible state management library for NestJS applications",
5
+ "main": "./src/index.js",
6
+ "types": "./src/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./src/index.mjs",
10
+ "require": "./src/index.js",
11
+ "types": "./src/index.d.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "src",
16
+ "README.md",
17
+ "LICENSE.md"
18
+ ],
19
+ "dependencies": {
20
+ "@nestjs/event-emitter": "3.0.1",
21
+ "@dismissible/nestjs-hooks": "1.0.3-alpha.064e57a.0",
22
+ "@dismissible/nestjs-item": "1.0.3-alpha.064e57a.0",
23
+ "@dismissible/nestjs-request": "1.0.3-alpha.064e57a.0",
24
+ "@dismissible/nestjs-storage": "1.0.3-alpha.064e57a.0",
25
+ "@dismissible/nestjs-logger": "1.0.3-alpha.064e57a.0",
26
+ "@dismissible/nestjs-validation": "1.0.3-alpha.064e57a.0"
27
+ },
28
+ "peerDependencies": {
29
+ "@nestjs/common": "10.0.0 || ^11.0.0",
30
+ "@nestjs/core": "10.0.0 || ^11.0.0",
31
+ "@nestjs/swagger": "10.0.0 || ^11.0.0",
32
+ "class-validator": "0.14.3",
33
+ "class-transformer": "0.5.1"
34
+ },
35
+ "peerDependenciesMeta": {
36
+ "@nestjs/common": {
37
+ "optional": false
38
+ },
39
+ "@nestjs/core": {
40
+ "optional": false
41
+ },
42
+ "@nestjs/swagger": {
43
+ "optional": false
44
+ },
45
+ "class-validator": {
46
+ "optional": false
47
+ },
48
+ "class-transformer": {
49
+ "optional": false
50
+ }
51
+ },
52
+ "keywords": [
53
+ "nestjs",
54
+ "dismissible",
55
+ "state-management",
56
+ "feature-flags",
57
+ "dismissals"
58
+ ],
59
+ "author": "",
60
+ "license": "MIT",
61
+ "repository": {
62
+ "type": "git",
63
+ "url": "https://github.com/DismissibleIo/dismissible-api"
64
+ },
65
+ "publishConfig": {
66
+ "access": "public"
67
+ },
68
+ "type": "commonjs"
69
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Response DTO for a dismissible item.
3
+ */
4
+ export declare class DismissibleItemResponseDto {
5
+ itemId: string;
6
+ userId: string;
7
+ createdAt: string;
8
+ dismissedAt?: string;
9
+ }
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DismissibleItemResponseDto = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const swagger_1 = require("@nestjs/swagger");
6
+ /**
7
+ * Response DTO for a dismissible item.
8
+ */
9
+ class DismissibleItemResponseDto {
10
+ }
11
+ exports.DismissibleItemResponseDto = DismissibleItemResponseDto;
12
+ tslib_1.__decorate([
13
+ (0, swagger_1.ApiProperty)({
14
+ description: 'Unique identifier for the item',
15
+ example: 'welcome-banner-v2',
16
+ }),
17
+ tslib_1.__metadata("design:type", String)
18
+ ], DismissibleItemResponseDto.prototype, "itemId", void 0);
19
+ tslib_1.__decorate([
20
+ (0, swagger_1.ApiProperty)({
21
+ description: 'User identifier who created the item',
22
+ example: 'user-123',
23
+ }),
24
+ tslib_1.__metadata("design:type", String)
25
+ ], DismissibleItemResponseDto.prototype, "userId", void 0);
26
+ tslib_1.__decorate([
27
+ (0, swagger_1.ApiProperty)({
28
+ description: 'When the item was created (ISO 8601)',
29
+ example: '2024-01-15T10:30:00.000Z',
30
+ }),
31
+ tslib_1.__metadata("design:type", String)
32
+ ], DismissibleItemResponseDto.prototype, "createdAt", void 0);
33
+ tslib_1.__decorate([
34
+ (0, swagger_1.ApiPropertyOptional)({
35
+ description: 'When the item was dismissed (ISO 8601)',
36
+ example: '2024-01-15T12:00:00.000Z',
37
+ }),
38
+ tslib_1.__metadata("design:type", String)
39
+ ], DismissibleItemResponseDto.prototype, "dismissedAt", void 0);
40
+ //# sourceMappingURL=dismissible-item-response.dto.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dismissible-item-response.dto.js","sourceRoot":"","sources":["../../../../../libs/core/src/api/dismissible-item-response.dto.ts"],"names":[],"mappings":";;;;AAAA,6CAAmE;AAEnE;;GAEG;AACH,MAAa,0BAA0B;CAwBtC;AAxBD,gEAwBC;AAnBC;IAJC,IAAA,qBAAW,EAAC;QACX,WAAW,EAAE,gCAAgC;QAC7C,OAAO,EAAE,mBAAmB;KAC7B,CAAC;;0DACc;AAMhB;IAJC,IAAA,qBAAW,EAAC;QACX,WAAW,EAAE,sCAAsC;QACnD,OAAO,EAAE,UAAU;KACpB,CAAC;;0DACc;AAMhB;IAJC,IAAA,qBAAW,EAAC;QACX,WAAW,EAAE,sCAAsC;QACnD,OAAO,EAAE,0BAA0B;KACpC,CAAC;;6DACiB;AAMnB;IAJC,IAAA,6BAAmB,EAAC;QACnB,WAAW,EAAE,wCAAwC;QACrD,OAAO,EAAE,0BAA0B;KACpC,CAAC;;+DACmB"}
@@ -0,0 +1,12 @@
1
+ import { DismissibleItemDto } from '@dismissible/nestjs-item';
2
+ import { DismissibleItemResponseDto } from './dismissible-item-response.dto';
3
+ /**
4
+ * Mapper for converting domain objects to DTOs.
5
+ */
6
+ export declare class DismissibleItemMapper {
7
+ /**
8
+ * Convert a dismissible item to a response DTO.
9
+ * Converts Date objects to ISO 8601 strings.
10
+ */
11
+ toResponseDto(item: DismissibleItemDto): DismissibleItemResponseDto;
12
+ }
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DismissibleItemMapper = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const common_1 = require("@nestjs/common");
6
+ const dismissible_item_response_dto_1 = require("./dismissible-item-response.dto");
7
+ /**
8
+ * Mapper for converting domain objects to DTOs.
9
+ */
10
+ let DismissibleItemMapper = class DismissibleItemMapper {
11
+ /**
12
+ * Convert a dismissible item to a response DTO.
13
+ * Converts Date objects to ISO 8601 strings.
14
+ */
15
+ toResponseDto(item) {
16
+ const dto = new dismissible_item_response_dto_1.DismissibleItemResponseDto();
17
+ dto.itemId = item.id;
18
+ dto.userId = item.userId;
19
+ dto.createdAt = item.createdAt.toISOString();
20
+ if (item.dismissedAt) {
21
+ dto.dismissedAt = item.dismissedAt.toISOString();
22
+ }
23
+ return dto;
24
+ }
25
+ };
26
+ exports.DismissibleItemMapper = DismissibleItemMapper;
27
+ exports.DismissibleItemMapper = DismissibleItemMapper = tslib_1.__decorate([
28
+ (0, common_1.Injectable)()
29
+ ], DismissibleItemMapper);
30
+ //# sourceMappingURL=dismissible-item.mapper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dismissible-item.mapper.js","sourceRoot":"","sources":["../../../../../libs/core/src/api/dismissible-item.mapper.ts"],"names":[],"mappings":";;;;AAAA,2CAA4C;AAE5C,mFAA6E;AAE7E;;GAEG;AAEI,IAAM,qBAAqB,GAA3B,MAAM,qBAAqB;IAChC;;;OAGG;IACH,aAAa,CAAC,IAAwB;QACpC,MAAM,GAAG,GAAG,IAAI,0DAA0B,EAAE,CAAC;QAE7C,GAAG,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;QACrB,GAAG,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QACzB,GAAG,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,WAAW,EAAE,CAAC;QAE7C,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACrB,GAAG,CAAC,WAAW,GAAG,IAAI,CAAC,WAAW,CAAC,WAAW,EAAE,CAAC;QACnD,CAAC;QAED,OAAO,GAAG,CAAC;IACb,CAAC;CACF,CAAA;AAlBY,sDAAqB;gCAArB,qBAAqB;IADjC,IAAA,mBAAU,GAAE;GACA,qBAAqB,CAkBjC"}
@@ -0,0 +1 @@
1
+ export {};