@facteurjs/core 1.0.0-beta.0

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 (111) hide show
  1. package/dist/api/handlers/notifications.js +77 -0
  2. package/dist/api/handlers/preferences.js +43 -0
  3. package/dist/api/index.d.ts +16 -0
  4. package/dist/api/index.js +21 -0
  5. package/dist/api/types.d.ts +22 -0
  6. package/dist/api/types.js +0 -0
  7. package/dist/channels/discord/channel.d.ts +18 -0
  8. package/dist/channels/discord/channel.js +15 -0
  9. package/dist/channels/discord/index.d.ts +3 -0
  10. package/dist/channels/discord/index.js +4 -0
  11. package/dist/channels/discord/message.d.ts +147 -0
  12. package/dist/channels/discord/message.js +176 -0
  13. package/dist/channels/discord/types.d.ts +52 -0
  14. package/dist/channels/discord/types.js +0 -0
  15. package/dist/channels/fcm/channel.d.ts +22 -0
  16. package/dist/channels/fcm/channel.js +44 -0
  17. package/dist/channels/fcm/index.d.ts +3 -0
  18. package/dist/channels/fcm/index.js +4 -0
  19. package/dist/channels/fcm/message.d.ts +64 -0
  20. package/dist/channels/fcm/message.js +122 -0
  21. package/dist/channels/fcm/types.d.ts +29 -0
  22. package/dist/channels/fcm/types.js +0 -0
  23. package/dist/channels/slack/channel.d.ts +18 -0
  24. package/dist/channels/slack/channel.js +15 -0
  25. package/dist/channels/slack/index.d.ts +3 -0
  26. package/dist/channels/slack/index.js +4 -0
  27. package/dist/channels/slack/message.d.ts +209 -0
  28. package/dist/channels/slack/message.js +390 -0
  29. package/dist/channels/slack/types.d.ts +7 -0
  30. package/dist/channels/slack/types.js +0 -0
  31. package/dist/channels/transmit/channel.d.ts +21 -0
  32. package/dist/channels/transmit/channel.js +27 -0
  33. package/dist/channels/transmit/index.d.ts +3 -0
  34. package/dist/channels/transmit/index.js +4 -0
  35. package/dist/channels/transmit/message.d.ts +11 -0
  36. package/dist/channels/transmit/message.js +17 -0
  37. package/dist/channels/transmit/types.d.ts +11 -0
  38. package/dist/channels/transmit/types.js +0 -0
  39. package/dist/channels/twilio/channel.d.ts +21 -0
  40. package/dist/channels/twilio/channel.js +56 -0
  41. package/dist/channels/twilio/index.d.ts +4 -0
  42. package/dist/channels/twilio/index.js +4 -0
  43. package/dist/channels/twilio/message.d.ts +86 -0
  44. package/dist/channels/twilio/message.js +152 -0
  45. package/dist/channels/twilio/types.d.ts +51 -0
  46. package/dist/channels/twilio/types.js +0 -0
  47. package/dist/channels/webhook/exceptions.d.ts +18 -0
  48. package/dist/channels/webhook/exceptions.js +24 -0
  49. package/dist/channels/webhook/index.d.ts +4 -0
  50. package/dist/channels/webhook/index.js +5 -0
  51. package/dist/channels/webhook/message.d.ts +24 -0
  52. package/dist/channels/webhook/message.js +40 -0
  53. package/dist/channels/webhook/provider.d.ts +19 -0
  54. package/dist/channels/webhook/provider.js +63 -0
  55. package/dist/channels/webhook/types.d.ts +15 -0
  56. package/dist/channels/webhook/types.js +0 -0
  57. package/dist/channels/webpush/channel.d.ts +26 -0
  58. package/dist/channels/webpush/channel.js +55 -0
  59. package/dist/channels/webpush/index.d.ts +3 -0
  60. package/dist/channels/webpush/index.js +4 -0
  61. package/dist/channels/webpush/message.d.ts +90 -0
  62. package/dist/channels/webpush/message.js +174 -0
  63. package/dist/channels/webpush/types.d.ts +50 -0
  64. package/dist/channels/webpush/types.js +0 -0
  65. package/dist/database/adapters/knex.d.ts +6 -0
  66. package/dist/database/adapters/knex.js +116 -0
  67. package/dist/database/adapters/kysely.d.ts +6 -0
  68. package/dist/database/adapters/kysely.js +101 -0
  69. package/dist/database/channel.d.ts +24 -0
  70. package/dist/database/channel.js +42 -0
  71. package/dist/database/database.d.ts +18 -0
  72. package/dist/database/database.js +89 -0
  73. package/dist/database/index.d.ts +3 -0
  74. package/dist/database/index.js +4 -0
  75. package/dist/database/message.d.ts +26 -0
  76. package/dist/database/message.js +51 -0
  77. package/dist/database/types.d.ts +147 -0
  78. package/dist/database/types.js +0 -0
  79. package/dist/debug.js +7 -0
  80. package/dist/errors/duplicate_notification_exception.d.ts +13 -0
  81. package/dist/errors/duplicate_notification_exception.js +21 -0
  82. package/dist/errors/http_error.d.ts +16 -0
  83. package/dist/errors/http_error.js +30 -0
  84. package/dist/errors/index.d.ts +38 -0
  85. package/dist/errors/index.js +39 -0
  86. package/dist/events/events.d.ts +90 -0
  87. package/dist/events/events.js +83 -0
  88. package/dist/facteur.d.ts +37 -0
  89. package/dist/facteur.js +83 -0
  90. package/dist/fake.d.ts +47 -0
  91. package/dist/fake.js +85 -0
  92. package/dist/index.d.ts +15 -0
  93. package/dist/index.js +16 -0
  94. package/dist/notifications/channel_resolver.js +91 -0
  95. package/dist/notifications/notification_discoverer.d.ts +40 -0
  96. package/dist/notifications/notification_discoverer.js +113 -0
  97. package/dist/notifications/notification_sender.js +210 -0
  98. package/dist/options.d.ts +22 -0
  99. package/dist/options.js +57 -0
  100. package/dist/types/channel.d.ts +18 -0
  101. package/dist/types/channel.js +5 -0
  102. package/dist/types/events.d.ts +27 -0
  103. package/dist/types/extend.d.ts +25 -0
  104. package/dist/types/index.d.ts +8 -0
  105. package/dist/types/index.js +4 -0
  106. package/dist/types/notifications.d.ts +60 -0
  107. package/dist/types/notifications.js +38 -0
  108. package/dist/types/options.d.ts +157 -0
  109. package/dist/types/preferences.d.ts +40 -0
  110. package/dist/types/queue.d.ts +11 -0
  111. package/package.json +65 -0
package/dist/fake.d.ts ADDED
@@ -0,0 +1,47 @@
1
+ import { SendOptions } from "./types/options.js";
2
+ import { Notification, NotificationSendResult } from "./types/notifications.js";
3
+
4
+ //#region src/fake.d.ts
5
+ interface SentNotification<N extends Notification = Notification> {
6
+ notification: N;
7
+ notifiable: N extends Notification<infer TNotifiable, any> ? TNotifiable : never;
8
+ params?: N extends Notification<any, infer P> ? P : never;
9
+ via?: any;
10
+ }
11
+ declare class FacteurFake {
12
+ #private;
13
+ /**
14
+ * Record a notification as sent during fake mode
15
+ */
16
+ recordSent(options: SendOptions<any>): NotificationSendResult;
17
+ /**
18
+ * Assert a total of expected number of notifications were sent
19
+ */
20
+ assertSentCount(count: number): void;
21
+ /**
22
+ * Assert the mentioned notification was sent for expected number of times
23
+ */
24
+ assertSentCount<N extends Notification>(notificationClass: new (...args: any[]) => N, count: number): void;
25
+ /**
26
+ * Assert zero notifications were sent
27
+ */
28
+ assertNoneSent(): void;
29
+ /**
30
+ * Returns a list of sent notifications captured by the fake
31
+ */
32
+ sent(): SentNotification[];
33
+ /**
34
+ * Returns a list of sent notifications of a specific type captured by the fake
35
+ */
36
+ sent<N extends Notification>(notificationClass: new (...args: any[]) => N): SentNotification<N>[];
37
+ /**
38
+ * Assert the mentioned notification was sent during the fake mode
39
+ */
40
+ assertSent<N extends Notification>(notificationClass: new (...args: any[]) => N, callback?: (sentNotification: SentNotification<N>) => void): void;
41
+ /**
42
+ * Clear all sent notifications from the fake
43
+ */
44
+ clear(): void;
45
+ }
46
+ //#endregion
47
+ export { FacteurFake };
package/dist/fake.js ADDED
@@ -0,0 +1,85 @@
1
+ import { AssertionError } from "node:assert";
2
+
3
+ //#region src/fake.ts
4
+ var FacteurFake = class {
5
+ #sentNotifications = [];
6
+ /**
7
+ * Record a notification as sent during fake mode
8
+ */
9
+ recordSent(options) {
10
+ const notification = new options.notification();
11
+ this.#sentNotifications.push({
12
+ notification,
13
+ notifiable: options.notifiable,
14
+ params: options.params,
15
+ via: options.via
16
+ });
17
+ return {
18
+ success: 1,
19
+ failed: 0,
20
+ results: [{
21
+ channel: "fake",
22
+ status: "success"
23
+ }]
24
+ };
25
+ }
26
+ assertSentCount(notificationClassOrCount, count) {
27
+ if (typeof notificationClassOrCount === "number") {
28
+ const totalCount = notificationClassOrCount;
29
+ if (this.#sentNotifications.length !== totalCount) throw new AssertionError({
30
+ message: `Expected ${totalCount} notifications to be sent, but ${this.#sentNotifications.length} were sent`,
31
+ actual: this.#sentNotifications.length,
32
+ expected: totalCount
33
+ });
34
+ return;
35
+ }
36
+ const notificationClass = notificationClassOrCount;
37
+ const actualCount = this.#sentNotifications.filter((sent) => sent.notification instanceof notificationClass).length;
38
+ if (actualCount !== count) throw new AssertionError({
39
+ message: `Expected ${count} notifications of type ${notificationClass.name} to be sent, but ${actualCount} were sent`,
40
+ actual: actualCount,
41
+ expected: count
42
+ });
43
+ }
44
+ /**
45
+ * Assert zero notifications were sent
46
+ */
47
+ assertNoneSent() {
48
+ if (this.#sentNotifications.length > 0) throw new AssertionError({
49
+ message: `Expected no notifications to be sent, but ${this.#sentNotifications.length} were sent`,
50
+ actual: this.#sentNotifications.length,
51
+ expected: 0
52
+ });
53
+ }
54
+ sent(notificationClass) {
55
+ if (!notificationClass) return this.#sentNotifications;
56
+ return this.#sentNotifications.filter((sent) => sent.notification instanceof notificationClass);
57
+ }
58
+ /**
59
+ * Assert the mentioned notification was sent during the fake mode
60
+ */
61
+ assertSent(notificationClass, callback) {
62
+ const sentNotifications = this.sent(notificationClass);
63
+ if (sentNotifications.length === 0) throw new AssertionError({ message: `Expected notification "${notificationClass.name}" was not sent` });
64
+ if (callback) {
65
+ const found = sentNotifications.some((sent) => {
66
+ try {
67
+ callback(sent);
68
+ return true;
69
+ } catch {
70
+ return false;
71
+ }
72
+ });
73
+ if (!found) throw new AssertionError({ message: `No notifications of type ${notificationClass.name} matched the given callback` });
74
+ }
75
+ }
76
+ /**
77
+ * Clear all sent notifications from the fake
78
+ */
79
+ clear() {
80
+ this.#sentNotifications = [];
81
+ }
82
+ };
83
+
84
+ //#endregion
85
+ export { FacteurFake };
@@ -0,0 +1,15 @@
1
+ import { Channel } from "./types/channel.js";
2
+ import { Facteur, createFacteur } from "./facteur.js";
3
+ import { E_MISSING_MESSAGE_METHOD, E_QUEUE_NOT_SET, E_SEND_NOTIFICATION_FAILED, E_UNAVAILABLE_TARGETS, errors } from "./errors/index.js";
4
+
5
+ //#region src/index.d.ts
6
+
7
+ /**
8
+ * Define a new provider
9
+ */
10
+ declare function defineProvider<Name, Options, Message, Response, Targets>(name: Name, factory: (options: Options) => Channel<Options, Message, Response, Targets>): (options: Options) => {
11
+ name: Name;
12
+ provider: Channel<Options, Message, Response, Targets>;
13
+ };
14
+ //#endregion
15
+ export { E_MISSING_MESSAGE_METHOD, E_QUEUE_NOT_SET, E_SEND_NOTIFICATION_FAILED, E_UNAVAILABLE_TARGETS, Facteur, createFacteur, defineProvider, errors };
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ import { E_MISSING_MESSAGE_METHOD, E_QUEUE_NOT_SET, E_SEND_NOTIFICATION_FAILED, E_UNAVAILABLE_TARGETS, errors } from "./errors/index.js";
2
+ import { Facteur, createFacteur } from "./facteur.js";
3
+
4
+ //#region src/index.ts
5
+ /**
6
+ * Define a new provider
7
+ */
8
+ function defineProvider(name, factory) {
9
+ return (options) => ({
10
+ name,
11
+ provider: factory(options)
12
+ });
13
+ }
14
+
15
+ //#endregion
16
+ export { E_MISSING_MESSAGE_METHOD, E_QUEUE_NOT_SET, E_SEND_NOTIFICATION_FAILED, E_UNAVAILABLE_TARGETS, Facteur, createFacteur, defineProvider, errors };
@@ -0,0 +1,91 @@
1
+ import { invoke } from "@julr/utils/functions";
2
+ import { mapEntries } from "@julr/utils/object";
3
+ import { is } from "@julr/utils/is";
4
+
5
+ //#region src/notifications/channel_resolver.ts
6
+ var ChannelResolver = class {
7
+ #database = null;
8
+ constructor(database) {
9
+ this.#database = database || null;
10
+ }
11
+ /**
12
+ * Resolve channels and targets provided by via
13
+ */
14
+ #resolveVia(options) {
15
+ const notifiableTargets = options.notifiable?.notificationTargets?.();
16
+ return mapEntries(options.via, (channelName, channelConfig) => {
17
+ const shouldSend = typeof channelConfig === "boolean" ? channelConfig : true;
18
+ const target = invoke(() => {
19
+ if (typeof channelConfig === "object") return channelConfig;
20
+ return notifiableTargets?.[channelName] || null;
21
+ });
22
+ return [channelName, {
23
+ shouldSend,
24
+ target
25
+ }];
26
+ });
27
+ }
28
+ async resolveChannels(options) {
29
+ const { notification, notifiable, params, via, tenantId } = options;
30
+ const notificationOptions = notification.options;
31
+ const notificationIdentifier = notificationOptions.identifier || notification.name;
32
+ const notifiableTargets = options.notifiable?.notificationTargets?.();
33
+ /**
34
+ * First, if via is provided it should override everything.
35
+ */
36
+ if (via) return this.#resolveVia(options);
37
+ /**
38
+ * Get preferences for the notifiable and tenant
39
+ */
40
+ const preferences = await this.#database?.getPreferences({
41
+ notifiableId: notifiable.id,
42
+ tenantId
43
+ });
44
+ /**
45
+ * Resolve channels based on deliverBy options
46
+ */
47
+ const fromDeliverBy = mapEntries(notificationOptions.deliverBy, (channelName, deliverBy) => {
48
+ const shouldSend = invoke(() => {
49
+ if (typeof deliverBy === "boolean") return deliverBy;
50
+ return deliverBy.if({
51
+ notifiable,
52
+ params,
53
+ preferences
54
+ });
55
+ });
56
+ const target = notifiableTargets?.[channelName] || null;
57
+ return [channelName, {
58
+ shouldSend,
59
+ target
60
+ }];
61
+ });
62
+ /**
63
+ * And then we can apply user preferences
64
+ */
65
+ const currentTenant = preferences?.tenants?.[tenantId || -1];
66
+ const tenantPreferences = currentTenant?.global;
67
+ const globalPreferences = preferences?.global.global;
68
+ const notificationTenantPreference = currentTenant?.notifications?.find(({ notification: notification$1 }) => notification$1.identifier === notificationIdentifier);
69
+ const notificationGlobalPreference = preferences?.global.notifications.find(({ notification: notification$1 }) => notification$1.identifier === notificationIdentifier);
70
+ return mapEntries(fromDeliverBy, (channelName, { shouldSend, target }) => {
71
+ if (shouldSend === false) return [channelName, {
72
+ shouldSend: false,
73
+ target
74
+ }];
75
+ const preferencesSources = [
76
+ notificationTenantPreference?.channels[channelName],
77
+ tenantPreferences?.channels[channelName],
78
+ notificationGlobalPreference?.channels[channelName],
79
+ globalPreferences?.channels[channelName]
80
+ ];
81
+ shouldSend = preferencesSources.find((preference) => !is.undefined(preference)) ?? shouldSend;
82
+ return [channelName, {
83
+ shouldSend,
84
+ target
85
+ }];
86
+ });
87
+ }
88
+ };
89
+
90
+ //#endregion
91
+ export { ChannelResolver };
@@ -0,0 +1,40 @@
1
+ import { Notification } from "../types/notifications.js";
2
+
3
+ //#region src/notifications/notification_discoverer.d.ts
4
+ interface NotificationDiscovererConfig {
5
+ /**
6
+ * The root directory to search for notifications
7
+ */
8
+ searchDirectory: URL;
9
+ /**
10
+ * The file suffix pattern for notification files
11
+ * @default '_notification'
12
+ */
13
+ fileSuffix?: string | undefined;
14
+ }
15
+ /**
16
+ * Discover and load notification classes from the application directory.
17
+ * Ensure that all notification names are unique.
18
+ */
19
+ declare class NotificationDiscoverer {
20
+ #private;
21
+ constructor(config: NotificationDiscovererConfig);
22
+ getNotifications(): Promise<Array<new (...args: any[]) => Notification>>;
23
+ discoverNotifications(): Promise<(new (...args: any[]) => Notification)[]>;
24
+ getAllNotificationTags(): Promise<string[]>;
25
+ /**
26
+ * Get notification identities with both display name and class identifier
27
+ */
28
+ getNotificationIdentities(): Promise<Array<{
29
+ name: string;
30
+ identifier: string;
31
+ tags?: string[];
32
+ category?: string;
33
+ }>>;
34
+ /**
35
+ * Clear the cache of discovered notifications
36
+ */
37
+ clearCache(): void;
38
+ }
39
+ //#endregion
40
+ export { NotificationDiscoverer };
@@ -0,0 +1,113 @@
1
+ import { errors } from "../errors/index.js";
2
+ import { Notification } from "../types/notifications.js";
3
+ import "../types/index.js";
4
+ import { fileURLToPath } from "node:url";
5
+ import { fsReadAll, isScriptFile } from "@poppinss/utils";
6
+
7
+ //#region src/notifications/notification_discoverer.ts
8
+ /**
9
+ * Discover and load notification classes from the application directory.
10
+ * Ensure that all notification names are unique.
11
+ */
12
+ var NotificationDiscoverer = class {
13
+ #config;
14
+ #cachedNotifications = null;
15
+ constructor(config) {
16
+ this.#config = {
17
+ fileSuffix: config.fileSuffix || "_notification",
18
+ searchDirectory: config.searchDirectory
19
+ };
20
+ }
21
+ /**
22
+ * Import notification files from the configured directory
23
+ */
24
+ async #importNotifications() {
25
+ const searchDir = this.#config.searchDirectory;
26
+ const notificationFiles = await fsReadAll(searchDir, {
27
+ pathType: "url",
28
+ ignoreMissingRoot: true,
29
+ filter: (file) => {
30
+ const isScript = isScriptFile(file);
31
+ if (!isScript) return false;
32
+ const isNodeModule = fileURLToPath(file).includes("/node_modules/");
33
+ if (isNodeModule) return false;
34
+ const fileName = file.toString().split("/").pop() || "";
35
+ return fileName.endsWith(`${this.#config.fileSuffix}.ts`) || fileName.endsWith(`${this.#config.fileSuffix}.js`);
36
+ }
37
+ });
38
+ return this.#importNotificationsFromFiles(notificationFiles);
39
+ }
40
+ /**
41
+ * Import notification classes from an array of file paths
42
+ */
43
+ async #importNotificationsFromFiles(files) {
44
+ const promises = files.map(async (file) => {
45
+ const i = await import(file.toString());
46
+ if (!i.default) return {
47
+ notification: null,
48
+ file
49
+ };
50
+ return {
51
+ notification: i.default,
52
+ file
53
+ };
54
+ });
55
+ return Promise.all(promises);
56
+ }
57
+ /**
58
+ * Validate that all notification names are unique and throw if duplicates are found
59
+ */
60
+ #validateUniqueNotificationNames(notifications) {
61
+ const notificationsByName = Object.groupBy(notifications, (i) => i.notification.name);
62
+ const duplicates = Object.entries(notificationsByName).filter(([_, notifications$1]) => (notifications$1?.length || 0) > 1).map(([notificationName, notifications$1]) => ({
63
+ notificationName,
64
+ notifications: notifications$1
65
+ }));
66
+ if (duplicates.length > 0) throw new errors.E_DUPLICATE_NOTIFICATION(this.#config.searchDirectory, duplicates);
67
+ }
68
+ async getNotifications() {
69
+ await this.#ensureNotificationsDiscovered();
70
+ return this.#cachedNotifications || [];
71
+ }
72
+ async discoverNotifications() {
73
+ if (this.#cachedNotifications !== null) return this.#cachedNotifications;
74
+ const notifications = await this.#importNotifications();
75
+ const validNotifications = notifications.filter((i) => i.notification && typeof i.notification === "function").filter((i) => i.notification.prototype instanceof Notification);
76
+ this.#validateUniqueNotificationNames(validNotifications);
77
+ this.#cachedNotifications = validNotifications.map(({ notification }) => notification);
78
+ return this.#cachedNotifications;
79
+ }
80
+ async #ensureNotificationsDiscovered() {
81
+ if (this.#cachedNotifications !== null) return;
82
+ throw new Error(`Notifications have not been discovered yet. Call 'discoverAndLoadNotifications' first.`);
83
+ }
84
+ async getAllNotificationTags() {
85
+ await this.#ensureNotificationsDiscovered();
86
+ const notifications = this.#cachedNotifications;
87
+ return notifications.map((notification) => notification.name);
88
+ }
89
+ /**
90
+ * Get notification identities with both display name and class identifier
91
+ */
92
+ async getNotificationIdentities() {
93
+ const notifications = await this.getNotifications();
94
+ return notifications.map((NotificationClass) => {
95
+ const options = NotificationClass.options || {};
96
+ return {
97
+ name: options.name,
98
+ identifier: NotificationClass.name || options.identifier,
99
+ tags: options.tags || [],
100
+ category: options.category
101
+ };
102
+ });
103
+ }
104
+ /**
105
+ * Clear the cache of discovered notifications
106
+ */
107
+ clearCache() {
108
+ this.#cachedNotifications = null;
109
+ }
110
+ };
111
+
112
+ //#endregion
113
+ export { NotificationDiscoverer };
@@ -0,0 +1,210 @@
1
+ import { errors } from "../errors/index.js";
2
+ import debug_default from "../debug.js";
3
+ import { facteurEvents } from "../events/events.js";
4
+ import "./channel_resolver.js";
5
+ import { capitalize } from "@julr/utils/string";
6
+
7
+ //#region src/notifications/notification_sender.ts
8
+ /**
9
+ * Responsible for sending notifications and messages
10
+ */
11
+ var NotificationSender = class {
12
+ constructor(channels, channelResolver, emitter) {
13
+ this.channels = channels;
14
+ this.channelResolver = channelResolver;
15
+ this.emitter = emitter;
16
+ }
17
+ /**
18
+ * Emit notification sending event
19
+ */
20
+ #emitNotificationSending(notification, resolvedChannels) {
21
+ const event = facteurEvents.notificationSending({
22
+ notification,
23
+ resolvedChannels
24
+ });
25
+ this.emitter.emit(event.name, event.data);
26
+ }
27
+ /**
28
+ * Emit notification sent event
29
+ */
30
+ #emitNotificationSent(notification, results) {
31
+ const event = facteurEvents.notificationSent({
32
+ notification,
33
+ results
34
+ });
35
+ this.emitter.emit(event.name, event.data);
36
+ }
37
+ /**
38
+ * Emit notification failed event
39
+ */
40
+ #emitNotificationFailed(notification, errors$1) {
41
+ const event = facteurEvents.notificationFailed({
42
+ notification,
43
+ errors: errors$1
44
+ });
45
+ this.emitter.emit(event.name, event.data);
46
+ }
47
+ /**
48
+ * Emit message sending event
49
+ */
50
+ #emitMessageSending(notification, channelName, message) {
51
+ const event = facteurEvents.messageSending({
52
+ notification,
53
+ channelName,
54
+ message
55
+ });
56
+ this.emitter.emit(event.name, event.data);
57
+ }
58
+ /**
59
+ * Emit message sent event
60
+ */
61
+ #emitMessageSent(notification, channelName, message) {
62
+ const event = facteurEvents.messageSent({
63
+ notification,
64
+ channelName,
65
+ message
66
+ });
67
+ this.emitter.emit(event.name, event.data);
68
+ }
69
+ /**
70
+ * Emit message failed event
71
+ */
72
+ #emitMessageFailed(notification, channelName, message, error) {
73
+ const event = facteurEvents.messageFailed({
74
+ notification,
75
+ channelName,
76
+ message,
77
+ error
78
+ });
79
+ this.emitter.emit(event.name, event.data);
80
+ }
81
+ /**
82
+ * Get a channel by its name
83
+ */
84
+ #getChannel(channelName) {
85
+ const channel = this.channels[channelName];
86
+ if (!channel) throw new Error(`Channel '${channelName}' is not registered`);
87
+ return channel;
88
+ }
89
+ /**
90
+ * Send a single message through a specific channel
91
+ */
92
+ async #sendMessage(options) {
93
+ const { channelName, options: sendOptions, channelConfig } = options;
94
+ /**
95
+ * First build the message content using the notification's
96
+ * `as<ChannelName>Message` method
97
+ */
98
+ const channel = this.#getChannel(channelName);
99
+ const channelMethodName = `as${capitalize(channelName)}Message`;
100
+ const messageBuilder = options.notification[channelMethodName];
101
+ if (typeof messageBuilder !== "function") throw new errors.E_MISSING_MESSAGE_METHOD([capitalize(channelName)]);
102
+ const messageContent = messageBuilder.call(options.notification, {
103
+ notifiable: sendOptions.notifiable,
104
+ params: sendOptions.params,
105
+ tenantId: sendOptions.tenantId
106
+ });
107
+ if (!messageContent) return null;
108
+ /**
109
+ * Send the message and emit appropriate events
110
+ */
111
+ debug_default(`Sending message via ${channelName}: %O`, messageContent);
112
+ this.#emitMessageSending(options.notification, channelName, messageContent);
113
+ try {
114
+ await channel.send({
115
+ tenantId: sendOptions.tenantId,
116
+ message: messageContent,
117
+ targets: channelConfig.target,
118
+ notifiable: sendOptions.notifiable
119
+ });
120
+ debug_default(`Message sent via ${channelName}`);
121
+ this.#emitMessageSent(options.notification, channelName, messageContent);
122
+ return {
123
+ channel: channelName,
124
+ status: "success"
125
+ };
126
+ } catch (error) {
127
+ this.#emitMessageFailed(options.notification, channelName, messageContent, error);
128
+ throw error;
129
+ }
130
+ }
131
+ /**
132
+ * Process results from sending messages and emit appropriate events
133
+ */
134
+ async #processResults(options) {
135
+ const { results, throwOnError, notification } = options;
136
+ const channelResults = results.filter((result) => result !== null);
137
+ const successes = channelResults.filter((r) => r.status === "success");
138
+ const failures = channelResults.filter((r) => r.status === "failed");
139
+ /**
140
+ * Everything succeeded
141
+ */
142
+ if (!failures.length) {
143
+ this.#emitNotificationSent(notification, channelResults);
144
+ await notification.afterSend();
145
+ return {
146
+ failed: failures.length,
147
+ success: successes.length,
148
+ results: channelResults
149
+ };
150
+ }
151
+ /**
152
+ * Some channels failed
153
+ */
154
+ const failureReasons = failures.map((r) => r.error);
155
+ this.#emitNotificationFailed(notification, failureReasons);
156
+ await notification.afterSend();
157
+ if (throwOnError !== false) throw new errors.E_SEND_NOTIFICATION_FAILED(failureReasons);
158
+ return {
159
+ failed: failures.length,
160
+ success: successes.length,
161
+ results: channelResults
162
+ };
163
+ }
164
+ /**
165
+ * Send a notification through all resolved channels
166
+ */
167
+ async send(options, notification) {
168
+ const { via, params, tenantId } = options;
169
+ const notifiable = "notifiable" in options ? options.notifiable : void 0;
170
+ const resolvedChannels = await this.channelResolver.resolveChannels({
171
+ notifiable,
172
+ params,
173
+ tenantId,
174
+ notification: options.notification,
175
+ ...via ? { via } : {}
176
+ });
177
+ debug_default(`Resolved channels: %O`, resolvedChannels);
178
+ this.#emitNotificationSending(notification, resolvedChannels);
179
+ /**
180
+ * Send messages for each resolved channel
181
+ */
182
+ const promises = Object.entries(resolvedChannels).map(async ([name, config]) => {
183
+ if (!config.shouldSend || !config.target) return null;
184
+ return await this.#sendMessage({
185
+ notification,
186
+ channelConfig: config,
187
+ channelName: name,
188
+ options
189
+ }).catch((error) => {
190
+ debug_default(`Failed to send notification via ${name}: %O`, error);
191
+ return {
192
+ channel: name,
193
+ status: "failed",
194
+ error
195
+ };
196
+ });
197
+ });
198
+ /**
199
+ * Process results and emit appropriate events
200
+ */
201
+ return await this.#processResults({
202
+ notification,
203
+ results: await Promise.all(promises),
204
+ throwOnError: options.throwOnError !== false
205
+ });
206
+ }
207
+ };
208
+
209
+ //#endregion
210
+ export { NotificationSender };
@@ -0,0 +1,22 @@
1
+ import { DatabaseAdapter } from "./database/types.js";
2
+ import { Channel } from "./types/channel.js";
3
+ import { Emitter } from "./types/events.js";
4
+ import { QueueAdapter } from "./types/queue.js";
5
+ import { ResolvedDefaultPreferences } from "./types/preferences.js";
6
+ import { FacteurConfiguration, NotificationResolver } from "./types/options.js";
7
+ import { Logger } from "@julr/utils/logger";
8
+
9
+ //#region src/options.d.ts
10
+ declare class FacteurOptions<KnownChannels extends Record<string, Channel>, DBAdapter extends DatabaseAdapter | null = null> {
11
+ #private;
12
+ logger: Logger;
13
+ emitter: Emitter;
14
+ channels: KnownChannels;
15
+ queueAdapter: QueueAdapter;
16
+ databaseAdapter: DBAdapter | null;
17
+ notificationResolver: NotificationResolver;
18
+ readonly defaultPreferences: ResolvedDefaultPreferences<KnownChannels>;
19
+ constructor(config: FacteurConfiguration<KnownChannels, DBAdapter>);
20
+ }
21
+ //#endregion
22
+ export { FacteurOptions };