@tstdl/base 0.93.115 → 0.93.117
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/notification/api/notification.api.d.ts +5 -2
- package/notification/api/notification.api.js +1 -0
- package/notification/client/index.d.ts +1 -0
- package/notification/client/index.js +1 -0
- package/notification/client/notification-client.d.ts +18 -0
- package/notification/client/notification-client.js +62 -0
- package/notification/index.d.ts +2 -0
- package/notification/index.js +2 -0
- package/notification/server/module.d.ts +1 -1
- package/notification/server/providers/in-app-channel-provider.js +4 -1
- package/notification/server/services/notification-sse.service.d.ts +5 -3
- package/notification/server/services/notification-sse.service.js +19 -7
- package/notification/server/services/notification.service.d.ts +2 -0
- package/notification/server/services/notification.service.js +30 -5
- package/notification/tests/notification-flow.test.js +28 -0
- package/notification/tests/notification-sse.service.test.js +1 -1
- package/notification/types.d.ts +9 -0
- package/notification/types.js +6 -0
- package/package.json +2 -2
- package/utils/try-ignore.d.ts +2 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DataStream } from '../../sse/data-stream.js';
|
|
2
2
|
import { InAppNotificationView } from '../models/in-app-notification.model.js';
|
|
3
|
+
import type { NotificationStreamItem } from '../types.js';
|
|
3
4
|
export type NotificationApiDefinition = typeof notificationApiDefinition;
|
|
4
5
|
export declare const notificationApiDefinition: {
|
|
5
6
|
resource: string;
|
|
@@ -8,7 +9,7 @@ export declare const notificationApiDefinition: {
|
|
|
8
9
|
resource: string;
|
|
9
10
|
method: "GET";
|
|
10
11
|
result: {
|
|
11
|
-
new (): DataStream<
|
|
12
|
+
new (): DataStream<NotificationStreamItem>;
|
|
12
13
|
parse<T>(eventSource: import("../../sse/server-sent-events.js").ServerSentEvents): import("rxjs").Observable<T>;
|
|
13
14
|
};
|
|
14
15
|
credentials: true;
|
|
@@ -19,6 +20,7 @@ export declare const notificationApiDefinition: {
|
|
|
19
20
|
parameters: import("../../schema/index.js").ObjectSchema<{
|
|
20
21
|
offset?: number | undefined;
|
|
21
22
|
limit?: number | undefined;
|
|
23
|
+
after?: string | undefined;
|
|
22
24
|
unreadOnly?: boolean | undefined;
|
|
23
25
|
includeArchived?: boolean | undefined;
|
|
24
26
|
}>;
|
|
@@ -104,7 +106,7 @@ declare const _NotificationApiClient: import("../../api/client/index.js").ApiCli
|
|
|
104
106
|
resource: string;
|
|
105
107
|
method: "GET";
|
|
106
108
|
result: {
|
|
107
|
-
new (): DataStream<
|
|
109
|
+
new (): DataStream<NotificationStreamItem>;
|
|
108
110
|
parse<T>(eventSource: import("../../sse/server-sent-events.js").ServerSentEvents): import("rxjs").Observable<T>;
|
|
109
111
|
};
|
|
110
112
|
credentials: true;
|
|
@@ -115,6 +117,7 @@ declare const _NotificationApiClient: import("../../api/client/index.js").ApiCli
|
|
|
115
117
|
parameters: import("../../schema/index.js").ObjectSchema<{
|
|
116
118
|
offset?: number | undefined;
|
|
117
119
|
limit?: number | undefined;
|
|
120
|
+
after?: string | undefined;
|
|
118
121
|
unreadOnly?: boolean | undefined;
|
|
119
122
|
includeArchived?: boolean | undefined;
|
|
120
123
|
}>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './notification-client.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './notification-client.js';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { NotificationApiClient } from '../../notification/api/index.js';
|
|
2
|
+
import type { InAppNotificationView, NotificationDefinitionMap } from '../../notification/models/index.js';
|
|
3
|
+
type NotificationState<Definitions extends NotificationDefinitionMap = NotificationDefinitionMap> = {
|
|
4
|
+
notifications: InAppNotificationView<Definitions>[];
|
|
5
|
+
unreadCount: number;
|
|
6
|
+
};
|
|
7
|
+
export declare class NotificationClient<Definitions extends NotificationDefinitionMap = NotificationDefinitionMap> {
|
|
8
|
+
#private;
|
|
9
|
+
readonly api: NotificationApiClient;
|
|
10
|
+
readonly state$: import("rxjs").Observable<NotificationState<Definitions>>;
|
|
11
|
+
readonly notifications$: import("rxjs").Observable<InAppNotificationView<Definitions>[]>;
|
|
12
|
+
readonly unreadCount$: import("rxjs").Observable<number>;
|
|
13
|
+
readonly state: import("../../signals/api.js").Signal<NotificationState<Definitions>>;
|
|
14
|
+
readonly notifications: import("../../signals/api.js").Signal<InAppNotificationView<Definitions>[]>;
|
|
15
|
+
readonly unreadCount: import("../../signals/api.js").Signal<number>;
|
|
16
|
+
loadNext(count?: number): void;
|
|
17
|
+
}
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { concatMap, defer, from, map, merge, of, scan, shareReplay, Subject, switchAll, switchMap } from 'rxjs';
|
|
8
|
+
import { AuthenticationClientService } from '../../authentication/client/authentication.service.js';
|
|
9
|
+
import { Singleton } from '../../injector/decorators.js';
|
|
10
|
+
import { inject } from '../../injector/inject.js';
|
|
11
|
+
import { NotificationApiClient } from '../../notification/api/index.js';
|
|
12
|
+
import { forceCast } from '../../rxjs-utils/cast.js';
|
|
13
|
+
import { computed, toSignal } from '../../signals/api.js';
|
|
14
|
+
import { isDefined, isUndefined } from '../../utils/type-guards.js';
|
|
15
|
+
let NotificationClient = class NotificationClient {
|
|
16
|
+
#pagination$ = new Subject();
|
|
17
|
+
#authenticationService = inject(AuthenticationClientService);
|
|
18
|
+
api = inject(NotificationApiClient);
|
|
19
|
+
state$ = this.#authenticationService.sessionId$.pipe(switchMap((sessionId) => {
|
|
20
|
+
if (isUndefined(sessionId)) {
|
|
21
|
+
return of({ notifications: [], unreadCount: 0 });
|
|
22
|
+
}
|
|
23
|
+
return merge(defer(() => from(this.api.listInApp({ limit: 20 }))).pipe(map((notifications) => ({ type: 'set-notifications', notifications: notifications }))), defer(() => from(this.api.unreadCount())).pipe(map((unreadCount) => ({ type: 'set-unread-count', unreadCount }))), defer(() => from(this.api.stream())).pipe(switchAll(), forceCast(), switchMap((item) => {
|
|
24
|
+
const actions = [{ type: 'set-unread-count', unreadCount: item.unreadCount }];
|
|
25
|
+
if (isDefined(item.notification)) {
|
|
26
|
+
actions.push({ type: 'prepend-notification', notification: item.notification });
|
|
27
|
+
}
|
|
28
|
+
return from(actions);
|
|
29
|
+
})), this.#pagination$.pipe(concatMap(({ count, after }) => from(this.api.listInApp({ limit: count, after }))), map((notifications) => ({ type: 'append-notifications', notifications: notifications })))).pipe(scan((acc, action) => {
|
|
30
|
+
switch (action.type) {
|
|
31
|
+
case 'set-notifications':
|
|
32
|
+
return { ...acc, notifications: action.notifications };
|
|
33
|
+
case 'prepend-notification':
|
|
34
|
+
if (acc.notifications.some((n) => n.id == action.notification.id)) {
|
|
35
|
+
return acc;
|
|
36
|
+
}
|
|
37
|
+
return { ...acc, notifications: [action.notification, ...acc.notifications] };
|
|
38
|
+
case 'append-notifications':
|
|
39
|
+
const filtered = action.notifications.filter((n) => !acc.notifications.some((c) => c.id == n.id));
|
|
40
|
+
return { ...acc, notifications: [...acc.notifications, ...filtered] };
|
|
41
|
+
case 'set-unread-count':
|
|
42
|
+
return { ...acc, unreadCount: action.unreadCount };
|
|
43
|
+
default:
|
|
44
|
+
return acc;
|
|
45
|
+
}
|
|
46
|
+
}, { notifications: [], unreadCount: 0 }));
|
|
47
|
+
}), shareReplay({ bufferSize: 1, refCount: true }));
|
|
48
|
+
notifications$ = this.state$.pipe(map((state) => state.notifications));
|
|
49
|
+
unreadCount$ = this.state$.pipe(map((state) => state.unreadCount));
|
|
50
|
+
state = toSignal(this.state$, { initialValue: { notifications: [], unreadCount: 0 } });
|
|
51
|
+
notifications = computed(() => this.state().notifications);
|
|
52
|
+
unreadCount = computed(() => this.state().unreadCount);
|
|
53
|
+
loadNext(count = 20) {
|
|
54
|
+
const current = this.notifications();
|
|
55
|
+
const after = current[current.length - 1]?.id;
|
|
56
|
+
this.#pagination$.next({ count, after });
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
NotificationClient = __decorate([
|
|
60
|
+
Singleton()
|
|
61
|
+
], NotificationClient);
|
|
62
|
+
export { NotificationClient };
|
package/notification/index.d.ts
CHANGED
package/notification/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { NotificationAncillaryService } from './services/notification-ancillary.
|
|
|
5
5
|
export declare class NotificationConfiguration {
|
|
6
6
|
database?: DatabaseConfig;
|
|
7
7
|
defaultChannels?: NotificationChannel[];
|
|
8
|
-
ancillaryService?: InjectionToken<NotificationAncillaryService
|
|
8
|
+
ancillaryService?: InjectionToken<NotificationAncillaryService<any>>;
|
|
9
9
|
}
|
|
10
10
|
export declare function configureNotification({ injector, ...config }: NotificationConfiguration & {
|
|
11
11
|
injector?: Injector;
|
|
@@ -8,9 +8,11 @@ import { inject, Singleton } from '../../../injector/index.js';
|
|
|
8
8
|
import { injectRepository } from '../../../orm/server/index.js';
|
|
9
9
|
import { InAppNotification, toInAppNotificationView } from '../../models/index.js';
|
|
10
10
|
import { NotificationSseService } from '../services/notification-sse.service.js';
|
|
11
|
+
import { NotificationService } from '../services/notification.service.js';
|
|
11
12
|
import { ChannelProvider } from './channel-provider.js';
|
|
12
13
|
let InAppChannelProvider = class InAppChannelProvider extends ChannelProvider {
|
|
13
14
|
#inAppRepository = injectRepository(InAppNotification);
|
|
15
|
+
#notificationService = inject(NotificationService);
|
|
14
16
|
#sseService = inject(NotificationSseService);
|
|
15
17
|
async send(notification, tx) {
|
|
16
18
|
const inApp = await this.#inAppRepository.withOptionalTransaction(tx).insert({
|
|
@@ -21,7 +23,8 @@ let InAppChannelProvider = class InAppChannelProvider extends ChannelProvider {
|
|
|
21
23
|
archiveTimestamp: null,
|
|
22
24
|
});
|
|
23
25
|
const inAppNotificationView = toInAppNotificationView(inApp, notification);
|
|
24
|
-
await this.#
|
|
26
|
+
const unreadCount = await this.#notificationService.withOptionalTransaction(tx).unreadCount(notification.tenantId, notification.userId);
|
|
27
|
+
await this.#sseService.send(inAppNotificationView, unreadCount);
|
|
25
28
|
}
|
|
26
29
|
};
|
|
27
30
|
InAppChannelProvider = __decorate([
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { Subject } from 'rxjs';
|
|
2
2
|
import { afterResolve } from '../../../injector/index.js';
|
|
3
3
|
import type { InAppNotificationView } from '../../models/index.js';
|
|
4
|
+
import type { NotificationStreamItem } from '../../types.js';
|
|
4
5
|
export declare class NotificationSseService {
|
|
5
6
|
#private;
|
|
6
7
|
[afterResolve](): void;
|
|
7
|
-
register(tenantId: string, userId: string): Subject<
|
|
8
|
-
unregister(tenantId: string, userId: string, source: Subject<
|
|
9
|
-
send(notification: InAppNotificationView): Promise<void>;
|
|
8
|
+
register(tenantId: string, userId: string): Subject<NotificationStreamItem>;
|
|
9
|
+
unregister(tenantId: string, userId: string, source: Subject<NotificationStreamItem>): void;
|
|
10
|
+
send(notification: InAppNotificationView, unreadCount: number): Promise<void>;
|
|
11
|
+
dispatchUnreadCountUpdate(tenantId: string, userId: string, unreadCount: number): Promise<void>;
|
|
10
12
|
private dispatchToLocal;
|
|
11
13
|
}
|
|
@@ -10,7 +10,7 @@ import { MessageBusProvider } from '../../../message-bus/index.js';
|
|
|
10
10
|
let NotificationSseService = class NotificationSseService {
|
|
11
11
|
#messageBusProvider = inject(MessageBusProvider);
|
|
12
12
|
#messageBus = this.#messageBusProvider.get('notification');
|
|
13
|
-
#sources = new Map(); // tenantId -> userId ->
|
|
13
|
+
#sources = new Map(); // tenantId -> userId -> sources
|
|
14
14
|
[afterResolve]() {
|
|
15
15
|
this.#messageBus.allMessages$.subscribe((message) => this.dispatchToLocal(message));
|
|
16
16
|
}
|
|
@@ -43,15 +43,27 @@ let NotificationSseService = class NotificationSseService {
|
|
|
43
43
|
}
|
|
44
44
|
source.complete();
|
|
45
45
|
}
|
|
46
|
-
async send(notification) {
|
|
47
|
-
await this.#messageBus.publish(
|
|
46
|
+
async send(notification, unreadCount) {
|
|
47
|
+
await this.#messageBus.publish({
|
|
48
|
+
tenantId: notification.tenantId,
|
|
49
|
+
userId: notification.userId,
|
|
50
|
+
notification,
|
|
51
|
+
unreadCount,
|
|
52
|
+
});
|
|
48
53
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
async dispatchUnreadCountUpdate(tenantId, userId, unreadCount) {
|
|
55
|
+
await this.#messageBus.publish({
|
|
56
|
+
tenantId,
|
|
57
|
+
userId,
|
|
58
|
+
unreadCount,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
dispatchToLocal(message) {
|
|
62
|
+
const tenantMap = this.#sources.get(message.tenantId);
|
|
63
|
+
const userSources = tenantMap?.get(message.userId);
|
|
52
64
|
if (userSources != null) {
|
|
53
65
|
for (const source of userSources) {
|
|
54
|
-
source.next(notification);
|
|
66
|
+
source.next({ notification: message.notification, unreadCount: message.unreadCount });
|
|
55
67
|
}
|
|
56
68
|
}
|
|
57
69
|
}
|
|
@@ -11,6 +11,7 @@ export declare class NotificationService<Definitions extends NotificationDefinit
|
|
|
11
11
|
listInApp(tenantId: string, userId: string, options?: {
|
|
12
12
|
limit?: number;
|
|
13
13
|
offset?: number;
|
|
14
|
+
after?: string;
|
|
14
15
|
includeArchived?: boolean;
|
|
15
16
|
unreadOnly?: boolean;
|
|
16
17
|
}): Promise<InAppNotificationView[]>;
|
|
@@ -22,4 +23,5 @@ export declare class NotificationService<Definitions extends NotificationDefinit
|
|
|
22
23
|
getPreferences(tenantId: string, userId: string): Promise<NotificationPreference[]>;
|
|
23
24
|
updatePreference(tenantId: string, userId: string, type: string, channel: NotificationChannel, enabled: boolean): Promise<void>;
|
|
24
25
|
registerWebPush(tenantId: string, userId: string, endpoint: string, p256dh: Uint8Array<ArrayBuffer>, auth: Uint8Array<ArrayBuffer>): Promise<void>;
|
|
26
|
+
private dispatchUnreadCountUpdate;
|
|
25
27
|
}
|
|
@@ -4,22 +4,30 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
4
4
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
|
-
|
|
7
|
+
var _a;
|
|
8
|
+
var NotificationService_1;
|
|
9
|
+
import { and, desc, eq, isNull, lt, or } from 'drizzle-orm';
|
|
8
10
|
import { BadRequestError } from '../../../errors/bad-request.error.js';
|
|
9
11
|
import { inject, Singleton } from '../../../injector/index.js';
|
|
12
|
+
import { Logger } from '../../../logger/logger.js';
|
|
10
13
|
import { TRANSACTION_TIMESTAMP } from '../../../orm/index.js';
|
|
11
14
|
import { injectRepository, Transactional } from '../../../orm/server/index.js';
|
|
12
15
|
import { TaskQueue } from '../../../task-queue/task-queue.js';
|
|
16
|
+
import { tryIgnoreLogAsync } from '../../../utils/try-ignore.js';
|
|
17
|
+
import { isDefined } from '../../../utils/type-guards.js';
|
|
13
18
|
import { InAppNotification, NotificationLogEntity, NotificationPreference, NotificationPriority, NotificationStatus, toInAppNotificationView, WebPushSubscription } from '../../models/index.js';
|
|
14
19
|
import { inAppNotification, notificationLog } from '../schemas.js';
|
|
15
20
|
import { NotificationAncillaryService } from './notification-ancillary.service.js';
|
|
16
|
-
|
|
21
|
+
import { NotificationSseService } from './notification-sse.service.js';
|
|
22
|
+
let NotificationService = NotificationService_1 = class NotificationService extends Transactional {
|
|
17
23
|
#notificationLogRepository = injectRepository(NotificationLogEntity);
|
|
18
24
|
#inAppNotificationRepository = injectRepository(InAppNotification);
|
|
19
25
|
#preferenceRepository = injectRepository(NotificationPreference);
|
|
20
26
|
#webPushSubscriptionRepository = injectRepository(WebPushSubscription);
|
|
21
27
|
#notificationAncillaryService = inject(NotificationAncillaryService);
|
|
22
28
|
#taskQueue = inject((TaskQueue), 'notification');
|
|
29
|
+
#sseService = inject(NotificationSseService);
|
|
30
|
+
#logger = inject(Logger, NotificationService_1.name);
|
|
23
31
|
async send(tenantId, userId, notification, options) {
|
|
24
32
|
await this.useTransaction(options?.transaction, async (tx) => {
|
|
25
33
|
const notificationToInsert = {
|
|
@@ -36,6 +44,11 @@ let NotificationService = class NotificationService extends Transactional {
|
|
|
36
44
|
});
|
|
37
45
|
}
|
|
38
46
|
async listInApp(tenantId, userId, options = {}) {
|
|
47
|
+
let afterNotification;
|
|
48
|
+
if (options.after != null) {
|
|
49
|
+
const inApp = await this.#inAppNotificationRepository.loadByQuery({ tenantId, userId, id: options.after });
|
|
50
|
+
afterNotification = await this.#notificationLogRepository.loadByQuery({ tenantId, userId, id: inApp.logId });
|
|
51
|
+
}
|
|
39
52
|
const rows = await this.#notificationLogRepository.session
|
|
40
53
|
.select({
|
|
41
54
|
notification: notificationLog,
|
|
@@ -43,10 +56,12 @@ let NotificationService = class NotificationService extends Transactional {
|
|
|
43
56
|
})
|
|
44
57
|
.from(notificationLog)
|
|
45
58
|
.innerJoin(inAppNotification, and(eq(inAppNotification.tenantId, notificationLog.tenantId), eq(inAppNotification.userId, notificationLog.userId), eq(inAppNotification.logId, notificationLog.id)))
|
|
46
|
-
.where(and(eq(notificationLog.tenantId, tenantId), eq(notificationLog.userId, userId), options.includeArchived ? undefined : isNull(inAppNotification.archiveTimestamp), options.unreadOnly ? isNull(inAppNotification.readTimestamp) : undefined)
|
|
59
|
+
.where(and(eq(notificationLog.tenantId, tenantId), eq(notificationLog.userId, userId), options.includeArchived ? undefined : isNull(inAppNotification.archiveTimestamp), options.unreadOnly ? isNull(inAppNotification.readTimestamp) : undefined, isDefined(afterNotification)
|
|
60
|
+
? or(lt(notificationLog.timestamp, afterNotification.timestamp), and(eq(notificationLog.timestamp, afterNotification.timestamp), lt(notificationLog.id, afterNotification.id)))
|
|
61
|
+
: undefined))
|
|
47
62
|
.limit(options.limit ?? 50)
|
|
48
63
|
.offset(options.offset ?? 0)
|
|
49
|
-
.orderBy(desc(notificationLog.timestamp));
|
|
64
|
+
.orderBy(desc(notificationLog.timestamp), desc(notificationLog.id));
|
|
50
65
|
const inAppRows = rows.map((row) => row.inApp);
|
|
51
66
|
const notificationRows = rows.map((row) => row.notification);
|
|
52
67
|
const notificationEntities = await this.#notificationLogRepository.mapManyToEntity(notificationRows);
|
|
@@ -62,15 +77,19 @@ let NotificationService = class NotificationService extends Transactional {
|
|
|
62
77
|
}
|
|
63
78
|
async markRead(tenantId, userId, id) {
|
|
64
79
|
await this.#inAppNotificationRepository.updateByQuery({ tenantId, id, userId }, { readTimestamp: TRANSACTION_TIMESTAMP });
|
|
80
|
+
await this.dispatchUnreadCountUpdate(tenantId, userId);
|
|
65
81
|
}
|
|
66
82
|
async markAllRead(tenantId, userId) {
|
|
67
83
|
await this.#inAppNotificationRepository.updateByQuery({ tenantId, userId, readTimestamp: null }, { readTimestamp: TRANSACTION_TIMESTAMP });
|
|
84
|
+
await this.dispatchUnreadCountUpdate(tenantId, userId);
|
|
68
85
|
}
|
|
69
86
|
async archive(tenantId, userId, id) {
|
|
70
87
|
await this.#inAppNotificationRepository.updateByQuery({ tenantId, id, userId }, { archiveTimestamp: TRANSACTION_TIMESTAMP });
|
|
88
|
+
await this.dispatchUnreadCountUpdate(tenantId, userId);
|
|
71
89
|
}
|
|
72
90
|
async archiveAll(tenantId, userId) {
|
|
73
91
|
await this.#inAppNotificationRepository.updateByQuery({ tenantId, userId, archiveTimestamp: null }, { archiveTimestamp: TRANSACTION_TIMESTAMP });
|
|
92
|
+
await this.dispatchUnreadCountUpdate(tenantId, userId);
|
|
74
93
|
}
|
|
75
94
|
async unreadCount(tenantId, userId) {
|
|
76
95
|
return await this.#inAppNotificationRepository.countByQuery({
|
|
@@ -113,8 +132,14 @@ let NotificationService = class NotificationService extends Transactional {
|
|
|
113
132
|
auth,
|
|
114
133
|
});
|
|
115
134
|
}
|
|
135
|
+
async dispatchUnreadCountUpdate(tenantId, userId) {
|
|
136
|
+
await tryIgnoreLogAsync(this.#logger, async () => {
|
|
137
|
+
const unreadCount = await this.unreadCount(tenantId, userId);
|
|
138
|
+
this.#sseService.dispatchUnreadCountUpdate(tenantId, userId, unreadCount);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
116
141
|
};
|
|
117
|
-
NotificationService = __decorate([
|
|
142
|
+
NotificationService = NotificationService_1 = __decorate([
|
|
118
143
|
Singleton()
|
|
119
144
|
], NotificationService);
|
|
120
145
|
export { NotificationService };
|
|
@@ -265,4 +265,32 @@ describe('Notification Flow (Integration)', () => {
|
|
|
265
265
|
expect(prefs[0].enabled).toBe(true);
|
|
266
266
|
});
|
|
267
267
|
});
|
|
268
|
+
test('should support keyset pagination with after and orderBy', async () => {
|
|
269
|
+
await runInInjectionContext(injector, async () => {
|
|
270
|
+
const logRepo = injectRepository(NotificationLogEntity);
|
|
271
|
+
const user = await subjectService.createUser({ tenantId, email: 'pagination@example.com', firstName: 'Pagination', lastName: 'User' });
|
|
272
|
+
await typeService.initializeTypes({ test: { label: 'Pagination Test' } });
|
|
273
|
+
// Create 3 notifications
|
|
274
|
+
await notificationService.send(tenantId, user.id, { type: 'test', triggerSubjectId: user.id, payload: { index: 1 } });
|
|
275
|
+
await notificationService.send(tenantId, user.id, { type: 'test', triggerSubjectId: user.id, payload: { index: 2 } });
|
|
276
|
+
await notificationService.send(tenantId, user.id, { type: 'test', triggerSubjectId: user.id, payload: { index: 3 } });
|
|
277
|
+
const logs = await logRepo.loadManyByQuery({ tenantId });
|
|
278
|
+
for (const log of logs) {
|
|
279
|
+
await worker.deliver(log.id);
|
|
280
|
+
}
|
|
281
|
+
// Default list (desc timestamp, then desc id)
|
|
282
|
+
const list = await notificationService.listInApp(tenantId, user.id);
|
|
283
|
+
expect(list).toHaveLength(3);
|
|
284
|
+
const firstId = list[0].id;
|
|
285
|
+
const secondId = list[1].id;
|
|
286
|
+
// After first
|
|
287
|
+
const afterFirst = await notificationService.listInApp(tenantId, user.id, { after: firstId });
|
|
288
|
+
expect(afterFirst).toHaveLength(2);
|
|
289
|
+
expect(afterFirst[0].id).toBe(secondId);
|
|
290
|
+
// After second
|
|
291
|
+
const afterSecond = await notificationService.listInApp(tenantId, user.id, { after: secondId });
|
|
292
|
+
expect(afterSecond).toHaveLength(1);
|
|
293
|
+
expect(afterSecond[0].id).toBe(list[2].id);
|
|
294
|
+
});
|
|
295
|
+
});
|
|
268
296
|
});
|
|
@@ -14,7 +14,7 @@ describe('NotificationSseService', () => {
|
|
|
14
14
|
// We can't easily spy on the LocalMessageBus internals without more complex setup,
|
|
15
15
|
// but we can verify that sending doesn't throw.
|
|
16
16
|
const msg = { tenantId, userId, logId: 'l1' };
|
|
17
|
-
await expect(service.send(msg)).resolves.not.toThrow();
|
|
17
|
+
await expect(service.send(msg, 1)).resolves.not.toThrow();
|
|
18
18
|
});
|
|
19
19
|
});
|
|
20
20
|
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { InAppNotificationView, type NotificationDefinitionMap } from './models/index.js';
|
|
2
|
+
export declare const notificationStreamItemSchema: import("../schema/index.js").ObjectSchema<{
|
|
3
|
+
unreadCount: number;
|
|
4
|
+
notification?: InAppNotificationView<Record<string, import("./models/notification-log.model.js").NotificationDefinition<import("../types/types.js").ObjectLiteral, import("../types/types.js").ObjectLiteral>>> | undefined;
|
|
5
|
+
}>;
|
|
6
|
+
export type NotificationStreamItem<Definitions extends NotificationDefinitionMap = NotificationDefinitionMap> = {
|
|
7
|
+
notification?: InAppNotificationView<Definitions>;
|
|
8
|
+
unreadCount: number;
|
|
9
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tstdl/base",
|
|
3
|
-
"version": "0.93.
|
|
3
|
+
"version": "0.93.117",
|
|
4
4
|
"author": "Patrick Hein",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -200,7 +200,7 @@
|
|
|
200
200
|
"typedoc-plugin-markdown": "4.10",
|
|
201
201
|
"typedoc-plugin-missing-exports": "4.1",
|
|
202
202
|
"typescript": "5.9",
|
|
203
|
-
"typescript-eslint": "8.
|
|
203
|
+
"typescript-eslint": "8.55",
|
|
204
204
|
"vite-tsconfig-paths": "6.1",
|
|
205
205
|
"vitest": "4.0"
|
|
206
206
|
},
|
package/utils/try-ignore.d.ts
CHANGED
|
@@ -2,8 +2,8 @@ import type { Logger } from '../logger/logger.js';
|
|
|
2
2
|
export declare function tryIgnore<R>(fn: () => R): R | undefined;
|
|
3
3
|
export declare function tryIgnore<R, F>(fn: () => R, fallback: F): R | F;
|
|
4
4
|
export declare function tryIgnoreAsync<R>(fn: () => Promise<R>): Promise<R | undefined>;
|
|
5
|
-
export declare function tryIgnoreAsync<R, F>(fn: () => Promise<R>, fallback: F): Promise<F>;
|
|
5
|
+
export declare function tryIgnoreAsync<R, F>(fn: () => Promise<R>, fallback: F): Promise<R | F>;
|
|
6
6
|
export declare function tryIgnoreLog<R>(logger: Logger, fn: () => R): R | undefined;
|
|
7
7
|
export declare function tryIgnoreLog<R, F>(logger: Logger, fn: () => R, fallback: F): R | F;
|
|
8
8
|
export declare function tryIgnoreLogAsync<R>(logger: Logger, fn: () => Promise<R>): Promise<R | undefined>;
|
|
9
|
-
export declare function tryIgnoreLogAsync<R, F>(logger: Logger, fn: () => Promise<R>, fallback: F): Promise<F>;
|
|
9
|
+
export declare function tryIgnoreLogAsync<R, F>(logger: Logger, fn: () => Promise<R>, fallback: F): Promise<R | F>;
|