@flusys/nestjs-notification 4.1.0 → 5.0.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 CHANGED
@@ -1,74 +1,9 @@
1
1
  # @flusys/nestjs-notification
2
2
 
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.
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
  [![npm version](https://img.shields.io/npm/v/@flusys/nestjs-notification.svg)](https://www.npmjs.com/package/@flusys/nestjs-notification)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
- [![NestJS](https://img.shields.io/badge/NestJS-11.x-red.svg)](https://nestjs.com/)
8
- [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue.svg)](https://www.typescriptlang.org/)
9
- [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18.x-green.svg)](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
- ## Quick Start
19
+ ### forRoot (sync)
85
20
 
86
- ### With Realtime Enabled
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: 'postgres',
39
+ type: 'mysql',
106
40
  host: process.env.DB_HOST,
107
- port: Number(process.env.DB_PORT ?? 5432),
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
- ### 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)
53
+ #### Mode 2: Multi-Tenant
149
54
 
150
55
  ```typescript
151
56
  NotificationModule.forRoot({
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
- })
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
- **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)
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: (configService: ConfigService) => ({
97
+ useFactory: (config: ConfigService) => ({
186
98
  enableRealtime: true,
187
- jwtSecret: configService.get('JWT_SECRET'),
99
+ jwtSecret: config.get('JWT_SECRET'),
188
100
  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'),
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
- socket.on('notification', (data) => {
256
- console.log('New notification:', data);
257
- // { id, title, message, type, data, createdAt }
258
- });
259
-
260
- socket.on('disconnect', () => {
261
- console.log('Disconnected');
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
- **Angular service:**
138
+ ## 2. Register Entities
266
139
 
267
140
  ```typescript
268
- import { io, Socket } from 'socket.io-client';
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
- ...NotificationModule.getEntities({ enableCompanyFeature: true }),
145
+ ...getNotificationEntitiesByConfig(false), // match enableCompanyFeature in bootstrapAppConfig
383
146
  ],
384
- })
147
+ });
385
148
  ```
386
149
 
387
- ---
388
-
389
- ## Cross-Module Adapter (NOTIFICATION_ADAPTER)
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
- **INotificationAdapter interface (from `@flusys/nestjs-shared`):**
156
+ ## 4. Send Notifications Programmatically
396
157
 
397
- ```typescript
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 { NOTIFICATION_ADAPTER, INotificationAdapter } from '@flusys/nestjs-shared/interfaces';
161
+ import { NotificationHelperService } from '@flusys/nestjs-notification';
419
162
 
420
163
  @Injectable()
421
164
  export class OrderService {
422
165
  constructor(
423
- @Optional() @Inject(NOTIFICATION_ADAPTER)
424
- private readonly notifications?: INotificationAdapter,
166
+ @Inject(NotificationHelperService)
167
+ private readonly notifications: NotificationHelperService,
425
168
  ) {}
426
169
 
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({
170
+ async afterShipment(orderId: string, userId: string): Promise<void> {
171
+ await this.notifications.send({
432
172
  userId,
433
173
  title: 'Order Shipped',
434
- message: `Your order has been shipped.`,
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
- ## Exported Services
445
-
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 |
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
- async notifyCompany(companyId: string): Promise<void> {
482
- await this.notificationHelper.broadcast(companyId, {
483
- title: 'Company Announcement',
484
- message: 'Check your email for important updates.',
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
- function _export(target, all) {
7
- for(var name in all)Object.defineProperty(target, name, {
8
- enumerable: true,
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
- function notificationSwaggerConfig() {
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
  }
@@ -73,7 +73,7 @@ _ts_decorate([
73
73
  ], Notification.prototype, "message", void 0);
74
74
  _ts_decorate([
75
75
  (0, _typeorm.Column)({
76
- type: 'simple-json',
76
+ type: 'json',
77
77
  nullable: true
78
78
  }),
79
79
  _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 specific user only on sockets connected with a specific company.
130
- * This is crucial for multi-company scenarios where a user may be logged in
131
- * with different companies on different devices.
132
- *
133
- * @param userId - The user ID
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?.sockets?.adapter) {
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
- const userRoom = `user:${userId}`;
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 _config = require("../config");
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 result = await this.repository.update({
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) {
@@ -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
- export function notificationSwaggerConfig() {
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
  }
@@ -63,7 +63,7 @@ _ts_decorate([
63
63
  ], Notification.prototype, "message", void 0);
64
64
  _ts_decorate([
65
65
  Column({
66
- type: 'simple-json',
66
+ type: 'json',
67
67
  nullable: true
68
68
  }),
69
69
  _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 specific user only on sockets connected with a specific company.
79
- * This is crucial for multi-company scenarios where a user may be logged in
80
- * with different companies on different devices.
81
- *
82
- * @param userId - The user ID
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?.sockets?.adapter) {
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
- const userRoom = `user:${userId}`;
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 { NOTIFICATION_MESSAGES } from '../config';
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 result = await this.repository.update({
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": "4.1.0",
3
+ "version": "5.0.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.1.0",
96
- "@flusys/nestjs-shared": "4.1.0",
95
+ "@flusys/nestjs-core": "5.0.0",
96
+ "@flusys/nestjs-shared": "5.0.0",
97
97
  "jsonwebtoken": "^9.0.0"
98
98
  }
99
99
  }