@dismissible/nestjs-dismissible 0.0.2-canary.738340d.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.
- package/README.md +506 -0
- package/jest.config.ts +29 -0
- package/package.json +63 -0
- package/project.json +42 -0
- package/src/api/dismissible-item-response.dto.ts +38 -0
- package/src/api/dismissible-item.mapper.spec.ts +63 -0
- package/src/api/dismissible-item.mapper.ts +33 -0
- package/src/api/index.ts +7 -0
- package/src/api/use-cases/api-tags.constants.ts +4 -0
- package/src/api/use-cases/dismiss/dismiss.controller.spec.ts +42 -0
- package/src/api/use-cases/dismiss/dismiss.controller.ts +63 -0
- package/src/api/use-cases/dismiss/dismiss.response.dto.ts +7 -0
- package/src/api/use-cases/dismiss/index.ts +2 -0
- package/src/api/use-cases/get-or-create/get-or-create.controller.spec.ts +76 -0
- package/src/api/use-cases/get-or-create/get-or-create.controller.ts +106 -0
- package/src/api/use-cases/get-or-create/get-or-create.request.dto.ts +17 -0
- package/src/api/use-cases/get-or-create/get-or-create.response.dto.ts +7 -0
- package/src/api/use-cases/get-or-create/index.ts +3 -0
- package/src/api/use-cases/index.ts +3 -0
- package/src/api/use-cases/restore/index.ts +2 -0
- package/src/api/use-cases/restore/restore.controller.spec.ts +42 -0
- package/src/api/use-cases/restore/restore.controller.ts +63 -0
- package/src/api/use-cases/restore/restore.response.dto.ts +7 -0
- package/src/core/create-options.ts +9 -0
- package/src/core/dismissible-core.service.spec.ts +357 -0
- package/src/core/dismissible-core.service.ts +161 -0
- package/src/core/dismissible.service.spec.ts +144 -0
- package/src/core/dismissible.service.ts +188 -0
- package/src/core/hook-runner.service.spec.ts +304 -0
- package/src/core/hook-runner.service.ts +267 -0
- package/src/core/index.ts +6 -0
- package/src/core/lifecycle-hook.interface.ts +122 -0
- package/src/core/service-responses.interface.ts +34 -0
- package/src/dismissible.module.ts +83 -0
- package/src/events/dismissible.events.ts +105 -0
- package/src/events/events.constants.ts +21 -0
- package/src/events/index.ts +2 -0
- package/src/exceptions/dismissible.exceptions.spec.ts +50 -0
- package/src/exceptions/dismissible.exceptions.ts +69 -0
- package/src/exceptions/index.ts +1 -0
- package/src/index.ts +8 -0
- package/src/request/index.ts +2 -0
- package/src/request/request-context.decorator.ts +14 -0
- package/src/request/request-context.interface.ts +6 -0
- package/src/response/dtos/base-response.dto.ts +11 -0
- package/src/response/dtos/error-response.dto.ts +36 -0
- package/src/response/dtos/index.ts +3 -0
- package/src/response/dtos/success-response.dto.ts +34 -0
- package/src/response/http-exception-filter.ts +21 -0
- package/src/response/index.ts +4 -0
- package/src/response/response.module.ts +9 -0
- package/src/response/response.service.spec.ts +86 -0
- package/src/response/response.service.ts +20 -0
- package/src/testing/factories.ts +45 -0
- package/src/testing/index.ts +1 -0
- package/src/utils/date/date.service.spec.ts +104 -0
- package/src/utils/date/date.service.ts +19 -0
- package/src/utils/date/index.ts +1 -0
- package/src/utils/dismissible.helper.ts +9 -0
- package/src/utils/index.ts +3 -0
- package/tsconfig.json +13 -0
- package/tsconfig.lib.json +14 -0
package/README.md
ADDED
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
# @dismissible/nestjs-dismissible
|
|
2
|
+
|
|
3
|
+
A powerful NestJS library for managing dismissible state in your applications. Perfect for feature flags, user preferences, onboarding flows, and any scenario where you need to track whether a user has dismissed or interacted with specific items.
|
|
4
|
+
|
|
5
|
+
> **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.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- 🚀 **Simple API** - Easy-to-use service methods for get-or-create, dismiss, and restore operations
|
|
10
|
+
- 💾 **Flexible Storage** - Default in-memory storage with support for custom storage adapters (PostgreSQL, Redis, etc.)
|
|
11
|
+
- 🔌 **Lifecycle Hooks** - Intercept and customize operations with pre/post hooks
|
|
12
|
+
- 📡 **Event-Driven** - Built-in event emission for all operations
|
|
13
|
+
- 🎯 **Type-Safe** - Full TypeScript support with generic metadata types
|
|
14
|
+
- 🛡️ **Validation** - Automatic validation of dismissible items
|
|
15
|
+
- 📝 **Swagger Integration** - Auto-generated API documentation
|
|
16
|
+
- ⚛️ **React Client** - Works out of the box with [@dismissible/react-client](https://www.npmjs.com/package/@dismissible/react-client)
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @dismissible/nestjs-dismissible
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Getting Started
|
|
25
|
+
|
|
26
|
+
### Basic Setup
|
|
27
|
+
|
|
28
|
+
The simplest way to get started is with the default configuration, which uses in-memory storage:
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { Module } from '@nestjs/common';
|
|
32
|
+
import { DismissibleModule } from '@dismissible/nestjs-dismissible';
|
|
33
|
+
|
|
34
|
+
@Module({
|
|
35
|
+
imports: [DismissibleModule.forRoot({})],
|
|
36
|
+
})
|
|
37
|
+
export class AppModule {}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Built-in REST API
|
|
41
|
+
|
|
42
|
+
The module automatically registers REST endpoints for all operations:
|
|
43
|
+
|
|
44
|
+
- `GET /v1/user/:userId/dismissible-item/:itemId` - Get or create an item
|
|
45
|
+
- `DELETE /v1/user/:userId/dismissible-item/:itemId` - Dismiss an item
|
|
46
|
+
- `POST /v1/user/:userId/dismissible-item/:itemId` - Restore a dismissed item
|
|
47
|
+
|
|
48
|
+
Example request:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# Get or create an item
|
|
52
|
+
curl http://localhost:3000/v1/user/user-123/dismissible-item/welcome-banner?metadata=version:2&metadata=category:onboarding
|
|
53
|
+
|
|
54
|
+
# Dismiss an item
|
|
55
|
+
curl -X DELETE http://localhost:3000/v1/user/user-123/dismissible-item/welcome-banner
|
|
56
|
+
|
|
57
|
+
# Restore a dismissed item
|
|
58
|
+
curl -X POST http://localhost:3000/v1/user/user-123/dismissible-item/welcome-banner
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### React Client Integration
|
|
62
|
+
|
|
63
|
+
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:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npm install @dismissible/react-client
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
import { DismissibleProvider, useDismissible } from '@dismissible/react-client';
|
|
71
|
+
|
|
72
|
+
function App() {
|
|
73
|
+
return (
|
|
74
|
+
<DismissibleProvider
|
|
75
|
+
apiUrl="http://localhost:3000"
|
|
76
|
+
userId="user-123"
|
|
77
|
+
>
|
|
78
|
+
<WelcomeBanner />
|
|
79
|
+
</DismissibleProvider>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function WelcomeBanner() {
|
|
84
|
+
const { item, dismiss, isLoading } = useDismissible('welcome-banner');
|
|
85
|
+
|
|
86
|
+
if (isLoading) return <div>Loading...</div>;
|
|
87
|
+
if (item?.dismissedAt) return null;
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div>
|
|
91
|
+
<h2>Welcome!</h2>
|
|
92
|
+
<button onClick={() => dismiss()}>Dismiss</button>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The React client automatically uses the built-in REST API endpoints, so no additional configuration is needed on the backend.
|
|
99
|
+
|
|
100
|
+
## Advanced Usage
|
|
101
|
+
|
|
102
|
+
### Using PostgreSQL Storage
|
|
103
|
+
|
|
104
|
+
To persist dismissible items in a PostgreSQL database, use the `PostgresStorageModule`:
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
import { Module } from '@nestjs/common';
|
|
108
|
+
import { DismissibleModule } from '@dismissible/nestjs-dismissible';
|
|
109
|
+
import { PostgresStorageModule } from '@dismissible/nestjs-postgres-storage';
|
|
110
|
+
|
|
111
|
+
@Module({
|
|
112
|
+
imports: [
|
|
113
|
+
DismissibleModule.forRoot({
|
|
114
|
+
storage: PostgresStorageModule,
|
|
115
|
+
}),
|
|
116
|
+
],
|
|
117
|
+
})
|
|
118
|
+
export class AppModule {}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Prerequisites:**
|
|
122
|
+
|
|
123
|
+
1. Install the PostgreSQL storage package:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
npm install @dismissible/nestjs-postgres-storage
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
2. Set up your database connection string:
|
|
130
|
+
|
|
131
|
+
```env
|
|
132
|
+
DISMISSIBLE_POSTGRES_STORAGE_CONNECTION_STRING=postgresql://user:password@localhost:5432/dismissible
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
3. Run Prisma migrations (if using Prisma):
|
|
136
|
+
```bash
|
|
137
|
+
npx prisma migrate dev
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The PostgreSQL adapter uses Prisma and automatically handles schema migrations. The storage persists all dismissible items across application restarts.
|
|
141
|
+
|
|
142
|
+
### Using the Service
|
|
143
|
+
|
|
144
|
+
Instead of using the built-in REST API endpoints, you can inject `DismissibleService` directly into your controllers or other services for more control:
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
import { Controller, Get, Param, Delete, Post } from '@nestjs/common';
|
|
148
|
+
import { DismissibleService } from '@dismissible/nestjs-dismissible';
|
|
149
|
+
|
|
150
|
+
@Controller('features')
|
|
151
|
+
export class FeaturesController {
|
|
152
|
+
constructor(private readonly dismissibleService: DismissibleService) {}
|
|
153
|
+
|
|
154
|
+
@Get(':userId/items/:itemId')
|
|
155
|
+
async getOrCreateItem(@Param('userId') userId: string, @Param('itemId') itemId: string) {
|
|
156
|
+
const result = await this.dismissibleService.getOrCreate(
|
|
157
|
+
itemId,
|
|
158
|
+
userId,
|
|
159
|
+
{
|
|
160
|
+
metadata: { version: '1.0', category: 'onboarding' },
|
|
161
|
+
},
|
|
162
|
+
undefined, // optional request context
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
item: result.item,
|
|
167
|
+
wasCreated: result.created,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
@Delete(':userId/items/:itemId')
|
|
172
|
+
async dismissItem(@Param('userId') userId: string, @Param('itemId') itemId: string) {
|
|
173
|
+
const result = await this.dismissibleService.dismiss(itemId, userId);
|
|
174
|
+
return { item: result.item };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
@Post(':userId/items/:itemId/restore')
|
|
178
|
+
async restoreItem(@Param('userId') userId: string, @Param('itemId') itemId: string) {
|
|
179
|
+
const result = await this.dismissibleService.restore(itemId, userId);
|
|
180
|
+
return { item: result.item };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Custom Lifecycle Hooks
|
|
186
|
+
|
|
187
|
+
Lifecycle hooks allow you to intercept operations and add custom logic, validation, or mutations:
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
import { Injectable } from '@nestjs/common';
|
|
191
|
+
import { IDismissibleLifecycleHook, IHookResult } from '@dismissible/nestjs-dismissible';
|
|
192
|
+
import { BaseMetadata } from '@dismissible/nestjs-dismissible-item';
|
|
193
|
+
|
|
194
|
+
@Injectable()
|
|
195
|
+
export class AuditHook implements IDismissibleLifecycleHook<BaseMetadata> {
|
|
196
|
+
// Lower priority runs first (default is 0)
|
|
197
|
+
readonly priority = 10;
|
|
198
|
+
|
|
199
|
+
async onBeforeDismiss(
|
|
200
|
+
itemId: string,
|
|
201
|
+
userId: string,
|
|
202
|
+
context?: IRequestContext,
|
|
203
|
+
): Promise<IHookResult> {
|
|
204
|
+
// Block dismissal of critical items
|
|
205
|
+
if (itemId.startsWith('critical-')) {
|
|
206
|
+
return {
|
|
207
|
+
proceed: false,
|
|
208
|
+
reason: 'Cannot dismiss critical items',
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Allow the operation to proceed
|
|
213
|
+
return { proceed: true };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async onAfterCreate(
|
|
217
|
+
itemId: string,
|
|
218
|
+
item: DismissibleItemDto<BaseMetadata>,
|
|
219
|
+
userId: string,
|
|
220
|
+
context?: IRequestContext,
|
|
221
|
+
): Promise<void> {
|
|
222
|
+
// Log item creation for analytics
|
|
223
|
+
console.log(`Item created: ${itemId} for user ${userId}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Mutate item ID before operation
|
|
227
|
+
async onBeforeGetOrCreate(
|
|
228
|
+
itemId: string,
|
|
229
|
+
userId: string,
|
|
230
|
+
context?: IRequestContext,
|
|
231
|
+
): Promise<IHookResult> {
|
|
232
|
+
// Normalize item IDs (e.g., lowercase)
|
|
233
|
+
return {
|
|
234
|
+
proceed: true,
|
|
235
|
+
mutations: {
|
|
236
|
+
id: itemId.toLowerCase(),
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Register hooks in your module:
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
import { DismissibleModule } from '@dismissible/nestjs-dismissible';
|
|
247
|
+
import { AuditHook } from './hooks/audit.hook';
|
|
248
|
+
|
|
249
|
+
@Module({
|
|
250
|
+
imports: [
|
|
251
|
+
DismissibleModule.forRoot({
|
|
252
|
+
hooks: [AuditHook],
|
|
253
|
+
}),
|
|
254
|
+
],
|
|
255
|
+
})
|
|
256
|
+
export class AppModule {}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Listening to Events
|
|
260
|
+
|
|
261
|
+
The library emits events for all operations. Listen to them using NestJS's `EventEmitter2`:
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
import { Injectable } from '@nestjs/common';
|
|
265
|
+
import { OnEvent } from '@nestjs/event-emitter';
|
|
266
|
+
import {
|
|
267
|
+
ItemCreatedEvent,
|
|
268
|
+
ItemDismissedEvent,
|
|
269
|
+
ItemRestoredEvent,
|
|
270
|
+
ItemRetrievedEvent,
|
|
271
|
+
DismissibleEvents,
|
|
272
|
+
} from '@dismissible/nestjs-dismissible';
|
|
273
|
+
|
|
274
|
+
@Injectable()
|
|
275
|
+
export class AnalyticsService {
|
|
276
|
+
@OnEvent(DismissibleEvents.ITEM_CREATED)
|
|
277
|
+
handleItemCreated(event: ItemCreatedEvent) {
|
|
278
|
+
// Track item creation in analytics
|
|
279
|
+
console.log(`Analytics: Item ${event.id} created for user ${event.userId}`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
@OnEvent(DismissibleEvents.ITEM_DISMISSED)
|
|
283
|
+
handleItemDismissed(event: ItemDismissedEvent) {
|
|
284
|
+
// Track dismissals
|
|
285
|
+
console.log(`Analytics: Item ${event.id} dismissed by user ${event.userId}`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
@OnEvent(DismissibleEvents.ITEM_RESTORED)
|
|
289
|
+
handleItemRestored(event: ItemRestoredEvent) {
|
|
290
|
+
// Track restorations
|
|
291
|
+
console.log(`Analytics: Item ${event.id} restored by user ${event.userId}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Custom Metadata Types
|
|
297
|
+
|
|
298
|
+
Define custom metadata types for type-safe item properties:
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
import { BaseMetadata } from '@dismissible/nestjs-dismissible-item';
|
|
302
|
+
|
|
303
|
+
interface OnboardingMetadata extends BaseMetadata {
|
|
304
|
+
step: number;
|
|
305
|
+
completedAt?: string;
|
|
306
|
+
skipped: boolean;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Use in your service
|
|
310
|
+
@Injectable()
|
|
311
|
+
export class OnboardingService {
|
|
312
|
+
constructor(private readonly dismissibleService: DismissibleService<OnboardingMetadata>) {}
|
|
313
|
+
|
|
314
|
+
async trackStep(userId: string, step: number) {
|
|
315
|
+
const result = await this.dismissibleService.getOrCreate(
|
|
316
|
+
`onboarding-step-${step}`,
|
|
317
|
+
userId,
|
|
318
|
+
{
|
|
319
|
+
metadata: {
|
|
320
|
+
step,
|
|
321
|
+
skipped: false,
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
undefined,
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
return result.item;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Custom Logger
|
|
333
|
+
|
|
334
|
+
Provide a custom logger implementation:
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
import { Injectable } from '@nestjs/common';
|
|
338
|
+
import { IDismissibleLogger } from '@dismissible/nestjs-logger';
|
|
339
|
+
import { DismissibleModule } from '@dismissible/nestjs-dismissible';
|
|
340
|
+
|
|
341
|
+
@Injectable()
|
|
342
|
+
export class CustomLogger implements IDismissibleLogger {
|
|
343
|
+
debug(message: string, context?: any) {
|
|
344
|
+
// Your custom logging logic
|
|
345
|
+
console.log(`[DEBUG] ${message}`, context);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
info(message: string, context?: any) {
|
|
349
|
+
console.log(`[INFO] ${message}`, context);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
warn(message: string, context?: any) {
|
|
353
|
+
console.warn(`[WARN] ${message}`, context);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
error(message: string, context?: any) {
|
|
357
|
+
console.error(`[ERROR] ${message}`, context);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
@Module({
|
|
362
|
+
imports: [
|
|
363
|
+
DismissibleModule.forRoot({
|
|
364
|
+
logger: CustomLogger,
|
|
365
|
+
}),
|
|
366
|
+
],
|
|
367
|
+
})
|
|
368
|
+
export class AppModule {}
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
## API Reference
|
|
372
|
+
|
|
373
|
+
### DismissibleService
|
|
374
|
+
|
|
375
|
+
The main service for interacting with dismissible items.
|
|
376
|
+
|
|
377
|
+
#### Methods
|
|
378
|
+
|
|
379
|
+
**`getOrCreate(itemId, userId, options?, context?)`**
|
|
380
|
+
|
|
381
|
+
Retrieves an existing item or creates a new one if it doesn't exist.
|
|
382
|
+
|
|
383
|
+
- `itemId: string` - Unique identifier for the item
|
|
384
|
+
- `userId: string` - User identifier (required)
|
|
385
|
+
- `options?: ICreateItemOptions<TMetadata>` - Optional creation options
|
|
386
|
+
- `metadata?: TMetadata` - Custom metadata to attach to the item
|
|
387
|
+
- `context?: IRequestContext` - Optional request context for tracing
|
|
388
|
+
|
|
389
|
+
Returns: `Promise<IGetOrCreateServiceResponse<TMetadata>>`
|
|
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<TMetadata>>`
|
|
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<TMetadata>>`
|
|
410
|
+
|
|
411
|
+
### Module Configuration
|
|
412
|
+
|
|
413
|
+
```typescript
|
|
414
|
+
interface IDismissibleModuleOptions<TMetadata extends BaseMetadata> {
|
|
415
|
+
// Custom storage module (defaults to in-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<TMetadata>>[];
|
|
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<TMetadata>` - 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<TMetadata>` - 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, BaseMetadata } from '@dismissible/nestjs-dismissible-item';
|
|
485
|
+
|
|
486
|
+
@Injectable()
|
|
487
|
+
export class RedisStorageAdapter<
|
|
488
|
+
TMetadata extends BaseMetadata,
|
|
489
|
+
> implements IDismissibleStorage<TMetadata> {
|
|
490
|
+
async get(userId: string, itemId: string): Promise<DismissibleItemDto<TMetadata> | null> {
|
|
491
|
+
// Your implementation
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async create(userId: string, item: DismissibleItemDto<TMetadata>): Promise<void> {
|
|
495
|
+
// Your implementation
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async update(userId: string, item: DismissibleItemDto<TMetadata>): Promise<void> {
|
|
499
|
+
// Your implementation
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
## License
|
|
505
|
+
|
|
506
|
+
MIT
|
package/jest.config.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
displayName: 'dismissible',
|
|
3
|
+
preset: '../../jest.preset.js',
|
|
4
|
+
testEnvironment: 'node',
|
|
5
|
+
transform: {
|
|
6
|
+
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.json' }],
|
|
7
|
+
},
|
|
8
|
+
moduleFileExtensions: ['ts', 'js', 'html'],
|
|
9
|
+
coverageDirectory: '../../coverage/libs/dismissible',
|
|
10
|
+
transformIgnorePatterns: ['node_modules/(?!(uuid)/)'],
|
|
11
|
+
collectCoverageFrom: [
|
|
12
|
+
'src/**/*.ts',
|
|
13
|
+
'!src/**/*.spec.ts',
|
|
14
|
+
'!src/**/*.interface.ts',
|
|
15
|
+
'!src/**/*.dto.ts',
|
|
16
|
+
'!src/**/*.enum.ts',
|
|
17
|
+
'!src/**/index.ts',
|
|
18
|
+
'!src/**/*.module.ts',
|
|
19
|
+
],
|
|
20
|
+
coverageReporters: ['text', 'text-summary', 'html'],
|
|
21
|
+
coverageThreshold: {
|
|
22
|
+
global: {
|
|
23
|
+
branches: 81,
|
|
24
|
+
functions: 84,
|
|
25
|
+
lines: 92,
|
|
26
|
+
statements: 92,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dismissible/nestjs-dismissible",
|
|
3
|
+
"version": "0.0.2-canary.738340d.0",
|
|
4
|
+
"description": "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
|
+
"dependencies": {
|
|
15
|
+
"@nestjs/event-emitter": "^3.0.0",
|
|
16
|
+
"uuid": "^11.0.0"
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"@nestjs/common": "^11.0.0",
|
|
20
|
+
"@nestjs/core": "^11.0.0",
|
|
21
|
+
"@nestjs/swagger": "^11.0.0",
|
|
22
|
+
"@dismissible/nestjs-dismissible-item": "^0.0.2-canary.738340d.0",
|
|
23
|
+
"@dismissible/nestjs-storage": "^0.0.2-canary.738340d.0",
|
|
24
|
+
"class-validator": "^0.14.0",
|
|
25
|
+
"class-transformer": "^0.5.0"
|
|
26
|
+
},
|
|
27
|
+
"peerDependenciesMeta": {
|
|
28
|
+
"@nestjs/common": {
|
|
29
|
+
"optional": false
|
|
30
|
+
},
|
|
31
|
+
"@nestjs/core": {
|
|
32
|
+
"optional": false
|
|
33
|
+
},
|
|
34
|
+
"@nestjs/swagger": {
|
|
35
|
+
"optional": false
|
|
36
|
+
},
|
|
37
|
+
"dismissible/nestjs-dismissible-item": {
|
|
38
|
+
"optional": false
|
|
39
|
+
},
|
|
40
|
+
"@dismissible/nestjs-storage": {
|
|
41
|
+
"optional": false
|
|
42
|
+
},
|
|
43
|
+
"class-validator": {
|
|
44
|
+
"optional": false
|
|
45
|
+
},
|
|
46
|
+
"class-transformer": {
|
|
47
|
+
"optional": false
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"keywords": [
|
|
51
|
+
"nestjs",
|
|
52
|
+
"dismissible",
|
|
53
|
+
"state-management",
|
|
54
|
+
"feature-flags",
|
|
55
|
+
"dismissals"
|
|
56
|
+
],
|
|
57
|
+
"author": "",
|
|
58
|
+
"license": "MIT",
|
|
59
|
+
"repository": {
|
|
60
|
+
"type": "git",
|
|
61
|
+
"url": ""
|
|
62
|
+
}
|
|
63
|
+
}
|
package/project.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dismissible",
|
|
3
|
+
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
|
4
|
+
"sourceRoot": "libs/dismissible/src",
|
|
5
|
+
"projectType": "library",
|
|
6
|
+
"tags": [],
|
|
7
|
+
"targets": {
|
|
8
|
+
"build": {
|
|
9
|
+
"executor": "@nx/js:tsc",
|
|
10
|
+
"outputs": ["{options.outputPath}"],
|
|
11
|
+
"options": {
|
|
12
|
+
"outputPath": "dist/libs/dismissible",
|
|
13
|
+
"main": "libs/dismissible/src/index.ts",
|
|
14
|
+
"tsConfig": "libs/dismissible/tsconfig.lib.json",
|
|
15
|
+
"assets": ["libs/dismissible/package.json", "libs/dismissible/README.md"],
|
|
16
|
+
"generatePackageJson": true
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"lint": {
|
|
20
|
+
"executor": "@nx/eslint:lint",
|
|
21
|
+
"outputs": ["{options.outputFile}"],
|
|
22
|
+
"options": {
|
|
23
|
+
"lintFilePatterns": ["libs/dismissible/**/*.ts"]
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"test": {
|
|
27
|
+
"executor": "@nx/jest:jest",
|
|
28
|
+
"outputs": ["{workspaceRoot}/coverage/libs/dismissible"],
|
|
29
|
+
"options": {
|
|
30
|
+
"jestConfig": "libs/dismissible/jest.config.ts",
|
|
31
|
+
"passWithNoTests": true
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"npm-publish": {
|
|
35
|
+
"executor": "nx:run-commands",
|
|
36
|
+
"options": {
|
|
37
|
+
"command": "npm publish --access public",
|
|
38
|
+
"cwd": "dist/libs/dismissible"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Response DTO for a dismissible item.
|
|
5
|
+
*/
|
|
6
|
+
export class DismissibleItemResponseDto {
|
|
7
|
+
@ApiProperty({
|
|
8
|
+
description: 'Unique identifier for the item',
|
|
9
|
+
example: 'welcome-banner-v2',
|
|
10
|
+
})
|
|
11
|
+
itemId!: string;
|
|
12
|
+
|
|
13
|
+
@ApiProperty({
|
|
14
|
+
description: 'User identifier who created the item',
|
|
15
|
+
example: 'user-123',
|
|
16
|
+
})
|
|
17
|
+
userId!: string;
|
|
18
|
+
|
|
19
|
+
@ApiProperty({
|
|
20
|
+
description: 'When the item was created (ISO 8601)',
|
|
21
|
+
example: '2024-01-15T10:30:00.000Z',
|
|
22
|
+
})
|
|
23
|
+
createdAt!: string;
|
|
24
|
+
|
|
25
|
+
@ApiPropertyOptional({
|
|
26
|
+
description: 'When the item was dismissed (ISO 8601)',
|
|
27
|
+
example: '2024-01-15T12:00:00.000Z',
|
|
28
|
+
})
|
|
29
|
+
dismissedAt?: string;
|
|
30
|
+
|
|
31
|
+
@ApiPropertyOptional({
|
|
32
|
+
description: 'Optional metadata associated with the item',
|
|
33
|
+
example: { version: 2, category: 'promotional' },
|
|
34
|
+
type: 'object',
|
|
35
|
+
additionalProperties: true,
|
|
36
|
+
})
|
|
37
|
+
metadata?: Record<string, unknown>;
|
|
38
|
+
}
|