@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.
Files changed (36) hide show
  1. package/api/server/gateway.js +6 -1
  2. package/api/types.d.ts +5 -1
  3. package/document-management/server/drizzle/{0000_silly_chimera.sql → 0000_complex_black_bird.sql} +1 -1
  4. package/document-management/server/drizzle/meta/0000_snapshot.json +3 -3
  5. package/document-management/server/drizzle/meta/_journal.json +2 -2
  6. package/notification/api/notification.api.d.ts +30 -4
  7. package/notification/api/notification.api.js +17 -3
  8. package/notification/client/notification-client.d.ts +6 -0
  9. package/notification/client/notification-client.js +47 -4
  10. package/notification/models/in-app-notification.model.d.ts +9 -3
  11. package/notification/models/in-app-notification.model.js +32 -11
  12. package/notification/models/notification-log.model.js +2 -3
  13. package/notification/server/api/notification.api-controller.d.ts +2 -1
  14. package/notification/server/api/notification.api-controller.js +21 -1
  15. package/notification/server/drizzle/{0000_oval_rage.sql → 0000_wise_pyro.sql} +22 -4
  16. package/notification/server/drizzle/meta/0000_snapshot.json +249 -37
  17. package/notification/server/drizzle/meta/_journal.json +2 -2
  18. package/notification/server/module.d.ts +5 -0
  19. package/notification/server/module.js +6 -1
  20. package/notification/server/providers/in-app-channel-provider.js +2 -1
  21. package/notification/server/schemas.d.ts +3 -2
  22. package/notification/server/schemas.js +3 -2
  23. package/notification/server/services/notification-sse.service.d.ts +14 -2
  24. package/notification/server/services/notification-sse.service.js +10 -11
  25. package/notification/server/services/notification.service.d.ts +16 -5
  26. package/notification/server/services/notification.service.js +160 -34
  27. package/notification/tests/notification-api.test.js +8 -1
  28. package/notification/tests/notification-flow.test.js +41 -4
  29. package/notification/tests/notification-sse.service.test.js +24 -3
  30. package/notification/types.d.ts +10 -2
  31. package/notification/types.js +6 -2
  32. package/orm/server/drizzle/schema-converter.js +5 -3
  33. package/orm/tests/schema-converter.test.js +1 -0
  34. package/package.json +1 -1
  35. package/sse/data-stream-source.d.ts +7 -4
  36. package/sse/data-stream-source.js +7 -4
@@ -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>;
@@ -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 "workflow_document_id_idx" ON "document_management"."workflow" USING btree ("document_id") WHERE "document_management"."workflow"."state" <> 'completed';
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": "fee5df5b-e30c-483a-a4f2-fff78256e1c2",
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
- "workflow_document_id_idx": {
2760
- "name": "workflow_document_id_idx",
2759
+ "workflow_document_id_partial_idx": {
2760
+ "name": "workflow_document_id_partial_idx",
2761
2761
  "columns": [
2762
2762
  {
2763
2763
  "expression": "document_id",
@@ -5,8 +5,8 @@
5
5
  {
6
6
  "idx": 0,
7
7
  "version": "7",
8
- "when": 1769464843006,
9
- "tag": "0000_silly_chimera",
8
+ "when": 1770754372969,
9
+ "tag": "0000_complex_black_bird",
10
10
  "breakpoints": true
11
11
  }
12
12
  ]
@@ -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
- includeArchived?: boolean | undefined;
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
- includeArchived?: boolean | undefined;
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
- includeArchived: optional(boolean()),
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 }))), defer(() => from(this.api.stream())).pipe(switchAll(), forceCast(), switchMap((item) => {
24
- const actions = [{ type: 'set-unread-count', unreadCount: item.unreadCount }];
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 InAppNotification extends TenantBaseEntity {
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 InAppNotificationView<Definitions extends NotificationDefinitionMap = NotificationDefinitionMap> extends InAppNotification {
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
- let InAppNotification = class InAppNotification extends TenantBaseEntity {
16
- static entityName = 'InAppNotification';
16
+ export class InAppNotificationBase extends TenantBaseEntity {
17
17
  userId;
18
18
  logId;
19
- // TODO: anpinnen?
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
- ], InAppNotification.prototype, "userId", void 0);
27
+ ], InAppNotificationBase.prototype, "userId", void 0);
28
28
  __decorate([
29
29
  UuidProperty(),
30
30
  __metadata("design:type", String)
31
- ], InAppNotification.prototype, "logId", void 0);
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
- ], InAppNotification.prototype, "readTimestamp", void 0);
39
+ ], InAppNotificationBase.prototype, "readTimestamp", void 0);
36
40
  __decorate([
37
41
  TimestampProperty({ nullable: true }),
38
42
  __metadata("design:type", Object)
39
- ], InAppNotification.prototype, "archiveTimestamp", void 0);
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
- Index(['tenantId', 'userId', 'logId'], { where: () => ({ archiveTimestamp: null }) })
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
- export class InAppNotificationView extends InAppNotification {
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 { Index, JsonProperty, Reference, TenantBaseEntity, TenantReference, TimestampProperty, Unique, UuidProperty } from '../../orm/index.js';
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', 'id', 'userId']),
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 "in_app_tenant_id_id_pk" PRIMARY KEY("tenant_id","id")
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 "log_tenant_id_id_user_id_unique" UNIQUE("tenant_id","id","user_id")
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 "in_app_tenant_id_user_id_log_id_idx" ON "notification"."in_app" USING btree ("tenant_id","user_id","log_id") WHERE "notification"."in_app"."archive_timestamp" is null;--> statement-breakpoint
84
- CREATE INDEX "log_tenant_id_user_id_timestamp_idx" ON "notification"."log" USING btree ("tenant_id","user_id","timestamp");
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);