@tstdl/base 0.93.122 → 0.93.125
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 +6 -1
- package/api/types.d.ts +5 -1
- package/document-management/server/drizzle/{0000_silly_chimera.sql → 0000_complex_black_bird.sql} +1 -1
- package/document-management/server/drizzle/meta/0000_snapshot.json +3 -3
- package/document-management/server/drizzle/meta/_journal.json +2 -2
- package/notification/api/notification.api.d.ts +30 -4
- package/notification/api/notification.api.js +17 -3
- package/notification/client/notification-client.d.ts +6 -0
- package/notification/client/notification-client.js +47 -4
- package/notification/models/in-app-notification.model.d.ts +9 -3
- package/notification/models/in-app-notification.model.js +32 -11
- package/notification/models/notification-log.model.js +2 -3
- package/notification/server/api/notification.api-controller.d.ts +2 -1
- package/notification/server/api/notification.api-controller.js +21 -1
- package/notification/server/drizzle/{0000_oval_rage.sql → 0000_wise_pyro.sql} +22 -4
- package/notification/server/drizzle/meta/0000_snapshot.json +249 -37
- package/notification/server/drizzle/meta/_journal.json +2 -2
- package/notification/server/module.d.ts +5 -0
- package/notification/server/module.js +6 -1
- package/notification/server/providers/in-app-channel-provider.js +2 -1
- package/notification/server/schemas.d.ts +3 -2
- package/notification/server/schemas.js +3 -2
- package/notification/server/services/notification-sse.service.d.ts +14 -2
- package/notification/server/services/notification-sse.service.js +10 -11
- package/notification/server/services/notification.service.d.ts +16 -5
- package/notification/server/services/notification.service.js +160 -34
- package/notification/tests/notification-api.test.js +8 -1
- package/notification/tests/notification-flow.test.js +41 -4
- package/notification/tests/notification-sse.service.test.js +24 -3
- package/notification/types.d.ts +10 -2
- package/notification/types.js +6 -2
- package/orm/server/drizzle/schema-converter.js +5 -3
- package/orm/tests/schema-converter.test.js +1 -0
- package/package.json +1 -1
- package/sse/data-stream-source.d.ts +7 -4
- package/sse/data-stream-source.js +7 -4
package/api/server/gateway.js
CHANGED
|
@@ -199,6 +199,11 @@ let ApiGateway = ApiGateway_1 = class ApiGateway {
|
|
|
199
199
|
body,
|
|
200
200
|
request: context.request,
|
|
201
201
|
abortSignal: context.abortSignal,
|
|
202
|
+
serverSentEvents: {
|
|
203
|
+
get lastEventId() {
|
|
204
|
+
return context.request.headers.tryGetSingle('Last-Event-ID');
|
|
205
|
+
},
|
|
206
|
+
},
|
|
202
207
|
tryGetToken: async () => {
|
|
203
208
|
return await requestTokenProvider.tryGetToken(requestContext);
|
|
204
209
|
},
|
|
@@ -238,7 +243,7 @@ let ApiGateway = ApiGateway_1 = class ApiGateway {
|
|
|
238
243
|
const { errorResponse } = logAndGetErrorResponse(this.#logger, this.#supressedErrors, error);
|
|
239
244
|
return errorResponse.error;
|
|
240
245
|
};
|
|
241
|
-
return ({ events: DataStreamSource.fromIterable(value, { errorFormatter }).eventSource });
|
|
246
|
+
return ({ events: DataStreamSource.fromIterable(value, { errorFormatter, ...context.endpoint.definition.dataStream }).eventSource });
|
|
242
247
|
})
|
|
243
248
|
.when(() => (context.endpoint.definition.result == String), (text) => ({ text: text }))
|
|
244
249
|
.otherwise((json) => ({ json }));
|
package/api/types.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { Token } from '../authentication/index.js';
|
|
|
4
4
|
import type { HttpServerRequest, HttpServerResponse } from '../http/server/index.js';
|
|
5
5
|
import type { HttpMethod } from '../http/types.js';
|
|
6
6
|
import type { SchemaOutput, SchemaTestable } from '../schema/index.js';
|
|
7
|
-
import type { DataStream, DataStreamSource } from '../sse/index.js';
|
|
7
|
+
import type { DataStream, DataStreamSource, DataStreamSourceOptions } from '../sse/index.js';
|
|
8
8
|
import type { ServerSentEventsSource } from '../sse/server-sent-events-source.js';
|
|
9
9
|
import type { ServerSentEvents } from '../sse/server-sent-events.js';
|
|
10
10
|
import type { NonUndefinable, OneOrMany, Record, ReturnTypeOrT } from '../types/index.js';
|
|
@@ -66,6 +66,7 @@ export type ApiEndpointDefinition = {
|
|
|
66
66
|
maxBytes?: number;
|
|
67
67
|
description?: string;
|
|
68
68
|
data?: Record;
|
|
69
|
+
dataStream?: DataStreamSourceOptions<any>;
|
|
69
70
|
/**
|
|
70
71
|
* If true, sets browsers fetch to { credentials: 'include' } and enables 'Access-Control-Allow-Credentials' header.
|
|
71
72
|
*
|
|
@@ -107,6 +108,9 @@ export type ApiRequestData<T extends ApiDefinition = ApiDefinition, K extends Ap
|
|
|
107
108
|
body: ApiServerBody<T, K>;
|
|
108
109
|
request: HttpServerRequest;
|
|
109
110
|
abortSignal: AbortSignal;
|
|
111
|
+
serverSentEvents: {
|
|
112
|
+
lastEventId?: string;
|
|
113
|
+
};
|
|
110
114
|
};
|
|
111
115
|
export type ApiRequestContext<T extends ApiDefinition = ApiDefinition, K extends ApiEndpointKeys<T> = ApiEndpointKeys<T>> = ApiRequestData<T, K> & {
|
|
112
116
|
tryGetToken<T extends Token>(): Promise<T | null>;
|
package/document-management/server/drizzle/{0000_silly_chimera.sql → 0000_complex_black_bird.sql}
RENAMED
|
@@ -384,4 +384,4 @@ CREATE INDEX "request_type_id_idx" ON "document_management"."request" USING btre
|
|
|
384
384
|
CREATE INDEX "request_state_idx" ON "document_management"."request" USING btree ("state");--> statement-breakpoint
|
|
385
385
|
CREATE INDEX "request_collection_assignment_collection_id_idx" ON "document_management"."request_collection_assignment" USING btree ("collection_id");--> statement-breakpoint
|
|
386
386
|
CREATE INDEX "document_type_validation_type_id_idx" ON "document_management"."document_type_validation" USING btree ("type_id");--> statement-breakpoint
|
|
387
|
-
CREATE UNIQUE INDEX "
|
|
387
|
+
CREATE UNIQUE INDEX "workflow_document_id_partial_idx" ON "document_management"."workflow" USING btree ("document_id") WHERE "document_management"."workflow"."state" <> 'completed';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"id": "
|
|
2
|
+
"id": "ff5ec8ad-f61c-4ce5-b858-b28db663121f",
|
|
3
3
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
|
4
4
|
"version": "7",
|
|
5
5
|
"dialect": "postgresql",
|
|
@@ -2756,8 +2756,8 @@
|
|
|
2756
2756
|
}
|
|
2757
2757
|
},
|
|
2758
2758
|
"indexes": {
|
|
2759
|
-
"
|
|
2760
|
-
"name": "
|
|
2759
|
+
"workflow_document_id_partial_idx": {
|
|
2760
|
+
"name": "workflow_document_id_partial_idx",
|
|
2761
2761
|
"columns": [
|
|
2762
2762
|
{
|
|
2763
2763
|
"expression": "document_id",
|
|
@@ -13,6 +13,9 @@ export declare const notificationApiDefinition: {
|
|
|
13
13
|
parse<T>(eventSource: import("../../sse/server-sent-events.js").ServerSentEvents): import("rxjs").Observable<T>;
|
|
14
14
|
};
|
|
15
15
|
credentials: true;
|
|
16
|
+
dataStream: {
|
|
17
|
+
idProvider: () => string;
|
|
18
|
+
};
|
|
16
19
|
};
|
|
17
20
|
types: {
|
|
18
21
|
resource: string;
|
|
@@ -26,9 +29,19 @@ export declare const notificationApiDefinition: {
|
|
|
26
29
|
parameters: import("../../schema/index.js").ObjectSchema<{
|
|
27
30
|
offset?: number | undefined;
|
|
28
31
|
limit?: number | undefined;
|
|
29
|
-
after?: string | undefined;
|
|
32
|
+
after?: string | number | undefined;
|
|
30
33
|
unreadOnly?: boolean | undefined;
|
|
31
|
-
|
|
34
|
+
}>;
|
|
35
|
+
result: import("../../schema/index.js").ArraySchema<InAppNotificationView<Record<string, import("../models/notification-log.model.js").NotificationDefinition<import("../../types/types.js").ObjectLiteral, import("../../types/types.js").ObjectLiteral>>>>;
|
|
36
|
+
credentials: true;
|
|
37
|
+
};
|
|
38
|
+
listArchivedInApp: {
|
|
39
|
+
resource: string;
|
|
40
|
+
method: "GET";
|
|
41
|
+
parameters: import("../../schema/index.js").ObjectSchema<{
|
|
42
|
+
offset?: number | undefined;
|
|
43
|
+
limit?: number | undefined;
|
|
44
|
+
after?: string | number | undefined;
|
|
32
45
|
}>;
|
|
33
46
|
result: import("../../schema/index.js").ArraySchema<InAppNotificationView<Record<string, import("../models/notification-log.model.js").NotificationDefinition<import("../../types/types.js").ObjectLiteral, import("../../types/types.js").ObjectLiteral>>>>;
|
|
34
47
|
credentials: true;
|
|
@@ -116,6 +129,9 @@ declare const _NotificationApiClient: import("../../api/client/index.js").ApiCli
|
|
|
116
129
|
parse<T>(eventSource: import("../../sse/server-sent-events.js").ServerSentEvents): import("rxjs").Observable<T>;
|
|
117
130
|
};
|
|
118
131
|
credentials: true;
|
|
132
|
+
dataStream: {
|
|
133
|
+
idProvider: () => string;
|
|
134
|
+
};
|
|
119
135
|
};
|
|
120
136
|
types: {
|
|
121
137
|
resource: string;
|
|
@@ -129,9 +145,19 @@ declare const _NotificationApiClient: import("../../api/client/index.js").ApiCli
|
|
|
129
145
|
parameters: import("../../schema/index.js").ObjectSchema<{
|
|
130
146
|
offset?: number | undefined;
|
|
131
147
|
limit?: number | undefined;
|
|
132
|
-
after?: string | undefined;
|
|
148
|
+
after?: string | number | undefined;
|
|
133
149
|
unreadOnly?: boolean | undefined;
|
|
134
|
-
|
|
150
|
+
}>;
|
|
151
|
+
result: import("../../schema/index.js").ArraySchema<InAppNotificationView<Record<string, import("../models/notification-log.model.js").NotificationDefinition<import("../../types/types.js").ObjectLiteral, import("../../types/types.js").ObjectLiteral>>>>;
|
|
152
|
+
credentials: true;
|
|
153
|
+
};
|
|
154
|
+
listArchivedInApp: {
|
|
155
|
+
resource: string;
|
|
156
|
+
method: "GET";
|
|
157
|
+
parameters: import("../../schema/index.js").ObjectSchema<{
|
|
158
|
+
offset?: number | undefined;
|
|
159
|
+
limit?: number | undefined;
|
|
160
|
+
after?: string | number | undefined;
|
|
135
161
|
}>;
|
|
136
162
|
result: import("../../schema/index.js").ArraySchema<InAppNotificationView<Record<string, import("../models/notification-log.model.js").NotificationDefinition<import("../../types/types.js").ObjectLiteral, import("../../types/types.js").ObjectLiteral>>>>;
|
|
137
163
|
credentials: true;
|
|
@@ -7,8 +7,9 @@ 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, literal, number, object, optional, record, string } from '../../schema/index.js';
|
|
10
|
+
import { array, boolean, enumeration, literal, number, object, optional, record, string, union } from '../../schema/index.js';
|
|
11
11
|
import { DataStream } from '../../sse/data-stream.js';
|
|
12
|
+
import { currentTimestamp } from '../../utils/date-time.js';
|
|
12
13
|
import { InAppNotificationView } from '../models/in-app-notification.model.js';
|
|
13
14
|
import { NotificationChannel } from '../models/index.js';
|
|
14
15
|
export const notificationApiDefinition = defineApi({
|
|
@@ -19,6 +20,9 @@ export const notificationApiDefinition = defineApi({
|
|
|
19
20
|
method: 'GET',
|
|
20
21
|
result: (DataStream),
|
|
21
22
|
credentials: true,
|
|
23
|
+
dataStream: {
|
|
24
|
+
idProvider: () => currentTimestamp().toString(),
|
|
25
|
+
},
|
|
22
26
|
},
|
|
23
27
|
types: {
|
|
24
28
|
resource: 'types',
|
|
@@ -32,9 +36,19 @@ export const notificationApiDefinition = defineApi({
|
|
|
32
36
|
parameters: object({
|
|
33
37
|
limit: optional(number()),
|
|
34
38
|
offset: optional(number()),
|
|
35
|
-
after: optional(string()),
|
|
39
|
+
after: optional(union(number(), string())),
|
|
36
40
|
unreadOnly: optional(boolean()),
|
|
37
|
-
|
|
41
|
+
}),
|
|
42
|
+
result: array(InAppNotificationView),
|
|
43
|
+
credentials: true,
|
|
44
|
+
},
|
|
45
|
+
listArchivedInApp: {
|
|
46
|
+
resource: 'in-app/archived',
|
|
47
|
+
method: 'GET',
|
|
48
|
+
parameters: object({
|
|
49
|
+
limit: optional(number()),
|
|
50
|
+
offset: optional(number()),
|
|
51
|
+
after: optional(union(number(), string())),
|
|
38
52
|
}),
|
|
39
53
|
result: array(InAppNotificationView),
|
|
40
54
|
credentials: true,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NotificationApiClient } from '../../notification/api/index.js';
|
|
2
2
|
import type { InAppNotificationView, NotificationDefinitionMap } from '../../notification/models/index.js';
|
|
3
|
+
import type { NotificationStreamItem } from '../../notification/types.js';
|
|
3
4
|
type NotificationState<Definitions extends NotificationDefinitionMap = NotificationDefinitionMap> = {
|
|
4
5
|
notifications: InAppNotificationView<Definitions>[];
|
|
5
6
|
unreadCount: number;
|
|
@@ -7,6 +8,9 @@ type NotificationState<Definitions extends NotificationDefinitionMap = Notificat
|
|
|
7
8
|
export declare class NotificationClient<Definitions extends NotificationDefinitionMap = NotificationDefinitionMap> {
|
|
8
9
|
#private;
|
|
9
10
|
readonly api: NotificationApiClient;
|
|
11
|
+
readonly stream$: import("rxjs").Observable<NotificationStreamItem<Definitions>>;
|
|
12
|
+
/** Emits whenever a new notification is received via the stream. */
|
|
13
|
+
readonly onNewNotification$: import("rxjs").Observable<InAppNotificationView<Definitions>>;
|
|
10
14
|
readonly state$: import("rxjs").Observable<NotificationState<Definitions>>;
|
|
11
15
|
readonly notifications$: import("rxjs").Observable<InAppNotificationView<Definitions>[]>;
|
|
12
16
|
readonly unreadCount$: import("rxjs").Observable<number>;
|
|
@@ -15,6 +19,8 @@ export declare class NotificationClient<Definitions extends NotificationDefiniti
|
|
|
15
19
|
readonly notifications: import("../../signals/api.js").Signal<InAppNotificationView<Definitions>[]>;
|
|
16
20
|
readonly unreadCount: import("../../signals/api.js").Signal<number>;
|
|
17
21
|
readonly types: import("../../signals/api.js").Signal<Record<keyof Definitions, string>>;
|
|
22
|
+
readonly unreadNotifications: import("../../signals/api.js").Signal<InAppNotificationView<Definitions>[]>;
|
|
23
|
+
readonly readNotifications: import("../../signals/api.js").Signal<InAppNotificationView<Definitions>[]>;
|
|
18
24
|
loadNext(count?: number): void;
|
|
19
25
|
}
|
|
20
26
|
export {};
|
|
@@ -4,27 +4,50 @@ 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
|
-
import { concatMap, defer, from, map, merge, of, scan, shareReplay, Subject, switchAll, switchMap } from 'rxjs';
|
|
7
|
+
import { concatMap, defer, EMPTY, filter, from, map, merge, of, scan, share, shareReplay, Subject, switchAll, switchMap } from 'rxjs';
|
|
8
8
|
import { AuthenticationClientService } from '../../authentication/client/authentication.service.js';
|
|
9
9
|
import { Singleton } from '../../injector/decorators.js';
|
|
10
10
|
import { inject } from '../../injector/inject.js';
|
|
11
11
|
import { NotificationApiClient } from '../../notification/api/index.js';
|
|
12
12
|
import { forceCast } from '../../rxjs-utils/cast.js';
|
|
13
13
|
import { computed, toSignal } from '../../signals/api.js';
|
|
14
|
-
import { isDefined, isUndefined } from '../../utils/type-guards.js';
|
|
14
|
+
import { isDefined, isNotNull, isNull, isUndefined } from '../../utils/type-guards.js';
|
|
15
15
|
let NotificationClient = class NotificationClient {
|
|
16
16
|
#pagination$ = new Subject();
|
|
17
17
|
#authenticationService = inject(AuthenticationClientService);
|
|
18
18
|
api = inject(NotificationApiClient);
|
|
19
|
+
stream$ = this.#authenticationService.sessionId$.pipe(switchMap((sessionId) => {
|
|
20
|
+
if (isUndefined(sessionId)) {
|
|
21
|
+
return EMPTY;
|
|
22
|
+
}
|
|
23
|
+
return defer(() => from(this.api.stream())).pipe(switchAll(), forceCast());
|
|
24
|
+
}), share());
|
|
25
|
+
/** Emits whenever a new notification is received via the stream. */
|
|
26
|
+
onNewNotification$ = this.stream$.pipe(filter((item) => isDefined(item.notification)), map((item) => item.notification));
|
|
19
27
|
state$ = this.#authenticationService.sessionId$.pipe(switchMap((sessionId) => {
|
|
20
28
|
if (isUndefined(sessionId)) {
|
|
21
29
|
return of({ notifications: [], unreadCount: 0 });
|
|
22
30
|
}
|
|
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 }))),
|
|
24
|
-
const actions = [
|
|
31
|
+
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 }))), this.stream$.pipe(switchMap((item) => {
|
|
32
|
+
const actions = [];
|
|
33
|
+
if (isDefined(item.unreadCount)) {
|
|
34
|
+
actions.push({ type: 'set-unread-count', unreadCount: item.unreadCount });
|
|
35
|
+
}
|
|
25
36
|
if (isDefined(item.notification)) {
|
|
26
37
|
actions.push({ type: 'prepend-notification', notification: item.notification });
|
|
27
38
|
}
|
|
39
|
+
if (isDefined(item.readId)) {
|
|
40
|
+
actions.push({ type: 'mark-read', id: item.readId });
|
|
41
|
+
}
|
|
42
|
+
if (item.readAll == true) {
|
|
43
|
+
actions.push({ type: 'mark-all-read' });
|
|
44
|
+
}
|
|
45
|
+
if (isDefined(item.archiveId)) {
|
|
46
|
+
actions.push({ type: 'archive', id: item.archiveId });
|
|
47
|
+
}
|
|
48
|
+
if (item.archiveAll == true) {
|
|
49
|
+
actions.push({ type: 'archive-all' });
|
|
50
|
+
}
|
|
28
51
|
return from(actions);
|
|
29
52
|
})), 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
53
|
switch (action.type) {
|
|
@@ -38,6 +61,24 @@ let NotificationClient = class NotificationClient {
|
|
|
38
61
|
case 'append-notifications':
|
|
39
62
|
const filtered = action.notifications.filter((n) => !acc.notifications.some((c) => c.id == n.id));
|
|
40
63
|
return { ...acc, notifications: [...acc.notifications, ...filtered] };
|
|
64
|
+
case 'mark-read':
|
|
65
|
+
return {
|
|
66
|
+
...acc,
|
|
67
|
+
notifications: acc.notifications.map((notification) => (notification.id == action.id) ? { ...notification, readTimestamp: Date.now() } : notification),
|
|
68
|
+
};
|
|
69
|
+
case 'mark-all-read':
|
|
70
|
+
const now = Date.now();
|
|
71
|
+
return {
|
|
72
|
+
...acc,
|
|
73
|
+
notifications: acc.notifications.map((notification) => ({ ...notification, readTimestamp: notification.readTimestamp ?? now })),
|
|
74
|
+
};
|
|
75
|
+
case 'archive':
|
|
76
|
+
return {
|
|
77
|
+
...acc,
|
|
78
|
+
notifications: acc.notifications.filter((notification) => notification.id != action.id),
|
|
79
|
+
};
|
|
80
|
+
case 'archive-all':
|
|
81
|
+
return { ...acc, notifications: [] };
|
|
41
82
|
case 'set-unread-count':
|
|
42
83
|
return { ...acc, unreadCount: action.unreadCount };
|
|
43
84
|
default:
|
|
@@ -57,6 +98,8 @@ let NotificationClient = class NotificationClient {
|
|
|
57
98
|
notifications = computed(() => this.state().notifications);
|
|
58
99
|
unreadCount = computed(() => this.state().unreadCount);
|
|
59
100
|
types = toSignal(this.types$, { initialValue: {} });
|
|
101
|
+
unreadNotifications = computed(() => this.notifications().filter((notification) => isNull(notification.readTimestamp)));
|
|
102
|
+
readNotifications = computed(() => this.notifications().filter((notification) => isNotNull(notification.readTimestamp)));
|
|
60
103
|
loadNext(count = 20) {
|
|
61
104
|
const current = this.notifications();
|
|
62
105
|
const after = current[current.length - 1]?.id;
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import { TenantBaseEntity, type Uuid } from '../../orm/index.js';
|
|
2
2
|
import type { Timestamp } from '../../orm/types.js';
|
|
3
3
|
import { type NotificationDefinitionMap, type NotificationLogView } from './notification-log.model.js';
|
|
4
|
-
export declare class
|
|
5
|
-
static readonly entityName = "InAppNotification";
|
|
4
|
+
export declare abstract class InAppNotificationBase extends TenantBaseEntity {
|
|
6
5
|
userId: Uuid;
|
|
7
6
|
logId: Uuid;
|
|
7
|
+
timestamp: Timestamp;
|
|
8
8
|
readTimestamp: Timestamp | null;
|
|
9
9
|
archiveTimestamp: Timestamp | null;
|
|
10
10
|
}
|
|
11
|
-
export declare class
|
|
11
|
+
export declare class InAppNotification extends InAppNotificationBase {
|
|
12
|
+
static readonly entityName = "InAppNotification";
|
|
13
|
+
}
|
|
14
|
+
export declare class InAppNotificationArchive extends InAppNotificationBase {
|
|
15
|
+
static readonly entityName = "InAppNotificationArchive";
|
|
16
|
+
}
|
|
17
|
+
export declare class InAppNotificationView<Definitions extends NotificationDefinitionMap = NotificationDefinitionMap> extends InAppNotificationBase {
|
|
12
18
|
notification: NotificationLogView<Definitions>;
|
|
13
19
|
}
|
|
14
20
|
export declare function toInAppNotificationView<Definitions extends NotificationDefinitionMap = NotificationDefinitionMap>(inAppNotification: InAppNotification, notification: NotificationLogView<Definitions>): InAppNotificationView<Definitions>;
|
|
@@ -7,43 +7,64 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
8
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
9
|
};
|
|
10
|
+
import { isNotNull, isNull } from 'drizzle-orm';
|
|
10
11
|
import { User } from '../../authentication/index.js';
|
|
11
|
-
import { ForeignKey, Index, TenantBaseEntity, TenantReference, TimestampProperty, UuidProperty } from '../../orm/index.js';
|
|
12
|
+
import { ForeignKey, Index, TenantBaseEntity, TenantReference, TimestampProperty, Unique, UuidProperty } from '../../orm/index.js';
|
|
12
13
|
import { Property } from '../../schema/index.js';
|
|
13
14
|
import { NotificationLogEntity } from './notification-log.model.js';
|
|
14
15
|
import { NotificationTable } from './notification-table.js';
|
|
15
|
-
|
|
16
|
-
static entityName = 'InAppNotification';
|
|
16
|
+
export class InAppNotificationBase extends TenantBaseEntity {
|
|
17
17
|
userId;
|
|
18
18
|
logId;
|
|
19
|
-
|
|
19
|
+
timestamp;
|
|
20
20
|
readTimestamp;
|
|
21
21
|
archiveTimestamp;
|
|
22
|
-
}
|
|
22
|
+
}
|
|
23
23
|
__decorate([
|
|
24
24
|
UuidProperty(),
|
|
25
25
|
TenantReference(() => User),
|
|
26
26
|
__metadata("design:type", String)
|
|
27
|
-
],
|
|
27
|
+
], InAppNotificationBase.prototype, "userId", void 0);
|
|
28
28
|
__decorate([
|
|
29
29
|
UuidProperty(),
|
|
30
30
|
__metadata("design:type", String)
|
|
31
|
-
],
|
|
31
|
+
], InAppNotificationBase.prototype, "logId", void 0);
|
|
32
|
+
__decorate([
|
|
33
|
+
TimestampProperty(),
|
|
34
|
+
__metadata("design:type", Number)
|
|
35
|
+
], InAppNotificationBase.prototype, "timestamp", void 0);
|
|
32
36
|
__decorate([
|
|
33
37
|
TimestampProperty({ nullable: true }),
|
|
34
38
|
__metadata("design:type", Object)
|
|
35
|
-
],
|
|
39
|
+
], InAppNotificationBase.prototype, "readTimestamp", void 0);
|
|
36
40
|
__decorate([
|
|
37
41
|
TimestampProperty({ nullable: true }),
|
|
38
42
|
__metadata("design:type", Object)
|
|
39
|
-
],
|
|
43
|
+
], InAppNotificationBase.prototype, "archiveTimestamp", void 0);
|
|
44
|
+
let InAppNotification = class InAppNotification extends InAppNotificationBase {
|
|
45
|
+
static entityName = 'InAppNotification';
|
|
46
|
+
};
|
|
40
47
|
InAppNotification = __decorate([
|
|
41
48
|
NotificationTable({ name: 'in_app' }),
|
|
42
49
|
ForeignKey(() => NotificationLogEntity, ['tenantId', 'logId', 'userId'], ['tenantId', 'id', 'userId']),
|
|
43
|
-
|
|
50
|
+
Unique(['tenantId', 'userId', 'logId']),
|
|
51
|
+
Index(['tenantId', 'userId', ['timestamp', 'desc'], ['logId', 'desc']]),
|
|
52
|
+
Index(['tenantId', 'userId', ['timestamp', 'desc'], ['logId', 'desc']], { where: (table) => isNull(table.readTimestamp) }),
|
|
53
|
+
Index(['tenantId', 'userId', 'readTimestamp'], { where: (table) => isNotNull(table.readTimestamp) }) // todo: include id as soon as drizzle supports this to make it a covering index for queries that fetch read notifications
|
|
54
|
+
,
|
|
55
|
+
Index(['timestamp'], { using: 'brin' })
|
|
44
56
|
], InAppNotification);
|
|
45
57
|
export { InAppNotification };
|
|
46
|
-
|
|
58
|
+
let InAppNotificationArchive = class InAppNotificationArchive extends InAppNotificationBase {
|
|
59
|
+
static entityName = 'InAppNotificationArchive';
|
|
60
|
+
};
|
|
61
|
+
InAppNotificationArchive = __decorate([
|
|
62
|
+
NotificationTable({ name: 'in_app_archive' }),
|
|
63
|
+
Index(['tenantId', 'userId', ['timestamp', 'desc'], ['logId', 'desc']]),
|
|
64
|
+
Index(['tenantId', 'userId', ['archiveTimestamp', 'desc']]) // todo: include id as soon as drizzle supports this to make it a covering index for queries that fetch archived notifications
|
|
65
|
+
], InAppNotificationArchive);
|
|
66
|
+
export { InAppNotificationArchive };
|
|
67
|
+
export class InAppNotificationView extends InAppNotificationBase {
|
|
47
68
|
notification;
|
|
48
69
|
}
|
|
49
70
|
__decorate([
|
|
@@ -9,7 +9,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
9
9
|
};
|
|
10
10
|
import { Subject, User } from '../../authentication/index.js';
|
|
11
11
|
import { defineEnum } from '../../enumeration/index.js';
|
|
12
|
-
import {
|
|
12
|
+
import { JsonProperty, Reference, TenantBaseEntity, TenantReference, TimestampProperty, Unique, UuidProperty } from '../../orm/index.js';
|
|
13
13
|
import { Enumeration, Integer, StringProperty } from '../../schema/index.js';
|
|
14
14
|
import { NotificationTable } from './notification-table.js';
|
|
15
15
|
import { NotificationType } from './notification-type.model.js';
|
|
@@ -79,7 +79,6 @@ __decorate([
|
|
|
79
79
|
], NotificationLogEntity.prototype, "payload", void 0);
|
|
80
80
|
NotificationLogEntity = __decorate([
|
|
81
81
|
NotificationTable({ name: 'log' }),
|
|
82
|
-
Unique(['tenantId', '
|
|
83
|
-
Index(['tenantId', 'userId', 'timestamp'])
|
|
82
|
+
Unique(['tenantId', 'userId', 'id'])
|
|
84
83
|
], NotificationLogEntity);
|
|
85
84
|
export { NotificationLogEntity };
|
|
@@ -7,9 +7,10 @@ export declare class NotificationApiController implements ApiController<Notifica
|
|
|
7
7
|
protected readonly notificationService: NotificationService<Record<string, import("../../index.js").NotificationDefinition<import("../../../types/types.js").ObjectLiteral, import("../../../types/types.js").ObjectLiteral>>>;
|
|
8
8
|
protected readonly notificationTypeService: NotificationTypeService;
|
|
9
9
|
protected readonly sseService: NotificationSseService;
|
|
10
|
-
stream({ abortSignal, getToken }: ApiRequestContext<NotificationApiDefinition, 'stream'>): ApiServerResult<NotificationApiDefinition, 'stream'>;
|
|
10
|
+
stream({ abortSignal, getToken, serverSentEvents: { lastEventId } }: ApiRequestContext<NotificationApiDefinition, 'stream'>): ApiServerResult<NotificationApiDefinition, 'stream'>;
|
|
11
11
|
types(): Promise<Record<string, string>>;
|
|
12
12
|
listInApp({ parameters, getToken }: ApiRequestContext<NotificationApiDefinition, 'listInApp'>): Promise<any>;
|
|
13
|
+
listArchivedInApp({ parameters, getToken }: ApiRequestContext<NotificationApiDefinition, 'listArchivedInApp'>): Promise<any>;
|
|
13
14
|
markRead({ parameters, getToken }: ApiRequestContext<NotificationApiDefinition, 'markRead'>): Promise<'ok'>;
|
|
14
15
|
markAllRead({ getToken }: ApiRequestContext<NotificationApiDefinition, 'markAllRead'>): Promise<'ok'>;
|
|
15
16
|
archive({ parameters, getToken }: ApiRequestContext<NotificationApiDefinition, 'archive'>): Promise<'ok'>;
|
|
@@ -8,6 +8,7 @@ import { apiController } from '../../../api/server/index.js';
|
|
|
8
8
|
import { inject } from '../../../injector/index.js';
|
|
9
9
|
import { toAsyncIterable } from '../../../rxjs-utils/index.js';
|
|
10
10
|
import { decodeBase64 } from '../../../utils/base64.js';
|
|
11
|
+
import { isDefined } from '../../../utils/type-guards.js';
|
|
11
12
|
import { notificationApiDefinition } from '../../api/notification.api.js';
|
|
12
13
|
import { NotificationSseService } from '../services/notification-sse.service.js';
|
|
13
14
|
import { NotificationTypeService } from '../services/notification-type.service.js';
|
|
@@ -16,12 +17,27 @@ let NotificationApiController = class NotificationApiController {
|
|
|
16
17
|
notificationService = inject(NotificationService);
|
|
17
18
|
notificationTypeService = inject(NotificationTypeService);
|
|
18
19
|
sseService = inject(NotificationSseService);
|
|
19
|
-
async *stream({ abortSignal, getToken }) {
|
|
20
|
+
async *stream({ abortSignal, getToken, serverSentEvents: { lastEventId } }) {
|
|
20
21
|
const token = await getToken();
|
|
21
22
|
const source = this.sseService.register(token.payload.tenant, token.payload.subject);
|
|
22
23
|
const asyncIterable = toAsyncIterable(source);
|
|
23
24
|
abortSignal.addEventListener('abort', () => this.sseService.unregister(token.payload.tenant, token.payload.subject, source));
|
|
24
25
|
try {
|
|
26
|
+
if (isDefined(lastEventId)) {
|
|
27
|
+
const lastEventIdNumber = Number(lastEventId);
|
|
28
|
+
const stateTimestamp = Number.isNaN(lastEventIdNumber) ? 0 : lastEventIdNumber;
|
|
29
|
+
const { unreadCount, missedNotifications, readIds, archiveIds } = await this.notificationService.getCatchupData(token.payload.tenant, token.payload.subject, stateTimestamp);
|
|
30
|
+
yield { unreadCount };
|
|
31
|
+
for (const readId of readIds) {
|
|
32
|
+
yield { readId };
|
|
33
|
+
}
|
|
34
|
+
for (const archiveId of archiveIds) {
|
|
35
|
+
yield { archiveId };
|
|
36
|
+
}
|
|
37
|
+
for (const notification of missedNotifications.reverse()) {
|
|
38
|
+
yield { notification };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
25
41
|
yield* asyncIterable;
|
|
26
42
|
}
|
|
27
43
|
finally {
|
|
@@ -35,6 +51,10 @@ let NotificationApiController = class NotificationApiController {
|
|
|
35
51
|
const token = await getToken();
|
|
36
52
|
return await this.notificationService.listInApp(token.payload.tenant, token.payload.subject, parameters);
|
|
37
53
|
}
|
|
54
|
+
async listArchivedInApp({ parameters, getToken }) {
|
|
55
|
+
const token = await getToken();
|
|
56
|
+
return await this.notificationService.listArchivedInApp(token.payload.tenant, token.payload.subject, parameters);
|
|
57
|
+
}
|
|
38
58
|
async markRead({ parameters, getToken }) {
|
|
39
59
|
const token = await getToken();
|
|
40
60
|
await this.notificationService.markRead(token.payload.tenant, token.payload.subject, parameters.id);
|
|
@@ -6,9 +6,22 @@ CREATE TABLE "notification"."in_app" (
|
|
|
6
6
|
"tenant_id" uuid NOT NULL,
|
|
7
7
|
"user_id" uuid NOT NULL,
|
|
8
8
|
"log_id" uuid NOT NULL,
|
|
9
|
+
"timestamp" timestamp with time zone NOT NULL,
|
|
10
|
+
"read_timestamp" timestamp with time zone,
|
|
11
|
+
"archive_timestamp" timestamp with time zone,
|
|
12
|
+
CONSTRAINT "in_app_tenant_id_id_pk" PRIMARY KEY("tenant_id","id"),
|
|
13
|
+
CONSTRAINT "in_app_tenant_id_user_id_log_id_unique" UNIQUE("tenant_id","user_id","log_id")
|
|
14
|
+
);
|
|
15
|
+
--> statement-breakpoint
|
|
16
|
+
CREATE TABLE "notification"."in_app_archive" (
|
|
17
|
+
"id" uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
18
|
+
"tenant_id" uuid NOT NULL,
|
|
19
|
+
"user_id" uuid NOT NULL,
|
|
20
|
+
"log_id" uuid NOT NULL,
|
|
21
|
+
"timestamp" timestamp with time zone NOT NULL,
|
|
9
22
|
"read_timestamp" timestamp with time zone,
|
|
10
23
|
"archive_timestamp" timestamp with time zone,
|
|
11
|
-
CONSTRAINT "
|
|
24
|
+
CONSTRAINT "in_app_archive_tenant_id_id_pk" PRIMARY KEY("tenant_id","id")
|
|
12
25
|
);
|
|
13
26
|
--> statement-breakpoint
|
|
14
27
|
CREATE TABLE "notification"."log" (
|
|
@@ -23,7 +36,7 @@ CREATE TABLE "notification"."log" (
|
|
|
23
36
|
"trigger_subject_id" uuid NOT NULL,
|
|
24
37
|
"payload" jsonb,
|
|
25
38
|
CONSTRAINT "log_tenant_id_id_pk" PRIMARY KEY("tenant_id","id"),
|
|
26
|
-
CONSTRAINT "
|
|
39
|
+
CONSTRAINT "log_tenant_id_user_id_id_unique" UNIQUE("tenant_id","user_id","id")
|
|
27
40
|
);
|
|
28
41
|
--> statement-breakpoint
|
|
29
42
|
CREATE TABLE "notification"."preference" (
|
|
@@ -74,11 +87,16 @@ CREATE TABLE "notification"."web_push_subscription" (
|
|
|
74
87
|
--> statement-breakpoint
|
|
75
88
|
ALTER TABLE "notification"."in_app" ADD CONSTRAINT "in_app_id_user_fkey" FOREIGN KEY ("tenant_id","user_id") REFERENCES "authentication"."user"("tenant_id","id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
|
76
89
|
ALTER TABLE "notification"."in_app" ADD CONSTRAINT "in_app_tenantId_logId_userId_log_fkey" FOREIGN KEY ("tenant_id","log_id","user_id") REFERENCES "notification"."log"("tenant_id","id","user_id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
|
90
|
+
ALTER TABLE "notification"."in_app_archive" ADD CONSTRAINT "in_app_archive_id_user_fkey" FOREIGN KEY ("tenant_id","user_id") REFERENCES "authentication"."user"("tenant_id","id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
|
77
91
|
ALTER TABLE "notification"."log" ADD CONSTRAINT "log_type_type_key_fk" FOREIGN KEY ("type") REFERENCES "notification"."type"("key") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
|
78
92
|
ALTER TABLE "notification"."log" ADD CONSTRAINT "log_id_user_fkey" FOREIGN KEY ("tenant_id","user_id") REFERENCES "authentication"."user"("tenant_id","id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
|
79
93
|
ALTER TABLE "notification"."log" ADD CONSTRAINT "log_id_subject_fkey" FOREIGN KEY ("tenant_id","trigger_subject_id") REFERENCES "authentication"."subject"("tenant_id","id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
|
80
94
|
ALTER TABLE "notification"."preference" ADD CONSTRAINT "preference_type_type_key_fk" FOREIGN KEY ("type") REFERENCES "notification"."type"("key") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
|
81
95
|
ALTER TABLE "notification"."preference" ADD CONSTRAINT "preference_id_user_fkey" FOREIGN KEY ("tenant_id","user_id") REFERENCES "authentication"."user"("tenant_id","id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
|
82
96
|
ALTER TABLE "notification"."web_push_subscription" ADD CONSTRAINT "web_push_subscription_id_user_fkey" FOREIGN KEY ("tenant_id","user_id") REFERENCES "authentication"."user"("tenant_id","id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
|
83
|
-
CREATE INDEX "
|
|
84
|
-
CREATE INDEX "
|
|
97
|
+
CREATE INDEX "in_app_timestamp_idx" ON "notification"."in_app" USING brin ("timestamp");--> statement-breakpoint
|
|
98
|
+
CREATE INDEX "in_app_tenant_id_user_id_read_timestamp_partial_idx" ON "notification"."in_app" USING btree ("tenant_id","user_id","read_timestamp") WHERE "notification"."in_app"."read_timestamp" is not null;--> statement-breakpoint
|
|
99
|
+
CREATE INDEX "in_app_tenant_id_user_id_timestamp_log_id_partial_idx" ON "notification"."in_app" USING btree ("tenant_id","user_id","timestamp" DESC NULLS LAST,"log_id" DESC NULLS LAST) WHERE "notification"."in_app"."read_timestamp" is null;--> statement-breakpoint
|
|
100
|
+
CREATE INDEX "in_app_tenant_id_user_id_timestamp_log_id_idx" ON "notification"."in_app" USING btree ("tenant_id","user_id","timestamp" DESC NULLS LAST,"log_id" DESC NULLS LAST);--> statement-breakpoint
|
|
101
|
+
CREATE INDEX "in_app_archive_tenant_id_user_id_archive_timestamp_idx" ON "notification"."in_app_archive" USING btree ("tenant_id","user_id","archive_timestamp" DESC NULLS LAST);--> statement-breakpoint
|
|
102
|
+
CREATE INDEX "in_app_archive_tenant_id_user_id_timestamp_log_id_idx" ON "notification"."in_app_archive" USING btree ("tenant_id","user_id","timestamp" DESC NULLS LAST,"log_id" DESC NULLS LAST);
|