@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.
Files changed (53) hide show
  1. package/api/server/gateway.js +2 -2
  2. package/index.d.ts +1 -0
  3. package/index.js +1 -0
  4. package/internal.d.ts +1 -0
  5. package/internal.js +1 -0
  6. package/notification/api/notification.api.d.ts +27 -12
  7. package/notification/api/notification.api.js +10 -3
  8. package/notification/client/index.d.ts +1 -0
  9. package/notification/client/index.js +1 -0
  10. package/notification/client/notification-client.d.ts +20 -0
  11. package/notification/client/notification-client.js +69 -0
  12. package/notification/index.d.ts +2 -0
  13. package/notification/index.js +2 -0
  14. package/notification/server/api/notification.api-controller.d.ts +3 -0
  15. package/notification/server/api/notification.api-controller.js +5 -0
  16. package/notification/server/providers/in-app-channel-provider.js +4 -1
  17. package/notification/server/services/notification-sse.service.d.ts +5 -3
  18. package/notification/server/services/notification-sse.service.js +19 -7
  19. package/notification/server/services/notification-type.service.d.ts +1 -0
  20. package/notification/server/services/notification-type.service.js +5 -0
  21. package/notification/server/services/notification.service.d.ts +2 -0
  22. package/notification/server/services/notification.service.js +30 -5
  23. package/notification/tests/notification-api.test.js +8 -0
  24. package/notification/tests/notification-flow.test.js +28 -0
  25. package/notification/tests/notification-sse.service.test.js +10 -1
  26. package/notification/tests/unit/notification-client.test.d.ts +1 -0
  27. package/notification/tests/unit/notification-client.test.js +112 -0
  28. package/notification/types.d.ts +9 -0
  29. package/notification/types.js +6 -0
  30. package/object-storage/object-storage.d.ts +10 -0
  31. package/object-storage/s3/s3.object-storage-provider.d.ts +11 -4
  32. package/object-storage/s3/s3.object-storage-provider.js +29 -26
  33. package/object-storage/s3/s3.object-storage.d.ts +7 -4
  34. package/object-storage/s3/s3.object-storage.js +141 -60
  35. package/object-storage/s3/s3.object.d.ts +6 -0
  36. package/object-storage/s3/s3.object.js +1 -1
  37. package/object-storage/s3/tests/s3.object-storage.integration.test.d.ts +1 -0
  38. package/object-storage/s3/tests/s3.object-storage.integration.test.js +334 -0
  39. package/package.json +4 -3
  40. package/rpc/adapters/readable-stream.adapter.js +27 -22
  41. package/rpc/endpoints/message-port.rpc-endpoint.d.ts +4 -0
  42. package/rpc/endpoints/message-port.rpc-endpoint.js +4 -0
  43. package/rpc/model.d.ts +11 -1
  44. package/rpc/rpc.d.ts +17 -1
  45. package/rpc/rpc.endpoint.js +4 -3
  46. package/rpc/rpc.error.d.ts +5 -1
  47. package/rpc/rpc.error.js +16 -3
  48. package/rpc/rpc.js +89 -15
  49. package/rpc/tests/rpc.integration.test.d.ts +1 -0
  50. package/rpc/tests/rpc.integration.test.js +619 -0
  51. package/unit-test/integration-setup.d.ts +1 -0
  52. package/unit-test/integration-setup.js +12 -0
  53. package/utils/try-ignore.d.ts +2 -2
@@ -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
@@ -5,4 +5,5 @@
5
5
  */
6
6
  export * from './core.js';
7
7
  export * from './import.js';
8
+ export * from './internal.js';
8
9
  export * from './require.js';
package/index.js CHANGED
@@ -5,4 +5,5 @@
5
5
  */
6
6
  export * from './core.js';
7
7
  export * from './import.js';
8
+ export * from './internal.js';
8
9
  export * from './require.js';
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<InAppNotificationView<Record<string, import("../models/notification-log.model.js").NotificationDefinition<import("../../types/types.js").ObjectLiteral, import("../../types/types.js").ObjectLiteral>>>>;
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
- readonly type: string;
79
- readonly channel: "email" | "in-app" | "web-push";
80
- readonly enabled: boolean;
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
- readonly endpoint: string;
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<InAppNotificationView<Record<string, import("../models/notification-log.model.js").NotificationDefinition<import("../../types/types.js").ObjectLiteral, import("../../types/types.js").ObjectLiteral>>>>;
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
- readonly type: string;
175
- readonly channel: "email" | "in-app" | "web-push";
176
- readonly enabled: boolean;
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
- readonly endpoint: string;
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, explicitObject, literal, number, object, optional, string } from '../../schema/index.js';
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: explicitObject({
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: explicitObject({
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 };
@@ -1,2 +1,4 @@
1
1
  export * from './api/index.js';
2
+ export * from './client/index.js';
2
3
  export * from './models/index.js';
4
+ export * from './types.js';
@@ -1,2 +1,4 @@
1
1
  export * from './api/index.js';
2
+ export * from './client/index.js';
2
3
  export * from './models/index.js';
4
+ export * from './types.js';
@@ -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.#sseService.send(inAppNotificationView);
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<InAppNotificationView>;
8
- unregister(tenantId: string, userId: string, source: Subject<InAppNotificationView>): void;
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 -> Sources
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(notification);
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
- dispatchToLocal(notification) {
50
- const tenantMap = this.#sources.get(notification.tenantId);
51
- const userSources = tenantMap?.get(notification.userId);
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
- import { and, desc, eq, isNull } from 'drizzle-orm';
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
- let NotificationService = class NotificationService extends Transactional {
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
  });