@facteurjs/core 1.0.0-beta.4 → 2.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 (189) hide show
  1. package/README.md +27 -0
  2. package/dist/api/handlers/{notifications.js → notifications.mjs} +39 -16
  3. package/dist/api/handlers/preferences.mjs +73 -0
  4. package/dist/api/handlers/utils.mjs +23 -0
  5. package/dist/api/types.d.mts +52 -0
  6. package/dist/api/types.mjs +1 -0
  7. package/dist/api.d.mts +22 -0
  8. package/dist/api.mjs +25 -0
  9. package/dist/channels/aws-sns/{channel.d.ts → channel.d.mts} +3 -3
  10. package/dist/channels/aws-sns/{channel.js → channel.mjs} +2 -3
  11. package/dist/channels/aws-sns/types.mjs +1 -0
  12. package/dist/channels/aws-sns.d.mts +3 -0
  13. package/dist/channels/aws-sns.mjs +4 -0
  14. package/dist/channels/discord/{channel.d.ts → channel.d.mts} +5 -5
  15. package/dist/channels/discord/{channel.js → channel.mjs} +1 -1
  16. package/dist/channels/discord/{message.d.ts → message.d.mts} +2 -2
  17. package/dist/channels/discord/{message.js → message.mjs} +1 -1
  18. package/dist/channels/discord/{types.d.ts → types.d.mts} +1 -1
  19. package/dist/channels/discord/types.mjs +1 -0
  20. package/dist/channels/discord.d.mts +3 -0
  21. package/dist/channels/discord.mjs +4 -0
  22. package/dist/channels/expo/{channel.d.ts → channel.d.mts} +5 -3
  23. package/dist/channels/expo/channel.mjs +69 -0
  24. package/dist/channels/expo/types.mjs +1 -0
  25. package/dist/channels/expo.d.mts +3 -0
  26. package/dist/channels/expo.mjs +4 -0
  27. package/dist/channels/fcm/{channel.d.ts → channel.d.mts} +5 -5
  28. package/dist/channels/fcm/channel.mjs +68 -0
  29. package/dist/channels/fcm/types.mjs +1 -0
  30. package/dist/channels/fcm.d.mts +3 -0
  31. package/dist/channels/fcm.mjs +4 -0
  32. package/dist/channels/slack/{channel.d.ts → channel.d.mts} +5 -5
  33. package/dist/channels/slack/{channel.js → channel.mjs} +2 -2
  34. package/dist/channels/slack/{message.d.ts → message.d.mts} +1 -1
  35. package/dist/channels/slack/{message.js → message.mjs} +1 -1
  36. package/dist/channels/slack/{types.d.ts → types.d.mts} +1 -1
  37. package/dist/channels/slack/types.mjs +1 -0
  38. package/dist/channels/slack.d.mts +3 -0
  39. package/dist/channels/slack.mjs +4 -0
  40. package/dist/channels/socketio/{channel.d.ts → channel.d.mts} +3 -3
  41. package/dist/channels/socketio/{channel.js → channel.mjs} +2 -3
  42. package/dist/channels/socketio/types.mjs +1 -0
  43. package/dist/channels/socketio.d.mts +3 -0
  44. package/dist/channels/socketio.mjs +4 -0
  45. package/dist/channels/transmit/{channel.d.ts → channel.d.mts} +3 -3
  46. package/dist/channels/transmit/{channel.js → channel.mjs} +2 -3
  47. package/dist/channels/transmit/types.mjs +1 -0
  48. package/dist/channels/transmit.d.mts +3 -0
  49. package/dist/channels/transmit.mjs +4 -0
  50. package/dist/channels/twilio/{channel.d.ts → channel.d.mts} +3 -3
  51. package/dist/channels/twilio/{channel.js → channel.mjs} +3 -6
  52. package/dist/channels/twilio/types.mjs +1 -0
  53. package/dist/channels/twilio.d.mts +4 -0
  54. package/dist/channels/twilio.mjs +4 -0
  55. package/dist/channels/webhook/{exceptions.d.ts → exceptions.d.mts} +1 -1
  56. package/dist/channels/webhook/{provider.d.ts → provider.d.mts} +3 -3
  57. package/dist/channels/webhook/{provider.js → provider.mjs} +6 -8
  58. package/dist/channels/webhook/types.mjs +1 -0
  59. package/dist/channels/webhook.d.mts +4 -0
  60. package/dist/channels/webhook.mjs +5 -0
  61. package/dist/channels/webpush/{channel.d.ts → channel.d.mts} +3 -3
  62. package/dist/channels/webpush/{channel.js → channel.mjs} +2 -3
  63. package/dist/channels/webpush/types.mjs +1 -0
  64. package/dist/channels/webpush.d.mts +3 -0
  65. package/dist/channels/webpush.mjs +4 -0
  66. package/dist/database/adapters/{knex.d.ts → knex.d.mts} +1 -1
  67. package/dist/database/adapters/{knex.js → knex.mjs} +19 -15
  68. package/dist/database/adapters/{kysely.d.ts → kysely.d.mts} +1 -1
  69. package/dist/database/adapters/{kysely.js → kysely.mjs} +2 -4
  70. package/dist/database/{channel.d.ts → channel.d.mts} +3 -3
  71. package/dist/database/{channel.js → channel.mjs} +4 -6
  72. package/dist/database/{database.d.ts → database.d.mts} +3 -3
  73. package/dist/database/{database.js → database.mjs} +15 -12
  74. package/dist/database/{message.d.ts → message.d.mts} +2 -2
  75. package/dist/database/{types.d.ts → types.d.mts} +3 -3
  76. package/dist/database/types.mjs +1 -0
  77. package/dist/database.d.mts +3 -0
  78. package/dist/database.mjs +4 -0
  79. package/dist/errors/{duplicate_notification_exception.js → duplicate_notification_exception.mjs} +1 -2
  80. package/dist/errors/{http_error.d.ts → http_error.d.mts} +0 -3
  81. package/dist/errors/{index.d.ts → index.d.mts} +1 -1
  82. package/dist/errors/{index.js → index.mjs} +2 -3
  83. package/dist/events/{events.d.ts → events.d.mts} +2 -2
  84. package/dist/facteur.d.mts +45 -0
  85. package/dist/facteur.mjs +167 -0
  86. package/dist/{fake.d.ts → fake.d.mts} +3 -3
  87. package/dist/{fake.js → fake.mjs} +2 -3
  88. package/dist/{index.d.ts → index.d.mts} +3 -3
  89. package/dist/{index.js → index.mjs} +2 -2
  90. package/dist/notifications/batching_sender.mjs +148 -0
  91. package/dist/notifications/{channel_resolver.js → channel_resolver.mjs} +40 -31
  92. package/dist/notifications/notification_builder.mjs +77 -0
  93. package/dist/notifications/{notification_discoverer.d.ts → notification_discoverer.d.mts} +1 -1
  94. package/dist/notifications/{notification_discoverer.js → notification_discoverer.mjs} +9 -14
  95. package/dist/notifications/notification_sender.mjs +356 -0
  96. package/dist/notifications/orchestration_sender.mjs +92 -0
  97. package/dist/{options.d.ts → options.d.mts} +7 -6
  98. package/dist/{options.js → options.mjs} +5 -4
  99. package/dist/types/builder.d.mts +122 -0
  100. package/dist/types/channel.d.mts +56 -0
  101. package/dist/types/{events.d.ts → events.d.mts} +1 -1
  102. package/dist/types/{extend.d.ts → extend.d.mts} +2 -2
  103. package/dist/types/{notifications.d.ts → notifications.d.mts} +3 -3
  104. package/dist/types/{options.d.ts → options.d.mts} +94 -11
  105. package/dist/types/{preferences.d.ts → preferences.d.mts} +1 -1
  106. package/dist/types.d.mts +9 -0
  107. package/dist/types.mjs +4 -0
  108. package/dist/utils/chunk.mjs +28 -0
  109. package/package.json +69 -54
  110. package/dist/api/handlers/preferences.js +0 -43
  111. package/dist/api/index.d.ts +0 -16
  112. package/dist/api/index.js +0 -21
  113. package/dist/api/types.d.ts +0 -22
  114. package/dist/api/types.js +0 -0
  115. package/dist/channels/aws-sns/index.d.ts +0 -3
  116. package/dist/channels/aws-sns/index.js +0 -4
  117. package/dist/channels/aws-sns/types.js +0 -0
  118. package/dist/channels/discord/index.d.ts +0 -3
  119. package/dist/channels/discord/index.js +0 -4
  120. package/dist/channels/discord/types.js +0 -0
  121. package/dist/channels/expo/channel.js +0 -38
  122. package/dist/channels/expo/index.d.ts +0 -3
  123. package/dist/channels/expo/index.js +0 -4
  124. package/dist/channels/expo/types.js +0 -0
  125. package/dist/channels/fcm/channel.js +0 -44
  126. package/dist/channels/fcm/index.d.ts +0 -3
  127. package/dist/channels/fcm/index.js +0 -4
  128. package/dist/channels/fcm/types.js +0 -0
  129. package/dist/channels/slack/index.d.ts +0 -3
  130. package/dist/channels/slack/index.js +0 -4
  131. package/dist/channels/slack/types.js +0 -0
  132. package/dist/channels/socketio/index.d.ts +0 -3
  133. package/dist/channels/socketio/index.js +0 -4
  134. package/dist/channels/socketio/types.js +0 -0
  135. package/dist/channels/transmit/index.d.ts +0 -3
  136. package/dist/channels/transmit/index.js +0 -4
  137. package/dist/channels/transmit/types.js +0 -0
  138. package/dist/channels/twilio/index.d.ts +0 -4
  139. package/dist/channels/twilio/index.js +0 -4
  140. package/dist/channels/twilio/types.js +0 -0
  141. package/dist/channels/webhook/index.d.ts +0 -4
  142. package/dist/channels/webhook/index.js +0 -5
  143. package/dist/channels/webhook/types.js +0 -0
  144. package/dist/channels/webpush/index.d.ts +0 -3
  145. package/dist/channels/webpush/index.js +0 -4
  146. package/dist/channels/webpush/types.js +0 -0
  147. package/dist/database/index.d.ts +0 -3
  148. package/dist/database/index.js +0 -4
  149. package/dist/database/types.js +0 -0
  150. package/dist/facteur.d.ts +0 -37
  151. package/dist/facteur.js +0 -100
  152. package/dist/notifications/notification_sender.js +0 -211
  153. package/dist/types/channel.d.ts +0 -18
  154. package/dist/types/index.d.ts +0 -8
  155. package/dist/types/index.js +0 -4
  156. /package/dist/channels/aws-sns/{message.d.ts → message.d.mts} +0 -0
  157. /package/dist/channels/aws-sns/{message.js → message.mjs} +0 -0
  158. /package/dist/channels/aws-sns/{types.d.ts → types.d.mts} +0 -0
  159. /package/dist/channels/expo/{message.d.ts → message.d.mts} +0 -0
  160. /package/dist/channels/expo/{message.js → message.mjs} +0 -0
  161. /package/dist/channels/expo/{types.d.ts → types.d.mts} +0 -0
  162. /package/dist/channels/fcm/{message.d.ts → message.d.mts} +0 -0
  163. /package/dist/channels/fcm/{message.js → message.mjs} +0 -0
  164. /package/dist/channels/fcm/{types.d.ts → types.d.mts} +0 -0
  165. /package/dist/channels/socketio/{message.d.ts → message.d.mts} +0 -0
  166. /package/dist/channels/socketio/{message.js → message.mjs} +0 -0
  167. /package/dist/channels/socketio/{types.d.ts → types.d.mts} +0 -0
  168. /package/dist/channels/transmit/{message.d.ts → message.d.mts} +0 -0
  169. /package/dist/channels/transmit/{message.js → message.mjs} +0 -0
  170. /package/dist/channels/transmit/{types.d.ts → types.d.mts} +0 -0
  171. /package/dist/channels/twilio/{message.d.ts → message.d.mts} +0 -0
  172. /package/dist/channels/twilio/{message.js → message.mjs} +0 -0
  173. /package/dist/channels/twilio/{types.d.ts → types.d.mts} +0 -0
  174. /package/dist/channels/webhook/{exceptions.js → exceptions.mjs} +0 -0
  175. /package/dist/channels/webhook/{message.d.ts → message.d.mts} +0 -0
  176. /package/dist/channels/webhook/{message.js → message.mjs} +0 -0
  177. /package/dist/channels/webhook/{types.d.ts → types.d.mts} +0 -0
  178. /package/dist/channels/webpush/{message.d.ts → message.d.mts} +0 -0
  179. /package/dist/channels/webpush/{message.js → message.mjs} +0 -0
  180. /package/dist/channels/webpush/{types.d.ts → types.d.mts} +0 -0
  181. /package/dist/database/{message.js → message.mjs} +0 -0
  182. /package/dist/{debug.js → debug.mjs} +0 -0
  183. /package/dist/errors/{duplicate_notification_exception.d.ts → duplicate_notification_exception.d.mts} +0 -0
  184. /package/dist/errors/{http_error.js → http_error.mjs} +0 -0
  185. /package/dist/events/{events.js → events.mjs} +0 -0
  186. /package/dist/{helpers.js → helpers.mjs} +0 -0
  187. /package/dist/types/{channel.js → channel.mjs} +0 -0
  188. /package/dist/types/{notifications.js → notifications.mjs} +0 -0
  189. /package/dist/types/{queue.d.ts → queue.d.mts} +0 -0
package/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # @facteurjs/core
2
+
3
+ Framework-agnostic notification system for Node.js with support for multiple channels.
4
+
5
+ ## Supported Channels
6
+
7
+ - Email (via your mail provider)
8
+ - SMS (Twilio, AWS SNS)
9
+ - Push notifications (FCM, Expo, Web Push)
10
+ - Real-time (Socket.IO, Transmit)
11
+ - Webhooks
12
+ - Chat (Slack, Discord)
13
+ - Database storage
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install @facteurjs/core
19
+ ```
20
+
21
+ ## Documentation
22
+
23
+ Full documentation available at [facteur.julr.dev](https://facteur.julr.dev)
24
+
25
+ ## License
26
+
27
+ MIT
@@ -1,37 +1,52 @@
1
- import { defineRoute } from "../index.js";
1
+ import { UNAUTHORIZED_RESPONSE, checkAuthorization, parseJsonSafe } from "./utils.mjs";
2
+ import { defineRoute } from "../../api.mjs";
2
3
 
3
4
  //#region src/api/handlers/notifications.ts
4
5
  /**
5
6
  * Get all notifications for a given user
6
7
  */
7
- const getNotificationRoute = defineRoute(({ facteur }) => ({
8
+ const getNotificationRoute = defineRoute(({ facteur, authorize }) => ({
8
9
  method: "get",
9
10
  route: "/notifications/notifiable/:notifiableId/notifications",
10
11
  handler: async (request) => {
11
- const userId = request.params.notifiableId;
12
- const notifications = await facteur.db.getNotifications({
13
- notifiableId: userId,
14
- tenantId: request.query.tenantId,
15
- page: request.query.page,
16
- limit: request.query.limit,
17
- status: request.query.status,
18
- tags: request.query.tags ? JSON.parse(request.query.tags) : void 0
19
- });
12
+ const notifiableId = request.params.notifiableId;
13
+ const tenantId = request.query.tenantId;
14
+ if (!await checkAuthorization({
15
+ authorize,
16
+ request,
17
+ notifiableId,
18
+ tenantId
19
+ })) return UNAUTHORIZED_RESPONSE;
20
20
  return {
21
21
  status: 200,
22
- body: notifications || []
22
+ body: await facteur.db.getNotifications({
23
+ notifiableId,
24
+ tenantId,
25
+ page: request.query.page,
26
+ limit: request.query.limit,
27
+ status: request.query.status,
28
+ tags: parseJsonSafe(request.query.tags)
29
+ }) || []
23
30
  };
24
31
  }
25
32
  }));
26
33
  /**
27
34
  * Mark a specific notification as read or seen
28
35
  */
29
- const markNotificationAsRoute = defineRoute(({ facteur }) => ({
36
+ const markNotificationAsRoute = defineRoute(({ facteur, authorize }) => ({
30
37
  method: "post",
31
38
  route: "/notifications/notifiable/:notifiableId/mark-as",
32
39
  handler: async (request) => {
40
+ const notifiableId = request.params.notifiableId;
41
+ const tenantId = request.body.tenantId;
33
42
  const notificationId = request.body.notificationId;
34
43
  const status = request.body.status;
44
+ if (!await checkAuthorization({
45
+ authorize,
46
+ request,
47
+ notifiableId,
48
+ tenantId
49
+ })) return UNAUTHORIZED_RESPONSE;
35
50
  if (!status) return {
36
51
  status: 400,
37
52
  body: { error: "Status is required" }
@@ -53,18 +68,26 @@ const markNotificationAsRoute = defineRoute(({ facteur }) => ({
53
68
  /**
54
69
  * Mark all notifications for a user as read or seen
55
70
  */
56
- const markAllNotificationsAsRoute = defineRoute(({ facteur }) => ({
71
+ const markAllNotificationsAsRoute = defineRoute(({ facteur, authorize }) => ({
57
72
  method: "post",
58
73
  route: "/notifications/notifiable/:notifiableId/mark-all",
59
74
  handler: async (request) => {
75
+ const notifiableId = request.params.notifiableId;
76
+ const tenantId = request.body.tenantId;
60
77
  const status = request.body.status;
78
+ if (!await checkAuthorization({
79
+ authorize,
80
+ request,
81
+ notifiableId,
82
+ tenantId
83
+ })) return UNAUTHORIZED_RESPONSE;
61
84
  if (!status) return {
62
85
  status: 400,
63
86
  body: { error: "Status is required" }
64
87
  };
65
88
  await facteur.db.updateAllNotifications({
66
- notifiableId: request.params.notifiableId,
67
- tenantId: request.body.tenantId,
89
+ notifiableId,
90
+ tenantId,
68
91
  status
69
92
  });
70
93
  return {
@@ -0,0 +1,73 @@
1
+ import { UNAUTHORIZED_RESPONSE, checkAuthorization } from "./utils.mjs";
2
+ import { defineRoute } from "../../api.mjs";
3
+
4
+ //#region src/api/handlers/preferences.ts
5
+ function validatePreferences(preferences) {
6
+ if (!preferences || typeof preferences !== "object" || Array.isArray(preferences)) return {
7
+ valid: false,
8
+ error: "Preferences must be an object"
9
+ };
10
+ for (const [channel, value] of Object.entries(preferences)) if (typeof value !== "boolean") return {
11
+ valid: false,
12
+ error: `Channel "${channel}" preference must be a boolean, got ${typeof value}`
13
+ };
14
+ return { valid: true };
15
+ }
16
+ const getPreferencesRoute = defineRoute(({ facteur, authorize }) => ({
17
+ method: "get",
18
+ route: "/notifications/notifiable/:notifiableId/preferences",
19
+ handler: async (request) => {
20
+ const notifiableId = request.params.notifiableId;
21
+ const tenantId = request.query.tenantId;
22
+ if (!await checkAuthorization({
23
+ authorize,
24
+ request,
25
+ notifiableId,
26
+ tenantId
27
+ })) return UNAUTHORIZED_RESPONSE;
28
+ return {
29
+ status: 200,
30
+ body: await facteur.db.getPreferences({
31
+ notifiableId,
32
+ tenantId
33
+ }) || {}
34
+ };
35
+ }
36
+ }));
37
+ const updatePreferencesRoute = defineRoute(({ facteur, authorize }) => ({
38
+ method: "post",
39
+ route: "/notifications/notifiable/:notifiableId/preferences",
40
+ handler: async (request) => {
41
+ const notifiableId = request.params.notifiableId;
42
+ const tenantId = request.body.tenantId;
43
+ const preferences = request.body.preferences;
44
+ if (!await checkAuthorization({
45
+ authorize,
46
+ request,
47
+ notifiableId,
48
+ tenantId
49
+ })) return UNAUTHORIZED_RESPONSE;
50
+ if (!preferences) return {
51
+ status: 400,
52
+ body: { error: "Preferences are required" }
53
+ };
54
+ const validation = validatePreferences(preferences);
55
+ if (!validation.valid) return {
56
+ status: 400,
57
+ body: { error: validation.error }
58
+ };
59
+ await facteur.db.updatePreferences({
60
+ notifiableId,
61
+ tenantId,
62
+ notificationName: request.body.notificationName,
63
+ channelPreferences: preferences
64
+ });
65
+ return {
66
+ status: 204,
67
+ body: {}
68
+ };
69
+ }
70
+ }));
71
+
72
+ //#endregion
73
+ export { getPreferencesRoute, updatePreferencesRoute };
@@ -0,0 +1,23 @@
1
+ //#region src/api/handlers/utils.ts
2
+ const UNAUTHORIZED_RESPONSE = {
3
+ status: 403,
4
+ body: { error: "Unauthorized" }
5
+ };
6
+ async function checkAuthorization(options) {
7
+ return options.authorize({
8
+ notifiableId: options.notifiableId,
9
+ tenantId: options.tenantId,
10
+ request: options.request
11
+ });
12
+ }
13
+ function parseJsonSafe(value) {
14
+ if (!value) return void 0;
15
+ try {
16
+ return JSON.parse(value);
17
+ } catch {
18
+ return;
19
+ }
20
+ }
21
+
22
+ //#endregion
23
+ export { UNAUTHORIZED_RESPONSE, checkAuthorization, parseJsonSafe };
@@ -0,0 +1,52 @@
1
+ import { Identifier } from "../database/types.mjs";
2
+
3
+ //#region src/api/types.d.ts
4
+ type HTTPMethod = 'get' | 'post' | 'put' | 'patch';
5
+ interface HTTPRequest<Context = unknown> {
6
+ body: Record<string, any>;
7
+ params: Record<string, any>;
8
+ query: Record<string, any>;
9
+ headers: Record<string, string | undefined>;
10
+ /**
11
+ * Framework-specific context (e.g., AdonisJS HttpContext, Hono Context).
12
+ * Use this to access framework-specific features like authentication.
13
+ */
14
+ context?: Context;
15
+ }
16
+ interface HTTPResponse {
17
+ status: number;
18
+ body: Record<string, any>;
19
+ }
20
+ interface RouteDefinition {
21
+ method: HTTPMethod;
22
+ route: string;
23
+ handler: (request: HTTPRequest) => Promise<HTTPResponse>;
24
+ }
25
+ interface ServerAdapter {
26
+ setRoutes(routes: RouteDefinition[]): void;
27
+ }
28
+ /**
29
+ * Context passed to the authorization callback
30
+ */
31
+ interface AuthorizationContext {
32
+ /**
33
+ * The notifiable ID from the request (URL param)
34
+ */
35
+ notifiableId: Identifier;
36
+ /**
37
+ * The tenant ID from the request (query or body)
38
+ */
39
+ tenantId: Identifier | undefined;
40
+ /**
41
+ * The HTTP request object
42
+ */
43
+ request: HTTPRequest;
44
+ }
45
+ /**
46
+ * Callback to authorize a request.
47
+ * Should return true if the request is authorized, false otherwise.
48
+ * Can also throw an error to provide a custom error response.
49
+ */
50
+ type AuthorizationCallback = (context: AuthorizationContext) => Promise<boolean> | boolean;
51
+ //#endregion
52
+ export { AuthorizationCallback, AuthorizationContext, HTTPMethod, HTTPRequest, HTTPResponse, RouteDefinition, ServerAdapter };
@@ -0,0 +1 @@
1
+ export { };
package/dist/api.d.mts ADDED
@@ -0,0 +1,22 @@
1
+ import { Facteur } from "./facteur.mjs";
2
+ import { AuthorizationCallback, RouteDefinition, ServerAdapter } from "./api/types.mjs";
3
+
4
+ //#region src/api/index.d.ts
5
+ interface DefineRouteOptions {
6
+ facteur: Facteur<any, any>;
7
+ authorize: AuthorizationCallback;
8
+ }
9
+ declare function defineRoute(routeDefinition: (options: DefineRouteOptions) => RouteDefinition): (options: DefineRouteOptions) => RouteDefinition;
10
+ declare const routes: (options: DefineRouteOptions) => RouteDefinition[];
11
+ interface CreateFacteurServerOptions {
12
+ adapter: ServerAdapter;
13
+ facteur: Facteur<any, any>;
14
+ /**
15
+ * Authorization callback to verify that the requester has access to the requested resources.
16
+ * This callback is required to ensure proper access control.
17
+ */
18
+ authorize: AuthorizationCallback;
19
+ }
20
+ declare function createFacteurServer(options: CreateFacteurServerOptions): void;
21
+ //#endregion
22
+ export { CreateFacteurServerOptions, DefineRouteOptions, createFacteurServer, defineRoute, routes };
package/dist/api.mjs ADDED
@@ -0,0 +1,25 @@
1
+ import { getPreferencesRoute, updatePreferencesRoute } from "./api/handlers/preferences.mjs";
2
+ import { getNotificationRoute, markAllNotificationsAsRoute, markNotificationAsRoute } from "./api/handlers/notifications.mjs";
3
+
4
+ //#region src/api/index.ts
5
+ function defineRoute(routeDefinition) {
6
+ return routeDefinition;
7
+ }
8
+ const routes = (options) => [
9
+ getNotificationRoute(options),
10
+ markNotificationAsRoute(options),
11
+ markAllNotificationsAsRoute(options),
12
+ getPreferencesRoute(options),
13
+ updatePreferencesRoute(options)
14
+ ];
15
+ function createFacteurServer(options) {
16
+ const { adapter, facteur, authorize } = options;
17
+ if (!authorize) throw new Error("Authorization callback is required. You must provide an authorize function to control access to notification resources.");
18
+ adapter.setRoutes(routes({
19
+ facteur,
20
+ authorize
21
+ }));
22
+ }
23
+
24
+ //#endregion
25
+ export { createFacteurServer, defineRoute, routes };
@@ -1,6 +1,6 @@
1
- import { Channel, ChannelSendParams, kTargetSymbol } from "../../types/channel.js";
2
- import { AwsSnsMessage } from "./message.js";
3
- import { AwsSnsConfig, AwsSnsTargets } from "./types.js";
1
+ import { Channel, ChannelSendParams, kTargetSymbol } from "../../types/channel.mjs";
2
+ import { AwsSnsMessage } from "./message.mjs";
3
+ import { AwsSnsConfig, AwsSnsTargets } from "./types.mjs";
4
4
 
5
5
  //#region src/channels/aws-sns/channel.d.ts
6
6
  declare function awsSnsChannel(config: AwsSnsConfig): AwsSnsChannel;
@@ -1,6 +1,5 @@
1
- import { errors } from "../../errors/index.js";
2
- import { kTargetSymbol } from "../../types/channel.js";
3
- import "../../types/index.js";
1
+ import { errors } from "../../errors/index.mjs";
2
+ import { kTargetSymbol } from "../../types/channel.mjs";
4
3
  import { PublishCommand, SNSClient } from "@aws-sdk/client-sns";
5
4
 
6
5
  //#region src/channels/aws-sns/channel.ts
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,3 @@
1
+ import { AwsSnsMessage } from "./aws-sns/message.mjs";
2
+ import { AwsSnsChannel, awsSnsChannel } from "./aws-sns/channel.mjs";
3
+ export { AwsSnsChannel, AwsSnsMessage, awsSnsChannel };
@@ -0,0 +1,4 @@
1
+ import { AwsSnsMessage } from "./aws-sns/message.mjs";
2
+ import { AwsSnsChannel, awsSnsChannel } from "./aws-sns/channel.mjs";
3
+
4
+ export { AwsSnsChannel, AwsSnsMessage, awsSnsChannel };
@@ -1,8 +1,8 @@
1
- import { Channel } from "../../types/channel.js";
2
- import { WebhookOptions, WebhookTargets } from "../webhook/types.js";
3
- import { DiscordResponse } from "./types.js";
4
- import { DiscordMessage } from "./message.js";
5
- import { WebhookChannel } from "../webhook/provider.js";
1
+ import { Channel } from "../../types/channel.mjs";
2
+ import { WebhookOptions, WebhookTargets } from "../webhook/types.mjs";
3
+ import { DiscordResponse } from "./types.mjs";
4
+ import { DiscordMessage } from "./message.mjs";
5
+ import { WebhookChannel } from "../webhook/provider.mjs";
6
6
 
7
7
  //#region src/channels/discord/channel.d.ts
8
8
  declare function discordWebhookChannel<Options extends WebhookOptions<any>>(options: Options): DiscordProvider<Options>;
@@ -1,4 +1,4 @@
1
- import { WebhookChannel } from "../webhook/provider.js";
1
+ import { WebhookChannel } from "../webhook/provider.mjs";
2
2
 
3
3
  //#region src/channels/discord/channel.ts
4
4
  function discordWebhookChannel(options) {
@@ -1,5 +1,5 @@
1
- import { WebhookMessage } from "../webhook/message.js";
2
- import { DiscordEmbedField, HexadecimalColor } from "./types.js";
1
+ import { DiscordEmbedField, HexadecimalColor } from "./types.mjs";
2
+ import { WebhookMessage } from "../webhook/message.mjs";
3
3
 
4
4
  //#region src/channels/discord/message.d.ts
5
5
  declare class DiscordMessage extends WebhookMessage {
@@ -1,4 +1,4 @@
1
- import { WebhookMessage } from "../webhook/message.js";
1
+ import { WebhookMessage } from "../webhook/message.mjs";
2
2
 
3
3
  //#region src/channels/discord/message.ts
4
4
  var DiscordMessage = class DiscordMessage extends WebhookMessage {
@@ -1,4 +1,4 @@
1
- import { WebhookOptions, WebhookTargets } from "../webhook/types.js";
1
+ import { WebhookOptions, WebhookTargets } from "../webhook/types.mjs";
2
2
 
3
3
  //#region src/channels/discord/types.d.ts
4
4
  type DiscordOptions<WebhooksNames extends string> = WebhookOptions<WebhooksNames>;
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,3 @@
1
+ import { DiscordMessage } from "./discord/message.mjs";
2
+ import { DiscordProvider, discordWebhookChannel } from "./discord/channel.mjs";
3
+ export { DiscordMessage, DiscordProvider, discordWebhookChannel };
@@ -0,0 +1,4 @@
1
+ import { DiscordMessage } from "./discord/message.mjs";
2
+ import { DiscordProvider, discordWebhookChannel } from "./discord/channel.mjs";
3
+
4
+ export { DiscordMessage, DiscordProvider, discordWebhookChannel };
@@ -1,6 +1,6 @@
1
- import { Channel, ChannelSendParams, kTargetSymbol } from "../../types/channel.js";
2
- import { ExpoMessage } from "./message.js";
3
- import { ExpoConfig, ExpoTargets } from "./types.js";
1
+ import { BatchConfig, BatchSendResult, Channel, ChannelSendParams, kTargetSymbol } from "../../types/channel.mjs";
2
+ import { ExpoMessage } from "./message.mjs";
3
+ import { ExpoConfig, ExpoTargets } from "./types.mjs";
4
4
 
5
5
  //#region src/channels/expo/channel.d.ts
6
6
  declare function expoChannel(config?: ExpoConfig): ExpoChannel;
@@ -8,11 +8,13 @@ declare class ExpoChannel implements Channel<ExpoConfig, ExpoMessage, any, ExpoT
8
8
  #private;
9
9
  name: "expo";
10
10
  [kTargetSymbol]: ExpoTargets;
11
+ batchConfig: BatchConfig;
11
12
  constructor(config: ExpoConfig);
12
13
  send(options: ChannelSendParams<ExpoMessage, ExpoTargets>): Promise<{
13
14
  id: string;
14
15
  status: "ok";
15
16
  }>;
17
+ sendBatch(messages: ChannelSendParams<ExpoMessage, ExpoTargets>[]): Promise<BatchSendResult>;
16
18
  }
17
19
  declare module '@facteurjs/core/types' {
18
20
  interface Notification {
@@ -0,0 +1,69 @@
1
+ import { errors } from "../../errors/index.mjs";
2
+ import { kTargetSymbol } from "../../types/channel.mjs";
3
+ import { Expo } from "expo-server-sdk";
4
+
5
+ //#region src/channels/expo/channel.ts
6
+ function expoChannel(config = {}) {
7
+ return new ExpoChannel(config);
8
+ }
9
+ var ExpoChannel = class {
10
+ name = "expo";
11
+ [kTargetSymbol] = null;
12
+ batchConfig = {
13
+ maxSize: 100,
14
+ enabled: true
15
+ };
16
+ #expo;
17
+ constructor(config) {
18
+ this.#expo = new Expo(config);
19
+ if (config.accessToken) this.#validateExpoToken(config.accessToken);
20
+ }
21
+ #validateExpoToken(token) {
22
+ if (!Expo.isExpoPushToken(token)) throw new Error(`Invalid Expo push token: ${token}`);
23
+ }
24
+ #resolveTargets(options) {
25
+ if (options.targets) return options.targets;
26
+ throw new errors.E_UNAVAILABLE_TARGETS(["Expo"]);
27
+ }
28
+ #buildMessage(options) {
29
+ const targets = this.#resolveTargets(options);
30
+ return options.message.serialize({ to: targets.expoToken });
31
+ }
32
+ async send(options) {
33
+ const message = this.#buildMessage(options);
34
+ const [ticket] = await this.#expo.sendPushNotificationsAsync([message]);
35
+ if (ticket?.status === "error") throw new Error(`Expo push notification failed: ${ticket.message}`);
36
+ return {
37
+ id: ticket?.id,
38
+ status: ticket?.status
39
+ };
40
+ }
41
+ async sendBatch(messages) {
42
+ const expoMessages = messages.map((msg) => this.#buildMessage(msg));
43
+ const tickets = await this.#expo.sendPushNotificationsAsync(expoMessages);
44
+ let successCount = 0;
45
+ let failedCount = 0;
46
+ const results = tickets.map((ticket, index) => {
47
+ const result = {
48
+ index,
49
+ status: ticket.status === "ok" ? "success" : "failed"
50
+ };
51
+ if (ticket.status === "ok") {
52
+ successCount++;
53
+ result.response = ticket.id;
54
+ } else {
55
+ failedCount++;
56
+ result.error = new Error(ticket.message || "Unknown Expo error", { cause: ticket.details });
57
+ }
58
+ return result;
59
+ });
60
+ return {
61
+ success: successCount,
62
+ failed: failedCount,
63
+ results
64
+ };
65
+ }
66
+ };
67
+
68
+ //#endregion
69
+ export { ExpoChannel, expoChannel };
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,3 @@
1
+ import { ExpoMessage } from "./expo/message.mjs";
2
+ import { ExpoChannel, expoChannel } from "./expo/channel.mjs";
3
+ export { ExpoChannel, ExpoMessage, expoChannel };
@@ -0,0 +1,4 @@
1
+ import { ExpoMessage } from "./expo/message.mjs";
2
+ import { ExpoChannel, expoChannel } from "./expo/channel.mjs";
3
+
4
+ export { ExpoChannel, ExpoMessage, expoChannel };
@@ -1,17 +1,17 @@
1
- import { Channel, ChannelSendParams, kTargetSymbol } from "../../types/channel.js";
2
- import { FcmMessage } from "./message.js";
3
- import { FcmConfig, FcmTargets } from "./types.js";
1
+ import { BatchConfig, BatchSendResult, Channel, ChannelSendParams, kTargetSymbol } from "../../types/channel.mjs";
2
+ import { FcmMessage } from "./message.mjs";
3
+ import { FcmConfig, FcmTargets } from "./types.mjs";
4
4
 
5
5
  //#region src/channels/fcm/channel.d.ts
6
6
  declare function fcmChannel(config: FcmConfig): FcmChannel;
7
7
  declare class FcmChannel implements Channel<FcmConfig, FcmMessage, any, FcmTargets> {
8
8
  #private;
9
- private config;
10
9
  name: "fcm";
11
10
  [kTargetSymbol]: FcmTargets;
12
- private messaging;
11
+ batchConfig: BatchConfig;
13
12
  constructor(config: FcmConfig);
14
13
  send(options: ChannelSendParams<FcmMessage, FcmTargets>): Promise<string>;
14
+ sendBatch(messages: ChannelSendParams<FcmMessage, FcmTargets>[]): Promise<BatchSendResult>;
15
15
  }
16
16
  declare module '@facteurjs/core/types' {
17
17
  interface Notification {
@@ -0,0 +1,68 @@
1
+ import { errors } from "../../errors/index.mjs";
2
+ import { kTargetSymbol } from "../../types/channel.mjs";
3
+ import { getMessaging } from "firebase-admin/messaging";
4
+ import { cert, initializeApp } from "firebase-admin/app";
5
+
6
+ //#region src/channels/fcm/channel.ts
7
+ function fcmChannel(config) {
8
+ return new FcmChannel(config);
9
+ }
10
+ var FcmChannel = class {
11
+ name = "fcm";
12
+ [kTargetSymbol] = null;
13
+ batchConfig = {
14
+ maxSize: 500,
15
+ enabled: true
16
+ };
17
+ #messaging;
18
+ #config;
19
+ constructor(config) {
20
+ this.#config = config;
21
+ this.#messaging = getMessaging(initializeApp({
22
+ ...config,
23
+ ...config.serviceAccountKeyPath ? { credential: cert(config.serviceAccountKeyPath) } : {}
24
+ }));
25
+ }
26
+ #resolveTargets(options) {
27
+ if (options.targets) return options.targets;
28
+ throw new errors.E_UNAVAILABLE_TARGETS(["FCM"]);
29
+ }
30
+ #handleError(error) {
31
+ if (error.code === "messaging/registration-token-not-registered") throw new Error(`FCM token is no longer valid: ${error.message}`);
32
+ if (error.code === "messaging/invalid-argument") throw new Error(`Invalid FCM message format: ${error.message}`);
33
+ throw error;
34
+ }
35
+ #buildMessage(options) {
36
+ const message = options.message.serialize();
37
+ const targets = this.#resolveTargets(options);
38
+ if (this.#config.debugToken) message.token = this.#config.debugToken;
39
+ if (targets.token) message.token = this.#config.debugToken || targets.token;
40
+ else if (targets.topic) message.topic = targets.topic;
41
+ else if (targets.condition) message.condition = targets.condition;
42
+ return message;
43
+ }
44
+ async send(options) {
45
+ const message = this.#buildMessage(options);
46
+ return await this.#messaging.send(message).catch((error) => this.#handleError(error));
47
+ }
48
+ async sendBatch(messages) {
49
+ const fcmMessages = messages.map((msg) => this.#buildMessage(msg));
50
+ const response = await this.#messaging.sendEach(fcmMessages);
51
+ return {
52
+ success: response.successCount,
53
+ failed: response.failureCount,
54
+ results: response.responses.map((r, index) => {
55
+ const result = {
56
+ index,
57
+ status: r.success ? "success" : "failed"
58
+ };
59
+ if (r.error) result.error = new Error(r.error.message, { cause: r.error });
60
+ if (r.messageId) result.response = r.messageId;
61
+ return result;
62
+ })
63
+ };
64
+ }
65
+ };
66
+
67
+ //#endregion
68
+ export { FcmChannel, fcmChannel };
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,3 @@
1
+ import { FcmMessage } from "./fcm/message.mjs";
2
+ import { FcmChannel, fcmChannel } from "./fcm/channel.mjs";
3
+ export { FcmChannel, FcmMessage, fcmChannel };
@@ -0,0 +1,4 @@
1
+ import { FcmMessage } from "./fcm/message.mjs";
2
+ import { FcmChannel, fcmChannel } from "./fcm/channel.mjs";
3
+
4
+ export { FcmChannel, FcmMessage, fcmChannel };
@@ -1,7 +1,7 @@
1
- import { Channel } from "../../types/channel.js";
2
- import { WebhookChannel } from "../webhook/provider.js";
3
- import { SlackMessage } from "./message.js";
4
- import { SlackOptions, SlackTargets } from "./types.js";
1
+ import { Channel } from "../../types/channel.mjs";
2
+ import { WebhookChannel } from "../webhook/provider.mjs";
3
+ import { SlackMessage } from "./message.mjs";
4
+ import { SlackOptions, SlackTargets } from "./types.mjs";
5
5
 
6
6
  //#region src/channels/slack/channel.d.ts
7
7
  declare function slackWebhookChannel<Options extends SlackOptions<any>>(options: Options): SlackWebhookChannel<Options>;
@@ -15,4 +15,4 @@ declare module '@facteurjs/core/types' {
15
15
  }
16
16
  }
17
17
  //#endregion
18
- export { slackWebhookChannel };
18
+ export { SlackWebhookChannel, slackWebhookChannel };