@tstdl/base 0.93.100 → 0.93.102

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 (84) hide show
  1. package/authentication/client/authentication.service.d.ts +1 -1
  2. package/authentication/client/authentication.service.js +23 -11
  3. package/notification/api/index.d.ts +1 -0
  4. package/notification/api/index.js +1 -0
  5. package/notification/api/notification.api.d.ts +8 -16
  6. package/notification/api/notification.api.js +13 -26
  7. package/notification/index.d.ts +1 -1
  8. package/notification/index.js +1 -1
  9. package/notification/models/in-app-notification.model.d.ts +9 -4
  10. package/notification/models/in-app-notification.model.js +25 -10
  11. package/notification/models/index.d.ts +1 -1
  12. package/notification/models/index.js +1 -1
  13. package/notification/models/notification-log.model.d.ts +42 -5
  14. package/notification/models/notification-log.model.js +34 -20
  15. package/notification/models/notification-preference.model.d.ts +2 -2
  16. package/notification/models/notification-preference.model.js +9 -9
  17. package/notification/models/notification-type.model.d.ts +17 -0
  18. package/notification/models/{notification-category.model.js → notification-type.model.js} +12 -13
  19. package/notification/models/web-push-subscription.model.d.ts +2 -2
  20. package/notification/models/web-push-subscription.model.js +8 -7
  21. package/notification/server/api/notification.api-controller.d.ts +2 -2
  22. package/notification/server/api/notification.api-controller.js +4 -3
  23. package/notification/server/drizzle/{0000_glorious_randall.sql → 0000_shiny_the_anarchist.sql} +27 -32
  24. package/notification/server/drizzle/meta/0000_snapshot.json +179 -179
  25. package/notification/server/drizzle/meta/_journal.json +2 -2
  26. package/notification/server/module.d.ts +2 -0
  27. package/notification/server/module.js +1 -0
  28. package/notification/server/providers/channel-provider.d.ts +4 -3
  29. package/notification/server/providers/channel-provider.js +2 -1
  30. package/notification/server/providers/email-channel-provider.d.ts +3 -3
  31. package/notification/server/providers/email-channel-provider.js +7 -9
  32. package/notification/server/providers/in-app-channel-provider.d.ts +5 -5
  33. package/notification/server/providers/in-app-channel-provider.js +15 -16
  34. package/notification/server/providers/index.d.ts +1 -1
  35. package/notification/server/providers/index.js +1 -1
  36. package/notification/server/providers/web-push-channel-provider.d.ts +5 -4
  37. package/notification/server/providers/web-push-channel-provider.js +8 -7
  38. package/notification/server/schemas.d.ts +3 -3
  39. package/notification/server/schemas.js +3 -4
  40. package/notification/server/services/index.d.ts +2 -4
  41. package/notification/server/services/index.js +2 -4
  42. package/notification/server/services/notification-delivery.worker.d.ts +7 -1
  43. package/notification/server/services/notification-delivery.worker.js +49 -37
  44. package/notification/server/services/notification-sse.service.d.ts +4 -7
  45. package/notification/server/services/notification-sse.service.js +4 -11
  46. package/notification/server/services/notification-template.d.ts +2 -2
  47. package/notification/server/services/notification-template.js +3 -1
  48. package/notification/server/services/notification-template.service.d.ts +1 -1
  49. package/notification/server/services/notification-template.service.js +7 -3
  50. package/notification/server/services/notification-type.service.d.ts +11 -0
  51. package/notification/server/services/notification-type.service.js +41 -0
  52. package/notification/server/services/notification.service.d.ts +4 -5
  53. package/notification/server/services/notification.service.js +44 -27
  54. package/notification/tests/notification-api.test.js +95 -0
  55. package/notification/tests/notification-flow.test.js +174 -28
  56. package/notification/tests/notification-type.service.test.d.ts +1 -0
  57. package/notification/tests/notification-type.service.test.js +35 -0
  58. package/package.json +1 -1
  59. package/rate-limit/postgres/postgres-rate-limiter.d.ts +9 -4
  60. package/rate-limit/postgres/postgres-rate-limiter.js +17 -10
  61. package/rate-limit/rate-limiter.d.ts +6 -6
  62. package/rate-limit/tests/postgres-rate-limiter.test.js +1 -1
  63. package/task-queue/postgres/task-queue.js +1 -1
  64. package/task-queue/task-context.d.ts +1 -14
  65. package/task-queue/task-context.js +0 -30
  66. package/task-queue/task-queue.d.ts +4 -12
  67. package/task-queue/task-queue.js +38 -89
  68. package/task-queue/tests/extensive-dependencies.test.d.ts +1 -0
  69. package/task-queue/tests/extensive-dependencies.test.js +234 -0
  70. package/task-queue/tests/worker.test.js +0 -21
  71. package/task-queue/types.d.ts +1 -8
  72. package/notification/enums.d.ts +0 -22
  73. package/notification/enums.js +0 -19
  74. package/notification/models/notification-category.model.d.ts +0 -17
  75. package/notification/server/services/notification-category.service.d.ts +0 -11
  76. package/notification/server/services/notification-category.service.js +0 -41
  77. package/notification/server/services/notification-delivery.task.d.ts +0 -9
  78. package/notification/server/services/notification-delivery.task.js +0 -1
  79. package/notification/server/services/singleton.d.ts +0 -3
  80. package/notification/server/services/singleton.js +0 -10
  81. package/notification/tests/notification-category.service.test.js +0 -36
  82. package/notification/tests/test-notification.model.d.ts +0 -4
  83. package/notification/tests/test-notification.model.js +0 -25
  84. /package/notification/tests/{notification-category.service.test.d.ts → notification-api.test.d.ts} +0 -0
@@ -1,7 +1,9 @@
1
1
  import { Injector } from '../../injector/index.js';
2
2
  import { type DatabaseConfig } from '../../orm/server/index.js';
3
+ import type { NotificationChannel } from '../models/index.js';
3
4
  export declare class NotificationConfiguration {
4
5
  database?: DatabaseConfig;
6
+ defaultChannels?: NotificationChannel[];
5
7
  }
6
8
  export declare function configureNotification({ injector, ...config }: NotificationConfiguration & {
7
9
  injector?: Injector;
@@ -2,6 +2,7 @@ import { inject, Injector } from '../../injector/index.js';
2
2
  import { Database, migrate } from '../../orm/server/index.js';
3
3
  export class NotificationConfiguration {
4
4
  database;
5
+ defaultChannels;
5
6
  }
6
7
  export function configureNotification({ injector, ...config }) {
7
8
  const targetInjector = injector ?? Injector;
@@ -1,4 +1,5 @@
1
- import { NotificationLog } from '../../models/notification-log.model.js';
2
- export interface ChannelProvider {
3
- send(notification: NotificationLog): Promise<void>;
1
+ import type { Transaction } from '../../../orm/server/index.js';
2
+ import type { NotificationLog } from '../../models/notification-log.model.js';
3
+ export declare abstract class ChannelProvider {
4
+ abstract send(notification: NotificationLog, tx?: Transaction): Promise<void>;
4
5
  }
@@ -1 +1,2 @@
1
- import { NotificationLog } from '../../models/notification-log.model.js';
1
+ export class ChannelProvider {
2
+ }
@@ -1,6 +1,6 @@
1
- import { NotificationLog } from '../../models/index.js';
2
- import type { ChannelProvider } from './channel-provider.js';
3
- export declare class EmailChannelProvider implements ChannelProvider {
1
+ import type { NotificationLog } from '../../models/index.js';
2
+ import { ChannelProvider } from './channel-provider.js';
3
+ export declare class EmailChannelProvider extends ChannelProvider {
4
4
  #private;
5
5
  send(notification: NotificationLog): Promise<void>;
6
6
  }
@@ -4,19 +4,17 @@ 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 { inject } from '../../../injector/index.js';
7
+ import { SubjectService } from '../../../authentication/server/subject.service.js';
8
+ import { inject, Singleton } from '../../../injector/index.js';
8
9
  import { MailService } from '../../../mail/mail.service.js';
9
- import { User } from '../../../authentication/models/user.model.js';
10
- import { injectRepository } from '../../../orm/server/index.js';
11
- import { NotificationLog } from '../../models/index.js';
12
- import { NotificationSingleton } from '../services/singleton.js';
13
10
  import { NotificationTemplateService } from '../services/notification-template.service.js';
14
- let EmailChannelProvider = class EmailChannelProvider {
11
+ import { ChannelProvider } from './channel-provider.js';
12
+ let EmailChannelProvider = class EmailChannelProvider extends ChannelProvider {
15
13
  #mailService = inject(MailService);
16
- #userRepository = injectRepository(User);
14
+ #subjectService = inject(SubjectService);
17
15
  #templateService = inject(NotificationTemplateService);
18
16
  async send(notification) {
19
- const user = await this.#userRepository.loadByQuery({ tenantId: notification.tenantId, id: notification.userId });
17
+ const user = await this.#subjectService.getUser(notification.tenantId, notification.userId);
20
18
  const content = await this.#templateService.render(notification, 'en'); // TODO: get user language
21
19
  await this.#mailService.send({
22
20
  to: user.email,
@@ -29,6 +27,6 @@ let EmailChannelProvider = class EmailChannelProvider {
29
27
  }
30
28
  };
31
29
  EmailChannelProvider = __decorate([
32
- NotificationSingleton()
30
+ Singleton()
33
31
  ], EmailChannelProvider);
34
32
  export { EmailChannelProvider };
@@ -1,7 +1,7 @@
1
- import { Transactional } from '../../../orm/server/index.js';
2
- import { NotificationLog } from '../../models/index.js';
3
- import type { ChannelProvider } from './channel-provider.js';
4
- export declare class InAppChannelProvider extends Transactional implements ChannelProvider {
1
+ import { type Transaction } from '../../../orm/server/index.js';
2
+ import { type NotificationLog } from '../../models/index.js';
3
+ import { ChannelProvider } from './channel-provider.js';
4
+ export declare class InAppChannelProvider extends ChannelProvider {
5
5
  #private;
6
- send(notification: NotificationLog): Promise<void>;
6
+ send(notification: NotificationLog, tx?: Transaction): Promise<void>;
7
7
  }
@@ -4,28 +4,27 @@ 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 { inject } from '../../../injector/index.js';
8
- import { injectRepository, Transactional } from '../../../orm/server/index.js';
9
- import { InAppNotification, NotificationLog } from '../../models/index.js';
10
- import { NotificationSingleton } from '../services/singleton.js';
7
+ import { inject, Singleton } from '../../../injector/index.js';
8
+ import { injectRepository } from '../../../orm/server/index.js';
9
+ import { InAppNotification, toInAppNotificationView } from '../../models/index.js';
11
10
  import { NotificationSseService } from '../services/notification-sse.service.js';
12
- let InAppChannelProvider = class InAppChannelProvider extends Transactional {
11
+ import { ChannelProvider } from './channel-provider.js';
12
+ let InAppChannelProvider = class InAppChannelProvider extends ChannelProvider {
13
13
  #inAppRepository = injectRepository(InAppNotification);
14
14
  #sseService = inject(NotificationSseService);
15
- async send(notification) {
16
- const inApp = await this.transaction(async (tx) => {
17
- return await this.#inAppRepository.withTransaction(tx).insert({
18
- tenantId: notification.tenantId,
19
- userId: notification.userId,
20
- logId: notification.id,
21
- readAt: null,
22
- archivedAt: null,
23
- });
15
+ async send(notification, tx) {
16
+ const inApp = await this.#inAppRepository.withOptionalTransaction(tx).insert({
17
+ tenantId: notification.tenantId,
18
+ userId: notification.userId,
19
+ logId: notification.id,
20
+ readTimestamp: null,
21
+ archiveTimestamp: null,
24
22
  });
25
- await this.#sseService.send({ ...inApp, log: notification });
23
+ const inAppNotificationView = toInAppNotificationView(inApp, notification);
24
+ await this.#sseService.send(inAppNotificationView);
26
25
  }
27
26
  };
28
27
  InAppChannelProvider = __decorate([
29
- NotificationSingleton()
28
+ Singleton()
30
29
  ], InAppChannelProvider);
31
30
  export { InAppChannelProvider };
@@ -1,4 +1,4 @@
1
+ export * from './channel-provider.js';
1
2
  export * from './email-channel-provider.js';
2
3
  export * from './in-app-channel-provider.js';
3
4
  export * from './web-push-channel-provider.js';
4
- export * from './channel-provider.js';
@@ -1,4 +1,4 @@
1
+ export * from './channel-provider.js';
1
2
  export * from './email-channel-provider.js';
2
3
  export * from './in-app-channel-provider.js';
3
4
  export * from './web-push-channel-provider.js';
4
- export * from './channel-provider.js';
@@ -1,6 +1,7 @@
1
- import { NotificationLog } from '../../models/index.js';
2
- import type { ChannelProvider } from './channel-provider.js';
3
- export declare class WebPushChannelProvider implements ChannelProvider {
1
+ import { type Transaction } from '../../../orm/server/index.js';
2
+ import { type NotificationLog } from '../../models/index.js';
3
+ import { ChannelProvider } from './channel-provider.js';
4
+ export declare class WebPushChannelProvider extends ChannelProvider {
4
5
  #private;
5
- send(notification: NotificationLog): Promise<void>;
6
+ send(notification: NotificationLog, tx?: Transaction): Promise<void>;
6
7
  }
@@ -4,15 +4,16 @@ 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 { Singleton } from '../../../injector/index.js';
7
8
  import { injectRepository } from '../../../orm/server/index.js';
8
- import { NotificationLog, WebPushSubscription } from '../../models/index.js';
9
- import { NotificationSingleton } from '../services/singleton.js';
10
- let WebPushChannelProvider = class WebPushChannelProvider {
9
+ import { WebPushSubscription } from '../../models/index.js';
10
+ import { ChannelProvider } from './channel-provider.js';
11
+ let WebPushChannelProvider = class WebPushChannelProvider extends ChannelProvider {
11
12
  #subscriptionRepository = injectRepository(WebPushSubscription);
12
- async send(notification) {
13
- const subscriptions = await this.#subscriptionRepository.loadManyByQuery({
13
+ async send(notification, tx) {
14
+ const subscriptions = await this.#subscriptionRepository.withOptionalTransaction(tx).loadManyByQuery({
14
15
  tenantId: notification.tenantId,
15
- userId: notification.userId
16
+ userId: notification.userId,
16
17
  });
17
18
  for (const subscription of subscriptions) {
18
19
  // TODO: Use a Web Push library (e.g. web-push) to send the notification
@@ -21,6 +22,6 @@ let WebPushChannelProvider = class WebPushChannelProvider {
21
22
  }
22
23
  };
23
24
  WebPushChannelProvider = __decorate([
24
- NotificationSingleton()
25
+ Singleton()
25
26
  ], WebPushChannelProvider);
26
27
  export { WebPushChannelProvider };
@@ -1,4 +1,4 @@
1
- import { InAppNotification, NotificationCategory, NotificationLog, NotificationPreference, WebPushSubscription } from '../models/index.js';
1
+ import { InAppNotification, NotificationLogEntity, NotificationPreference, NotificationType, WebPushSubscription } from '../models/index.js';
2
2
  export declare const notificationSchema: import("../../orm/server/index.js").DatabaseSchema<"notification">;
3
3
  export declare const notificationChannel: import("../../orm/enums.js").PgEnumFromEnumeration<{
4
4
  readonly InApp: "in-app";
@@ -19,7 +19,7 @@ export declare const notificationStatus: import("../../orm/enums.js").PgEnumFrom
19
19
  readonly Failed: "failed";
20
20
  }>;
21
21
  export declare const inAppNotification: import("../../orm/server/types.js").PgTableFromType<typeof InAppNotification, "notification">;
22
- export declare const notificationCategory: import("../../orm/server/types.js").PgTableFromType<typeof NotificationCategory, "notification">;
23
- export declare const notificationLog: import("../../orm/server/types.js").PgTableFromType<typeof NotificationLog, "notification">;
22
+ export declare const notificationType: import("../../orm/server/types.js").PgTableFromType<typeof NotificationType, "notification">;
23
+ export declare const notificationLog: import("../../orm/server/types.js").PgTableFromType<typeof NotificationLogEntity, "notification">;
24
24
  export declare const notificationPreference: import("../../orm/server/types.js").PgTableFromType<typeof NotificationPreference, "notification">;
25
25
  export declare const webPushSubscription: import("../../orm/server/types.js").PgTableFromType<typeof WebPushSubscription, "notification">;
@@ -1,12 +1,11 @@
1
1
  import { databaseSchema } from '../../orm/server/index.js';
2
- import { NotificationChannel, NotificationPriority, NotificationStatus } from '../enums.js';
3
- import { InAppNotification, NotificationCategory, NotificationLog, NotificationPreference, WebPushSubscription } from '../models/index.js';
2
+ import { InAppNotification, NotificationChannel, NotificationLogEntity, NotificationPreference, NotificationPriority, NotificationStatus, NotificationType, WebPushSubscription } from '../models/index.js';
4
3
  export const notificationSchema = databaseSchema('notification');
5
4
  export const notificationChannel = notificationSchema.getEnum(NotificationChannel);
6
5
  export const notificationPriority = notificationSchema.getEnum(NotificationPriority);
7
6
  export const notificationStatus = notificationSchema.getEnum(NotificationStatus);
8
7
  export const inAppNotification = notificationSchema.getTable(InAppNotification);
9
- export const notificationCategory = notificationSchema.getTable(NotificationCategory);
10
- export const notificationLog = notificationSchema.getTable(NotificationLog);
8
+ export const notificationType = notificationSchema.getTable(NotificationType);
9
+ export const notificationLog = notificationSchema.getTable(NotificationLogEntity);
11
10
  export const notificationPreference = notificationSchema.getTable(NotificationPreference);
12
11
  export const webPushSubscription = notificationSchema.getTable(WebPushSubscription);
@@ -1,8 +1,6 @@
1
- export * from './notification-category.service.js';
2
- export * from './notification-delivery.task.js';
3
1
  export * from './notification-delivery.worker.js';
4
2
  export * from './notification-sse.service.js';
5
- export * from './notification-template.service.js';
6
3
  export * from './notification-template.js';
4
+ export * from './notification-template.service.js';
5
+ export * from './notification-type.service.js';
7
6
  export * from './notification.service.js';
8
- export * from './singleton.js';
@@ -1,8 +1,6 @@
1
- export * from './notification-category.service.js';
2
- export * from './notification-delivery.task.js';
3
1
  export * from './notification-delivery.worker.js';
4
2
  export * from './notification-sse.service.js';
5
- export * from './notification-template.service.js';
6
3
  export * from './notification-template.js';
4
+ export * from './notification-template.service.js';
5
+ export * from './notification-type.service.js';
7
6
  export * from './notification.service.js';
8
- export * from './singleton.js';
@@ -1,8 +1,14 @@
1
1
  import type { CancellationSignal } from '../../../cancellation/token.js';
2
2
  import { Transactional } from '../../../orm/server/index.js';
3
+ import type { TaskDefinition, TaskDefinitionMap } from '../../../task-queue/index.js';
3
4
  import { TaskProcessResult } from '../../../task-queue/task-queue.js';
4
- import { NotificationChannel } from '../../enums.js';
5
+ import { NotificationChannel } from '../../models/index.js';
5
6
  import type { ChannelProvider } from '../providers/channel-provider.js';
7
+ export type NotificationTaskDefinitions = TaskDefinitionMap<{
8
+ 'notification/deliver': TaskDefinition<{
9
+ notificationId: string;
10
+ }>;
11
+ }>;
6
12
  export declare class NotificationDeliveryWorker extends Transactional {
7
13
  #private;
8
14
  registerProvider(channel: NotificationChannel, provider: ChannelProvider): void;
@@ -4,23 +4,23 @@ 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 { inject } from '../../../injector/index.js';
7
+ import { inject, Singleton } from '../../../injector/index.js';
8
8
  import { Logger } from '../../../logger/logger.js';
9
9
  import { injectRepository, Transactional } from '../../../orm/server/index.js';
10
10
  import { RateLimiter } from '../../../rate-limit/rate-limiter.js';
11
11
  import { TaskProcessResult, TaskQueue } from '../../../task-queue/task-queue.js';
12
- import { isDefined, isNotNull, isUndefined } from '../../../utils/type-guards.js';
13
- import { NotificationChannel, NotificationStatus } from '../../enums.js';
14
- import { InAppNotification, NotificationCategory, NotificationLog, NotificationPreference } from '../../models/index.js';
15
- import { NotificationSingleton } from './singleton.js';
12
+ import { isNotNull, isNotNullOrUndefined, isUndefined } from '../../../utils/type-guards.js';
13
+ import { InAppNotification, NotificationChannel, NotificationLogEntity, NotificationPreference, NotificationStatus, NotificationType } from '../../models/index.js';
14
+ import { NotificationConfiguration } from '../module.js';
16
15
  let NotificationDeliveryWorker = class NotificationDeliveryWorker extends Transactional {
17
- #notificationLogRepository = injectRepository(NotificationLog);
16
+ #notificationLogRepository = injectRepository(NotificationLogEntity);
18
17
  #inAppRepository = injectRepository(InAppNotification);
19
18
  #preferenceRepository = injectRepository(NotificationPreference);
20
- #categoryRepository = injectRepository(NotificationCategory);
19
+ #typeRepository = injectRepository(NotificationType);
21
20
  #taskQueue = inject((TaskQueue), 'notification');
22
21
  #rateLimiter = inject(RateLimiter, 'notification');
23
22
  #logger = inject(Logger, 'NotificationDeliveryWorker');
23
+ #configuration = inject(NotificationConfiguration, undefined, { optional: true });
24
24
  #providers = new Map();
25
25
  registerProvider(channel, provider) {
26
26
  this.#providers.set(channel, provider);
@@ -40,16 +40,19 @@ let NotificationDeliveryWorker = class NotificationDeliveryWorker extends Transa
40
40
  }
41
41
  async deliver(notificationId) {
42
42
  return await this.transaction(async (tx) => {
43
- const notification = await this.#notificationLogRepository.withTransaction(tx).loadByQuery({ id: notificationId });
44
- const category = await this.#categoryRepository.withTransaction(tx).loadByQuery({ id: notification.categoryId });
43
+ const notification = await this.#notificationLogRepository.withTransaction(tx).load(notificationId);
44
+ const type = await this.#typeRepository.withTransaction(tx).loadByQuery({ key: notification.type });
45
45
  const step = notification.currentStep;
46
46
  // 1. Throttling
47
- if (step == 0 && isNotNull(category.throttling)) {
48
- const resource = `${notification.tenantId}:${notification.userId}:${category.key}`;
49
- const acquired = await this.#rateLimiter.withTransaction(tx).tryAcquire(resource);
47
+ if (step == 0 && isNotNull(type.throttling)) {
48
+ const resource = `${notification.tenantId}:${notification.userId}:${type.key}`;
49
+ const acquired = await this.#rateLimiter.withTransaction(tx).tryAcquire(resource, 1, {
50
+ burstCapacity: type.throttling.limit,
51
+ refillInterval: type.throttling.interval,
52
+ });
50
53
  if (!acquired) {
51
- this.#logger.info(`Throttled notification ${notificationId} for user ${notification.userId} in category ${category.key}`);
52
- return TaskProcessResult.RescheduleBy(category.throttling.intervalMs / category.throttling.limit);
54
+ this.#logger.info(`Throttled notification ${notificationId} for user ${notification.userId} in category ${type.key}`);
55
+ return TaskProcessResult.RescheduleBy(type.throttling.interval / type.throttling.limit);
53
56
  }
54
57
  }
55
58
  // 2. Initial Delivery
@@ -57,52 +60,61 @@ let NotificationDeliveryWorker = class NotificationDeliveryWorker extends Transa
57
60
  const preferences = await this.#preferenceRepository.withTransaction(tx).loadManyByQuery({
58
61
  tenantId: notification.tenantId,
59
62
  userId: notification.userId,
60
- categoryId: notification.categoryId,
63
+ type: notification.type,
61
64
  });
62
- const enabledChannels = preferences.length > 0
63
- ? preferences.filter((p) => p.enabled).map((p) => p.channel)
64
- : [NotificationChannel.InApp];
65
+ const defaultChannels = this.#configuration?.defaultChannels ?? [NotificationChannel.InApp];
66
+ const preferenceMap = new Map(preferences.map((p) => [p.channel, p.enabled]));
67
+ const enabledChannels = new Set();
68
+ for (const channel of defaultChannels) {
69
+ if (preferenceMap.get(channel) !== false) {
70
+ enabledChannels.add(channel);
71
+ }
72
+ }
73
+ for (const preference of preferences) {
74
+ if (preference.enabled) {
75
+ enabledChannels.add(preference.channel);
76
+ }
77
+ }
65
78
  for (const channel of enabledChannels) {
66
- await this.sendToChannel(notification, channel);
79
+ // TODO: what if an error occurs here? partial delivery? Should we use tasks to allow retrying individual channels?
80
+ await this.sendToChannel(notification, channel, tx);
67
81
  }
68
- await this.#notificationLogRepository.withTransaction(tx).updateByQuery({ id: notificationId }, { status: NotificationStatus.Sent, currentStep: 1 });
69
- // Check for escalation
70
- if (isNotNull(category.escalations) && category.escalations.length > 0) {
71
- const firstEscalation = category.escalations[0];
72
- return TaskProcessResult.RescheduleBy(firstEscalation.delayMs);
82
+ await this.#notificationLogRepository.withTransaction(tx).update(notificationId, { status: NotificationStatus.Sent, currentStep: 1 });
83
+ if (isNotNull(type.escalations) && (type.escalations.length > 0)) {
84
+ const firstEscalation = type.escalations[0];
85
+ return TaskProcessResult.RescheduleBy(firstEscalation.delay);
73
86
  }
74
87
  return TaskProcessResult.Complete();
75
88
  }
76
89
  // 3. Escalation Steps
77
- if (isNotNull(category.escalations) && step > 0 && step <= category.escalations.length) {
78
- const rule = category.escalations[step - 1];
90
+ if (isNotNull(type.escalations) && (step > 0) && (step <= type.escalations.length)) {
91
+ const rule = type.escalations[step - 1];
79
92
  // Check if already read (for In-App escalation)
80
- const inApp = await this.#inAppRepository.withTransaction(tx).tryLoadByQuery({ logId: notificationId });
81
- if (isDefined(inApp?.readAt)) {
93
+ const inApp = await this.#inAppRepository.withTransaction(tx).tryLoadByQuery({ tenantId: notification.tenantId, userId: notification.userId, logId: notificationId });
94
+ if (isNotNullOrUndefined(inApp?.readTimestamp)) {
82
95
  this.#logger.debug(`Notification ${notificationId} already read, skipping escalation step ${step}`);
83
96
  return TaskProcessResult.Complete();
84
97
  }
85
- await this.sendToChannel(notification, rule.channel);
86
- const nextStep = step + 1;
87
- await this.#notificationLogRepository.withTransaction(tx).updateByQuery({ id: notificationId }, { currentStep: nextStep });
88
- if (step < category.escalations.length) {
89
- const nextRule = category.escalations[step];
90
- return TaskProcessResult.RescheduleBy(nextRule.delayMs);
98
+ await this.sendToChannel(notification, rule.channel, tx);
99
+ await this.#notificationLogRepository.withTransaction(tx).update(notificationId, { currentStep: step + 1 });
100
+ if (step < type.escalations.length) {
101
+ const nextRule = type.escalations[step];
102
+ return TaskProcessResult.RescheduleBy(nextRule.delay);
91
103
  }
92
104
  }
93
105
  return TaskProcessResult.Complete();
94
106
  });
95
107
  }
96
- async sendToChannel(notification, channel) {
108
+ async sendToChannel(notification, channel, tx) {
97
109
  const provider = this.#providers.get(channel);
98
110
  if (isUndefined(provider)) {
99
111
  this.#logger.warn(`No provider registered for channel ${channel}`);
100
112
  return;
101
113
  }
102
- await provider.send(notification);
114
+ await provider.send(notification, tx);
103
115
  }
104
116
  };
105
117
  NotificationDeliveryWorker = __decorate([
106
- NotificationSingleton()
118
+ Singleton()
107
119
  ], NotificationDeliveryWorker);
108
120
  export { NotificationDeliveryWorker };
@@ -1,13 +1,10 @@
1
+ import { afterResolve } from '../../../injector/index.js';
1
2
  import { ServerSentEventsSource } from '../../../sse/server-sent-events-source.js';
2
- import type { InAppNotification } from '../../models/index.js';
3
- type NotificationMessage = InAppNotification & {
4
- log?: any;
5
- };
3
+ import type { InAppNotificationView } from '../../models/index.js';
6
4
  export declare class NotificationSseService {
7
5
  #private;
8
- constructor();
6
+ [afterResolve](): void;
9
7
  register(tenantId: string, userId: string): ServerSentEventsSource;
10
- send(notification: NotificationMessage): Promise<void>;
8
+ send(notification: InAppNotificationView): Promise<void>;
11
9
  private dispatchToLocal;
12
10
  }
13
- export {};
@@ -4,22 +4,16 @@ 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
- var __metadata = (this && this.__metadata) || function (k, v) {
8
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
- };
10
- import { inject } from '../../../injector/index.js';
7
+ import { afterResolve, inject, Singleton } from '../../../injector/index.js';
11
8
  import { MessageBusProvider } from '../../../message-bus/message-bus-provider.js';
12
9
  import { toObservable } from '../../../signals/api.js';
13
10
  import { ServerSentEventsSource } from '../../../sse/server-sent-events-source.js';
14
- import { NotificationSingleton } from './singleton.js';
15
11
  let NotificationSseService = class NotificationSseService {
16
12
  #messageBusProvider = inject(MessageBusProvider);
17
13
  #messageBus = this.#messageBusProvider.get('notification');
18
14
  #sources = new Map(); // tenantId -> userId -> Sources
19
- constructor() {
20
- this.#messageBus.allMessages$.subscribe((message) => {
21
- this.dispatchToLocal(message);
22
- });
15
+ [afterResolve]() {
16
+ this.#messageBus.allMessages$.subscribe((message) => this.dispatchToLocal(message));
23
17
  }
24
18
  register(tenantId, userId) {
25
19
  const source = new ServerSentEventsSource();
@@ -68,7 +62,6 @@ let NotificationSseService = class NotificationSseService {
68
62
  }
69
63
  };
70
64
  NotificationSseService = __decorate([
71
- NotificationSingleton(),
72
- __metadata("design:paramtypes", [])
65
+ Singleton()
73
66
  ], NotificationSseService);
74
67
  export { NotificationSseService };
@@ -1,7 +1,7 @@
1
1
  import type { NotificationLog } from '../../models/index.js';
2
- export interface NotificationTemplate<T extends NotificationLog = NotificationLog> {
2
+ export declare abstract class NotificationTemplate<T extends NotificationLog = NotificationLog> {
3
3
  key: string;
4
- render(notification: T, language: string): Promise<NotificationContent>;
4
+ abstract render(notification: T, language: string): Promise<NotificationContent>;
5
5
  }
6
6
  export type NotificationContent = {
7
7
  title: string;
@@ -1 +1,3 @@
1
- export {};
1
+ export class NotificationTemplate {
2
+ key;
3
+ }
@@ -1,4 +1,4 @@
1
- import type { NotificationLog } from '../../models/index.js';
1
+ import { type NotificationLog } from '../../models/index.js';
2
2
  import type { NotificationContent, NotificationTemplate } from './notification-template.js';
3
3
  export declare class NotificationTemplateService {
4
4
  #private;
@@ -4,15 +4,19 @@ 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 { Singleton } from '../../../injector/index.js';
8
+ import { injectRepository } from '../../../orm/server/repository.js';
7
9
  import { isUndefined } from '../../../utils/type-guards.js';
8
- import { NotificationSingleton } from './singleton.js';
10
+ import { NotificationType } from '../../models/index.js';
9
11
  let NotificationTemplateService = class NotificationTemplateService {
12
+ #typeRepository = injectRepository(NotificationType);
10
13
  #templates = new Map();
11
14
  register(template) {
12
15
  this.#templates.set(template.key, template);
13
16
  }
14
17
  async render(notification, language = 'en') {
15
- const template = this.#templates.get(notification.type);
18
+ const type = await this.#typeRepository.loadByQuery({ key: notification.type });
19
+ const template = this.#templates.get(type.key);
16
20
  if (isUndefined(template)) {
17
21
  // Fallback for simple notifications without a specific template
18
22
  return {
@@ -24,6 +28,6 @@ let NotificationTemplateService = class NotificationTemplateService {
24
28
  }
25
29
  };
26
30
  NotificationTemplateService = __decorate([
27
- NotificationSingleton()
31
+ Singleton()
28
32
  ], NotificationTemplateService);
29
33
  export { NotificationTemplateService };
@@ -0,0 +1,11 @@
1
+ import { Transactional } from '../../../orm/server/index.js';
2
+ import { NotificationType, type EscalationRule, type ThrottlingConfig } from '../../models/index.js';
3
+ export type TypeInitializationData = {
4
+ label: string;
5
+ throttling?: ThrottlingConfig;
6
+ escalations?: EscalationRule[];
7
+ };
8
+ export declare class NotificationTypeService extends Transactional {
9
+ readonly repository: import("../../../orm/server/index.js").EntityRepository<NotificationType>;
10
+ initializeTypes<T extends string>(typeData: Record<T, TypeInitializationData>): Promise<Record<T, NotificationType>>;
11
+ }
@@ -0,0 +1,41 @@
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 { Singleton } from '../../../injector/index.js';
8
+ import { injectRepository, Transactional } from '../../../orm/server/index.js';
9
+ import { fromEntries, objectEntries } from '../../../utils/object/index.js';
10
+ import { assertDefinedPass, isUndefined } from '../../../utils/type-guards.js';
11
+ import { NotificationType } from '../../models/index.js';
12
+ let NotificationTypeService = class NotificationTypeService extends Transactional {
13
+ repository = injectRepository(NotificationType);
14
+ async initializeTypes(typeData) {
15
+ const typeEntries = objectEntries(typeData);
16
+ const typeMap = await this.transaction(async (tx) => {
17
+ const dbTypes = await this.repository.withTransaction(tx).loadAll();
18
+ const enumKeyTypeMap = new Map(dbTypes.map((type) => [type.key, type]));
19
+ for (const [key, data] of typeEntries) {
20
+ const type = enumKeyTypeMap.get(key);
21
+ const throttling = data.throttling ?? null;
22
+ const escalations = data.escalations ?? null;
23
+ if (isUndefined(type)) {
24
+ const newType = await this.repository.withTransaction(tx).insert({ key, label: data.label, throttling, escalations });
25
+ enumKeyTypeMap.set(key, newType);
26
+ }
27
+ else if ((type.label != data.label) || (JSON.stringify(type.throttling) != JSON.stringify(throttling)) || (JSON.stringify(type.escalations) != JSON.stringify(escalations))) {
28
+ const updatedType = await this.repository.withTransaction(tx).updateByQuery({ id: type.id }, { label: data.label, throttling, escalations });
29
+ enumKeyTypeMap.set(key, updatedType);
30
+ }
31
+ }
32
+ return enumKeyTypeMap;
33
+ });
34
+ const mappedTypes = typeEntries.map(([key]) => [key, assertDefinedPass(typeMap.get(key), 'Could not map notification type.')]);
35
+ return fromEntries(mappedTypes);
36
+ }
37
+ };
38
+ NotificationTypeService = __decorate([
39
+ Singleton()
40
+ ], NotificationTypeService);
41
+ export { NotificationTypeService };
@@ -1,6 +1,5 @@
1
1
  import { Transactional } from '../../../orm/server/index.js';
2
- import type { NotificationChannel } from '../../enums.js';
3
- import { NotificationLog, NotificationPreference } from '../../models/index.js';
2
+ import { NotificationPreference, type InAppNotificationView, type NotificationChannel, type NotificationLog } from '../../models/index.js';
4
3
  export declare class NotificationService extends Transactional {
5
4
  #private;
6
5
  send(notification: NotificationLog): Promise<void>;
@@ -8,10 +7,10 @@ export declare class NotificationService extends Transactional {
8
7
  limit?: number;
9
8
  offset?: number;
10
9
  includeArchived?: boolean;
11
- }): Promise<any[]>;
10
+ }): Promise<InAppNotificationView[]>;
12
11
  markRead(tenantId: string, userId: string, id: string): Promise<void>;
13
12
  archive(tenantId: string, userId: string, id: string): Promise<void>;
14
13
  getPreferences(tenantId: string, userId: string): Promise<NotificationPreference[]>;
15
- updatePreference(tenantId: string, userId: string, categoryId: string, channel: NotificationChannel, enabled: boolean): Promise<void>;
16
- registerWebPush(tenantId: string, userId: string, endpoint: string, p256dh: string, auth: string): Promise<void>;
14
+ updatePreference(tenantId: string, userId: string, type: string, channel: NotificationChannel, enabled: boolean): Promise<void>;
15
+ registerWebPush(tenantId: string, userId: string, endpoint: string, p256dh: Uint8Array<ArrayBuffer>, auth: Uint8Array<ArrayBuffer>): Promise<void>;
17
16
  }