@flusys/nestjs-notification 4.0.2 → 4.1.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 +343 -771
- package/controllers/notification.controller.d.ts +5 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,99 +1,92 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @flusys/nestjs-notification
|
|
2
2
|
|
|
3
|
-
>
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
> Real-time notification system for NestJS — WebSocket via Socket.IO, persistent PostgreSQL storage, read/unread tracking, company broadcasting, and a cross-module adapter for dependency-free integration.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@flusys/nestjs-notification)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://nestjs.com/)
|
|
8
|
+
[](https://www.typescriptlang.org/)
|
|
9
|
+
[](https://nodejs.org/)
|
|
10
|
+
|
|
11
|
+
---
|
|
6
12
|
|
|
7
13
|
## Table of Contents
|
|
8
14
|
|
|
9
15
|
- [Overview](#overview)
|
|
16
|
+
- [Features](#features)
|
|
17
|
+
- [Compatibility](#compatibility)
|
|
10
18
|
- [Installation](#installation)
|
|
11
|
-
- [
|
|
12
|
-
- [Module
|
|
13
|
-
- [
|
|
14
|
-
- [
|
|
15
|
-
- [
|
|
16
|
-
- [
|
|
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)
|
|
17
25
|
- [WebSocket Gateway](#websocket-gateway)
|
|
18
|
-
- [
|
|
26
|
+
- [Client Connection](#client-connection)
|
|
27
|
+
- [WebSocket Events](#websocket-events)
|
|
19
28
|
- [API Endpoints](#api-endpoints)
|
|
20
|
-
- [
|
|
21
|
-
- [
|
|
22
|
-
- [
|
|
23
|
-
- [
|
|
24
|
-
- [
|
|
25
|
-
- [
|
|
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)
|
|
26
36
|
|
|
27
37
|
---
|
|
28
38
|
|
|
29
39
|
## Overview
|
|
30
40
|
|
|
31
|
-
`@flusys/nestjs-notification`
|
|
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.
|
|
32
42
|
|
|
33
|
-
|
|
34
|
-
- **Bulk Notifications** - Send to multiple users simultaneously
|
|
35
|
-
- **Company Broadcasts** - Send to all users in a company
|
|
36
|
-
- **Real-time Delivery** - WebSocket support via Socket.io
|
|
37
|
-
- **Read Tracking** - Mark notifications as read with timestamps
|
|
38
|
-
- **Unread Count** - Get user's unread notification count
|
|
39
|
-
- **Multi-Tenant Support** - Company-scoped notifications
|
|
43
|
+
---
|
|
40
44
|
|
|
41
|
-
|
|
45
|
+
## Features
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
50
58
|
|
|
51
59
|
---
|
|
52
60
|
|
|
53
|
-
##
|
|
61
|
+
## Compatibility
|
|
54
62
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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` |
|
|
61
72
|
|
|
62
73
|
---
|
|
63
74
|
|
|
64
|
-
##
|
|
75
|
+
## Installation
|
|
65
76
|
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
|
|
77
|
+
```bash
|
|
78
|
+
npm install @flusys/nestjs-notification @flusys/nestjs-shared @flusys/nestjs-core
|
|
79
|
+
npm install @nestjs/platform-socket.io socket.io
|
|
69
80
|
```
|
|
70
81
|
|
|
71
82
|
---
|
|
72
83
|
|
|
73
|
-
##
|
|
84
|
+
## Quick Start
|
|
74
85
|
|
|
75
|
-
###
|
|
76
|
-
|
|
77
|
-
```typescript
|
|
78
|
-
interface INotificationModuleConfig extends IDataSourceServiceOptions {
|
|
79
|
-
enableRealtime?: boolean; // WebSocket support (default: true)
|
|
80
|
-
jwtSecret?: string; // For WebSocket authentication
|
|
81
|
-
defaultDatabaseConfig?: IDatabaseConfig;
|
|
82
|
-
tenantDefaultDatabaseConfig?: IDatabaseConfig;
|
|
83
|
-
tenants?: ITenantDatabaseConfig[];
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
interface NotificationModuleOptions extends IDynamicModuleConfig {
|
|
87
|
-
bootstrapAppConfig?: IBootstrapAppConfig;
|
|
88
|
-
config?: INotificationModuleConfig;
|
|
89
|
-
global?: boolean;
|
|
90
|
-
includeController?: boolean;
|
|
91
|
-
}
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
### Basic Setup
|
|
86
|
+
### With Realtime Enabled
|
|
95
87
|
|
|
96
88
|
```typescript
|
|
89
|
+
import { Module } from '@nestjs/common';
|
|
97
90
|
import { NotificationModule } from '@flusys/nestjs-notification';
|
|
98
91
|
|
|
99
92
|
@Module({
|
|
@@ -110,11 +103,11 @@ import { NotificationModule } from '@flusys/nestjs-notification';
|
|
|
110
103
|
jwtSecret: process.env.JWT_SECRET,
|
|
111
104
|
defaultDatabaseConfig: {
|
|
112
105
|
type: 'postgres',
|
|
113
|
-
host:
|
|
114
|
-
port: 5432,
|
|
115
|
-
username:
|
|
116
|
-
password:
|
|
117
|
-
database:
|
|
106
|
+
host: process.env.DB_HOST,
|
|
107
|
+
port: Number(process.env.DB_PORT ?? 5432),
|
|
108
|
+
username: process.env.DB_USER,
|
|
109
|
+
password: process.env.DB_PASSWORD,
|
|
110
|
+
database: process.env.DB_NAME,
|
|
118
111
|
},
|
|
119
112
|
},
|
|
120
113
|
}),
|
|
@@ -123,27 +116,64 @@ import { NotificationModule } from '@flusys/nestjs-notification';
|
|
|
123
116
|
export class AppModule {}
|
|
124
117
|
```
|
|
125
118
|
|
|
126
|
-
###
|
|
119
|
+
### Using NOTIFICATION_ADAPTER in Another Module
|
|
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)
|
|
127
149
|
|
|
128
150
|
```typescript
|
|
129
151
|
NotificationModule.forRoot({
|
|
130
|
-
global
|
|
131
|
-
includeController: true
|
|
132
|
-
bootstrapAppConfig
|
|
133
|
-
databaseMode: 'single'
|
|
134
|
-
enableCompanyFeature:
|
|
135
|
-
}
|
|
136
|
-
config
|
|
137
|
-
enableRealtime: true
|
|
138
|
-
jwtSecret
|
|
139
|
-
defaultDatabaseConfig
|
|
140
|
-
|
|
141
|
-
}
|
|
152
|
+
global?: boolean;
|
|
153
|
+
includeController?: boolean; // Default: true
|
|
154
|
+
bootstrapAppConfig?: {
|
|
155
|
+
databaseMode: 'single' | 'multi-tenant';
|
|
156
|
+
enableCompanyFeature: boolean;
|
|
157
|
+
};
|
|
158
|
+
config?: {
|
|
159
|
+
enableRealtime?: boolean; // Default: true — registers Socket.IO gateway
|
|
160
|
+
jwtSecret?: string; // Required for WebSocket JWT authentication
|
|
161
|
+
defaultDatabaseConfig?: IDatabaseConfig;
|
|
162
|
+
tenants?: ITenantDatabaseConfig[];
|
|
163
|
+
};
|
|
164
|
+
})
|
|
142
165
|
```
|
|
143
166
|
|
|
144
|
-
|
|
167
|
+
**Behavior:**
|
|
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)
|
|
145
173
|
|
|
146
174
|
```typescript
|
|
175
|
+
import { ConfigService } from '@nestjs/config';
|
|
176
|
+
|
|
147
177
|
NotificationModule.forRootAsync({
|
|
148
178
|
global: true,
|
|
149
179
|
includeController: true,
|
|
@@ -152,476 +182,258 @@ NotificationModule.forRootAsync({
|
|
|
152
182
|
enableCompanyFeature: true,
|
|
153
183
|
},
|
|
154
184
|
imports: [ConfigModule],
|
|
155
|
-
useFactory:
|
|
185
|
+
useFactory: (configService: ConfigService) => ({
|
|
156
186
|
enableRealtime: true,
|
|
157
187
|
jwtSecret: configService.get('JWT_SECRET'),
|
|
158
|
-
defaultDatabaseConfig:
|
|
188
|
+
defaultDatabaseConfig: {
|
|
189
|
+
type: 'postgres',
|
|
190
|
+
host: configService.get('DB_HOST'),
|
|
191
|
+
port: configService.get<number>('DB_PORT'),
|
|
192
|
+
username: configService.get('DB_USER'),
|
|
193
|
+
password: configService.get('DB_PASSWORD'),
|
|
194
|
+
database: configService.get('DB_NAME'),
|
|
195
|
+
},
|
|
159
196
|
}),
|
|
160
197
|
inject: [ConfigService],
|
|
161
|
-
});
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
### Module Import Order
|
|
165
|
-
|
|
166
|
-
**Critical:** NotificationModule must be imported BEFORE AuthModule so `NOTIFICATION_ADAPTER` is available:
|
|
167
|
-
|
|
168
|
-
```typescript
|
|
169
|
-
@Module({
|
|
170
|
-
imports: [
|
|
171
|
-
// Must be first!
|
|
172
|
-
NotificationModule.forRoot(getNotificationModuleOptions()),
|
|
173
|
-
// Then other modules...
|
|
174
|
-
AuthModule.forRoot(...),
|
|
175
|
-
],
|
|
176
198
|
})
|
|
177
|
-
export class AppModule {}
|
|
178
199
|
```
|
|
179
200
|
|
|
180
|
-
|
|
201
|
+
**Exported services:**
|
|
202
|
+
- `NotificationConfigService`
|
|
203
|
+
- `NotificationDataSourceProvider`
|
|
204
|
+
- `NotificationService`
|
|
205
|
+
- `NotificationHelperService`
|
|
206
|
+
- `NotificationGateway` *(when `enableRealtime: true`)*
|
|
207
|
+
- `NOTIFICATION_ADAPTER`
|
|
181
208
|
|
|
182
|
-
|
|
209
|
+
---
|
|
183
210
|
|
|
184
|
-
|
|
211
|
+
## Configuration Reference
|
|
185
212
|
|
|
186
213
|
```typescript
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
// Company-specific entities
|
|
191
|
-
export const NotificationCompanyEntities = [NotificationWithCompany];
|
|
214
|
+
interface INotificationModuleConfig extends IDataSourceServiceOptions {
|
|
215
|
+
/** Enable Socket.IO WebSocket gateway (default: true) */
|
|
216
|
+
enableRealtime?: boolean;
|
|
192
217
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
return enableCompanyFeature ? NotificationCompanyEntities : NotificationCoreEntities;
|
|
218
|
+
/** JWT secret for WebSocket connection authentication */
|
|
219
|
+
jwtSecret?: string;
|
|
196
220
|
}
|
|
197
|
-
|
|
198
|
-
// Base type alias
|
|
199
|
-
export type NotificationBase = Notification | NotificationWithCompany;
|
|
200
221
|
```
|
|
201
222
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
```typescript
|
|
205
|
-
@Entity({ name: 'notification' })
|
|
206
|
-
@Index(['userId', 'isRead'])
|
|
207
|
-
@Index(['userId', 'createdAt'])
|
|
208
|
-
export class Notification extends Identity {
|
|
209
|
-
@Column({ name: 'user_id' })
|
|
210
|
-
@Index()
|
|
211
|
-
userId: string;
|
|
212
|
-
|
|
213
|
-
@Column({ length: 50, default: 'info' })
|
|
214
|
-
type: string;
|
|
215
|
-
|
|
216
|
-
@Column({ length: 255 })
|
|
217
|
-
title: string;
|
|
218
|
-
|
|
219
|
-
@Column({ type: 'text', nullable: true })
|
|
220
|
-
message: string | null;
|
|
223
|
+
---
|
|
221
224
|
|
|
222
|
-
|
|
223
|
-
data: Record<string, unknown> | null;
|
|
225
|
+
## Feature Toggles
|
|
224
226
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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 |
|
|
228
232
|
|
|
229
|
-
|
|
230
|
-
readAt: Date | null;
|
|
231
|
-
}
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
### NotificationWithCompany Entity
|
|
235
|
-
|
|
236
|
-
```typescript
|
|
237
|
-
@Entity({ name: 'notification' })
|
|
238
|
-
@Index(['companyId', 'userId', 'isRead'])
|
|
239
|
-
export class NotificationWithCompany extends Notification {
|
|
240
|
-
@Column({ name: 'company_id', nullable: true })
|
|
241
|
-
companyId: string | null;
|
|
242
|
-
}
|
|
243
|
-
```
|
|
233
|
+
---
|
|
244
234
|
|
|
245
|
-
|
|
235
|
+
## WebSocket Gateway
|
|
246
236
|
|
|
247
|
-
|
|
248
|
-
|-------|---------|---------|
|
|
249
|
-
| Single | `user_id` | User lookups |
|
|
250
|
-
| Single | `is_read` | Unread filtering |
|
|
251
|
-
| Composite | `user_id, created_at` | Sorted user notifications |
|
|
252
|
-
| Composite | `user_id, is_read` | Common filter queries |
|
|
253
|
-
| Composite | `company_id, user_id, is_read` | Company filtering (with company feature) |
|
|
237
|
+
The gateway runs on the `/notifications` Socket.IO namespace. JWT authentication is required on connection.
|
|
254
238
|
|
|
255
|
-
|
|
239
|
+
### Client Connection
|
|
256
240
|
|
|
257
|
-
|
|
241
|
+
**Browser (JavaScript):**
|
|
258
242
|
|
|
259
|
-
|
|
243
|
+
```javascript
|
|
244
|
+
import { io } from 'socket.io-client';
|
|
260
245
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
title: string;
|
|
266
|
-
message: string | null;
|
|
267
|
-
data: Record<string, unknown> | null;
|
|
268
|
-
isRead: boolean;
|
|
269
|
-
readAt: Date | null;
|
|
270
|
-
companyId?: string | null;
|
|
271
|
-
}
|
|
272
|
-
```
|
|
246
|
+
const socket = io('http://localhost:2002/notifications', {
|
|
247
|
+
auth: { token: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' },
|
|
248
|
+
transports: ['websocket'],
|
|
249
|
+
});
|
|
273
250
|
|
|
274
|
-
|
|
251
|
+
socket.on('connect', () => {
|
|
252
|
+
console.log('Connected to notification gateway');
|
|
253
|
+
});
|
|
275
254
|
|
|
276
|
-
|
|
255
|
+
socket.on('notification', (data) => {
|
|
256
|
+
console.log('New notification:', data);
|
|
257
|
+
// { id, title, message, type, data, createdAt }
|
|
258
|
+
});
|
|
277
259
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
type: string;
|
|
282
|
-
title: string;
|
|
283
|
-
message: string | null;
|
|
284
|
-
data: Record<string, unknown> | null;
|
|
285
|
-
createdAt: Date;
|
|
286
|
-
}
|
|
260
|
+
socket.on('disconnect', () => {
|
|
261
|
+
console.log('Disconnected');
|
|
262
|
+
});
|
|
287
263
|
```
|
|
288
264
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
## DTOs
|
|
292
|
-
|
|
293
|
-
### Input DTOs
|
|
265
|
+
**Angular service:**
|
|
294
266
|
|
|
295
267
|
```typescript
|
|
296
|
-
|
|
297
|
-
@IsUUID()
|
|
298
|
-
userId: string;
|
|
299
|
-
|
|
300
|
-
@IsString()
|
|
301
|
-
@MaxLength(255)
|
|
302
|
-
title: string;
|
|
268
|
+
import { io, Socket } from 'socket.io-client';
|
|
303
269
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
270
|
+
@Injectable({ providedIn: 'root' })
|
|
271
|
+
export class NotificationSocketService {
|
|
272
|
+
private socket: Socket;
|
|
307
273
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
274
|
+
connect(token: string): void {
|
|
275
|
+
this.socket = io(`${environment.apiUrl}/notifications`, {
|
|
276
|
+
auth: { token: `Bearer ${token}` },
|
|
277
|
+
});
|
|
311
278
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
279
|
+
this.socket.on('notification', (data) => {
|
|
280
|
+
this.handleNewNotification(data);
|
|
281
|
+
});
|
|
282
|
+
}
|
|
315
283
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
284
|
+
disconnect(): void {
|
|
285
|
+
this.socket?.disconnect();
|
|
286
|
+
}
|
|
319
287
|
}
|
|
288
|
+
```
|
|
320
289
|
|
|
321
|
-
|
|
322
|
-
@IsUUID()
|
|
323
|
-
id: string;
|
|
324
|
-
}
|
|
290
|
+
### WebSocket Events
|
|
325
291
|
|
|
326
|
-
|
|
327
|
-
@IsUUID()
|
|
328
|
-
userId: string;
|
|
292
|
+
**Server → Client (emitted by the gateway):**
|
|
329
293
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
294
|
+
| Event | Payload | Description |
|
|
295
|
+
|-------|---------|-------------|
|
|
296
|
+
| `notification` | `INotificationPayload` | New notification delivered in real-time |
|
|
297
|
+
| `notification:count` | `{ count: number }` | Updated unread count |
|
|
333
298
|
|
|
334
|
-
|
|
335
|
-
@IsString()
|
|
336
|
-
message?: string;
|
|
299
|
+
**Client → Server (handled by the gateway):**
|
|
337
300
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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 |
|
|
341
306
|
|
|
342
|
-
|
|
343
|
-
@IsObject()
|
|
344
|
-
data?: Record<string, unknown>;
|
|
307
|
+
---
|
|
345
308
|
|
|
346
|
-
|
|
347
|
-
@IsUUID()
|
|
348
|
-
companyId?: string;
|
|
349
|
-
}
|
|
309
|
+
## API Endpoints
|
|
350
310
|
|
|
351
|
-
|
|
352
|
-
@IsArray()
|
|
353
|
-
@IsUUID('4', { each: true })
|
|
354
|
-
userIds: string[];
|
|
311
|
+
All endpoints use **POST** and require JWT authentication.
|
|
355
312
|
|
|
356
|
-
|
|
357
|
-
@MaxLength(255)
|
|
358
|
-
title: string;
|
|
313
|
+
### Notifications — `POST /notification/*`
|
|
359
314
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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 |
|
|
363
323
|
|
|
364
|
-
|
|
365
|
-
@IsEnum(NotificationType)
|
|
366
|
-
type?: NotificationType;
|
|
324
|
+
### Admin — `POST /notification/admin/*`
|
|
367
325
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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) |
|
|
371
331
|
|
|
372
|
-
|
|
373
|
-
@IsUUID()
|
|
374
|
-
companyId?: string;
|
|
375
|
-
}
|
|
332
|
+
**Send notification request:**
|
|
376
333
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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://..." }
|
|
380
342
|
}
|
|
381
343
|
```
|
|
382
344
|
|
|
383
|
-
|
|
345
|
+
**Broadcast to company:**
|
|
384
346
|
|
|
385
|
-
```
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
data: Record<string, unknown> | null;
|
|
393
|
-
isRead: boolean;
|
|
394
|
-
readAt: Date | null;
|
|
395
|
-
companyId?: string | null;
|
|
396
|
-
createdAt: Date;
|
|
397
|
-
updatedAt: Date;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
class UnreadCountResponseDto {
|
|
401
|
-
count: number;
|
|
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"
|
|
402
354
|
}
|
|
403
355
|
```
|
|
404
356
|
|
|
405
357
|
---
|
|
406
358
|
|
|
407
|
-
##
|
|
408
|
-
|
|
409
|
-
### NotificationService
|
|
410
|
-
|
|
411
|
-
Main CRUD service. Extends `RequestScopedApiService`.
|
|
412
|
-
|
|
413
|
-
```typescript
|
|
414
|
-
@Injectable({ scope: Scope.REQUEST })
|
|
415
|
-
export class NotificationService extends RequestScopedApiService<
|
|
416
|
-
CreateNotificationDto,
|
|
417
|
-
UpdateNotificationDto,
|
|
418
|
-
INotification,
|
|
419
|
-
NotificationBase
|
|
420
|
-
> {
|
|
421
|
-
// Mark single notification as read
|
|
422
|
-
async markAsRead(id: string, user: ILoggedUserInfo): Promise<NotificationBase>;
|
|
423
|
-
|
|
424
|
-
// Mark all user's unread as read (returns count)
|
|
425
|
-
async markAllAsRead(user: ILoggedUserInfo): Promise<number>;
|
|
426
|
-
|
|
427
|
-
// Count unread notifications
|
|
428
|
-
async getUnreadCount(user: ILoggedUserInfo): Promise<number>;
|
|
429
|
-
}
|
|
430
|
-
```
|
|
431
|
-
|
|
432
|
-
**Automatic Features:**
|
|
433
|
-
- User scoping (users can only see own notifications)
|
|
434
|
-
- Company filtering (when enabled)
|
|
435
|
-
- WebSocket events on insert/read
|
|
436
|
-
- Cache clearing after modifications
|
|
437
|
-
- Soft delete support
|
|
438
|
-
|
|
439
|
-
### NotificationConfigService
|
|
440
|
-
|
|
441
|
-
Configuration service:
|
|
442
|
-
|
|
443
|
-
```typescript
|
|
444
|
-
@Injectable()
|
|
445
|
-
export class NotificationConfigService implements IModuleConfigService {
|
|
446
|
-
isCompanyFeatureEnabled(): boolean;
|
|
447
|
-
getDatabaseMode(): 'single' | 'multi-tenant';
|
|
448
|
-
isMultiTenant(): boolean;
|
|
449
|
-
isRealtimeEnabled(): boolean;
|
|
450
|
-
getJwtSecret(): string | undefined;
|
|
451
|
-
getOptions(): NotificationModuleOptions;
|
|
452
|
-
}
|
|
453
|
-
```
|
|
359
|
+
## Entities
|
|
454
360
|
|
|
455
|
-
###
|
|
361
|
+
### Core Entities
|
|
456
362
|
|
|
457
|
-
|
|
363
|
+
| Entity | Table | Description |
|
|
364
|
+
|--------|-------|-------------|
|
|
365
|
+
| `Notification` | `notification` | User notification with type, title, message, JSON data, isRead, readAt |
|
|
458
366
|
|
|
459
|
-
|
|
460
|
-
@Injectable()
|
|
461
|
-
export class NotificationHelperService {
|
|
462
|
-
// Send to single user
|
|
463
|
-
async send(options: SendNotificationOptions): Promise<NotificationBase>;
|
|
464
|
-
|
|
465
|
-
// Send to multiple users
|
|
466
|
-
async sendToMany(options: SendBulkNotificationOptions): Promise<NotificationBase[]>;
|
|
467
|
-
|
|
468
|
-
// Broadcast to company via WebSocket
|
|
469
|
-
async sendToCompany(
|
|
470
|
-
companyId: string,
|
|
471
|
-
title: string,
|
|
472
|
-
message?: string,
|
|
473
|
-
type?: NotificationType,
|
|
474
|
-
data?: Record<string, unknown>,
|
|
475
|
-
): Promise<void>;
|
|
476
|
-
|
|
477
|
-
// Check if user is connected
|
|
478
|
-
isUserOnline(userId: string): boolean;
|
|
367
|
+
**Indexes:** `(userId, isRead)`, `(userId, createdAt)` for fast unread queries.
|
|
479
368
|
|
|
480
|
-
|
|
481
|
-
getOnlineUserCount(): number;
|
|
482
|
-
}
|
|
483
|
-
```
|
|
369
|
+
### Company Feature Entities (`enableCompanyFeature: true`)
|
|
484
370
|
|
|
485
|
-
|
|
371
|
+
| Entity | Table | Description |
|
|
372
|
+
|--------|-------|-------------|
|
|
373
|
+
| `NotificationWithCompany` | `notification` | Same + `companyId` column |
|
|
486
374
|
|
|
487
|
-
|
|
375
|
+
**Additional index:** `(companyId, userId, isRead)`
|
|
488
376
|
|
|
489
377
|
```typescript
|
|
490
|
-
|
|
491
|
-
export class NotificationDataSourceProvider extends MultiTenantDataSourceService {
|
|
492
|
-
async getNotificationEntities(enableCompanyFeature?: boolean): Promise<any[]>;
|
|
493
|
-
async createDataSourceFromConfig(config: IDatabaseConfig): Promise<DataSource>;
|
|
494
|
-
async getSingleDataSource(): Promise<DataSource>;
|
|
495
|
-
async getOrCreateTenantConnection(tenant: ITenantDatabaseConfig): Promise<DataSource>;
|
|
496
|
-
}
|
|
497
|
-
```
|
|
498
|
-
|
|
499
|
-
---
|
|
500
|
-
|
|
501
|
-
## WebSocket Gateway
|
|
502
|
-
|
|
503
|
-
### Gateway Configuration
|
|
378
|
+
import { NotificationModule } from '@flusys/nestjs-notification';
|
|
504
379
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
380
|
+
TypeOrmModule.forRoot({
|
|
381
|
+
entities: [
|
|
382
|
+
...NotificationModule.getEntities({ enableCompanyFeature: true }),
|
|
383
|
+
],
|
|
509
384
|
})
|
|
510
|
-
export class NotificationGateway {
|
|
511
|
-
// Emit to all user's sockets (regardless of company)
|
|
512
|
-
emitToUser(userId: string, event: string, data: any): void;
|
|
513
|
-
|
|
514
|
-
// Emit to user's sockets only within a specific company context
|
|
515
|
-
emitToUserInCompany(userId: string, companyId: string | null | undefined, event: string, data: any): void;
|
|
516
|
-
|
|
517
|
-
// Emit to all sockets in a company room
|
|
518
|
-
emitToCompany(companyId: string, event: string, data: any): void;
|
|
519
|
-
|
|
520
|
-
// Broadcast to all connected clients
|
|
521
|
-
broadcast(event: string, data: any): void;
|
|
522
|
-
|
|
523
|
-
// Check if user is connected
|
|
524
|
-
isUserOnline(userId: string): boolean;
|
|
525
|
-
|
|
526
|
-
// Get total connected clients
|
|
527
|
-
getOnlineUserCount(): number;
|
|
528
|
-
}
|
|
529
385
|
```
|
|
530
386
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
When a user logs in with **different companies on different devices**, the gateway correctly isolates notifications:
|
|
534
|
-
|
|
535
|
-
| Device | Company Context | Socket Rooms |
|
|
536
|
-
|--------|-----------------|--------------|
|
|
537
|
-
| Device 1 | Company A | `user:{userId}`, `company:{companyA.id}` |
|
|
538
|
-
| Device 2 | Company B | `user:{userId}`, `company:{companyB.id}` |
|
|
539
|
-
|
|
540
|
-
**Notification Delivery:**
|
|
541
|
-
|
|
542
|
-
| Notification Type | Target Method | Device 1 (Company A) | Device 2 (Company B) |
|
|
543
|
-
|-------------------|---------------|----------------------|----------------------|
|
|
544
|
-
| User notification (no companyId) | `emitToUser()` | Receives | Receives |
|
|
545
|
-
| Company A notification | `emitToUserInCompany()` | Receives | Does NOT receive |
|
|
546
|
-
| Company B notification | `emitToUserInCompany()` | Does NOT receive | Receives |
|
|
547
|
-
| Company-wide broadcast | `emitToCompany()` | Receives (if Company A) | Receives (if Company B) |
|
|
548
|
-
|
|
549
|
-
This ensures that notifications are only delivered to devices connected with the matching company context, preventing confusion when users work with multiple companies.
|
|
550
|
-
|
|
551
|
-
### Client Connection
|
|
552
|
-
|
|
553
|
-
```javascript
|
|
554
|
-
// Client-side connection
|
|
555
|
-
const socket = io('ws://localhost:2002/notifications', {
|
|
556
|
-
auth: { token: 'jwt-token' },
|
|
557
|
-
// OR
|
|
558
|
-
query: { token: 'jwt-token' },
|
|
559
|
-
// OR
|
|
560
|
-
extraHeaders: { authorization: 'Bearer jwt-token' },
|
|
561
|
-
});
|
|
562
|
-
```
|
|
563
|
-
|
|
564
|
-
### WebSocket Events
|
|
565
|
-
|
|
566
|
-
**Server → Client:**
|
|
567
|
-
|
|
568
|
-
| Event | Payload | Trigger |
|
|
569
|
-
|-------|---------|---------|
|
|
570
|
-
| `notification:new` | `INotificationPayload` | New notification created |
|
|
571
|
-
| `notification:read` | `{ id: string }` | Single notification marked read |
|
|
572
|
-
| `notification:read-all` | `{}` | All notifications marked read |
|
|
573
|
-
|
|
574
|
-
### Room Structure
|
|
387
|
+
---
|
|
575
388
|
|
|
576
|
-
|
|
577
|
-
- `user:{userId}` - User-specific room
|
|
578
|
-
- `company:{companyId}` - Company room (if company feature enabled)
|
|
389
|
+
## Cross-Module Adapter (NOTIFICATION_ADAPTER)
|
|
579
390
|
|
|
580
|
-
|
|
391
|
+
The `NOTIFICATION_ADAPTER` token is defined in `@flusys/nestjs-shared` so any module can send notifications without importing `@flusys/nestjs-notification`.
|
|
581
392
|
|
|
582
|
-
|
|
393
|
+
When `NotificationModule` is registered in the app, it provides the implementation. When it is not registered, `@Optional()` prevents injection errors.
|
|
583
394
|
|
|
584
|
-
|
|
395
|
+
**INotificationAdapter interface (from `@flusys/nestjs-shared`):**
|
|
585
396
|
|
|
586
397
|
```typescript
|
|
587
398
|
interface INotificationAdapter {
|
|
588
|
-
send(
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
isUserOnline
|
|
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;
|
|
592
403
|
}
|
|
593
404
|
|
|
594
|
-
interface
|
|
405
|
+
interface INotificationSendData {
|
|
595
406
|
userId: string;
|
|
596
407
|
title: string;
|
|
597
|
-
message
|
|
598
|
-
type?:
|
|
599
|
-
data?: Record<string,
|
|
408
|
+
message: string;
|
|
409
|
+
type?: 'info' | 'success' | 'warning' | 'error';
|
|
410
|
+
data?: Record<string, any>;
|
|
600
411
|
companyId?: string;
|
|
601
412
|
}
|
|
602
|
-
|
|
603
|
-
export const NOTIFICATION_ADAPTER = 'NOTIFICATION_ADAPTER';
|
|
604
413
|
```
|
|
605
414
|
|
|
606
|
-
|
|
415
|
+
**Usage pattern:**
|
|
607
416
|
|
|
608
417
|
```typescript
|
|
609
|
-
import { NOTIFICATION_ADAPTER, INotificationAdapter } from '@flusys/nestjs-shared';
|
|
418
|
+
import { NOTIFICATION_ADAPTER, INotificationAdapter } from '@flusys/nestjs-shared/interfaces';
|
|
610
419
|
|
|
611
420
|
@Injectable()
|
|
612
|
-
export class
|
|
421
|
+
export class OrderService {
|
|
613
422
|
constructor(
|
|
614
|
-
@Inject(NOTIFICATION_ADAPTER)
|
|
615
|
-
private readonly
|
|
423
|
+
@Optional() @Inject(NOTIFICATION_ADAPTER)
|
|
424
|
+
private readonly notifications?: INotificationAdapter,
|
|
616
425
|
) {}
|
|
617
426
|
|
|
618
|
-
async
|
|
619
|
-
//
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
427
|
+
async shipOrder(orderId: string, userId: string): Promise<void> {
|
|
428
|
+
// ... ship order logic
|
|
429
|
+
|
|
430
|
+
// Send notification if adapter is available
|
|
431
|
+
await this.notifications?.send({
|
|
432
|
+
userId,
|
|
433
|
+
title: 'Order Shipped',
|
|
434
|
+
message: `Your order has been shipped.`,
|
|
435
|
+
type: 'success',
|
|
436
|
+
data: { orderId },
|
|
625
437
|
});
|
|
626
438
|
}
|
|
627
439
|
}
|
|
@@ -629,370 +441,130 @@ export class SomeService {
|
|
|
629
441
|
|
|
630
442
|
---
|
|
631
443
|
|
|
632
|
-
##
|
|
633
|
-
|
|
634
|
-
All endpoints use POST (RPC pattern) and require JWT authentication.
|
|
635
|
-
|
|
636
|
-
### User Endpoints
|
|
637
|
-
|
|
638
|
-
| Endpoint | Permission | Description |
|
|
639
|
-
|----------|-----------|-------------|
|
|
640
|
-
| `POST /notification/get-all` | JWT | Get paginated notifications |
|
|
641
|
-
| `POST /notification/get/:id` | JWT | Get single notification |
|
|
642
|
-
| `POST /notification/mark-read` | JWT | Mark notification as read |
|
|
643
|
-
| `POST /notification/mark-all-read` | JWT | Mark all as read |
|
|
644
|
-
| `POST /notification/unread-count` | JWT | Get unread count |
|
|
645
|
-
|
|
646
|
-
### Admin Endpoints
|
|
647
|
-
|
|
648
|
-
| Endpoint | Permission | Description |
|
|
649
|
-
|----------|-----------|-------------|
|
|
650
|
-
| `POST /notification/send` | `notification.create` | Send to single user |
|
|
651
|
-
| `POST /notification/send-bulk` | `notification.create` | Send to multiple users |
|
|
652
|
-
| `POST /notification/insert` | `notification.create` | Direct CRUD insert |
|
|
653
|
-
| `POST /notification/update` | `notification.update` | Direct CRUD update |
|
|
654
|
-
| `POST /notification/delete` | `notification.delete` | Direct CRUD delete |
|
|
655
|
-
|
|
656
|
-
### Example Requests
|
|
657
|
-
|
|
658
|
-
**Mark as Read:**
|
|
659
|
-
|
|
660
|
-
```bash
|
|
661
|
-
curl -X POST http://localhost:2002/notification/mark-read \
|
|
662
|
-
-H "Authorization: Bearer <token>" \
|
|
663
|
-
-H "Content-Type: application/json" \
|
|
664
|
-
-d '{ "id": "notification-uuid" }'
|
|
665
|
-
```
|
|
666
|
-
|
|
667
|
-
**Send Notification:**
|
|
668
|
-
|
|
669
|
-
```bash
|
|
670
|
-
curl -X POST http://localhost:2002/notification/send \
|
|
671
|
-
-H "Authorization: Bearer <token>" \
|
|
672
|
-
-H "Content-Type: application/json" \
|
|
673
|
-
-d '{
|
|
674
|
-
"userId": "user-uuid",
|
|
675
|
-
"title": "Order Confirmed",
|
|
676
|
-
"message": "Your order #12345 has been confirmed",
|
|
677
|
-
"type": "success",
|
|
678
|
-
"data": { "orderId": "12345" }
|
|
679
|
-
}'
|
|
680
|
-
```
|
|
681
|
-
|
|
682
|
-
**Send Bulk:**
|
|
444
|
+
## Exported Services
|
|
683
445
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
"type": "warning"
|
|
693
|
-
}'
|
|
694
|
-
```
|
|
695
|
-
|
|
696
|
-
**Get Unread Count:**
|
|
697
|
-
|
|
698
|
-
```bash
|
|
699
|
-
curl -X POST http://localhost:2002/notification/unread-count \
|
|
700
|
-
-H "Authorization: Bearer <token>"
|
|
701
|
-
|
|
702
|
-
# Response: { "success": true, "data": { "count": 5 } }
|
|
703
|
-
```
|
|
446
|
+
| Service | Description |
|
|
447
|
+
|---------|-------------|
|
|
448
|
+
| `NotificationService` | Notification CRUD (REQUEST-scoped) |
|
|
449
|
+
| `NotificationHelperService` | Singleton service for programmatic sending |
|
|
450
|
+
| `NotificationGateway` | Socket.IO gateway *(when `enableRealtime: true`)* |
|
|
451
|
+
| `NotificationConfigService` | Runtime config and feature flags |
|
|
452
|
+
| `NotificationDataSourceProvider` | Dynamic DataSource per request |
|
|
453
|
+
| `NOTIFICATION_ADAPTER` | Cross-module adapter implementation |
|
|
704
454
|
|
|
705
455
|
---
|
|
706
456
|
|
|
707
|
-
##
|
|
457
|
+
## Sending Notifications Programmatically
|
|
708
458
|
|
|
709
|
-
|
|
459
|
+
Use `NotificationHelperService` (singleton) within the same application:
|
|
710
460
|
|
|
711
461
|
```typescript
|
|
712
|
-
import {
|
|
713
|
-
import { setupSwaggerDocs } from '@flusys/nestjs-core/docs';
|
|
714
|
-
|
|
715
|
-
// In your bootstrap function
|
|
716
|
-
setupSwaggerDocs(app, notificationSwaggerConfig(bootstrapAppConfig));
|
|
717
|
-
```
|
|
718
|
-
|
|
719
|
-
### Swagger Access
|
|
720
|
-
|
|
721
|
-
- **URL:** `http://localhost:2002/api/docs/notification`
|
|
722
|
-
- **Features:** Bearer auth, auto-generated schemas
|
|
723
|
-
- **Company Feature:** When disabled, `companyId` fields are hidden from schemas
|
|
462
|
+
import { NotificationHelperService } from '@flusys/nestjs-notification';
|
|
724
463
|
|
|
725
|
-
|
|
464
|
+
@Injectable()
|
|
465
|
+
export class PaymentService {
|
|
466
|
+
constructor(
|
|
467
|
+
@Inject(NotificationHelperService)
|
|
468
|
+
private readonly notificationHelper: NotificationHelperService,
|
|
469
|
+
) {}
|
|
726
470
|
|
|
727
|
-
|
|
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
|
+
});
|
|
479
|
+
}
|
|
728
480
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
}
|
|
481
|
+
async notifyCompany(companyId: string): Promise<void> {
|
|
482
|
+
await this.notificationHelper.broadcast(companyId, {
|
|
483
|
+
title: 'Company Announcement',
|
|
484
|
+
message: 'Check your email for important updates.',
|
|
485
|
+
type: 'info',
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
}
|
|
736
489
|
```
|
|
737
490
|
|
|
738
|
-
**Endpoint Security:**
|
|
739
|
-
|
|
740
|
-
| Endpoint | Permission | Access |
|
|
741
|
-
|----------|-----------|--------|
|
|
742
|
-
| `get-all`, `get/:id` | JWT only | Own notifications only |
|
|
743
|
-
| `mark-read`, `mark-all-read`, `unread-count` | JWT only | Own notifications only |
|
|
744
|
-
| `send`, `send-bulk` | `notification.create` | Admin/privileged users |
|
|
745
|
-
| CRUD endpoints | Respective permissions | Admin |
|
|
746
|
-
|
|
747
491
|
---
|
|
748
492
|
|
|
749
|
-
##
|
|
750
|
-
|
|
751
|
-
### Company Isolation
|
|
493
|
+
## Frontend Integration
|
|
752
494
|
|
|
753
|
-
|
|
754
|
-
- Notifications are company-scoped
|
|
755
|
-
- Users can only see notifications for their company
|
|
756
|
-
- WebSocket broadcasts to company rooms
|
|
757
|
-
|
|
758
|
-
### Company Filtering
|
|
759
|
-
|
|
760
|
-
Services automatically filter by `companyId`:
|
|
495
|
+
### Unread Badge Count
|
|
761
496
|
|
|
762
497
|
```typescript
|
|
763
|
-
//
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
});
|
|
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;
|
|
768
502
|
}
|
|
769
503
|
```
|
|
770
504
|
|
|
771
|
-
###
|
|
772
|
-
|
|
773
|
-
```typescript
|
|
774
|
-
protected resolveEntity(): EntityTarget<NotificationBase> {
|
|
775
|
-
return this.notificationConfig.isCompanyFeatureEnabled()
|
|
776
|
-
? NotificationWithCompany
|
|
777
|
-
: Notification;
|
|
778
|
-
}
|
|
779
|
-
```
|
|
780
|
-
|
|
781
|
-
---
|
|
782
|
-
|
|
783
|
-
## Integration with Other Modules
|
|
784
|
-
|
|
785
|
-
### Auth Module Integration
|
|
786
|
-
|
|
787
|
-
The Auth module can send login notifications via the adapter:
|
|
505
|
+
### Notification List
|
|
788
506
|
|
|
789
507
|
```typescript
|
|
790
|
-
|
|
791
|
-
{
|
|
792
|
-
provide: AUTH_NOTIFICATION_PROVIDER,
|
|
793
|
-
useExisting: NotificationAdapter,
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
// In AuthenticationService
|
|
797
|
-
@Optional() @Inject(NOTIFICATION_ADAPTER)
|
|
798
|
-
private readonly notificationAdapter?: INotificationAdapter;
|
|
799
|
-
|
|
800
|
-
async onLoginSuccess(userId: string, companyId?: string): Promise<void> {
|
|
801
|
-
if (!this.notificationAdapter) return;
|
|
802
|
-
|
|
803
|
-
await this.notificationAdapter.send({
|
|
804
|
-
userId,
|
|
805
|
-
title: 'Login successful',
|
|
806
|
-
message: 'You have successfully logged in to your account.',
|
|
807
|
-
type: NotificationType.INFO,
|
|
808
|
-
companyId,
|
|
809
|
-
});
|
|
508
|
+
async getNotifications(page = 1, pageSize = 20) {
|
|
509
|
+
return this.http.post('/notification/get-all', { page, pageSize }).toPromise();
|
|
810
510
|
}
|
|
811
511
|
```
|
|
812
512
|
|
|
813
|
-
###
|
|
814
|
-
|
|
815
|
-
**IMPORTANT:** NotificationModule must be registered BEFORE AuthModule:
|
|
513
|
+
### Mark as Read
|
|
816
514
|
|
|
817
515
|
```typescript
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
// NotificationModule FIRST - provides NOTIFICATION_ADAPTER
|
|
821
|
-
NotificationModule.forRootAsync(getNotificationModuleAsyncOptions()),
|
|
822
|
-
|
|
823
|
-
// AuthModule SECOND - consumes NOTIFICATION_ADAPTER
|
|
824
|
-
AuthModule.forRootAsync(getAuthModuleAsyncOptions()),
|
|
516
|
+
// Single
|
|
517
|
+
await this.http.post('/notification/mark-read', { id: notificationId }).toPromise();
|
|
825
518
|
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
})
|
|
829
|
-
export class AppModule {}
|
|
519
|
+
// All
|
|
520
|
+
await this.http.post('/notification/mark-all-read', {}).toPromise();
|
|
830
521
|
```
|
|
831
522
|
|
|
832
523
|
---
|
|
833
524
|
|
|
834
|
-
##
|
|
525
|
+
## Troubleshooting
|
|
835
526
|
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
```typescript
|
|
839
|
-
// In any module - use adapter interface
|
|
840
|
-
await this.notificationAdapter.send({
|
|
841
|
-
userId: user.id,
|
|
842
|
-
title: 'Email Sent',
|
|
843
|
-
type: NotificationType.SUCCESS,
|
|
844
|
-
});
|
|
845
|
-
```
|
|
527
|
+
**WebSocket connection rejected (401)**
|
|
846
528
|
|
|
847
|
-
|
|
529
|
+
The JWT token in the `auth.token` field is invalid or expired. Ensure you pass `Bearer <token>` format:
|
|
848
530
|
|
|
849
|
-
```
|
|
850
|
-
|
|
851
|
-
userId: user.id,
|
|
852
|
-
title: 'Order Shipped',
|
|
853
|
-
message: 'Your order #12345 is on its way',
|
|
854
|
-
type: NotificationType.INFO,
|
|
855
|
-
data: {
|
|
856
|
-
orderId: '12345',
|
|
857
|
-
trackingNumber: 'ABC123',
|
|
858
|
-
estimatedDelivery: '2026-03-01',
|
|
859
|
-
},
|
|
860
|
-
}, user);
|
|
531
|
+
```javascript
|
|
532
|
+
auth: { token: `Bearer ${accessToken}` }
|
|
861
533
|
```
|
|
862
534
|
|
|
863
|
-
|
|
535
|
+
---
|
|
864
536
|
|
|
865
|
-
|
|
866
|
-
|------|---------|
|
|
867
|
-
| `INFO` | General information, updates |
|
|
868
|
-
| `SUCCESS` | Successful operations |
|
|
869
|
-
| `WARNING` | Important notices, upcoming deadlines |
|
|
870
|
-
| `ERROR` | Failed operations, errors |
|
|
537
|
+
**Notifications delivered via REST but not real-time**
|
|
871
538
|
|
|
872
|
-
|
|
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`).
|
|
873
540
|
|
|
874
|
-
|
|
541
|
+
---
|
|
875
542
|
|
|
876
|
-
|
|
877
|
-
socket.on('disconnect', () => {
|
|
878
|
-
// Optionally reload unread count on reconnect
|
|
879
|
-
});
|
|
543
|
+
**Company broadcast not reaching all users**
|
|
880
544
|
|
|
881
|
-
|
|
882
|
-
// Refresh notifications after reconnect
|
|
883
|
-
});
|
|
884
|
-
```
|
|
545
|
+
Users must join the company room via the `join:company` WebSocket event after connecting. If `enableCompanyFeature: false`, company rooms are not created.
|
|
885
546
|
|
|
886
547
|
---
|
|
887
548
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
### Main Exports
|
|
549
|
+
**`NOTIFICATION_ADAPTER` is `undefined`**
|
|
891
550
|
|
|
892
|
-
|
|
893
|
-
// Module
|
|
894
|
-
import { NotificationModule } from '@flusys/nestjs-notification';
|
|
551
|
+
`NotificationModule` is not registered. Use `@Optional() @Inject(NOTIFICATION_ADAPTER)` so the injection doesn't throw when the module is absent.
|
|
895
552
|
|
|
896
|
-
|
|
897
|
-
import {
|
|
898
|
-
NotificationService,
|
|
899
|
-
NotificationConfigService,
|
|
900
|
-
NotificationHelperService,
|
|
901
|
-
NotificationDataSourceProvider,
|
|
902
|
-
} from '@flusys/nestjs-notification/services';
|
|
903
|
-
|
|
904
|
-
// Entities
|
|
905
|
-
import {
|
|
906
|
-
Notification,
|
|
907
|
-
NotificationBase,
|
|
908
|
-
NotificationWithCompany,
|
|
909
|
-
NotificationCoreEntities,
|
|
910
|
-
NotificationCompanyEntities,
|
|
911
|
-
getNotificationEntitiesByConfig,
|
|
912
|
-
} from '@flusys/nestjs-notification/entities';
|
|
913
|
-
|
|
914
|
-
// DTOs
|
|
915
|
-
import {
|
|
916
|
-
CreateNotificationDto,
|
|
917
|
-
UpdateNotificationDto,
|
|
918
|
-
SendNotificationDto,
|
|
919
|
-
SendBulkNotificationDto,
|
|
920
|
-
MarkReadDto,
|
|
921
|
-
NotificationResponseDto,
|
|
922
|
-
UnreadCountResponseDto,
|
|
923
|
-
} from '@flusys/nestjs-notification/dtos';
|
|
924
|
-
|
|
925
|
-
// Interfaces
|
|
926
|
-
import {
|
|
927
|
-
INotification,
|
|
928
|
-
INotificationPayload,
|
|
929
|
-
INotificationModuleConfig,
|
|
930
|
-
NotificationModuleOptions,
|
|
931
|
-
NotificationModuleAsyncOptions,
|
|
932
|
-
NotificationOptionsFactory,
|
|
933
|
-
} from '@flusys/nestjs-notification/interfaces';
|
|
934
|
-
|
|
935
|
-
// Gateway
|
|
936
|
-
import { NotificationGateway } from '@flusys/nestjs-notification/gateways';
|
|
937
|
-
|
|
938
|
-
// Adapter
|
|
939
|
-
import { NotificationAdapter } from '@flusys/nestjs-notification/adapters';
|
|
940
|
-
|
|
941
|
-
// Controller
|
|
942
|
-
import { NotificationController } from '@flusys/nestjs-notification/controllers';
|
|
943
|
-
|
|
944
|
-
// Constants
|
|
945
|
-
import { NOTIFICATION_MODULE_OPTIONS } from '@flusys/nestjs-notification/config';
|
|
946
|
-
|
|
947
|
-
// Swagger Config
|
|
948
|
-
import { notificationSwaggerConfig } from '@flusys/nestjs-notification/docs';
|
|
949
|
-
```
|
|
950
|
-
|
|
951
|
-
### Service Methods Reference
|
|
553
|
+
---
|
|
952
554
|
|
|
953
|
-
|
|
555
|
+
**`No metadata for entity`**
|
|
954
556
|
|
|
557
|
+
Register entities in `TypeOrmModule`:
|
|
955
558
|
```typescript
|
|
956
|
-
|
|
957
|
-
// Standard CRUD (inherited from RequestScopedApiService)
|
|
958
|
-
insert(dto, user): Promise<NotificationBase>;
|
|
959
|
-
getById(id, user): Promise<NotificationBase | null>;
|
|
960
|
-
getAll(filterDto, user): Promise<{ list: NotificationBase[]; total: number }>;
|
|
961
|
-
update(dto, user): Promise<NotificationBase>;
|
|
962
|
-
delete(id, user): Promise<boolean>;
|
|
963
|
-
|
|
964
|
-
// Custom methods
|
|
965
|
-
markAsRead(id, user): Promise<NotificationBase>;
|
|
966
|
-
markAllAsRead(user): Promise<number>;
|
|
967
|
-
getUnreadCount(user): Promise<number>;
|
|
968
|
-
}
|
|
559
|
+
entities: [...NotificationModule.getEntities({ enableCompanyFeature: true })]
|
|
969
560
|
```
|
|
970
561
|
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
```typescript
|
|
974
|
-
class NotificationHelperService {
|
|
975
|
-
send(options: SendNotificationOptions): Promise<NotificationBase>;
|
|
976
|
-
sendToMany(options: SendBulkNotificationOptions): Promise<NotificationBase[]>;
|
|
977
|
-
sendToCompany(companyId, title, message?, type?, data?): Promise<void>;
|
|
978
|
-
isUserOnline(userId: string): boolean;
|
|
979
|
-
getOnlineUserCount(): number;
|
|
980
|
-
}
|
|
981
|
-
```
|
|
562
|
+
---
|
|
982
563
|
|
|
983
|
-
|
|
564
|
+
## License
|
|
984
565
|
|
|
985
|
-
|
|
986
|
-
class NotificationGateway {
|
|
987
|
-
emitToUser(userId, event, data): void;
|
|
988
|
-
emitToCompany(companyId, event, data): void;
|
|
989
|
-
broadcast(event, data): void;
|
|
990
|
-
isUserOnline(userId): boolean;
|
|
991
|
-
getOnlineUserCount(): number;
|
|
992
|
-
}
|
|
993
|
-
```
|
|
566
|
+
MIT © FLUSYS
|
|
994
567
|
|
|
995
568
|
---
|
|
996
569
|
|
|
997
|
-
**
|
|
998
|
-
**Version:** 3.0.0
|
|
570
|
+
> Part of the **FLUSYS** framework — a full-stack monorepo powering Angular 21 + NestJS 11 applications.
|
|
@@ -4,12 +4,17 @@ import { CreateNotificationDto, MarkReadDto, NotificationResponseDto, SendBulkNo
|
|
|
4
4
|
import { NotificationHelperService } from '../services/notification-helper.service';
|
|
5
5
|
import { NotificationService } from '../services/notification.service';
|
|
6
6
|
declare const NotificationController_base: abstract new (service: NotificationService) => {
|
|
7
|
+
readonly enabledEndpoints: import("@flusys/nestjs-shared/classes").ApiEndpoint[] | "all";
|
|
7
8
|
service: NotificationService;
|
|
9
|
+
isEnabled(endpoint: import("@flusys/nestjs-shared/classes").ApiEndpoint): boolean;
|
|
8
10
|
insert(addDto: CreateNotificationDto, user: ILoggedUserInfo | null): Promise<SingleResponseDto<NotificationResponseDto>>;
|
|
9
11
|
insertMany(addDto: CreateNotificationDto[], user: ILoggedUserInfo | null): Promise<BulkResponseDto<NotificationResponseDto>>;
|
|
10
12
|
getById(id: string, body: import("@flusys/nestjs-shared/dtos").GetByIdBodyDto, user: ILoggedUserInfo | null): Promise<SingleResponseDto<NotificationResponseDto>>;
|
|
13
|
+
getByIds(body: import("@flusys/nestjs-shared/dtos").GetByIdsDto, user: ILoggedUserInfo | null): Promise<import("@flusys/nestjs-shared/dtos").ListResponseDto<NotificationResponseDto>>;
|
|
11
14
|
update(updateDto: UpdateNotificationDto, user: ILoggedUserInfo | null): Promise<SingleResponseDto<NotificationResponseDto>>;
|
|
12
15
|
updateMany(updateDtos: UpdateNotificationDto[], user: ILoggedUserInfo | null): Promise<BulkResponseDto<NotificationResponseDto>>;
|
|
16
|
+
bulkUpsert(dtos: (CreateNotificationDto | UpdateNotificationDto)[], user: ILoggedUserInfo | null): Promise<BulkResponseDto<NotificationResponseDto>>;
|
|
17
|
+
getByFilter(filter: Record<string, any>, user: ILoggedUserInfo | null): Promise<SingleResponseDto<NotificationResponseDto>>;
|
|
13
18
|
getAll(filterAndPaginationDto: import("@flusys/nestjs-shared/dtos").FilterAndPaginationDto, user: ILoggedUserInfo | null, search?: string): Promise<import("@flusys/nestjs-shared/dtos").ListResponseDto<NotificationResponseDto>>;
|
|
14
19
|
delete(deleteDto: import("@flusys/nestjs-shared/dtos").DeleteDto, user: ILoggedUserInfo | null): Promise<MessageResponseDto>;
|
|
15
20
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flusys/nestjs-notification",
|
|
3
|
-
"version": "4.0
|
|
3
|
+
"version": "4.1.0",
|
|
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": "4.0
|
|
96
|
-
"@flusys/nestjs-shared": "4.0
|
|
95
|
+
"@flusys/nestjs-core": "4.1.0",
|
|
96
|
+
"@flusys/nestjs-shared": "4.1.0",
|
|
97
97
|
"jsonwebtoken": "^9.0.0"
|
|
98
98
|
}
|
|
99
99
|
}
|