@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.
- package/dist/api/handlers/notifications.js +77 -0
- package/dist/api/handlers/preferences.js +43 -0
- package/dist/api/index.d.ts +16 -0
- package/dist/api/index.js +21 -0
- package/dist/api/types.d.ts +22 -0
- package/dist/api/types.js +0 -0
- package/dist/channels/discord/channel.d.ts +18 -0
- package/dist/channels/discord/channel.js +15 -0
- package/dist/channels/discord/index.d.ts +3 -0
- package/dist/channels/discord/index.js +4 -0
- package/dist/channels/discord/message.d.ts +147 -0
- package/dist/channels/discord/message.js +176 -0
- package/dist/channels/discord/types.d.ts +52 -0
- package/dist/channels/discord/types.js +0 -0
- package/dist/channels/fcm/channel.d.ts +22 -0
- package/dist/channels/fcm/channel.js +44 -0
- package/dist/channels/fcm/index.d.ts +3 -0
- package/dist/channels/fcm/index.js +4 -0
- package/dist/channels/fcm/message.d.ts +64 -0
- package/dist/channels/fcm/message.js +122 -0
- package/dist/channels/fcm/types.d.ts +29 -0
- package/dist/channels/fcm/types.js +0 -0
- package/dist/channels/slack/channel.d.ts +18 -0
- package/dist/channels/slack/channel.js +15 -0
- package/dist/channels/slack/index.d.ts +3 -0
- package/dist/channels/slack/index.js +4 -0
- package/dist/channels/slack/message.d.ts +209 -0
- package/dist/channels/slack/message.js +390 -0
- package/dist/channels/slack/types.d.ts +7 -0
- package/dist/channels/slack/types.js +0 -0
- package/dist/channels/transmit/channel.d.ts +21 -0
- package/dist/channels/transmit/channel.js +27 -0
- package/dist/channels/transmit/index.d.ts +3 -0
- package/dist/channels/transmit/index.js +4 -0
- package/dist/channels/transmit/message.d.ts +11 -0
- package/dist/channels/transmit/message.js +17 -0
- package/dist/channels/transmit/types.d.ts +11 -0
- package/dist/channels/transmit/types.js +0 -0
- package/dist/channels/twilio/channel.d.ts +21 -0
- package/dist/channels/twilio/channel.js +56 -0
- package/dist/channels/twilio/index.d.ts +4 -0
- package/dist/channels/twilio/index.js +4 -0
- package/dist/channels/twilio/message.d.ts +86 -0
- package/dist/channels/twilio/message.js +152 -0
- package/dist/channels/twilio/types.d.ts +51 -0
- package/dist/channels/twilio/types.js +0 -0
- package/dist/channels/webhook/exceptions.d.ts +18 -0
- package/dist/channels/webhook/exceptions.js +24 -0
- package/dist/channels/webhook/index.d.ts +4 -0
- package/dist/channels/webhook/index.js +5 -0
- package/dist/channels/webhook/message.d.ts +24 -0
- package/dist/channels/webhook/message.js +40 -0
- package/dist/channels/webhook/provider.d.ts +19 -0
- package/dist/channels/webhook/provider.js +63 -0
- package/dist/channels/webhook/types.d.ts +15 -0
- package/dist/channels/webhook/types.js +0 -0
- package/dist/channels/webpush/channel.d.ts +26 -0
- package/dist/channels/webpush/channel.js +55 -0
- package/dist/channels/webpush/index.d.ts +3 -0
- package/dist/channels/webpush/index.js +4 -0
- package/dist/channels/webpush/message.d.ts +90 -0
- package/dist/channels/webpush/message.js +174 -0
- package/dist/channels/webpush/types.d.ts +50 -0
- package/dist/channels/webpush/types.js +0 -0
- package/dist/database/adapters/knex.d.ts +6 -0
- package/dist/database/adapters/knex.js +116 -0
- package/dist/database/adapters/kysely.d.ts +6 -0
- package/dist/database/adapters/kysely.js +101 -0
- package/dist/database/channel.d.ts +24 -0
- package/dist/database/channel.js +42 -0
- package/dist/database/database.d.ts +18 -0
- package/dist/database/database.js +89 -0
- package/dist/database/index.d.ts +3 -0
- package/dist/database/index.js +4 -0
- package/dist/database/message.d.ts +26 -0
- package/dist/database/message.js +51 -0
- package/dist/database/types.d.ts +147 -0
- package/dist/database/types.js +0 -0
- package/dist/debug.js +7 -0
- package/dist/errors/duplicate_notification_exception.d.ts +13 -0
- package/dist/errors/duplicate_notification_exception.js +21 -0
- package/dist/errors/http_error.d.ts +16 -0
- package/dist/errors/http_error.js +30 -0
- package/dist/errors/index.d.ts +38 -0
- package/dist/errors/index.js +39 -0
- package/dist/events/events.d.ts +90 -0
- package/dist/events/events.js +83 -0
- package/dist/facteur.d.ts +37 -0
- package/dist/facteur.js +83 -0
- package/dist/fake.d.ts +47 -0
- package/dist/fake.js +85 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +16 -0
- package/dist/notifications/channel_resolver.js +91 -0
- package/dist/notifications/notification_discoverer.d.ts +40 -0
- package/dist/notifications/notification_discoverer.js +113 -0
- package/dist/notifications/notification_sender.js +210 -0
- package/dist/options.d.ts +22 -0
- package/dist/options.js +57 -0
- package/dist/types/channel.d.ts +18 -0
- package/dist/types/channel.js +5 -0
- package/dist/types/events.d.ts +27 -0
- package/dist/types/extend.d.ts +25 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.js +4 -0
- package/dist/types/notifications.d.ts +60 -0
- package/dist/types/notifications.js +38 -0
- package/dist/types/options.d.ts +157 -0
- package/dist/types/preferences.d.ts +40 -0
- package/dist/types/queue.d.ts +11 -0
- 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 };
|
package/dist/index.d.ts
ADDED
|
@@ -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 };
|