@flusys/nestjs-notification 6.0.1 → 6.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.
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ const _modules = require("@flusys/nestjs-shared/modules");
6
+ const _notificationdatasourceprovider = require("./notification-datasource.provider");
7
+ const _entities = require("../entities");
8
+ function buildConfigService(overrides = {}) {
9
+ return {
10
+ isCompanyFeatureEnabled: jest.fn().mockReturnValue(false),
11
+ getDatabaseMode: jest.fn().mockReturnValue('single'),
12
+ isMultiTenant: jest.fn().mockReturnValue(false),
13
+ getJwtSecret: jest.fn().mockReturnValue(undefined),
14
+ getOptions: jest.fn().mockReturnValue({
15
+ bootstrapAppConfig: {
16
+ databaseMode: 'single',
17
+ enableCompanyFeature: false
18
+ },
19
+ config: {
20
+ defaultDatabaseConfig: {
21
+ type: 'mysql',
22
+ host: 'localhost',
23
+ port: 3306,
24
+ username: 'root',
25
+ password: 'pass',
26
+ database: 'flusys'
27
+ },
28
+ tenants: []
29
+ }
30
+ }),
31
+ ...overrides
32
+ };
33
+ }
34
+ describe('NotificationDataSourceProvider', ()=>{
35
+ let configService;
36
+ let provider;
37
+ beforeEach(()=>{
38
+ configService = buildConfigService();
39
+ provider = new _notificationdatasourceprovider.NotificationDataSourceProvider(configService, undefined);
40
+ });
41
+ describe('getEnableCompanyFeatureForTenant', ()=>{
42
+ it('returns the tenant override when the tenant explicitly enables the company feature', ()=>{
43
+ const result = provider.getEnableCompanyFeatureForTenant({
44
+ id: 'tenant-1',
45
+ database: 'tenant_db',
46
+ enableCompanyFeature: true
47
+ });
48
+ expect(result).toBe(true);
49
+ expect(configService.isCompanyFeatureEnabled).not.toHaveBeenCalled();
50
+ });
51
+ it('returns the tenant override when the tenant explicitly disables the company feature', ()=>{
52
+ const result = provider.getEnableCompanyFeatureForTenant({
53
+ id: 'tenant-1',
54
+ database: 'tenant_db',
55
+ enableCompanyFeature: false
56
+ });
57
+ expect(result).toBe(false);
58
+ });
59
+ it('falls back to the config service when the tenant has no override', ()=>{
60
+ configService.isCompanyFeatureEnabled.mockReturnValue(true);
61
+ const result = provider.getEnableCompanyFeatureForTenant(undefined);
62
+ expect(result).toBe(true);
63
+ expect(configService.isCompanyFeatureEnabled).toHaveBeenCalled();
64
+ });
65
+ });
66
+ describe('getNotificationEntities', ()=>{
67
+ it('returns [NotificationWithCompany] when enableCompanyFeature is explicitly true', async ()=>{
68
+ const entities = await provider.getNotificationEntities(true);
69
+ expect(entities).toEqual([
70
+ _entities.NotificationWithCompany
71
+ ]);
72
+ });
73
+ it('returns [Notification] when enableCompanyFeature is explicitly false', async ()=>{
74
+ const entities = await provider.getNotificationEntities(false);
75
+ expect(entities).toEqual([
76
+ _entities.Notification
77
+ ]);
78
+ });
79
+ it('falls back to the config service when enableCompanyFeature is not provided', async ()=>{
80
+ configService.isCompanyFeatureEnabled.mockReturnValue(true);
81
+ const entities = await provider.getNotificationEntities();
82
+ expect(entities).toEqual([
83
+ _entities.NotificationWithCompany
84
+ ]);
85
+ expect(configService.isCompanyFeatureEnabled).toHaveBeenCalled();
86
+ });
87
+ });
88
+ describe('createDataSourceFromConfig', ()=>{
89
+ it('resolves company-aware entities and delegates to the parent implementation', async ()=>{
90
+ configService.isCompanyFeatureEnabled.mockReturnValue(true);
91
+ const fakeDataSource = {};
92
+ const superSpy = jest.spyOn(_modules.MultiTenantDataSourceService.prototype, 'createDataSourceFromConfig').mockResolvedValue(fakeDataSource);
93
+ const config = {
94
+ type: 'mysql',
95
+ host: 'localhost',
96
+ port: 3306,
97
+ username: 'root',
98
+ password: 'pass',
99
+ database: 'flusys'
100
+ };
101
+ const result = await provider.createDataSourceFromConfig(config);
102
+ expect(result).toBe(fakeDataSource);
103
+ expect(superSpy).toHaveBeenCalledWith(config, [
104
+ _entities.NotificationWithCompany
105
+ ]);
106
+ superSpy.mockRestore();
107
+ });
108
+ it('resolves core entities when the company feature is disabled', async ()=>{
109
+ configService.isCompanyFeatureEnabled.mockReturnValue(false);
110
+ const fakeDataSource = {};
111
+ const superSpy = jest.spyOn(_modules.MultiTenantDataSourceService.prototype, 'createDataSourceFromConfig').mockResolvedValue(fakeDataSource);
112
+ const config = {
113
+ type: 'mysql',
114
+ host: 'localhost',
115
+ port: 3306,
116
+ username: 'root',
117
+ password: 'pass',
118
+ database: 'flusys'
119
+ };
120
+ await provider.createDataSourceFromConfig(config);
121
+ expect(superSpy).toHaveBeenCalledWith(config, [
122
+ _entities.Notification
123
+ ]);
124
+ superSpy.mockRestore();
125
+ });
126
+ });
127
+ describe('inherited configuration accessors', ()=>{
128
+ it('reports single database mode by default', ()=>{
129
+ expect(provider.getDatabaseMode()).toBe('single');
130
+ expect(provider.isMultiTenant()).toBe(false);
131
+ });
132
+ it('returns null for the current tenant when no request is bound', ()=>{
133
+ expect(provider.getCurrentTenant()).toBeNull();
134
+ });
135
+ });
136
+ });
@@ -0,0 +1,306 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ const _testing = require("@nestjs/testing");
6
+ const _modules = require("@flusys/nestjs-shared/modules");
7
+ const _enums = require("@flusys/nestjs-shared/enums");
8
+ const _notificationhelperservice = require("./notification-helper.service");
9
+ const _notificationconfigservice = require("./notification-config.service");
10
+ const _notificationdatasourceprovider = require("./notification-datasource.provider");
11
+ const _gateways = require("../gateways");
12
+ const _entities = require("../entities");
13
+ function createMockRepo() {
14
+ return {
15
+ save: jest.fn()
16
+ };
17
+ }
18
+ function createMockDataSource(repo) {
19
+ return {
20
+ getRepository: jest.fn().mockReturnValue(repo)
21
+ };
22
+ }
23
+ describe('NotificationHelperService', ()=>{
24
+ let mockCacheManager;
25
+ let mockUtilsService;
26
+ let mockConfigService;
27
+ let mockDataSourceProvider;
28
+ let mockGateway;
29
+ let mockRepo;
30
+ let mockDataSource;
31
+ async function buildService(withGateway = true) {
32
+ const providers = [
33
+ _notificationhelperservice.NotificationHelperService,
34
+ {
35
+ provide: 'CACHE_INSTANCE',
36
+ useValue: mockCacheManager
37
+ },
38
+ {
39
+ provide: _modules.UtilsService,
40
+ useValue: mockUtilsService
41
+ },
42
+ {
43
+ provide: _notificationconfigservice.NotificationConfigService,
44
+ useValue: mockConfigService
45
+ },
46
+ {
47
+ provide: _notificationdatasourceprovider.NotificationDataSourceProvider,
48
+ useValue: mockDataSourceProvider
49
+ }
50
+ ];
51
+ if (withGateway) {
52
+ providers.push({
53
+ provide: _gateways.NotificationGateway,
54
+ useValue: mockGateway
55
+ });
56
+ }
57
+ const module = await _testing.Test.createTestingModule({
58
+ providers
59
+ }).compile();
60
+ return module.get(_notificationhelperservice.NotificationHelperService);
61
+ }
62
+ beforeEach(()=>{
63
+ mockRepo = createMockRepo();
64
+ mockDataSource = createMockDataSource(mockRepo);
65
+ mockCacheManager = {};
66
+ mockUtilsService = {
67
+ clearCache: jest.fn().mockResolvedValue(undefined)
68
+ };
69
+ mockConfigService = {
70
+ isCompanyFeatureEnabled: jest.fn().mockReturnValue(false)
71
+ };
72
+ mockDataSourceProvider = {
73
+ getDataSource: jest.fn().mockResolvedValue(mockDataSource)
74
+ };
75
+ mockGateway = {
76
+ emitToUser: jest.fn(),
77
+ emitToUserInCompany: jest.fn(),
78
+ isUserOnline: jest.fn(),
79
+ getOnlineUserCount: jest.fn()
80
+ };
81
+ });
82
+ describe('send', ()=>{
83
+ it('saves the notification, clears the cache, and emits to the user in real time', async ()=>{
84
+ const service = await buildService();
85
+ const options = {
86
+ userId: 'user-1',
87
+ title: 'Hi',
88
+ message: 'body',
89
+ type: _enums.NotificationType.SUCCESS,
90
+ data: {
91
+ a: 1
92
+ }
93
+ };
94
+ const saved = {
95
+ id: 'notif-1',
96
+ ...options,
97
+ isRead: false
98
+ };
99
+ mockRepo.save.mockResolvedValue(saved);
100
+ const result = await service.send(options);
101
+ expect(mockDataSource.getRepository).toHaveBeenCalledWith(_entities.Notification);
102
+ expect(mockRepo.save).toHaveBeenCalledWith(expect.objectContaining({
103
+ userId: 'user-1',
104
+ title: 'Hi',
105
+ isRead: false
106
+ }));
107
+ expect(mockUtilsService.clearCache).toHaveBeenCalledWith('notification', mockCacheManager);
108
+ expect(mockGateway.emitToUser).toHaveBeenCalledWith('user-1', 'notification:new', expect.objectContaining({
109
+ id: 'notif-1',
110
+ title: 'Hi'
111
+ }));
112
+ expect(result).toBe(saved);
113
+ });
114
+ it('defaults type to INFO and message/data to null when omitted', async ()=>{
115
+ const service = await buildService();
116
+ mockRepo.save.mockImplementation(async (entity)=>({
117
+ id: 'notif-2',
118
+ ...entity
119
+ }));
120
+ await service.send({
121
+ userId: 'user-1',
122
+ title: 'Only title'
123
+ });
124
+ expect(mockRepo.save).toHaveBeenCalledWith(expect.objectContaining({
125
+ type: _enums.NotificationType.INFO,
126
+ message: null,
127
+ data: null
128
+ }));
129
+ });
130
+ it('does not emit in real time when realtime is explicitly false', async ()=>{
131
+ const service = await buildService();
132
+ mockRepo.save.mockResolvedValue({
133
+ id: 'notif-3',
134
+ userId: 'user-1',
135
+ title: 'Silent'
136
+ });
137
+ await service.send({
138
+ userId: 'user-1',
139
+ title: 'Silent',
140
+ realtime: false
141
+ });
142
+ expect(mockGateway.emitToUser).not.toHaveBeenCalled();
143
+ expect(mockGateway.emitToUserInCompany).not.toHaveBeenCalled();
144
+ });
145
+ it('does not throw when no gateway is bound (gateway not registered)', async ()=>{
146
+ const service = await buildService(false);
147
+ mockRepo.save.mockResolvedValue({
148
+ id: 'notif-4',
149
+ userId: 'user-1',
150
+ title: 'No gateway'
151
+ });
152
+ await expect(service.send({
153
+ userId: 'user-1',
154
+ title: 'No gateway'
155
+ })).resolves.toBeDefined();
156
+ });
157
+ it('uses NotificationWithCompany and includes companyId when the company feature is enabled', async ()=>{
158
+ mockConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
159
+ const service = await buildService();
160
+ mockRepo.save.mockImplementation(async (entity)=>({
161
+ id: 'notif-5',
162
+ ...entity
163
+ }));
164
+ await service.send({
165
+ userId: 'user-1',
166
+ title: 'Scoped',
167
+ companyId: 'company-1'
168
+ });
169
+ expect(mockDataSource.getRepository).toHaveBeenCalledWith(_entities.NotificationWithCompany);
170
+ expect(mockRepo.save).toHaveBeenCalledWith(expect.objectContaining({
171
+ companyId: 'company-1'
172
+ }));
173
+ expect(mockGateway.emitToUserInCompany).toHaveBeenCalledWith('user-1', 'company-1', 'notification:new', expect.any(Object));
174
+ });
175
+ it('omits companyId when the company feature is enabled but none was provided', async ()=>{
176
+ mockConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
177
+ const service = await buildService();
178
+ mockRepo.save.mockImplementation(async (entity)=>({
179
+ id: 'notif-6',
180
+ ...entity
181
+ }));
182
+ await service.send({
183
+ userId: 'user-1',
184
+ title: 'No company'
185
+ });
186
+ const savedArg = mockRepo.save.mock.calls[0][0];
187
+ expect(savedArg.companyId).toBeUndefined();
188
+ });
189
+ it('reuses the same cached data source across multiple calls', async ()=>{
190
+ const service = await buildService();
191
+ mockRepo.save.mockResolvedValue({
192
+ id: 'notif-7',
193
+ userId: 'user-1',
194
+ title: 'A'
195
+ });
196
+ await service.send({
197
+ userId: 'user-1',
198
+ title: 'A'
199
+ });
200
+ await service.send({
201
+ userId: 'user-1',
202
+ title: 'B'
203
+ });
204
+ expect(mockDataSourceProvider.getDataSource).toHaveBeenCalledTimes(1);
205
+ });
206
+ });
207
+ describe('sendToMany', ()=>{
208
+ it('saves a notification for every user id and clears the cache once', async ()=>{
209
+ const service = await buildService();
210
+ const options = {
211
+ userIds: [
212
+ 'user-1',
213
+ 'user-2',
214
+ 'user-3'
215
+ ],
216
+ title: 'Broadcast'
217
+ };
218
+ mockRepo.save.mockImplementation(async (entities)=>entities.map((e, i)=>({
219
+ id: `notif-${i}`,
220
+ ...e
221
+ })));
222
+ const result = await service.sendToMany(options);
223
+ expect(mockRepo.save).toHaveBeenCalledWith([
224
+ expect.objectContaining({
225
+ userId: 'user-1'
226
+ }),
227
+ expect.objectContaining({
228
+ userId: 'user-2'
229
+ }),
230
+ expect.objectContaining({
231
+ userId: 'user-3'
232
+ })
233
+ ]);
234
+ expect(mockUtilsService.clearCache).toHaveBeenCalledTimes(1);
235
+ expect(result).toHaveLength(3);
236
+ });
237
+ it('handles an empty userIds array without saving or emitting', async ()=>{
238
+ const service = await buildService();
239
+ mockRepo.save.mockResolvedValue([]);
240
+ const result = await service.sendToMany({
241
+ userIds: [],
242
+ title: 'Nobody'
243
+ });
244
+ expect(mockRepo.save).toHaveBeenCalledWith([]);
245
+ expect(result).toEqual([]);
246
+ });
247
+ it('emits to each user asynchronously (after the microtask queue flushes) when realtime', async ()=>{
248
+ const service = await buildService();
249
+ mockRepo.save.mockImplementation(async (entities)=>entities.map((e, i)=>({
250
+ id: `notif-${i}`,
251
+ ...e
252
+ })));
253
+ await service.sendToMany({
254
+ userIds: [
255
+ 'user-1',
256
+ 'user-2'
257
+ ],
258
+ title: 'Broadcast'
259
+ });
260
+ expect(mockGateway.emitToUser).not.toHaveBeenCalled();
261
+ await new Promise((resolve)=>setImmediate(resolve));
262
+ expect(mockGateway.emitToUser).toHaveBeenCalledTimes(2);
263
+ expect(mockGateway.emitToUser).toHaveBeenCalledWith('user-1', 'notification:new', expect.any(Object));
264
+ expect(mockGateway.emitToUser).toHaveBeenCalledWith('user-2', 'notification:new', expect.any(Object));
265
+ });
266
+ it('does not schedule emits when realtime is false', async ()=>{
267
+ const service = await buildService();
268
+ mockRepo.save.mockImplementation(async (entities)=>entities.map((e, i)=>({
269
+ id: `notif-${i}`,
270
+ ...e
271
+ })));
272
+ await service.sendToMany({
273
+ userIds: [
274
+ 'user-1'
275
+ ],
276
+ title: 'Silent',
277
+ realtime: false
278
+ });
279
+ await new Promise((resolve)=>setImmediate(resolve));
280
+ expect(mockGateway.emitToUser).not.toHaveBeenCalled();
281
+ });
282
+ });
283
+ describe('isUserOnline', ()=>{
284
+ it('delegates to the gateway when present', async ()=>{
285
+ const service = await buildService();
286
+ mockGateway.isUserOnline.mockReturnValue(true);
287
+ expect(service.isUserOnline('user-1')).toBe(true);
288
+ expect(mockGateway.isUserOnline).toHaveBeenCalledWith('user-1');
289
+ });
290
+ it('returns false when no gateway is bound', async ()=>{
291
+ const service = await buildService(false);
292
+ expect(service.isUserOnline('user-1')).toBe(false);
293
+ });
294
+ });
295
+ describe('getOnlineUserCount', ()=>{
296
+ it('delegates to the gateway when present', async ()=>{
297
+ const service = await buildService();
298
+ mockGateway.getOnlineUserCount.mockReturnValue(5);
299
+ expect(service.getOnlineUserCount()).toBe(5);
300
+ });
301
+ it('returns 0 when no gateway is bound', async ()=>{
302
+ const service = await buildService(false);
303
+ expect(service.getOnlineUserCount()).toBe(0);
304
+ });
305
+ });
306
+ });