@flusys/nestjs-notification 9.1.1 → 9.2.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 +8 -1
- package/controllers/notification.controller.d.ts +1 -0
- package/fesm/244.js +8 -2
- package/fesm/446.js +40 -33
- package/fesm/docs/index.js +2 -2
- package/fesm/dtos/index.js +2 -1
- package/fesm/gateways/index.js +5 -1
- package/fesm/index.js +2 -2
- package/fesm/services/index.js +38 -32
- package/package.json +3 -3
- package/services/notification.service.d.ts +8 -3
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ import { NotificationModule } from '@flusys/nestjs-notification';
|
|
|
49
49
|
export class AppModule {}
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
The Socket.IO gateway is always registered. Real-time delivery is chosen per send: pass `realtime: false` to store a notification without pushing it.
|
|
52
|
+
The Socket.IO gateway is always registered. Real-time delivery is chosen per send: pass `realtime: false` to store a notification without pushing it. Clients connect to the `/notifications` namespace with an **access** token (`auth.token`, `?token=` or `Authorization: Bearer`) signed with `config.jwtSecret` (HS256); refresh tokens and tokens without a user id are refused.
|
|
53
53
|
|
|
54
54
|
#### Mode 2: Multi-Tenant
|
|
55
55
|
|
|
@@ -201,6 +201,13 @@ export class OrderService {
|
|
|
201
201
|
|
|
202
202
|
`NotificationModule` provides and exports `NOTIFICATION_ADAPTER` (`nestjs-shared`), implemented by `NotificationAdapter`: `send({ userId, ...content })`, `sendToMany({ userIds, ...content })` and `isUserOnline(userId)`. It is a singleton: it resolves the request-scoped `NotificationHelperService` (tenant datasource) per call and asks the gateway who is online, so a singleton that injects the token stays one. A feature package injects it `@Optional()` and never imports this package (auth's login notice and the entity builder's **Send notification** flow step use it).
|
|
203
203
|
|
|
204
|
+
## 5. Caching and Multiple Instances
|
|
205
|
+
|
|
206
|
+
- **No cached reads.** `NotificationService` is not cacheable: every list is scoped to the caller (user + company), and the unread count is always a live `COUNT`. Every write still bumps the `notification` cache stamp after it commits (`ApiService` CRUD, `mark-read`, `mark-all-read`, `send`, `send-bulk`), so a service that lists `NOTIFICATION_EVENT_ENTITIES.NOTIFICATION` in its `cacheDependencies()` stays correct.
|
|
207
|
+
- **`notification:new` is pushed after commit.** CRUD inserts emit from `publishLifecycleEvents` (after the transaction commits), so a client that refetches on the event always finds the row.
|
|
208
|
+
- **Socket fan-out is per instance.** The gateway uses Socket.IO's default in-memory adapter: an emit reaches only sockets connected to the instance that handled the write, and `isUserOnline()` / `getOnlineUserCount()` only see local sockets. Behind several instances, install a Socket.IO pub/sub adapter (e.g. `@socket.io/redis-adapter` via a custom `IoAdapter` in the app) so rooms span instances; the online checks stay instance-local even then.
|
|
209
|
+
- **Socket auth is checked at connect only.** The handshake verifies the access token's signature, expiry and `type`, but not nestjs-auth's session revocation markers (logout, deactivation): a revoked but unexpired token can still connect, and an open socket keeps receiving its rooms' events after revocation or expiry until it reconnects.
|
|
210
|
+
|
|
204
211
|
## License
|
|
205
212
|
|
|
206
213
|
MIT © FLUSYS
|
|
@@ -7,6 +7,7 @@ declare const NotificationController_base: abstract new (service: NotificationSe
|
|
|
7
7
|
readonly enabledEndpoints: import("@flusys/nestjs-shared").ApiEndpoint[] | "all";
|
|
8
8
|
service: NotificationService;
|
|
9
9
|
isEnabled(endpoint: import("@flusys/nestjs-shared").ApiEndpoint): boolean;
|
|
10
|
+
assertEnabled(endpoint: import("@flusys/nestjs-shared").ApiEndpoint): void;
|
|
10
11
|
insert(addDto: CreateNotificationDto, user: ILoggedUserInfo | null): Promise<SingleResponseDto<NotificationResponseDto>>;
|
|
11
12
|
insertMany(addDto: CreateNotificationDto[], user: ILoggedUserInfo | null): Promise<BulkResponseDto<NotificationResponseDto>>;
|
|
12
13
|
getById(id: string, body: import("@flusys/nestjs-shared").GetByIdBodyDto, user: ILoggedUserInfo | null): Promise<SingleResponseDto<NotificationResponseDto>>;
|
package/fesm/244.js
CHANGED
|
@@ -389,12 +389,16 @@ class NotificationGateway {
|
|
|
389
389
|
if (!secret) {
|
|
390
390
|
return null;
|
|
391
391
|
}
|
|
392
|
-
|
|
392
|
+
const payload = this.jwtService.verify(token, {
|
|
393
393
|
secret,
|
|
394
394
|
algorithms: [
|
|
395
395
|
'HS256'
|
|
396
396
|
]
|
|
397
397
|
});
|
|
398
|
+
if (payload?.type !== 'access' || typeof payload.id !== 'string' || !payload.id) {
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
return payload;
|
|
398
402
|
} catch {
|
|
399
403
|
return null;
|
|
400
404
|
}
|
|
@@ -729,8 +733,10 @@ class NotificationHelperService {
|
|
|
729
733
|
createdAt: notification.createdAt
|
|
730
734
|
};
|
|
731
735
|
}
|
|
736
|
+
// Nothing caches notifications today, but NotificationService bumps this stamp on every write too, so a
|
|
737
|
+
// service that later lists 'notification' as a cache dependency can rely on it
|
|
732
738
|
async clearCache() {
|
|
733
|
-
await this.utilsService.clearCache(_config_index_js__rspack_import_10/* .NOTIFICATION_EVENT_ENTITIES.NOTIFICATION */.BA.NOTIFICATION, this.cacheManager,
|
|
739
|
+
await this.utilsService.clearCache(_config_index_js__rspack_import_10/* .NOTIFICATION_EVENT_ENTITIES.NOTIFICATION */.BA.NOTIFICATION, this.cacheManager, (0,_flusys_nestjs_shared_utils__rspack_import_4.cacheTenantId)(this.dataSourceProvider));
|
|
734
740
|
}
|
|
735
741
|
}
|
|
736
742
|
_ts_decorate([
|
package/fesm/446.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IsEnum, IsNotEmpty, IsObject, IsOptional, IsString, IsUUID, MaxLength } from "class-validator";
|
|
1
|
+
import { IsArray, IsEnum, IsNotEmpty, IsObject, IsOptional, IsString, IsUUID, MaxLength } from "class-validator";
|
|
2
2
|
import * as __rspack_external__nestjs_swagger_7cd450b5 from "@nestjs/swagger";
|
|
3
3
|
import * as __rspack_external_class_transformer_c5a479fb from "class-transformer";
|
|
4
4
|
import * as __rspack_external__flusys_nestjs_shared_decorators_c129b601 from "@flusys/nestjs-shared/decorators";
|
|
@@ -453,6 +453,7 @@ _ts_decorate([
|
|
|
453
453
|
]
|
|
454
454
|
}),
|
|
455
455
|
IsNotEmpty(),
|
|
456
|
+
IsArray(),
|
|
456
457
|
IsUUID('4', {
|
|
457
458
|
each: true
|
|
458
459
|
}),
|
|
@@ -674,13 +675,16 @@ class NotificationService extends _flusys_nestjs_shared_classes__rspack_import_1
|
|
|
674
675
|
}
|
|
675
676
|
async convertSingleDtoToEntity(dto, user) {
|
|
676
677
|
let entity = {};
|
|
677
|
-
|
|
678
|
+
const companyFeature = this.notificationConfig.isCompanyFeatureEnabled();
|
|
679
|
+
const isUpdate = 'id' in dto && !!dto.id;
|
|
680
|
+
if (isUpdate) {
|
|
678
681
|
const existing = await this.repository.findOne({
|
|
679
682
|
where: {
|
|
680
683
|
id: dto.id
|
|
681
684
|
}
|
|
682
685
|
});
|
|
683
|
-
|
|
686
|
+
const otherCompany = companyFeature && (!user?.companyId || existing?.companyId !== user.companyId);
|
|
687
|
+
if (!existing || otherCompany) {
|
|
684
688
|
throw new _nestjs_common__rspack_import_4.NotFoundException({
|
|
685
689
|
message: 'Notification not found',
|
|
686
690
|
messageKey: _config_index_js__rspack_import_10/* .NOTIFICATION_MESSAGES.NOT_FOUND */.x.NOT_FOUND
|
|
@@ -688,9 +692,10 @@ class NotificationService extends _flusys_nestjs_shared_classes__rspack_import_1
|
|
|
688
692
|
}
|
|
689
693
|
entity = existing;
|
|
690
694
|
}
|
|
695
|
+
const recipient = 'userId' in dto ? dto.userId : undefined;
|
|
691
696
|
const merged = {
|
|
692
697
|
...entity,
|
|
693
|
-
userId:
|
|
698
|
+
userId: isUpdate ? entity.userId : recipient ?? user?.id,
|
|
694
699
|
type: dto.type ?? entity.type ?? 'info',
|
|
695
700
|
title: dto.title ?? entity.title,
|
|
696
701
|
message: dto.message ?? entity.message,
|
|
@@ -698,8 +703,8 @@ class NotificationService extends _flusys_nestjs_shared_classes__rspack_import_1
|
|
|
698
703
|
isRead: entity.isRead ?? false,
|
|
699
704
|
readAt: entity.readAt
|
|
700
705
|
};
|
|
701
|
-
if (
|
|
702
|
-
merged.companyId = dto.companyId ?? user?.companyId ??
|
|
706
|
+
if (companyFeature && !isUpdate) {
|
|
707
|
+
merged.companyId = dto.companyId ?? user?.companyId ?? null;
|
|
703
708
|
}
|
|
704
709
|
return merged;
|
|
705
710
|
}
|
|
@@ -759,11 +764,16 @@ class NotificationService extends _flusys_nestjs_shared_classes__rspack_import_1
|
|
|
759
764
|
}
|
|
760
765
|
return result;
|
|
761
766
|
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
767
|
+
// Pushed only once the insert has committed: a client refetching on `notification:new` must find the row
|
|
768
|
+
async publishLifecycleEvents(groups, user) {
|
|
769
|
+
for (const group of groups){
|
|
770
|
+
if (group.action !== 'created') continue;
|
|
771
|
+
for (const entity of group.entities){
|
|
772
|
+
const ext = entity;
|
|
773
|
+
this.emitToUser(entity.userId, ext.companyId, 'notification:new', this.toPayload(entity));
|
|
774
|
+
}
|
|
766
775
|
}
|
|
776
|
+
await super.publishLifecycleEvents(groups, user);
|
|
767
777
|
}
|
|
768
778
|
async markAsRead(id, user) {
|
|
769
779
|
await this.ensureDataSourceRepository();
|
|
@@ -782,13 +792,7 @@ class NotificationService extends _flusys_nestjs_shared_classes__rspack_import_1
|
|
|
782
792
|
notification.isRead = true;
|
|
783
793
|
notification.readAt = new Date();
|
|
784
794
|
await this.repository.save(notification);
|
|
785
|
-
await
|
|
786
|
-
this.clearCacheForAll(),
|
|
787
|
-
this.clearCacheForId([
|
|
788
|
-
notification
|
|
789
|
-
])
|
|
790
|
-
]);
|
|
791
|
-
// Emit to user's sockets in the same company context
|
|
795
|
+
await this.invalidateCache();
|
|
792
796
|
const ext = notification;
|
|
793
797
|
this.emitToUser(user.id, ext.companyId ?? user.companyId, 'notification:read', {
|
|
794
798
|
id
|
|
@@ -802,20 +806,14 @@ class NotificationService extends _flusys_nestjs_shared_classes__rspack_import_1
|
|
|
802
806
|
return notification;
|
|
803
807
|
}
|
|
804
808
|
async markAllAsRead(user) {
|
|
809
|
+
const where = this.unreadWhere(user);
|
|
810
|
+
if (!where) return 0;
|
|
805
811
|
await this.ensureDataSourceRepository();
|
|
806
|
-
const where = {
|
|
807
|
-
userId: user.id,
|
|
808
|
-
isRead: false
|
|
809
|
-
};
|
|
810
|
-
if (this.notificationConfig.isCompanyFeatureEnabled() && user.companyId) {
|
|
811
|
-
where.companyId = user.companyId;
|
|
812
|
-
}
|
|
813
812
|
const result = await this.repository.update(where, {
|
|
814
813
|
isRead: true,
|
|
815
814
|
readAt: new Date()
|
|
816
815
|
});
|
|
817
|
-
await this.
|
|
818
|
-
// Emit to user's sockets in the current company context
|
|
816
|
+
await this.invalidateCache();
|
|
819
817
|
this.emitToUser(user.id, user.companyId, 'notification:read-all', {});
|
|
820
818
|
await this.publishDomainAction(_config_index_js__rspack_import_9/* .NOTIFICATION_EVENT_ACTIONS.ALL_READ */.R.ALL_READ, {
|
|
821
819
|
metadata: {
|
|
@@ -826,18 +824,27 @@ class NotificationService extends _flusys_nestjs_shared_classes__rspack_import_1
|
|
|
826
824
|
return result.affected ?? 0;
|
|
827
825
|
}
|
|
828
826
|
async getUnreadCount(user) {
|
|
827
|
+
const where = this.unreadWhere(user);
|
|
828
|
+
if (!where) return 0;
|
|
829
829
|
await this.ensureDataSourceRepository();
|
|
830
|
-
const where = {
|
|
831
|
-
userId: user.id,
|
|
832
|
-
isRead: false
|
|
833
|
-
};
|
|
834
|
-
if (this.notificationConfig.isCompanyFeatureEnabled() && user.companyId) {
|
|
835
|
-
where.companyId = user.companyId;
|
|
836
|
-
}
|
|
837
830
|
return this.repository.count({
|
|
838
831
|
where
|
|
839
832
|
});
|
|
840
833
|
}
|
|
834
|
+
unreadWhere(user) {
|
|
835
|
+
if (!this.notificationConfig.isCompanyFeatureEnabled()) {
|
|
836
|
+
return {
|
|
837
|
+
userId: user.id,
|
|
838
|
+
isRead: false
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
if (!user.companyId) return null;
|
|
842
|
+
return {
|
|
843
|
+
userId: user.id,
|
|
844
|
+
isRead: false,
|
|
845
|
+
companyId: user.companyId
|
|
846
|
+
};
|
|
847
|
+
}
|
|
841
848
|
toPayload(entity) {
|
|
842
849
|
return {
|
|
843
850
|
id: entity.id,
|
package/fesm/docs/index.js
CHANGED
|
@@ -74,9 +74,9 @@ Real-time notification system with WebSocket support.
|
|
|
74
74
|
|
|
75
75
|
### WebSocket Events
|
|
76
76
|
|
|
77
|
-
Connect to
|
|
77
|
+
Connect to the \`/notifications\` Socket.IO namespace with an access token (\`auth.token\`, \`?token=\` or \`Authorization: Bearer\`):
|
|
78
78
|
\`\`\`
|
|
79
|
-
ws://localhost:2002/
|
|
79
|
+
ws://localhost:2002/notifications?token=<access-token>
|
|
80
80
|
\`\`\`
|
|
81
81
|
|
|
82
82
|
**Events:**
|
package/fesm/dtos/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IsEnum, IsNotEmpty, IsObject, IsOptional, IsString, IsUUID, MaxLength } from "class-validator";
|
|
1
|
+
import { IsArray, IsEnum, IsNotEmpty, IsObject, IsOptional, IsString, IsUUID, MaxLength } from "class-validator";
|
|
2
2
|
import * as __rspack_external__flusys_nestjs_shared_enums_327421f8 from "@flusys/nestjs-shared/enums";
|
|
3
3
|
import * as __rspack_external__nestjs_swagger_7cd450b5 from "@nestjs/swagger";
|
|
4
4
|
import * as __rspack_external_class_transformer_c5a479fb from "class-transformer";
|
|
@@ -111,6 +111,7 @@ _ts_decorate([
|
|
|
111
111
|
]
|
|
112
112
|
}),
|
|
113
113
|
IsNotEmpty(),
|
|
114
|
+
IsArray(),
|
|
114
115
|
IsUUID('4', {
|
|
115
116
|
each: true
|
|
116
117
|
}),
|
package/fesm/gateways/index.js
CHANGED
|
@@ -173,12 +173,16 @@ class NotificationGateway {
|
|
|
173
173
|
if (!secret) {
|
|
174
174
|
return null;
|
|
175
175
|
}
|
|
176
|
-
|
|
176
|
+
const payload = this.jwtService.verify(token, {
|
|
177
177
|
secret,
|
|
178
178
|
algorithms: [
|
|
179
179
|
'HS256'
|
|
180
180
|
]
|
|
181
181
|
});
|
|
182
|
+
if (payload?.type !== 'access' || typeof payload.id !== 'string' || !payload.id) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
return payload;
|
|
182
186
|
} catch {
|
|
183
187
|
return null;
|
|
184
188
|
}
|
package/fesm/index.js
CHANGED
|
@@ -175,9 +175,9 @@ Real-time notification system with WebSocket support.
|
|
|
175
175
|
|
|
176
176
|
### WebSocket Events
|
|
177
177
|
|
|
178
|
-
Connect to
|
|
178
|
+
Connect to the \`/notifications\` Socket.IO namespace with an access token (\`auth.token\`, \`?token=\` or \`Authorization: Bearer\`):
|
|
179
179
|
\`\`\`
|
|
180
|
-
ws://localhost:2002/
|
|
180
|
+
ws://localhost:2002/notifications?token=<access-token>
|
|
181
181
|
\`\`\`
|
|
182
182
|
|
|
183
183
|
**Events:**
|
package/fesm/services/index.js
CHANGED
|
@@ -93,13 +93,16 @@ class NotificationService extends _flusys_nestjs_shared_classes__rspack_import_1
|
|
|
93
93
|
}
|
|
94
94
|
async convertSingleDtoToEntity(dto, user) {
|
|
95
95
|
let entity = {};
|
|
96
|
-
|
|
96
|
+
const companyFeature = this.notificationConfig.isCompanyFeatureEnabled();
|
|
97
|
+
const isUpdate = 'id' in dto && !!dto.id;
|
|
98
|
+
if (isUpdate) {
|
|
97
99
|
const existing = await this.repository.findOne({
|
|
98
100
|
where: {
|
|
99
101
|
id: dto.id
|
|
100
102
|
}
|
|
101
103
|
});
|
|
102
|
-
|
|
104
|
+
const otherCompany = companyFeature && (!user?.companyId || existing?.companyId !== user.companyId);
|
|
105
|
+
if (!existing || otherCompany) {
|
|
103
106
|
throw new _nestjs_common__rspack_import_4.NotFoundException({
|
|
104
107
|
message: 'Notification not found',
|
|
105
108
|
messageKey: _config_index_js__rspack_import_10/* .NOTIFICATION_MESSAGES.NOT_FOUND */.x.NOT_FOUND
|
|
@@ -107,9 +110,10 @@ class NotificationService extends _flusys_nestjs_shared_classes__rspack_import_1
|
|
|
107
110
|
}
|
|
108
111
|
entity = existing;
|
|
109
112
|
}
|
|
113
|
+
const recipient = 'userId' in dto ? dto.userId : undefined;
|
|
110
114
|
const merged = {
|
|
111
115
|
...entity,
|
|
112
|
-
userId:
|
|
116
|
+
userId: isUpdate ? entity.userId : recipient ?? user?.id,
|
|
113
117
|
type: dto.type ?? entity.type ?? 'info',
|
|
114
118
|
title: dto.title ?? entity.title,
|
|
115
119
|
message: dto.message ?? entity.message,
|
|
@@ -117,8 +121,8 @@ class NotificationService extends _flusys_nestjs_shared_classes__rspack_import_1
|
|
|
117
121
|
isRead: entity.isRead ?? false,
|
|
118
122
|
readAt: entity.readAt
|
|
119
123
|
};
|
|
120
|
-
if (
|
|
121
|
-
merged.companyId = dto.companyId ?? user?.companyId ??
|
|
124
|
+
if (companyFeature && !isUpdate) {
|
|
125
|
+
merged.companyId = dto.companyId ?? user?.companyId ?? null;
|
|
122
126
|
}
|
|
123
127
|
return merged;
|
|
124
128
|
}
|
|
@@ -178,11 +182,16 @@ class NotificationService extends _flusys_nestjs_shared_classes__rspack_import_1
|
|
|
178
182
|
}
|
|
179
183
|
return result;
|
|
180
184
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
+
// Pushed only once the insert has committed: a client refetching on `notification:new` must find the row
|
|
186
|
+
async publishLifecycleEvents(groups, user) {
|
|
187
|
+
for (const group of groups){
|
|
188
|
+
if (group.action !== 'created') continue;
|
|
189
|
+
for (const entity of group.entities){
|
|
190
|
+
const ext = entity;
|
|
191
|
+
this.emitToUser(entity.userId, ext.companyId, 'notification:new', this.toPayload(entity));
|
|
192
|
+
}
|
|
185
193
|
}
|
|
194
|
+
await super.publishLifecycleEvents(groups, user);
|
|
186
195
|
}
|
|
187
196
|
async markAsRead(id, user) {
|
|
188
197
|
await this.ensureDataSourceRepository();
|
|
@@ -201,13 +210,7 @@ class NotificationService extends _flusys_nestjs_shared_classes__rspack_import_1
|
|
|
201
210
|
notification.isRead = true;
|
|
202
211
|
notification.readAt = new Date();
|
|
203
212
|
await this.repository.save(notification);
|
|
204
|
-
await
|
|
205
|
-
this.clearCacheForAll(),
|
|
206
|
-
this.clearCacheForId([
|
|
207
|
-
notification
|
|
208
|
-
])
|
|
209
|
-
]);
|
|
210
|
-
// Emit to user's sockets in the same company context
|
|
213
|
+
await this.invalidateCache();
|
|
211
214
|
const ext = notification;
|
|
212
215
|
this.emitToUser(user.id, ext.companyId ?? user.companyId, 'notification:read', {
|
|
213
216
|
id
|
|
@@ -221,20 +224,14 @@ class NotificationService extends _flusys_nestjs_shared_classes__rspack_import_1
|
|
|
221
224
|
return notification;
|
|
222
225
|
}
|
|
223
226
|
async markAllAsRead(user) {
|
|
227
|
+
const where = this.unreadWhere(user);
|
|
228
|
+
if (!where) return 0;
|
|
224
229
|
await this.ensureDataSourceRepository();
|
|
225
|
-
const where = {
|
|
226
|
-
userId: user.id,
|
|
227
|
-
isRead: false
|
|
228
|
-
};
|
|
229
|
-
if (this.notificationConfig.isCompanyFeatureEnabled() && user.companyId) {
|
|
230
|
-
where.companyId = user.companyId;
|
|
231
|
-
}
|
|
232
230
|
const result = await this.repository.update(where, {
|
|
233
231
|
isRead: true,
|
|
234
232
|
readAt: new Date()
|
|
235
233
|
});
|
|
236
|
-
await this.
|
|
237
|
-
// Emit to user's sockets in the current company context
|
|
234
|
+
await this.invalidateCache();
|
|
238
235
|
this.emitToUser(user.id, user.companyId, 'notification:read-all', {});
|
|
239
236
|
await this.publishDomainAction(_config_index_js__rspack_import_9/* .NOTIFICATION_EVENT_ACTIONS.ALL_READ */.R.ALL_READ, {
|
|
240
237
|
metadata: {
|
|
@@ -245,18 +242,27 @@ class NotificationService extends _flusys_nestjs_shared_classes__rspack_import_1
|
|
|
245
242
|
return result.affected ?? 0;
|
|
246
243
|
}
|
|
247
244
|
async getUnreadCount(user) {
|
|
245
|
+
const where = this.unreadWhere(user);
|
|
246
|
+
if (!where) return 0;
|
|
248
247
|
await this.ensureDataSourceRepository();
|
|
249
|
-
const where = {
|
|
250
|
-
userId: user.id,
|
|
251
|
-
isRead: false
|
|
252
|
-
};
|
|
253
|
-
if (this.notificationConfig.isCompanyFeatureEnabled() && user.companyId) {
|
|
254
|
-
where.companyId = user.companyId;
|
|
255
|
-
}
|
|
256
248
|
return this.repository.count({
|
|
257
249
|
where
|
|
258
250
|
});
|
|
259
251
|
}
|
|
252
|
+
unreadWhere(user) {
|
|
253
|
+
if (!this.notificationConfig.isCompanyFeatureEnabled()) {
|
|
254
|
+
return {
|
|
255
|
+
userId: user.id,
|
|
256
|
+
isRead: false
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
if (!user.companyId) return null;
|
|
260
|
+
return {
|
|
261
|
+
userId: user.id,
|
|
262
|
+
isRead: false,
|
|
263
|
+
companyId: user.companyId
|
|
264
|
+
};
|
|
265
|
+
}
|
|
260
266
|
toPayload(entity) {
|
|
261
267
|
return {
|
|
262
268
|
id: entity.id,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flusys/nestjs-notification",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.2.0",
|
|
4
4
|
"description": "Notification module with real-time Socket.IO support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./fesm/index.js",
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"express": "^5.0.0"
|
|
83
83
|
},
|
|
84
84
|
"dependencies": {
|
|
85
|
-
"@flusys/nestjs-core": "9.
|
|
86
|
-
"@flusys/nestjs-shared": "9.
|
|
85
|
+
"@flusys/nestjs-core": "9.2.0",
|
|
86
|
+
"@flusys/nestjs-shared": "9.2.0"
|
|
87
87
|
}
|
|
88
88
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ApiService, HybridCache } from '@flusys/nestjs-shared/classes';
|
|
2
|
-
import { type ILoggedUserInfo } from '@flusys/nestjs-shared/interfaces';
|
|
2
|
+
import { type CrudEventActionName, type ILoggedUserInfo } from '@flusys/nestjs-shared/interfaces';
|
|
3
3
|
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
4
|
-
import {
|
|
4
|
+
import { SelectQueryBuilder } from 'typeorm';
|
|
5
5
|
import { CreateNotificationDto, UpdateNotificationDto } from '../dtos/index.js';
|
|
6
6
|
import { NotificationBase } from '../entities/index.js';
|
|
7
7
|
import { NotificationGateway } from '../gateways/index.js';
|
|
@@ -27,10 +27,15 @@ export declare class NotificationService extends ApiService<CreateNotificationDt
|
|
|
27
27
|
query: SelectQueryBuilder<NotificationBase>;
|
|
28
28
|
isRaw: boolean;
|
|
29
29
|
}>;
|
|
30
|
-
protected
|
|
30
|
+
protected publishLifecycleEvents(groups: {
|
|
31
|
+
action: CrudEventActionName;
|
|
32
|
+
entities: NotificationBase[];
|
|
33
|
+
idsOnly?: boolean;
|
|
34
|
+
}[], user: ILoggedUserInfo | null): Promise<void>;
|
|
31
35
|
markAsRead(id: string, user: ILoggedUserInfo): Promise<NotificationBase>;
|
|
32
36
|
markAllAsRead(user: ILoggedUserInfo): Promise<number>;
|
|
33
37
|
getUnreadCount(user: ILoggedUserInfo): Promise<number>;
|
|
38
|
+
private unreadWhere;
|
|
34
39
|
private toPayload;
|
|
35
40
|
private emitToUser;
|
|
36
41
|
}
|