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