@tstdl/base 0.93.116 → 0.93.118
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/api/server/gateway.js +2 -2
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/internal.d.ts +1 -0
- package/internal.js +1 -0
- package/notification/api/notification.api.d.ts +27 -12
- package/notification/api/notification.api.js +10 -3
- package/notification/client/index.d.ts +1 -0
- package/notification/client/index.js +1 -0
- package/notification/client/notification-client.d.ts +20 -0
- package/notification/client/notification-client.js +69 -0
- package/notification/index.d.ts +2 -0
- package/notification/index.js +2 -0
- package/notification/server/api/notification.api-controller.d.ts +3 -0
- package/notification/server/api/notification.api-controller.js +5 -0
- 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-type.service.d.ts +1 -0
- package/notification/server/services/notification-type.service.js +5 -0
- package/notification/server/services/notification.service.d.ts +2 -0
- package/notification/server/services/notification.service.js +30 -5
- package/notification/tests/notification-api.test.js +8 -0
- package/notification/tests/notification-flow.test.js +28 -0
- package/notification/tests/notification-sse.service.test.js +10 -1
- package/notification/tests/unit/notification-client.test.d.ts +1 -0
- package/notification/tests/unit/notification-client.test.js +112 -0
- package/notification/types.d.ts +9 -0
- package/notification/types.js +6 -0
- package/object-storage/object-storage.d.ts +10 -0
- package/object-storage/s3/s3.object-storage-provider.d.ts +11 -4
- package/object-storage/s3/s3.object-storage-provider.js +29 -26
- package/object-storage/s3/s3.object-storage.d.ts +7 -4
- package/object-storage/s3/s3.object-storage.js +141 -60
- package/object-storage/s3/s3.object.d.ts +6 -0
- package/object-storage/s3/s3.object.js +1 -1
- package/object-storage/s3/tests/s3.object-storage.integration.test.d.ts +1 -0
- package/object-storage/s3/tests/s3.object-storage.integration.test.js +334 -0
- package/package.json +4 -3
- package/rpc/adapters/readable-stream.adapter.js +27 -22
- package/rpc/endpoints/message-port.rpc-endpoint.d.ts +4 -0
- package/rpc/endpoints/message-port.rpc-endpoint.js +4 -0
- package/rpc/model.d.ts +11 -1
- package/rpc/rpc.d.ts +17 -1
- package/rpc/rpc.endpoint.js +4 -3
- package/rpc/rpc.error.d.ts +5 -1
- package/rpc/rpc.error.js +16 -3
- package/rpc/rpc.js +89 -15
- package/rpc/tests/rpc.integration.test.d.ts +1 -0
- package/rpc/tests/rpc.integration.test.js +619 -0
- package/unit-test/integration-setup.d.ts +1 -0
- package/unit-test/integration-setup.js +12 -0
- package/utils/try-ignore.d.ts +2 -2
package/api/server/gateway.js
CHANGED
|
@@ -190,7 +190,7 @@ let ApiGateway = ApiGateway_1 = class ApiGateway {
|
|
|
190
190
|
const decodedUrlParameters = mapObjectValues(context.resourcePatternResult.pathname.groups, (value) => isDefined(value) ? decodeURIComponent(value) : undefined);
|
|
191
191
|
const parameters = { ...context.request.query.asObject(), ...bodyAsParameters, ...decodedUrlParameters };
|
|
192
192
|
const validatedParameters = isDefined(context.endpoint.definition.parameters)
|
|
193
|
-
? Schema.parse(context.endpoint.definition.parameters, parameters)
|
|
193
|
+
? Schema.parse(context.endpoint.definition.parameters, parameters, { coerce: true })
|
|
194
194
|
: parameters;
|
|
195
195
|
const requestTokenProvider = this.#requestTokenProvider;
|
|
196
196
|
const auditor = this.#auditor;
|
|
@@ -279,5 +279,5 @@ async function getBody(request, options, schema) {
|
|
|
279
279
|
body = await request.body.readAsBuffer(options);
|
|
280
280
|
}
|
|
281
281
|
}
|
|
282
|
-
return Schema.parse(schema, body);
|
|
282
|
+
return Schema.parse(schema, body, { coerce: true });
|
|
283
283
|
}
|
package/index.d.ts
CHANGED
package/index.js
CHANGED
package/internal.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const internal: unique symbol;
|
package/internal.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const internal = Symbol('internal');
|
|
@@ -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,17 +9,24 @@ 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;
|
|
15
16
|
};
|
|
17
|
+
types: {
|
|
18
|
+
resource: string;
|
|
19
|
+
method: "GET";
|
|
20
|
+
result: import("../../schema/index.js").ObjectSchema<Partial<import("../../schema/index.js").Record<string, string>>>;
|
|
21
|
+
credentials: true;
|
|
22
|
+
};
|
|
16
23
|
listInApp: {
|
|
17
24
|
resource: string;
|
|
18
25
|
method: "GET";
|
|
19
26
|
parameters: import("../../schema/index.js").ObjectSchema<{
|
|
20
27
|
offset?: number | undefined;
|
|
21
28
|
limit?: number | undefined;
|
|
29
|
+
after?: string | undefined;
|
|
22
30
|
unreadOnly?: boolean | undefined;
|
|
23
31
|
includeArchived?: boolean | undefined;
|
|
24
32
|
}>;
|
|
@@ -75,9 +83,9 @@ export declare const notificationApiDefinition: {
|
|
|
75
83
|
resource: string;
|
|
76
84
|
method: "POST";
|
|
77
85
|
parameters: import("../../schema/index.js").ObjectSchema<{
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
86
|
+
type: string;
|
|
87
|
+
enabled: boolean;
|
|
88
|
+
channel: "email" | "in-app" | "web-push";
|
|
81
89
|
}>;
|
|
82
90
|
result: import("../../schema/index.js").LiteralSchema<"ok">;
|
|
83
91
|
credentials: true;
|
|
@@ -86,11 +94,11 @@ export declare const notificationApiDefinition: {
|
|
|
86
94
|
resource: string;
|
|
87
95
|
method: "POST";
|
|
88
96
|
parameters: import("../../schema/index.js").ObjectSchema<{
|
|
89
|
-
|
|
90
|
-
readonly keys: {
|
|
97
|
+
keys: {
|
|
91
98
|
p256dhBase64: string;
|
|
92
99
|
authBase64: string;
|
|
93
100
|
};
|
|
101
|
+
endpoint: string;
|
|
94
102
|
}>;
|
|
95
103
|
result: import("../../schema/index.js").LiteralSchema<"ok">;
|
|
96
104
|
credentials: true;
|
|
@@ -104,17 +112,24 @@ declare const _NotificationApiClient: import("../../api/client/index.js").ApiCli
|
|
|
104
112
|
resource: string;
|
|
105
113
|
method: "GET";
|
|
106
114
|
result: {
|
|
107
|
-
new (): DataStream<
|
|
115
|
+
new (): DataStream<NotificationStreamItem>;
|
|
108
116
|
parse<T>(eventSource: import("../../sse/server-sent-events.js").ServerSentEvents): import("rxjs").Observable<T>;
|
|
109
117
|
};
|
|
110
118
|
credentials: true;
|
|
111
119
|
};
|
|
120
|
+
types: {
|
|
121
|
+
resource: string;
|
|
122
|
+
method: "GET";
|
|
123
|
+
result: import("../../schema/index.js").ObjectSchema<Partial<import("../../schema/index.js").Record<string, string>>>;
|
|
124
|
+
credentials: true;
|
|
125
|
+
};
|
|
112
126
|
listInApp: {
|
|
113
127
|
resource: string;
|
|
114
128
|
method: "GET";
|
|
115
129
|
parameters: import("../../schema/index.js").ObjectSchema<{
|
|
116
130
|
offset?: number | undefined;
|
|
117
131
|
limit?: number | undefined;
|
|
132
|
+
after?: string | undefined;
|
|
118
133
|
unreadOnly?: boolean | undefined;
|
|
119
134
|
includeArchived?: boolean | undefined;
|
|
120
135
|
}>;
|
|
@@ -171,9 +186,9 @@ declare const _NotificationApiClient: import("../../api/client/index.js").ApiCli
|
|
|
171
186
|
resource: string;
|
|
172
187
|
method: "POST";
|
|
173
188
|
parameters: import("../../schema/index.js").ObjectSchema<{
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
189
|
+
type: string;
|
|
190
|
+
enabled: boolean;
|
|
191
|
+
channel: "email" | "in-app" | "web-push";
|
|
177
192
|
}>;
|
|
178
193
|
result: import("../../schema/index.js").LiteralSchema<"ok">;
|
|
179
194
|
credentials: true;
|
|
@@ -182,11 +197,11 @@ declare const _NotificationApiClient: import("../../api/client/index.js").ApiCli
|
|
|
182
197
|
resource: string;
|
|
183
198
|
method: "POST";
|
|
184
199
|
parameters: import("../../schema/index.js").ObjectSchema<{
|
|
185
|
-
|
|
186
|
-
readonly keys: {
|
|
200
|
+
keys: {
|
|
187
201
|
p256dhBase64: string;
|
|
188
202
|
authBase64: string;
|
|
189
203
|
};
|
|
204
|
+
endpoint: string;
|
|
190
205
|
}>;
|
|
191
206
|
result: import("../../schema/index.js").LiteralSchema<"ok">;
|
|
192
207
|
credentials: true;
|
|
@@ -7,7 +7,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
import { compileClient } from '../../api/client/index.js';
|
|
8
8
|
import { defineApi } from '../../api/types.js';
|
|
9
9
|
import { ReplaceClass } from '../../injector/decorators.js';
|
|
10
|
-
import { array, boolean, enumeration,
|
|
10
|
+
import { array, boolean, enumeration, literal, number, object, optional, record, string } from '../../schema/index.js';
|
|
11
11
|
import { DataStream } from '../../sse/data-stream.js';
|
|
12
12
|
import { InAppNotificationView } from '../models/in-app-notification.model.js';
|
|
13
13
|
import { NotificationChannel } from '../models/index.js';
|
|
@@ -20,12 +20,19 @@ export const notificationApiDefinition = defineApi({
|
|
|
20
20
|
result: (DataStream),
|
|
21
21
|
credentials: true,
|
|
22
22
|
},
|
|
23
|
+
types: {
|
|
24
|
+
resource: 'types',
|
|
25
|
+
method: 'GET',
|
|
26
|
+
result: record(string(), string()),
|
|
27
|
+
credentials: true,
|
|
28
|
+
},
|
|
23
29
|
listInApp: {
|
|
24
30
|
resource: 'in-app',
|
|
25
31
|
method: 'GET',
|
|
26
32
|
parameters: object({
|
|
27
33
|
limit: optional(number()),
|
|
28
34
|
offset: optional(number()),
|
|
35
|
+
after: optional(string()),
|
|
29
36
|
unreadOnly: optional(boolean()),
|
|
30
37
|
includeArchived: optional(boolean()),
|
|
31
38
|
}),
|
|
@@ -77,7 +84,7 @@ export const notificationApiDefinition = defineApi({
|
|
|
77
84
|
updatePreference: {
|
|
78
85
|
resource: 'preferences',
|
|
79
86
|
method: 'POST',
|
|
80
|
-
parameters:
|
|
87
|
+
parameters: object({
|
|
81
88
|
type: string(),
|
|
82
89
|
channel: enumeration(NotificationChannel),
|
|
83
90
|
enabled: boolean(),
|
|
@@ -88,7 +95,7 @@ export const notificationApiDefinition = defineApi({
|
|
|
88
95
|
registerWebPush: {
|
|
89
96
|
resource: 'web-push/register',
|
|
90
97
|
method: 'POST',
|
|
91
|
-
parameters:
|
|
98
|
+
parameters: object({
|
|
92
99
|
endpoint: string(),
|
|
93
100
|
keys: object({
|
|
94
101
|
p256dhBase64: string(),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './notification-client.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './notification-client.js';
|
|
@@ -0,0 +1,20 @@
|
|
|
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 types$: import("rxjs").Observable<Record<keyof Definitions, string>>;
|
|
14
|
+
readonly state: import("../../signals/api.js").Signal<NotificationState<Definitions>>;
|
|
15
|
+
readonly notifications: import("../../signals/api.js").Signal<InAppNotificationView<Definitions>[]>;
|
|
16
|
+
readonly unreadCount: import("../../signals/api.js").Signal<number>;
|
|
17
|
+
readonly types: import("../../signals/api.js").Signal<Record<keyof Definitions, string>>;
|
|
18
|
+
loadNext(count?: number): void;
|
|
19
|
+
}
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
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
|
+
types$ = this.#authenticationService.sessionId$.pipe(switchMap((sessionId) => {
|
|
51
|
+
if (isUndefined(sessionId)) {
|
|
52
|
+
return of({});
|
|
53
|
+
}
|
|
54
|
+
return this.api.types();
|
|
55
|
+
}), shareReplay({ bufferSize: 1, refCount: true }));
|
|
56
|
+
state = toSignal(this.state$, { initialValue: { notifications: [], unreadCount: 0 } });
|
|
57
|
+
notifications = computed(() => this.state().notifications);
|
|
58
|
+
unreadCount = computed(() => this.state().unreadCount);
|
|
59
|
+
types = toSignal(this.types$, { initialValue: {} });
|
|
60
|
+
loadNext(count = 20) {
|
|
61
|
+
const current = this.notifications();
|
|
62
|
+
const after = current[current.length - 1]?.id;
|
|
63
|
+
this.#pagination$.next({ count, after });
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
NotificationClient = __decorate([
|
|
67
|
+
Singleton()
|
|
68
|
+
], NotificationClient);
|
|
69
|
+
export { NotificationClient };
|
package/notification/index.d.ts
CHANGED
package/notification/index.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import type { ApiController, ApiRequestContext, ApiServerResult } from '../../../api/types.js';
|
|
2
2
|
import type { NotificationApiDefinition } from '../../api/notification.api.js';
|
|
3
3
|
import { NotificationSseService } from '../services/notification-sse.service.js';
|
|
4
|
+
import { NotificationTypeService } from '../services/notification-type.service.js';
|
|
4
5
|
import { NotificationService } from '../services/notification.service.js';
|
|
5
6
|
export declare class NotificationApiController implements ApiController<NotificationApiDefinition> {
|
|
6
7
|
protected readonly notificationService: NotificationService<Record<string, import("../../index.js").NotificationDefinition<import("../../../types/types.js").ObjectLiteral, import("../../../types/types.js").ObjectLiteral>>>;
|
|
8
|
+
protected readonly notificationTypeService: NotificationTypeService;
|
|
7
9
|
protected readonly sseService: NotificationSseService;
|
|
8
10
|
stream({ abortSignal, getToken }: ApiRequestContext<NotificationApiDefinition, 'stream'>): ApiServerResult<NotificationApiDefinition, 'stream'>;
|
|
11
|
+
types(): Promise<Record<string, string>>;
|
|
9
12
|
listInApp({ parameters, getToken }: ApiRequestContext<NotificationApiDefinition, 'listInApp'>): Promise<any>;
|
|
10
13
|
markRead({ parameters, getToken }: ApiRequestContext<NotificationApiDefinition, 'markRead'>): Promise<'ok'>;
|
|
11
14
|
markAllRead({ getToken }: ApiRequestContext<NotificationApiDefinition, 'markAllRead'>): Promise<'ok'>;
|
|
@@ -10,9 +10,11 @@ import { toAsyncIterable } from '../../../rxjs-utils/index.js';
|
|
|
10
10
|
import { decodeBase64 } from '../../../utils/base64.js';
|
|
11
11
|
import { notificationApiDefinition } from '../../api/notification.api.js';
|
|
12
12
|
import { NotificationSseService } from '../services/notification-sse.service.js';
|
|
13
|
+
import { NotificationTypeService } from '../services/notification-type.service.js';
|
|
13
14
|
import { NotificationService } from '../services/notification.service.js';
|
|
14
15
|
let NotificationApiController = class NotificationApiController {
|
|
15
16
|
notificationService = inject(NotificationService);
|
|
17
|
+
notificationTypeService = inject(NotificationTypeService);
|
|
16
18
|
sseService = inject(NotificationSseService);
|
|
17
19
|
async *stream({ abortSignal, getToken }) {
|
|
18
20
|
const token = await getToken();
|
|
@@ -26,6 +28,9 @@ let NotificationApiController = class NotificationApiController {
|
|
|
26
28
|
this.sseService.unregister(token.payload.tenant, token.payload.subject, source);
|
|
27
29
|
}
|
|
28
30
|
}
|
|
31
|
+
async types() {
|
|
32
|
+
return await this.notificationTypeService.getTypes();
|
|
33
|
+
}
|
|
29
34
|
async listInApp({ parameters, getToken }) {
|
|
30
35
|
const token = await getToken();
|
|
31
36
|
return await this.notificationService.listInApp(token.payload.tenant, token.payload.subject, parameters);
|
|
@@ -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
|
}
|
|
@@ -8,4 +8,5 @@ export type TypeInitializationData = {
|
|
|
8
8
|
export declare class NotificationTypeService extends Transactional {
|
|
9
9
|
readonly repository: import("../../../orm/server/index.js").EntityRepository<NotificationType>;
|
|
10
10
|
initializeTypes<T extends string>(typeData: Record<T, TypeInitializationData>): Promise<Record<T, NotificationType>>;
|
|
11
|
+
getTypes(): Promise<Record<string, string>>;
|
|
11
12
|
}
|
|
@@ -34,6 +34,11 @@ let NotificationTypeService = class NotificationTypeService extends Transactiona
|
|
|
34
34
|
const mappedTypes = typeEntries.map(([key]) => [key, assertDefinedPass(typeMap.get(key), 'Could not map notification type.')]);
|
|
35
35
|
return fromEntries(mappedTypes);
|
|
36
36
|
}
|
|
37
|
+
async getTypes() {
|
|
38
|
+
const types = await this.repository.loadAll();
|
|
39
|
+
const entries = types.map((type) => [type.key, type.label]);
|
|
40
|
+
return fromEntries(entries);
|
|
41
|
+
}
|
|
37
42
|
};
|
|
38
43
|
NotificationTypeService = __decorate([
|
|
39
44
|
Singleton()
|
|
@@ -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 };
|
|
@@ -5,6 +5,7 @@ import { setupIntegrationTest, truncateTables } from '../../unit-test/index.js';
|
|
|
5
5
|
import { NotificationChannel } from '../models/index.js';
|
|
6
6
|
import { NotificationApiController } from '../server/api/notification.api-controller.js';
|
|
7
7
|
import { NotificationSseService } from '../server/services/notification-sse.service.js';
|
|
8
|
+
import { NotificationTypeService } from '../server/services/notification-type.service.js';
|
|
8
9
|
import { NotificationService } from '../server/services/notification.service.js';
|
|
9
10
|
describe('Notification API (Integration)', () => {
|
|
10
11
|
let injector;
|
|
@@ -64,6 +65,13 @@ describe('Notification API (Integration)', () => {
|
|
|
64
65
|
await nextPromise;
|
|
65
66
|
expect(registerSpy).toHaveBeenCalledWith(tenantId, userId);
|
|
66
67
|
});
|
|
68
|
+
test('types should call service', async () => {
|
|
69
|
+
const notificationTypeService = injector.resolve(NotificationTypeService);
|
|
70
|
+
const getTypesSpy = vi.spyOn(notificationTypeService, 'getTypes').mockResolvedValue({ test: 'Test' });
|
|
71
|
+
const result = await controller.types();
|
|
72
|
+
expect(result).toEqual({ test: 'Test' });
|
|
73
|
+
expect(getTypesSpy).toHaveBeenCalled();
|
|
74
|
+
});
|
|
67
75
|
test('listInApp should call service', async () => {
|
|
68
76
|
const listInAppSpy = vi.spyOn(notificationService, 'listInApp').mockResolvedValue([]);
|
|
69
77
|
const params = { limit: 10, offset: 0, includeArchived: false };
|
|
@@ -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,16 @@ 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
|
+
});
|
|
19
|
+
});
|
|
20
|
+
test('should dispatch unread count update', async () => {
|
|
21
|
+
const { injector } = await setupIntegrationTest({ modules: { messageBus: true, signals: true } });
|
|
22
|
+
const service = injector.resolve(NotificationSseService);
|
|
23
|
+
const tenantId = 't1';
|
|
24
|
+
const userId = 'u1';
|
|
25
|
+
await runInInjectionContext(injector, async () => {
|
|
26
|
+
await expect(service.dispatchUnreadCountUpdate(tenantId, userId, 5)).resolves.not.toThrow();
|
|
18
27
|
});
|
|
19
28
|
});
|
|
20
29
|
});
|