@flusys/nestjs-notification 4.1.1 → 5.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 +99 -465
- package/cjs/config/message-keys.js +3 -23
- package/cjs/docs/notification-swagger.config.js +39 -3
- package/cjs/entities/notification.entity.js +3 -2
- package/cjs/gateways/notification.gateway.js +9 -21
- package/cjs/services/notification.service.js +16 -8
- package/config/message-keys.d.ts +0 -28
- package/docs/notification-swagger.config.d.ts +2 -1
- package/fesm/config/message-keys.js +0 -13
- package/fesm/docs/notification-swagger.config.js +39 -3
- package/fesm/entities/notification.entity.js +3 -2
- package/fesm/gateways/notification.gateway.js +9 -21
- package/fesm/services/notification.service.js +16 -8
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,74 +1,9 @@
|
|
|
1
1
|
# @flusys/nestjs-notification
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Real-time notification system for NestJS — Socket.IO WebSocket delivery, persistent PostgreSQL storage, read/unread tracking, and a cross-module adapter so any package can send notifications without importing this one directly.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@flusys/nestjs-notification)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
|
-
[](https://nestjs.com/)
|
|
8
|
-
[](https://www.typescriptlang.org/)
|
|
9
|
-
[](https://nodejs.org/)
|
|
10
|
-
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
## Table of Contents
|
|
14
|
-
|
|
15
|
-
- [Overview](#overview)
|
|
16
|
-
- [Features](#features)
|
|
17
|
-
- [Compatibility](#compatibility)
|
|
18
|
-
- [Installation](#installation)
|
|
19
|
-
- [Quick Start](#quick-start)
|
|
20
|
-
- [Module Registration](#module-registration)
|
|
21
|
-
- [forRoot (Sync)](#forroot-sync)
|
|
22
|
-
- [forRootAsync (Factory)](#forrootasync-factory)
|
|
23
|
-
- [Configuration Reference](#configuration-reference)
|
|
24
|
-
- [Feature Toggles](#feature-toggles)
|
|
25
|
-
- [WebSocket Gateway](#websocket-gateway)
|
|
26
|
-
- [Client Connection](#client-connection)
|
|
27
|
-
- [WebSocket Events](#websocket-events)
|
|
28
|
-
- [API Endpoints](#api-endpoints)
|
|
29
|
-
- [Entities](#entities)
|
|
30
|
-
- [Cross-Module Adapter (NOTIFICATION_ADAPTER)](#cross-module-adapter-notification_adapter)
|
|
31
|
-
- [Exported Services](#exported-services)
|
|
32
|
-
- [Sending Notifications Programmatically](#sending-notifications-programmatically)
|
|
33
|
-
- [Frontend Integration](#frontend-integration)
|
|
34
|
-
- [Troubleshooting](#troubleshooting)
|
|
35
|
-
- [License](#license)
|
|
36
|
-
|
|
37
|
-
---
|
|
38
|
-
|
|
39
|
-
## Overview
|
|
40
|
-
|
|
41
|
-
`@flusys/nestjs-notification` delivers notifications both in real-time (Socket.IO WebSocket) and persistently (PostgreSQL). Unread notifications survive disconnections and are delivered on the next connection. The `NOTIFICATION_ADAPTER` allows auth, email, and other modules to send notifications without importing this package directly.
|
|
42
|
-
|
|
43
|
-
---
|
|
44
|
-
|
|
45
|
-
## Features
|
|
46
|
-
|
|
47
|
-
- **Persistent storage** — Notifications stored in PostgreSQL with read/unread tracking
|
|
48
|
-
- **Real-time WebSocket** — Socket.IO gateway at `/notifications` namespace
|
|
49
|
-
- **JWT authentication on WebSocket** — Token verified on connection handshake
|
|
50
|
-
- **Read tracking** — `isRead` + `readAt` timestamp per notification
|
|
51
|
-
- **Mark as read** — Single notification or bulk "mark all as read"
|
|
52
|
-
- **Unread count** — Fast count query for notification badge
|
|
53
|
-
- **Company scoping** — Filter by company in multi-company setups
|
|
54
|
-
- **Broadcast to company** — Emit to all users in a company Socket.IO room
|
|
55
|
-
- **Online presence** — Check if a user is currently connected
|
|
56
|
-
- **Cross-module adapter** — `NOTIFICATION_ADAPTER` for sending from any module
|
|
57
|
-
- **Configurable realtime** — Disable Socket.IO gateway when not needed
|
|
58
|
-
|
|
59
|
-
---
|
|
60
|
-
|
|
61
|
-
## Compatibility
|
|
62
|
-
|
|
63
|
-
| Package | Version |
|
|
64
|
-
|---------|---------|
|
|
65
|
-
| `@flusys/nestjs-core` | `^4.0.0` |
|
|
66
|
-
| `@flusys/nestjs-shared` | `^4.0.0` |
|
|
67
|
-
| `@nestjs/websockets` | `^11.0.0` |
|
|
68
|
-
| `@nestjs/platform-socket.io` | `^11.0.0` |
|
|
69
|
-
| `socket.io` | `^4.0.0` |
|
|
70
|
-
| `jsonwebtoken` | `^9.0.0` |
|
|
71
|
-
| Node.js | `>= 18.x` |
|
|
72
7
|
|
|
73
8
|
---
|
|
74
9
|
|
|
@@ -79,14 +14,13 @@ npm install @flusys/nestjs-notification @flusys/nestjs-shared @flusys/nestjs-cor
|
|
|
79
14
|
npm install @nestjs/platform-socket.io socket.io
|
|
80
15
|
```
|
|
81
16
|
|
|
82
|
-
|
|
17
|
+
## 1. Module Registration
|
|
83
18
|
|
|
84
|
-
|
|
19
|
+
### forRoot (sync)
|
|
85
20
|
|
|
86
|
-
|
|
21
|
+
#### Mode 1: Single Database
|
|
87
22
|
|
|
88
23
|
```typescript
|
|
89
|
-
import { Module } from '@nestjs/common';
|
|
90
24
|
import { NotificationModule } from '@flusys/nestjs-notification';
|
|
91
25
|
|
|
92
26
|
@Module({
|
|
@@ -99,12 +33,12 @@ import { NotificationModule } from '@flusys/nestjs-notification';
|
|
|
99
33
|
enableCompanyFeature: false,
|
|
100
34
|
},
|
|
101
35
|
config: {
|
|
102
|
-
enableRealtime: true,
|
|
103
|
-
jwtSecret: process.env.JWT_SECRET,
|
|
36
|
+
enableRealtime: true, // false = no Socket.IO gateway registered
|
|
37
|
+
jwtSecret: process.env.JWT_SECRET, // required for WebSocket auth
|
|
104
38
|
defaultDatabaseConfig: {
|
|
105
|
-
type: '
|
|
39
|
+
type: 'mysql',
|
|
106
40
|
host: process.env.DB_HOST,
|
|
107
|
-
port: Number(process.env.DB_PORT ??
|
|
41
|
+
port: Number(process.env.DB_PORT ?? 3306),
|
|
108
42
|
username: process.env.DB_USER,
|
|
109
43
|
password: process.env.DB_PASSWORD,
|
|
110
44
|
database: process.env.DB_NAME,
|
|
@@ -116,64 +50,42 @@ import { NotificationModule } from '@flusys/nestjs-notification';
|
|
|
116
50
|
export class AppModule {}
|
|
117
51
|
```
|
|
118
52
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
```typescript
|
|
122
|
-
// In auth.service.ts — sends a notification after login
|
|
123
|
-
import { NOTIFICATION_ADAPTER } from '@flusys/nestjs-shared/interfaces';
|
|
124
|
-
import { INotificationAdapter } from '@flusys/nestjs-shared/interfaces';
|
|
125
|
-
|
|
126
|
-
@Injectable()
|
|
127
|
-
export class AuthenticationService {
|
|
128
|
-
constructor(
|
|
129
|
-
@Optional() @Inject(NOTIFICATION_ADAPTER)
|
|
130
|
-
private readonly notificationAdapter?: INotificationAdapter,
|
|
131
|
-
) {}
|
|
132
|
-
|
|
133
|
-
async afterLogin(userId: string): Promise<void> {
|
|
134
|
-
await this.notificationAdapter?.send({
|
|
135
|
-
userId,
|
|
136
|
-
title: 'New Login',
|
|
137
|
-
message: 'Your account was accessed from a new device.',
|
|
138
|
-
type: 'warning',
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
---
|
|
145
|
-
|
|
146
|
-
## Module Registration
|
|
147
|
-
|
|
148
|
-
### forRoot (Sync)
|
|
53
|
+
#### Mode 2: Multi-Tenant
|
|
149
54
|
|
|
150
55
|
```typescript
|
|
151
56
|
NotificationModule.forRoot({
|
|
152
|
-
global
|
|
153
|
-
includeController
|
|
154
|
-
bootstrapAppConfig
|
|
155
|
-
databaseMode: '
|
|
156
|
-
enableCompanyFeature:
|
|
157
|
-
}
|
|
158
|
-
config
|
|
159
|
-
enableRealtime
|
|
160
|
-
jwtSecret
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
57
|
+
global: true,
|
|
58
|
+
includeController: true,
|
|
59
|
+
bootstrapAppConfig: {
|
|
60
|
+
databaseMode: 'multi-tenant',
|
|
61
|
+
enableCompanyFeature: true,
|
|
62
|
+
},
|
|
63
|
+
config: {
|
|
64
|
+
enableRealtime: true,
|
|
65
|
+
jwtSecret: process.env.JWT_SECRET,
|
|
66
|
+
tenantDefaultDatabaseConfig: {
|
|
67
|
+
type: 'mysql',
|
|
68
|
+
host: process.env.TENANT_DB_HOST,
|
|
69
|
+
port: Number(process.env.TENANT_DB_PORT ?? 3306),
|
|
70
|
+
username: process.env.TENANT_DB_USER,
|
|
71
|
+
password: process.env.TENANT_DB_PASSWORD,
|
|
72
|
+
database: process.env.TENANT_DB_NAME,
|
|
73
|
+
},
|
|
74
|
+
tenants: [
|
|
75
|
+
{ id: 'tenant-a', database: 'tenant_a_db' },
|
|
76
|
+
{ id: 'tenant-b', database: 'tenant_b_db' },
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
});
|
|
165
80
|
```
|
|
166
81
|
|
|
167
|
-
|
|
168
|
-
- `enableRealtime: false` → `NotificationGateway` is NOT registered (no WebSocket)
|
|
169
|
-
- `includeController: false` → No HTTP endpoints registered
|
|
170
|
-
- `global: true` → Exports available application-wide
|
|
171
|
-
|
|
172
|
-
### forRootAsync (Factory)
|
|
82
|
+
### forRootAsync (factory)
|
|
173
83
|
|
|
174
84
|
```typescript
|
|
175
|
-
import { ConfigService } from '@nestjs/config';
|
|
85
|
+
import { ConfigModule, ConfigService } from '@nestjs/config';
|
|
86
|
+
import { NotificationModule, ITenantDatabaseConfig } from '@flusys/nestjs-notification';
|
|
176
87
|
|
|
88
|
+
// Single database
|
|
177
89
|
NotificationModule.forRootAsync({
|
|
178
90
|
global: true,
|
|
179
91
|
includeController: true,
|
|
@@ -182,389 +94,111 @@ NotificationModule.forRootAsync({
|
|
|
182
94
|
enableCompanyFeature: true,
|
|
183
95
|
},
|
|
184
96
|
imports: [ConfigModule],
|
|
185
|
-
useFactory: (
|
|
97
|
+
useFactory: (config: ConfigService) => ({
|
|
186
98
|
enableRealtime: true,
|
|
187
|
-
jwtSecret:
|
|
99
|
+
jwtSecret: config.get('JWT_SECRET'),
|
|
188
100
|
defaultDatabaseConfig: {
|
|
189
|
-
type: '
|
|
190
|
-
host:
|
|
191
|
-
port:
|
|
192
|
-
username:
|
|
193
|
-
password:
|
|
194
|
-
database:
|
|
101
|
+
type: 'mysql',
|
|
102
|
+
host: config.get('DB_HOST'),
|
|
103
|
+
port: config.get<number>('DB_PORT'),
|
|
104
|
+
username: config.get('DB_USER'),
|
|
105
|
+
password: config.get('DB_PASSWORD'),
|
|
106
|
+
database: config.get('DB_NAME'),
|
|
195
107
|
},
|
|
196
108
|
}),
|
|
197
109
|
inject: [ConfigService],
|
|
198
|
-
})
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
**Exported services:**
|
|
202
|
-
- `NotificationConfigService`
|
|
203
|
-
- `NotificationDataSourceProvider`
|
|
204
|
-
- `NotificationService`
|
|
205
|
-
- `NotificationHelperService`
|
|
206
|
-
- `NotificationGateway` *(when `enableRealtime: true`)*
|
|
207
|
-
- `NOTIFICATION_ADAPTER`
|
|
208
|
-
|
|
209
|
-
---
|
|
210
|
-
|
|
211
|
-
## Configuration Reference
|
|
212
|
-
|
|
213
|
-
```typescript
|
|
214
|
-
interface INotificationModuleConfig extends IDataSourceServiceOptions {
|
|
215
|
-
/** Enable Socket.IO WebSocket gateway (default: true) */
|
|
216
|
-
enableRealtime?: boolean;
|
|
217
|
-
|
|
218
|
-
/** JWT secret for WebSocket connection authentication */
|
|
219
|
-
jwtSecret?: string;
|
|
220
|
-
}
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
---
|
|
224
|
-
|
|
225
|
-
## Feature Toggles
|
|
226
|
-
|
|
227
|
-
| Feature | Config | Default | Effect |
|
|
228
|
-
|---------|--------|---------|--------|
|
|
229
|
-
| WebSocket gateway | `enableRealtime: true` | `true` | Registers `NotificationGateway` on `/notifications` namespace |
|
|
230
|
-
| Company scoping | `enableCompanyFeature: true` | `false` | Uses `NotificationWithCompany` entity; adds company room broadcasting |
|
|
231
|
-
| Multi-tenant | `databaseMode: 'multi-tenant'` | `'single'` | Per-tenant DataSource connections |
|
|
232
|
-
|
|
233
|
-
---
|
|
234
|
-
|
|
235
|
-
## WebSocket Gateway
|
|
236
|
-
|
|
237
|
-
The gateway runs on the `/notifications` Socket.IO namespace. JWT authentication is required on connection.
|
|
238
|
-
|
|
239
|
-
### Client Connection
|
|
240
|
-
|
|
241
|
-
**Browser (JavaScript):**
|
|
242
|
-
|
|
243
|
-
```javascript
|
|
244
|
-
import { io } from 'socket.io-client';
|
|
245
|
-
|
|
246
|
-
const socket = io('http://localhost:2002/notifications', {
|
|
247
|
-
auth: { token: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' },
|
|
248
|
-
transports: ['websocket'],
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
socket.on('connect', () => {
|
|
252
|
-
console.log('Connected to notification gateway');
|
|
253
110
|
});
|
|
254
111
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
112
|
+
// Multi-tenant
|
|
113
|
+
NotificationModule.forRootAsync({
|
|
114
|
+
global: true,
|
|
115
|
+
includeController: true,
|
|
116
|
+
bootstrapAppConfig: {
|
|
117
|
+
databaseMode: 'multi-tenant',
|
|
118
|
+
enableCompanyFeature: true,
|
|
119
|
+
},
|
|
120
|
+
imports: [ConfigModule],
|
|
121
|
+
useFactory: (config: ConfigService) => ({
|
|
122
|
+
enableRealtime: true,
|
|
123
|
+
jwtSecret: config.get('JWT_SECRET'),
|
|
124
|
+
tenantDefaultDatabaseConfig: {
|
|
125
|
+
type: 'mysql',
|
|
126
|
+
host: config.get('TENANT_DB_HOST'),
|
|
127
|
+
port: config.get<number>('TENANT_DB_PORT'),
|
|
128
|
+
username: config.get('TENANT_DB_USER'),
|
|
129
|
+
password: config.get('TENANT_DB_PASSWORD'),
|
|
130
|
+
database: config.get('TENANT_DB_NAME'),
|
|
131
|
+
},
|
|
132
|
+
tenants: config.get<ITenantDatabaseConfig[]>('TENANTS'),
|
|
133
|
+
}),
|
|
134
|
+
inject: [ConfigService],
|
|
262
135
|
});
|
|
263
136
|
```
|
|
264
137
|
|
|
265
|
-
|
|
138
|
+
## 2. Register Entities
|
|
266
139
|
|
|
267
140
|
```typescript
|
|
268
|
-
import {
|
|
269
|
-
|
|
270
|
-
@Injectable({ providedIn: 'root' })
|
|
271
|
-
export class NotificationSocketService {
|
|
272
|
-
private socket: Socket;
|
|
273
|
-
|
|
274
|
-
connect(token: string): void {
|
|
275
|
-
this.socket = io(`${environment.apiUrl}/notifications`, {
|
|
276
|
-
auth: { token: `Bearer ${token}` },
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
this.socket.on('notification', (data) => {
|
|
280
|
-
this.handleNewNotification(data);
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
disconnect(): void {
|
|
285
|
-
this.socket?.disconnect();
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
### WebSocket Events
|
|
291
|
-
|
|
292
|
-
**Server → Client (emitted by the gateway):**
|
|
293
|
-
|
|
294
|
-
| Event | Payload | Description |
|
|
295
|
-
|-------|---------|-------------|
|
|
296
|
-
| `notification` | `INotificationPayload` | New notification delivered in real-time |
|
|
297
|
-
| `notification:count` | `{ count: number }` | Updated unread count |
|
|
298
|
-
|
|
299
|
-
**Client → Server (handled by the gateway):**
|
|
300
|
-
|
|
301
|
-
| Event | Payload | Description |
|
|
302
|
-
|-------|---------|-------------|
|
|
303
|
-
| `join:company` | `{ companyId: string }` | Join a company room for broadcast notifications |
|
|
304
|
-
| `leave:company` | `{ companyId: string }` | Leave a company room |
|
|
305
|
-
| `ping` | — | Keepalive |
|
|
306
|
-
|
|
307
|
-
---
|
|
308
|
-
|
|
309
|
-
## API Endpoints
|
|
310
|
-
|
|
311
|
-
All endpoints use **POST** and require JWT authentication.
|
|
312
|
-
|
|
313
|
-
### Notifications — `POST /notification/*`
|
|
314
|
-
|
|
315
|
-
| Endpoint | Auth | Description |
|
|
316
|
-
|----------|------|-------------|
|
|
317
|
-
| `POST /notification/get-all` | JWT | Get current user's notifications |
|
|
318
|
-
| `POST /notification/get-unread-count` | JWT | Get unread notification count |
|
|
319
|
-
| `POST /notification/mark-read` | JWT | Mark a single notification as read |
|
|
320
|
-
| `POST /notification/mark-all-read` | JWT | Mark all notifications as read |
|
|
321
|
-
| `POST /notification/delete` | JWT | Delete a notification |
|
|
322
|
-
| `POST /notification/get/:id` | JWT | Get a notification by ID |
|
|
323
|
-
|
|
324
|
-
### Admin — `POST /notification/admin/*`
|
|
325
|
-
|
|
326
|
-
| Endpoint | Permission | Description |
|
|
327
|
-
|----------|-----------|-------------|
|
|
328
|
-
| `POST /notification/admin/send` | `notification.create` | Send notification to specific user(s) |
|
|
329
|
-
| `POST /notification/admin/broadcast` | `notification.create` | Broadcast to all users in a company |
|
|
330
|
-
| `POST /notification/admin/get-all` | `notification.read` | List all notifications (all users) |
|
|
331
|
-
|
|
332
|
-
**Send notification request:**
|
|
333
|
-
|
|
334
|
-
```json
|
|
335
|
-
POST /notification/admin/send
|
|
336
|
-
{
|
|
337
|
-
"userId": "uuid",
|
|
338
|
-
"title": "Order Shipped",
|
|
339
|
-
"message": "Your order #12345 has been shipped.",
|
|
340
|
-
"type": "success",
|
|
341
|
-
"data": { "orderId": "12345", "trackingUrl": "https://..." }
|
|
342
|
-
}
|
|
343
|
-
```
|
|
344
|
-
|
|
345
|
-
**Broadcast to company:**
|
|
346
|
-
|
|
347
|
-
```json
|
|
348
|
-
POST /notification/admin/broadcast
|
|
349
|
-
{
|
|
350
|
-
"companyId": "uuid",
|
|
351
|
-
"title": "System Maintenance",
|
|
352
|
-
"message": "The system will be down for maintenance at 2:00 AM.",
|
|
353
|
-
"type": "warning"
|
|
354
|
-
}
|
|
355
|
-
```
|
|
356
|
-
|
|
357
|
-
---
|
|
358
|
-
|
|
359
|
-
## Entities
|
|
360
|
-
|
|
361
|
-
### Core Entities
|
|
362
|
-
|
|
363
|
-
| Entity | Table | Description |
|
|
364
|
-
|--------|-------|-------------|
|
|
365
|
-
| `Notification` | `notification` | User notification with type, title, message, JSON data, isRead, readAt |
|
|
366
|
-
|
|
367
|
-
**Indexes:** `(userId, isRead)`, `(userId, createdAt)` for fast unread queries.
|
|
368
|
-
|
|
369
|
-
### Company Feature Entities (`enableCompanyFeature: true`)
|
|
370
|
-
|
|
371
|
-
| Entity | Table | Description |
|
|
372
|
-
|--------|-------|-------------|
|
|
373
|
-
| `NotificationWithCompany` | `notification` | Same + `companyId` column |
|
|
374
|
-
|
|
375
|
-
**Additional index:** `(companyId, userId, isRead)`
|
|
376
|
-
|
|
377
|
-
```typescript
|
|
378
|
-
import { NotificationModule } from '@flusys/nestjs-notification';
|
|
141
|
+
import { getNotificationEntitiesByConfig } from '@flusys/nestjs-notification/entities';
|
|
379
142
|
|
|
380
143
|
TypeOrmModule.forRoot({
|
|
381
144
|
entities: [
|
|
382
|
-
...
|
|
145
|
+
...getNotificationEntitiesByConfig(false), // match enableCompanyFeature in bootstrapAppConfig
|
|
383
146
|
],
|
|
384
|
-
})
|
|
147
|
+
});
|
|
385
148
|
```
|
|
386
149
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
The `NOTIFICATION_ADAPTER` token is defined in `@flusys/nestjs-shared` so any module can send notifications without importing `@flusys/nestjs-notification`.
|
|
150
|
+
| `enableCompanyFeature` | Entity registered |
|
|
151
|
+
| ---------------------- | --------------------------------------------------- |
|
|
152
|
+
| `false` | `Notification` |
|
|
153
|
+
| `true` | `NotificationWithCompany` (adds `companyId` column) |
|
|
392
154
|
|
|
393
|
-
When `NotificationModule` is registered in the app, it provides the implementation. When it is not registered, `@Optional()` prevents injection errors.
|
|
394
155
|
|
|
395
|
-
|
|
156
|
+
## 4. Send Notifications Programmatically
|
|
396
157
|
|
|
397
|
-
|
|
398
|
-
interface INotificationAdapter {
|
|
399
|
-
send(data: INotificationSendData): Promise<void>;
|
|
400
|
-
sendBulk(data: INotificationSendData[]): Promise<void>;
|
|
401
|
-
broadcast(companyId: string, data: Omit<INotificationSendData, 'userId'>): Promise<void>;
|
|
402
|
-
isUserOnline(userId: string): boolean;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
interface INotificationSendData {
|
|
406
|
-
userId: string;
|
|
407
|
-
title: string;
|
|
408
|
-
message: string;
|
|
409
|
-
type?: 'info' | 'success' | 'warning' | 'error';
|
|
410
|
-
data?: Record<string, any>;
|
|
411
|
-
companyId?: string;
|
|
412
|
-
}
|
|
413
|
-
```
|
|
414
|
-
|
|
415
|
-
**Usage pattern:**
|
|
158
|
+
Use `NotificationHelperService` (singleton) from within the same application:
|
|
416
159
|
|
|
417
160
|
```typescript
|
|
418
|
-
import {
|
|
161
|
+
import { NotificationHelperService } from '@flusys/nestjs-notification';
|
|
419
162
|
|
|
420
163
|
@Injectable()
|
|
421
164
|
export class OrderService {
|
|
422
165
|
constructor(
|
|
423
|
-
@
|
|
424
|
-
private readonly notifications
|
|
166
|
+
@Inject(NotificationHelperService)
|
|
167
|
+
private readonly notifications: NotificationHelperService,
|
|
425
168
|
) {}
|
|
426
169
|
|
|
427
|
-
async
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
// Send notification if adapter is available
|
|
431
|
-
await this.notifications?.send({
|
|
170
|
+
async afterShipment(orderId: string, userId: string): Promise<void> {
|
|
171
|
+
await this.notifications.send({
|
|
432
172
|
userId,
|
|
433
173
|
title: 'Order Shipped',
|
|
434
|
-
message: `
|
|
435
|
-
type: 'success',
|
|
436
|
-
data: { orderId },
|
|
174
|
+
message: `Order ${orderId} is on its way.`,
|
|
175
|
+
type: 'success', // 'info' | 'success' | 'warning' | 'error'
|
|
176
|
+
data: { orderId }, // optional JSON payload for the client
|
|
437
177
|
});
|
|
438
178
|
}
|
|
439
|
-
}
|
|
440
|
-
```
|
|
441
|
-
|
|
442
|
-
---
|
|
443
179
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
| `NotificationDataSourceProvider` | Dynamic DataSource per request |
|
|
453
|
-
| `NOTIFICATION_ADAPTER` | Cross-module adapter implementation |
|
|
454
|
-
|
|
455
|
-
---
|
|
456
|
-
|
|
457
|
-
## Sending Notifications Programmatically
|
|
458
|
-
|
|
459
|
-
Use `NotificationHelperService` (singleton) within the same application:
|
|
460
|
-
|
|
461
|
-
```typescript
|
|
462
|
-
import { NotificationHelperService } from '@flusys/nestjs-notification';
|
|
463
|
-
|
|
464
|
-
@Injectable()
|
|
465
|
-
export class PaymentService {
|
|
466
|
-
constructor(
|
|
467
|
-
@Inject(NotificationHelperService)
|
|
468
|
-
private readonly notificationHelper: NotificationHelperService,
|
|
469
|
-
) {}
|
|
470
|
-
|
|
471
|
-
async afterPayment(userId: string, amount: number): Promise<void> {
|
|
472
|
-
await this.notificationHelper.send({
|
|
473
|
-
userId,
|
|
474
|
-
title: 'Payment Received',
|
|
475
|
-
message: `We received your payment of $${amount}.`,
|
|
476
|
-
type: 'success',
|
|
477
|
-
data: { amount },
|
|
478
|
-
});
|
|
180
|
+
// Broadcast to all users in a company (requires enableCompanyFeature: true)
|
|
181
|
+
async broadcastMaintenance(companyId: string): Promise<void> {
|
|
182
|
+
await this.notifications.sendToCompany(
|
|
183
|
+
companyId,
|
|
184
|
+
'Scheduled Maintenance',
|
|
185
|
+
'The system will be offline at 2:00 AM.',
|
|
186
|
+
'warning',
|
|
187
|
+
);
|
|
479
188
|
}
|
|
480
189
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
190
|
+
// Send to multiple users at once
|
|
191
|
+
async notifyTeam(userIds: string[], message: string): Promise<void> {
|
|
192
|
+
await this.notifications.sendToMany({
|
|
193
|
+
userIds,
|
|
194
|
+
title: 'Team Update',
|
|
195
|
+
message,
|
|
485
196
|
type: 'info',
|
|
486
197
|
});
|
|
487
198
|
}
|
|
488
199
|
}
|
|
489
200
|
```
|
|
490
201
|
|
|
491
|
-
---
|
|
492
|
-
|
|
493
|
-
## Frontend Integration
|
|
494
|
-
|
|
495
|
-
### Unread Badge Count
|
|
496
|
-
|
|
497
|
-
```typescript
|
|
498
|
-
// Poll for unread count (or update via WebSocket event 'notification:count')
|
|
499
|
-
async getUnreadCount(): Promise<number> {
|
|
500
|
-
const response = await this.http.post('/notification/get-unread-count', {}).toPromise();
|
|
501
|
-
return response.data.count;
|
|
502
|
-
}
|
|
503
|
-
```
|
|
504
|
-
|
|
505
|
-
### Notification List
|
|
506
|
-
|
|
507
|
-
```typescript
|
|
508
|
-
async getNotifications(page = 1, pageSize = 20) {
|
|
509
|
-
return this.http.post('/notification/get-all', { page, pageSize }).toPromise();
|
|
510
|
-
}
|
|
511
|
-
```
|
|
512
|
-
|
|
513
|
-
### Mark as Read
|
|
514
|
-
|
|
515
|
-
```typescript
|
|
516
|
-
// Single
|
|
517
|
-
await this.http.post('/notification/mark-read', { id: notificationId }).toPromise();
|
|
518
|
-
|
|
519
|
-
// All
|
|
520
|
-
await this.http.post('/notification/mark-all-read', {}).toPromise();
|
|
521
|
-
```
|
|
522
|
-
|
|
523
|
-
---
|
|
524
|
-
|
|
525
|
-
## Troubleshooting
|
|
526
|
-
|
|
527
|
-
**WebSocket connection rejected (401)**
|
|
528
|
-
|
|
529
|
-
The JWT token in the `auth.token` field is invalid or expired. Ensure you pass `Bearer <token>` format:
|
|
530
|
-
|
|
531
|
-
```javascript
|
|
532
|
-
auth: { token: `Bearer ${accessToken}` }
|
|
533
|
-
```
|
|
534
|
-
|
|
535
|
-
---
|
|
536
|
-
|
|
537
|
-
**Notifications delivered via REST but not real-time**
|
|
538
|
-
|
|
539
|
-
Check that `enableRealtime: true` is set in the config and `@nestjs/platform-socket.io` is installed. Also verify that the client is connecting to the correct namespace (`/notifications`).
|
|
540
|
-
|
|
541
|
-
---
|
|
542
|
-
|
|
543
|
-
**Company broadcast not reaching all users**
|
|
544
|
-
|
|
545
|
-
Users must join the company room via the `join:company` WebSocket event after connecting. If `enableCompanyFeature: false`, company rooms are not created.
|
|
546
|
-
|
|
547
|
-
---
|
|
548
|
-
|
|
549
|
-
**`NOTIFICATION_ADAPTER` is `undefined`**
|
|
550
|
-
|
|
551
|
-
`NotificationModule` is not registered. Use `@Optional() @Inject(NOTIFICATION_ADAPTER)` so the injection doesn't throw when the module is absent.
|
|
552
|
-
|
|
553
|
-
---
|
|
554
|
-
|
|
555
|
-
**`No metadata for entity`**
|
|
556
|
-
|
|
557
|
-
Register entities in `TypeOrmModule`:
|
|
558
|
-
```typescript
|
|
559
|
-
entities: [...NotificationModule.getEntities({ enableCompanyFeature: true })]
|
|
560
|
-
```
|
|
561
|
-
|
|
562
|
-
---
|
|
563
|
-
|
|
564
202
|
## License
|
|
565
203
|
|
|
566
204
|
MIT © FLUSYS
|
|
567
|
-
|
|
568
|
-
---
|
|
569
|
-
|
|
570
|
-
> Part of the **FLUSYS** framework — a full-stack monorepo powering Angular 21 + NestJS 11 applications.
|
|
@@ -3,37 +3,17 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
get: Object.getOwnPropertyDescriptor(all, name).get
|
|
10
|
-
});
|
|
11
|
-
}
|
|
12
|
-
_export(exports, {
|
|
13
|
-
get NOTIFICATION_MESSAGES () {
|
|
6
|
+
Object.defineProperty(exports, "NOTIFICATION_MESSAGES", {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
get: function() {
|
|
14
9
|
return NOTIFICATION_MESSAGES;
|
|
15
|
-
},
|
|
16
|
-
get NOTIFICATION_MODULE_MESSAGES () {
|
|
17
|
-
return NOTIFICATION_MODULE_MESSAGES;
|
|
18
10
|
}
|
|
19
11
|
});
|
|
20
12
|
const NOTIFICATION_MESSAGES = {
|
|
21
|
-
CREATE_SUCCESS: 'notification.create.success',
|
|
22
|
-
CREATE_MANY_SUCCESS: 'notification.create.many.success',
|
|
23
|
-
GET_SUCCESS: 'notification.get.success',
|
|
24
|
-
GET_ALL_SUCCESS: 'notification.get.all.success',
|
|
25
|
-
UPDATE_SUCCESS: 'notification.update.success',
|
|
26
|
-
UPDATE_MANY_SUCCESS: 'notification.update.many.success',
|
|
27
|
-
DELETE_SUCCESS: 'notification.delete.success',
|
|
28
|
-
RESTORE_SUCCESS: 'notification.restore.success',
|
|
29
13
|
NOT_FOUND: 'notification.not.found',
|
|
30
14
|
MARK_READ_SUCCESS: 'notification.mark.read.success',
|
|
31
15
|
MARK_ALL_READ_SUCCESS: 'notification.mark.all.read.success',
|
|
32
16
|
UNREAD_COUNT_SUCCESS: 'notification.unread.count.success',
|
|
33
|
-
MY_NOTIFICATIONS_SUCCESS: 'notification.my.notifications.success',
|
|
34
17
|
SEND_SUCCESS: 'notification.send.success',
|
|
35
18
|
BROADCAST_SUCCESS: 'notification.broadcast.success'
|
|
36
19
|
};
|
|
37
|
-
const NOTIFICATION_MODULE_MESSAGES = {
|
|
38
|
-
NOTIFICATION: NOTIFICATION_MESSAGES
|
|
39
|
-
};
|
|
@@ -8,12 +8,47 @@ Object.defineProperty(exports, "notificationSwaggerConfig", {
|
|
|
8
8
|
return notificationSwaggerConfig;
|
|
9
9
|
}
|
|
10
10
|
});
|
|
11
|
-
|
|
11
|
+
/** Schema properties to exclude when company feature is disabled */ const COMPANY_SCHEMA_EXCLUSIONS = [
|
|
12
|
+
{
|
|
13
|
+
schemaName: 'NotificationResponseDto',
|
|
14
|
+
properties: [
|
|
15
|
+
'companyId'
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
schemaName: 'CreateNotificationDto',
|
|
20
|
+
properties: [
|
|
21
|
+
'companyId'
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
schemaName: 'SendNotificationDto',
|
|
26
|
+
properties: [
|
|
27
|
+
'companyId'
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
schemaName: 'SendBulkNotificationDto',
|
|
32
|
+
properties: [
|
|
33
|
+
'companyId'
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
schemaName: 'UpdateNotificationDto',
|
|
38
|
+
properties: [
|
|
39
|
+
'companyId'
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
];
|
|
43
|
+
function notificationSwaggerConfig(bootstrapConfig) {
|
|
44
|
+
const enableCompanyFeature = bootstrapConfig?.enableCompanyFeature ?? true;
|
|
45
|
+
const isMultiTenant = bootstrapConfig?.databaseMode === 'multi-tenant';
|
|
46
|
+
const multiTenantNote = isMultiTenant ? `\n> **Multi-Tenant Mode**: Include \`x-tenant-id\` header to target a specific tenant database.\n` : '';
|
|
12
47
|
return {
|
|
13
48
|
title: 'Notification API',
|
|
14
49
|
description: `
|
|
15
50
|
## Notification API
|
|
16
|
-
|
|
51
|
+
${multiTenantNote}
|
|
17
52
|
Real-time notification system with WebSocket support.
|
|
18
53
|
|
|
19
54
|
### Features
|
|
@@ -56,6 +91,7 @@ ws://localhost:2002/notification?token=<jwt>
|
|
|
56
91
|
`,
|
|
57
92
|
version: '1.0',
|
|
58
93
|
path: 'api/docs/notification',
|
|
59
|
-
bearerAuth: true
|
|
94
|
+
bearerAuth: true,
|
|
95
|
+
excludeSchemaProperties: enableCompanyFeature ? undefined : COMPANY_SCHEMA_EXCLUSIONS
|
|
60
96
|
};
|
|
61
97
|
}
|
|
@@ -8,9 +8,10 @@ Object.defineProperty(exports, "Notification", {
|
|
|
8
8
|
return Notification;
|
|
9
9
|
}
|
|
10
10
|
});
|
|
11
|
+
const _nestjsshared = require("@flusys/nestjs-shared");
|
|
11
12
|
const _entities = require("@flusys/nestjs-shared/entities");
|
|
12
|
-
const _typeorm = require("typeorm");
|
|
13
13
|
const _enums = require("@flusys/nestjs-shared/enums");
|
|
14
|
+
const _typeorm = require("typeorm");
|
|
14
15
|
function _define_property(obj, key, value) {
|
|
15
16
|
if (key in obj) {
|
|
16
17
|
Object.defineProperty(obj, key, {
|
|
@@ -73,7 +74,7 @@ _ts_decorate([
|
|
|
73
74
|
], Notification.prototype, "message", void 0);
|
|
74
75
|
_ts_decorate([
|
|
75
76
|
(0, _typeorm.Column)({
|
|
76
|
-
type:
|
|
77
|
+
type: (0, _nestjsshared.getJsonColumnType)(),
|
|
77
78
|
nullable: true
|
|
78
79
|
}),
|
|
79
80
|
_ts_metadata("design:type", Object)
|
|
@@ -10,8 +10,8 @@ Object.defineProperty(exports, "NotificationGateway", {
|
|
|
10
10
|
});
|
|
11
11
|
const _common = require("@nestjs/common");
|
|
12
12
|
const _websockets = require("@nestjs/websockets");
|
|
13
|
-
const _socketio = require("socket.io");
|
|
14
13
|
const _jsonwebtoken = /*#__PURE__*/ _interop_require_wildcard(require("jsonwebtoken"));
|
|
14
|
+
const _socketio = require("socket.io");
|
|
15
15
|
const _notificationconfigservice = require("../services/notification-config.service");
|
|
16
16
|
function _define_property(obj, key, value) {
|
|
17
17
|
if (key in obj) {
|
|
@@ -107,6 +107,7 @@ let NotificationGateway = class NotificationGateway {
|
|
|
107
107
|
client.join(`user:${userId}`);
|
|
108
108
|
if (payload.companyId) {
|
|
109
109
|
client.join(`company:${payload.companyId}`);
|
|
110
|
+
client.join(`user:${userId}:company:${payload.companyId}`);
|
|
110
111
|
}
|
|
111
112
|
} catch {
|
|
112
113
|
client.disconnect();
|
|
@@ -126,33 +127,20 @@ let NotificationGateway = class NotificationGateway {
|
|
|
126
127
|
this.server?.to(`user:${userId}`).emit(event, data);
|
|
127
128
|
}
|
|
128
129
|
/**
|
|
129
|
-
* Emit to a
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
* @param companyId - The company ID (if null/undefined, emits to all user's sockets)
|
|
135
|
-
* @param event - The event name
|
|
136
|
-
* @param data - The payload
|
|
130
|
+
* Emit to a user scoped to a specific company session.
|
|
131
|
+
* Uses the combined room `user:{id}:company:{cid}` so only sockets
|
|
132
|
+
* authenticated under that company receive the event — critical for
|
|
133
|
+
* users logged in across multiple companies on different devices.
|
|
134
|
+
* Falls back to all user sockets when companyId is absent.
|
|
137
135
|
*/ emitToUserInCompany(userId, companyId, event, data) {
|
|
138
|
-
if (!this.server
|
|
136
|
+
if (!this.server) {
|
|
139
137
|
return;
|
|
140
138
|
}
|
|
141
139
|
if (!companyId) {
|
|
142
140
|
this.emitToUser(userId, event, data);
|
|
143
141
|
return;
|
|
144
142
|
}
|
|
145
|
-
|
|
146
|
-
const sockets = this.server.sockets.adapter.rooms.get(userRoom);
|
|
147
|
-
if (!sockets) {
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
for (const socketId of sockets){
|
|
151
|
-
const socket = this.server.sockets.sockets.get(socketId);
|
|
152
|
-
if (socket && socket.data.companyId === companyId) {
|
|
153
|
-
socket.emit(event, data);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
143
|
+
this.server.to(`user:${userId}:company:${companyId}`).emit(event, data);
|
|
156
144
|
}
|
|
157
145
|
emitToCompany(companyId, event, data) {
|
|
158
146
|
this.server?.to(`company:${companyId}`).emit(event, data);
|
|
@@ -8,13 +8,13 @@ Object.defineProperty(exports, "NotificationService", {
|
|
|
8
8
|
return NotificationService;
|
|
9
9
|
}
|
|
10
10
|
});
|
|
11
|
-
const _classes = require("@flusys/nestjs-shared/classes");
|
|
12
11
|
const _nestjsshared = require("@flusys/nestjs-shared");
|
|
13
|
-
const
|
|
12
|
+
const _classes = require("@flusys/nestjs-shared/classes");
|
|
14
13
|
const _interfaces = require("@flusys/nestjs-shared/interfaces");
|
|
15
14
|
const _modules = require("@flusys/nestjs-shared/modules");
|
|
16
15
|
const _utils = require("@flusys/nestjs-shared/utils");
|
|
17
16
|
const _common = require("@nestjs/common");
|
|
17
|
+
const _config = require("../config");
|
|
18
18
|
const _entities = require("../entities");
|
|
19
19
|
const _gateways = require("../gateways");
|
|
20
20
|
const _notificationconfigservice = require("./notification-config.service");
|
|
@@ -160,10 +160,14 @@ let NotificationService = class NotificationService extends _classes.RequestScop
|
|
|
160
160
|
}
|
|
161
161
|
async markAllAsRead(user) {
|
|
162
162
|
await this.ensureRepositoryInitialized();
|
|
163
|
-
const
|
|
163
|
+
const where = {
|
|
164
164
|
userId: user.id,
|
|
165
165
|
isRead: false
|
|
166
|
-
}
|
|
166
|
+
};
|
|
167
|
+
if (this.notificationConfig.isCompanyFeatureEnabled() && user.companyId) {
|
|
168
|
+
where.companyId = user.companyId;
|
|
169
|
+
}
|
|
170
|
+
const result = await this.repository.update(where, {
|
|
167
171
|
isRead: true,
|
|
168
172
|
readAt: new Date()
|
|
169
173
|
});
|
|
@@ -174,11 +178,15 @@ let NotificationService = class NotificationService extends _classes.RequestScop
|
|
|
174
178
|
}
|
|
175
179
|
async getUnreadCount(user) {
|
|
176
180
|
await this.ensureRepositoryInitialized();
|
|
181
|
+
const where = {
|
|
182
|
+
userId: user.id,
|
|
183
|
+
isRead: false
|
|
184
|
+
};
|
|
185
|
+
if (this.notificationConfig.isCompanyFeatureEnabled() && user.companyId) {
|
|
186
|
+
where.companyId = user.companyId;
|
|
187
|
+
}
|
|
177
188
|
return this.repository.count({
|
|
178
|
-
where
|
|
179
|
-
userId: user.id,
|
|
180
|
-
isRead: false
|
|
181
|
-
}
|
|
189
|
+
where
|
|
182
190
|
});
|
|
183
191
|
}
|
|
184
192
|
toPayload(entity) {
|
package/config/message-keys.d.ts
CHANGED
|
@@ -1,36 +1,8 @@
|
|
|
1
1
|
export declare const NOTIFICATION_MESSAGES: {
|
|
2
|
-
readonly CREATE_SUCCESS: "notification.create.success";
|
|
3
|
-
readonly CREATE_MANY_SUCCESS: "notification.create.many.success";
|
|
4
|
-
readonly GET_SUCCESS: "notification.get.success";
|
|
5
|
-
readonly GET_ALL_SUCCESS: "notification.get.all.success";
|
|
6
|
-
readonly UPDATE_SUCCESS: "notification.update.success";
|
|
7
|
-
readonly UPDATE_MANY_SUCCESS: "notification.update.many.success";
|
|
8
|
-
readonly DELETE_SUCCESS: "notification.delete.success";
|
|
9
|
-
readonly RESTORE_SUCCESS: "notification.restore.success";
|
|
10
2
|
readonly NOT_FOUND: "notification.not.found";
|
|
11
3
|
readonly MARK_READ_SUCCESS: "notification.mark.read.success";
|
|
12
4
|
readonly MARK_ALL_READ_SUCCESS: "notification.mark.all.read.success";
|
|
13
5
|
readonly UNREAD_COUNT_SUCCESS: "notification.unread.count.success";
|
|
14
|
-
readonly MY_NOTIFICATIONS_SUCCESS: "notification.my.notifications.success";
|
|
15
6
|
readonly SEND_SUCCESS: "notification.send.success";
|
|
16
7
|
readonly BROADCAST_SUCCESS: "notification.broadcast.success";
|
|
17
8
|
};
|
|
18
|
-
export declare const NOTIFICATION_MODULE_MESSAGES: {
|
|
19
|
-
readonly NOTIFICATION: {
|
|
20
|
-
readonly CREATE_SUCCESS: "notification.create.success";
|
|
21
|
-
readonly CREATE_MANY_SUCCESS: "notification.create.many.success";
|
|
22
|
-
readonly GET_SUCCESS: "notification.get.success";
|
|
23
|
-
readonly GET_ALL_SUCCESS: "notification.get.all.success";
|
|
24
|
-
readonly UPDATE_SUCCESS: "notification.update.success";
|
|
25
|
-
readonly UPDATE_MANY_SUCCESS: "notification.update.many.success";
|
|
26
|
-
readonly DELETE_SUCCESS: "notification.delete.success";
|
|
27
|
-
readonly RESTORE_SUCCESS: "notification.restore.success";
|
|
28
|
-
readonly NOT_FOUND: "notification.not.found";
|
|
29
|
-
readonly MARK_READ_SUCCESS: "notification.mark.read.success";
|
|
30
|
-
readonly MARK_ALL_READ_SUCCESS: "notification.mark.all.read.success";
|
|
31
|
-
readonly UNREAD_COUNT_SUCCESS: "notification.unread.count.success";
|
|
32
|
-
readonly MY_NOTIFICATIONS_SUCCESS: "notification.my.notifications.success";
|
|
33
|
-
readonly SEND_SUCCESS: "notification.send.success";
|
|
34
|
-
readonly BROADCAST_SUCCESS: "notification.broadcast.success";
|
|
35
|
-
};
|
|
36
|
-
};
|
|
@@ -1,2 +1,3 @@
|
|
|
1
|
+
import { IBootstrapAppConfig } from '@flusys/nestjs-core';
|
|
1
2
|
import { IModuleSwaggerOptions } from '@flusys/nestjs-core/docs';
|
|
2
|
-
export declare function notificationSwaggerConfig(): IModuleSwaggerOptions;
|
|
3
|
+
export declare function notificationSwaggerConfig(bootstrapConfig?: IBootstrapAppConfig): IModuleSwaggerOptions;
|
|
@@ -1,22 +1,9 @@
|
|
|
1
1
|
// ==================== NOTIFICATION MODULE MESSAGE KEYS ====================
|
|
2
2
|
export const NOTIFICATION_MESSAGES = {
|
|
3
|
-
CREATE_SUCCESS: 'notification.create.success',
|
|
4
|
-
CREATE_MANY_SUCCESS: 'notification.create.many.success',
|
|
5
|
-
GET_SUCCESS: 'notification.get.success',
|
|
6
|
-
GET_ALL_SUCCESS: 'notification.get.all.success',
|
|
7
|
-
UPDATE_SUCCESS: 'notification.update.success',
|
|
8
|
-
UPDATE_MANY_SUCCESS: 'notification.update.many.success',
|
|
9
|
-
DELETE_SUCCESS: 'notification.delete.success',
|
|
10
|
-
RESTORE_SUCCESS: 'notification.restore.success',
|
|
11
3
|
NOT_FOUND: 'notification.not.found',
|
|
12
4
|
MARK_READ_SUCCESS: 'notification.mark.read.success',
|
|
13
5
|
MARK_ALL_READ_SUCCESS: 'notification.mark.all.read.success',
|
|
14
6
|
UNREAD_COUNT_SUCCESS: 'notification.unread.count.success',
|
|
15
|
-
MY_NOTIFICATIONS_SUCCESS: 'notification.my.notifications.success',
|
|
16
7
|
SEND_SUCCESS: 'notification.send.success',
|
|
17
8
|
BROADCAST_SUCCESS: 'notification.broadcast.success'
|
|
18
9
|
};
|
|
19
|
-
// Aggregated export for backward compatibility
|
|
20
|
-
export const NOTIFICATION_MODULE_MESSAGES = {
|
|
21
|
-
NOTIFICATION: NOTIFICATION_MESSAGES
|
|
22
|
-
};
|
|
@@ -1,9 +1,44 @@
|
|
|
1
|
-
|
|
1
|
+
/** Schema properties to exclude when company feature is disabled */ const COMPANY_SCHEMA_EXCLUSIONS = [
|
|
2
|
+
{
|
|
3
|
+
schemaName: 'NotificationResponseDto',
|
|
4
|
+
properties: [
|
|
5
|
+
'companyId'
|
|
6
|
+
]
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
schemaName: 'CreateNotificationDto',
|
|
10
|
+
properties: [
|
|
11
|
+
'companyId'
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
schemaName: 'SendNotificationDto',
|
|
16
|
+
properties: [
|
|
17
|
+
'companyId'
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
schemaName: 'SendBulkNotificationDto',
|
|
22
|
+
properties: [
|
|
23
|
+
'companyId'
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
schemaName: 'UpdateNotificationDto',
|
|
28
|
+
properties: [
|
|
29
|
+
'companyId'
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
];
|
|
33
|
+
export function notificationSwaggerConfig(bootstrapConfig) {
|
|
34
|
+
const enableCompanyFeature = bootstrapConfig?.enableCompanyFeature ?? true;
|
|
35
|
+
const isMultiTenant = bootstrapConfig?.databaseMode === 'multi-tenant';
|
|
36
|
+
const multiTenantNote = isMultiTenant ? `\n> **Multi-Tenant Mode**: Include \`x-tenant-id\` header to target a specific tenant database.\n` : '';
|
|
2
37
|
return {
|
|
3
38
|
title: 'Notification API',
|
|
4
39
|
description: `
|
|
5
40
|
## Notification API
|
|
6
|
-
|
|
41
|
+
${multiTenantNote}
|
|
7
42
|
Real-time notification system with WebSocket support.
|
|
8
43
|
|
|
9
44
|
### Features
|
|
@@ -46,6 +81,7 @@ ws://localhost:2002/notification?token=<jwt>
|
|
|
46
81
|
`,
|
|
47
82
|
version: '1.0',
|
|
48
83
|
path: 'api/docs/notification',
|
|
49
|
-
bearerAuth: true
|
|
84
|
+
bearerAuth: true,
|
|
85
|
+
excludeSchemaProperties: enableCompanyFeature ? undefined : COMPANY_SCHEMA_EXCLUSIONS
|
|
50
86
|
};
|
|
51
87
|
}
|
|
@@ -20,9 +20,10 @@ function _ts_decorate(decorators, target, key, desc) {
|
|
|
20
20
|
function _ts_metadata(k, v) {
|
|
21
21
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
22
22
|
}
|
|
23
|
+
import { getJsonColumnType } from '@flusys/nestjs-shared';
|
|
23
24
|
import { Identity } from '@flusys/nestjs-shared/entities';
|
|
24
|
-
import { Column, Entity, Index } from 'typeorm';
|
|
25
25
|
import { NotificationType } from '@flusys/nestjs-shared/enums';
|
|
26
|
+
import { Column, Entity, Index } from 'typeorm';
|
|
26
27
|
export class Notification extends Identity {
|
|
27
28
|
constructor(...args){
|
|
28
29
|
super(...args), _define_property(this, "userId", void 0), _define_property(this, "type", void 0), _define_property(this, "title", void 0), _define_property(this, "message", void 0), _define_property(this, "data", void 0), _define_property(this, "isRead", void 0), _define_property(this, "readAt", void 0);
|
|
@@ -63,7 +64,7 @@ _ts_decorate([
|
|
|
63
64
|
], Notification.prototype, "message", void 0);
|
|
64
65
|
_ts_decorate([
|
|
65
66
|
Column({
|
|
66
|
-
type:
|
|
67
|
+
type: getJsonColumnType(),
|
|
67
68
|
nullable: true
|
|
68
69
|
}),
|
|
69
70
|
_ts_metadata("design:type", Object)
|
|
@@ -27,8 +27,8 @@ function _ts_param(paramIndex, decorator) {
|
|
|
27
27
|
}
|
|
28
28
|
import { Inject } from '@nestjs/common';
|
|
29
29
|
import { WebSocketGateway, WebSocketServer } from '@nestjs/websockets';
|
|
30
|
-
import { Server } from 'socket.io';
|
|
31
30
|
import * as jwt from 'jsonwebtoken';
|
|
31
|
+
import { Server } from 'socket.io';
|
|
32
32
|
import { NotificationConfigService } from '../services/notification-config.service';
|
|
33
33
|
export class NotificationGateway {
|
|
34
34
|
afterInit() {
|
|
@@ -56,6 +56,7 @@ export class NotificationGateway {
|
|
|
56
56
|
client.join(`user:${userId}`);
|
|
57
57
|
if (payload.companyId) {
|
|
58
58
|
client.join(`company:${payload.companyId}`);
|
|
59
|
+
client.join(`user:${userId}:company:${payload.companyId}`);
|
|
59
60
|
}
|
|
60
61
|
} catch {
|
|
61
62
|
client.disconnect();
|
|
@@ -75,33 +76,20 @@ export class NotificationGateway {
|
|
|
75
76
|
this.server?.to(`user:${userId}`).emit(event, data);
|
|
76
77
|
}
|
|
77
78
|
/**
|
|
78
|
-
* Emit to a
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
* @param companyId - The company ID (if null/undefined, emits to all user's sockets)
|
|
84
|
-
* @param event - The event name
|
|
85
|
-
* @param data - The payload
|
|
79
|
+
* Emit to a user scoped to a specific company session.
|
|
80
|
+
* Uses the combined room `user:{id}:company:{cid}` so only sockets
|
|
81
|
+
* authenticated under that company receive the event — critical for
|
|
82
|
+
* users logged in across multiple companies on different devices.
|
|
83
|
+
* Falls back to all user sockets when companyId is absent.
|
|
86
84
|
*/ emitToUserInCompany(userId, companyId, event, data) {
|
|
87
|
-
if (!this.server
|
|
85
|
+
if (!this.server) {
|
|
88
86
|
return;
|
|
89
87
|
}
|
|
90
88
|
if (!companyId) {
|
|
91
89
|
this.emitToUser(userId, event, data);
|
|
92
90
|
return;
|
|
93
91
|
}
|
|
94
|
-
|
|
95
|
-
const sockets = this.server.sockets.adapter.rooms.get(userRoom);
|
|
96
|
-
if (!sockets) {
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
for (const socketId of sockets){
|
|
100
|
-
const socket = this.server.sockets.sockets.get(socketId);
|
|
101
|
-
if (socket && socket.data.companyId === companyId) {
|
|
102
|
-
socket.emit(event, data);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
92
|
+
this.server.to(`user:${userId}:company:${companyId}`).emit(event, data);
|
|
105
93
|
}
|
|
106
94
|
emitToCompany(companyId, event, data) {
|
|
107
95
|
this.server?.to(`company:${companyId}`).emit(event, data);
|
|
@@ -25,13 +25,13 @@ function _ts_param(paramIndex, decorator) {
|
|
|
25
25
|
decorator(target, key, paramIndex);
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
-
import { HybridCache, RequestScopedApiService } from '@flusys/nestjs-shared/classes';
|
|
29
28
|
import { LogAction } from '@flusys/nestjs-shared';
|
|
30
|
-
import {
|
|
29
|
+
import { HybridCache, RequestScopedApiService } from '@flusys/nestjs-shared/classes';
|
|
31
30
|
import { ILoggedUserInfo } from '@flusys/nestjs-shared/interfaces';
|
|
32
31
|
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
33
32
|
import { applyCompanyFilter } from '@flusys/nestjs-shared/utils';
|
|
34
33
|
import { Inject, Injectable, NotFoundException, Optional, Scope } from '@nestjs/common';
|
|
34
|
+
import { NOTIFICATION_MESSAGES } from '../config';
|
|
35
35
|
import { Notification, NotificationWithCompany } from '../entities';
|
|
36
36
|
import { NotificationGateway } from '../gateways';
|
|
37
37
|
import { NotificationConfigService } from './notification-config.service';
|
|
@@ -150,10 +150,14 @@ export class NotificationService extends RequestScopedApiService {
|
|
|
150
150
|
}
|
|
151
151
|
async markAllAsRead(user) {
|
|
152
152
|
await this.ensureRepositoryInitialized();
|
|
153
|
-
const
|
|
153
|
+
const where = {
|
|
154
154
|
userId: user.id,
|
|
155
155
|
isRead: false
|
|
156
|
-
}
|
|
156
|
+
};
|
|
157
|
+
if (this.notificationConfig.isCompanyFeatureEnabled() && user.companyId) {
|
|
158
|
+
where.companyId = user.companyId;
|
|
159
|
+
}
|
|
160
|
+
const result = await this.repository.update(where, {
|
|
157
161
|
isRead: true,
|
|
158
162
|
readAt: new Date()
|
|
159
163
|
});
|
|
@@ -164,11 +168,15 @@ export class NotificationService extends RequestScopedApiService {
|
|
|
164
168
|
}
|
|
165
169
|
async getUnreadCount(user) {
|
|
166
170
|
await this.ensureRepositoryInitialized();
|
|
171
|
+
const where = {
|
|
172
|
+
userId: user.id,
|
|
173
|
+
isRead: false
|
|
174
|
+
};
|
|
175
|
+
if (this.notificationConfig.isCompanyFeatureEnabled() && user.companyId) {
|
|
176
|
+
where.companyId = user.companyId;
|
|
177
|
+
}
|
|
167
178
|
return this.repository.count({
|
|
168
|
-
where
|
|
169
|
-
userId: user.id,
|
|
170
|
-
isRead: false
|
|
171
|
-
}
|
|
179
|
+
where
|
|
172
180
|
});
|
|
173
181
|
}
|
|
174
182
|
toPayload(entity) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flusys/nestjs-notification",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.1",
|
|
4
4
|
"description": "Notification module with real-time Socket.IO support",
|
|
5
5
|
"main": "cjs/index.js",
|
|
6
6
|
"module": "fesm/index.js",
|
|
@@ -92,8 +92,8 @@
|
|
|
92
92
|
"express": "^4.18.0"
|
|
93
93
|
},
|
|
94
94
|
"dependencies": {
|
|
95
|
-
"@flusys/nestjs-core": "
|
|
96
|
-
"@flusys/nestjs-shared": "
|
|
95
|
+
"@flusys/nestjs-core": "5.0.1",
|
|
96
|
+
"@flusys/nestjs-shared": "5.0.1",
|
|
97
97
|
"jsonwebtoken": "^9.0.0"
|
|
98
98
|
}
|
|
99
99
|
}
|