@buenojs/bueno 0.8.3 → 0.8.5

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 (218) hide show
  1. package/README.md +136 -16
  2. package/dist/cli/{index.js → bin.js} +3036 -1421
  3. package/dist/container/index.js +250 -0
  4. package/dist/context/index.js +219 -0
  5. package/dist/database/index.js +493 -0
  6. package/dist/frontend/index.js +7697 -0
  7. package/dist/health/index.js +364 -0
  8. package/dist/i18n/index.js +345 -0
  9. package/dist/index.js +11043 -6482
  10. package/dist/jobs/index.js +819 -0
  11. package/dist/lock/index.js +367 -0
  12. package/dist/logger/index.js +281 -0
  13. package/dist/metrics/index.js +289 -0
  14. package/dist/middleware/index.js +77 -0
  15. package/dist/migrations/index.js +571 -0
  16. package/dist/modules/index.js +3346 -0
  17. package/dist/notification/index.js +484 -0
  18. package/dist/observability/index.js +331 -0
  19. package/dist/openapi/index.js +776 -0
  20. package/dist/orm/index.js +1356 -0
  21. package/dist/router/index.js +886 -0
  22. package/dist/rpc/index.js +691 -0
  23. package/dist/schema/index.js +400 -0
  24. package/dist/telemetry/index.js +595 -0
  25. package/dist/template/index.js +640 -0
  26. package/dist/templates/index.js +640 -0
  27. package/dist/testing/index.js +1111 -0
  28. package/dist/types/index.js +60 -0
  29. package/package.json +121 -27
  30. package/src/cache/index.ts +2 -1
  31. package/src/cli/bin.ts +2 -2
  32. package/src/cli/commands/build.ts +183 -165
  33. package/src/cli/commands/dev.ts +96 -89
  34. package/src/cli/commands/generate.ts +142 -111
  35. package/src/cli/commands/help.ts +20 -16
  36. package/src/cli/commands/index.ts +3 -6
  37. package/src/cli/commands/migration.ts +124 -105
  38. package/src/cli/commands/new.ts +392 -438
  39. package/src/cli/commands/start.ts +81 -79
  40. package/src/cli/core/args.ts +68 -50
  41. package/src/cli/core/console.ts +89 -95
  42. package/src/cli/core/index.ts +4 -4
  43. package/src/cli/core/prompt.ts +65 -62
  44. package/src/cli/core/spinner.ts +23 -20
  45. package/src/cli/index.ts +46 -38
  46. package/src/cli/templates/database/index.ts +61 -0
  47. package/src/cli/templates/database/mysql.ts +14 -0
  48. package/src/cli/templates/database/none.ts +16 -0
  49. package/src/cli/templates/database/postgresql.ts +14 -0
  50. package/src/cli/templates/database/sqlite.ts +14 -0
  51. package/src/cli/templates/deploy.ts +29 -26
  52. package/src/cli/templates/docker.ts +41 -30
  53. package/src/cli/templates/frontend/index.ts +63 -0
  54. package/src/cli/templates/frontend/none.ts +17 -0
  55. package/src/cli/templates/frontend/react.ts +140 -0
  56. package/src/cli/templates/frontend/solid.ts +134 -0
  57. package/src/cli/templates/frontend/svelte.ts +131 -0
  58. package/src/cli/templates/frontend/vue.ts +130 -0
  59. package/src/cli/templates/generators/index.ts +339 -0
  60. package/src/cli/templates/generators/types.ts +56 -0
  61. package/src/cli/templates/index.ts +35 -2
  62. package/src/cli/templates/project/api.ts +81 -0
  63. package/src/cli/templates/project/default.ts +140 -0
  64. package/src/cli/templates/project/fullstack.ts +111 -0
  65. package/src/cli/templates/project/index.ts +95 -0
  66. package/src/cli/templates/project/minimal.ts +45 -0
  67. package/src/cli/templates/project/types.ts +94 -0
  68. package/src/cli/templates/project/website.ts +263 -0
  69. package/src/cli/utils/fs.ts +55 -41
  70. package/src/cli/utils/index.ts +3 -2
  71. package/src/cli/utils/strings.ts +47 -33
  72. package/src/cli/utils/version.ts +47 -0
  73. package/src/config/env-validation.ts +100 -0
  74. package/src/config/env.ts +169 -41
  75. package/src/config/index.ts +28 -20
  76. package/src/config/loader.ts +25 -16
  77. package/src/config/merge.ts +21 -10
  78. package/src/config/types.ts +545 -25
  79. package/src/config/validation.ts +215 -7
  80. package/src/container/forward-ref.ts +22 -22
  81. package/src/container/index.ts +34 -12
  82. package/src/context/index.ts +11 -1
  83. package/src/database/index.ts +7 -190
  84. package/src/database/orm/builder.ts +457 -0
  85. package/src/database/orm/casts/index.ts +130 -0
  86. package/src/database/orm/casts/types.ts +25 -0
  87. package/src/database/orm/compiler.ts +304 -0
  88. package/src/database/orm/hooks/index.ts +114 -0
  89. package/src/database/orm/index.ts +61 -0
  90. package/src/database/orm/model-registry.ts +59 -0
  91. package/src/database/orm/model.ts +821 -0
  92. package/src/database/orm/relationships/base.ts +146 -0
  93. package/src/database/orm/relationships/belongs-to-many.ts +179 -0
  94. package/src/database/orm/relationships/belongs-to.ts +56 -0
  95. package/src/database/orm/relationships/has-many.ts +45 -0
  96. package/src/database/orm/relationships/has-one.ts +41 -0
  97. package/src/database/orm/relationships/index.ts +11 -0
  98. package/src/database/orm/scopes/index.ts +55 -0
  99. package/src/events/__tests__/event-system.test.ts +235 -0
  100. package/src/events/config.ts +238 -0
  101. package/src/events/example-usage.ts +185 -0
  102. package/src/events/index.ts +278 -0
  103. package/src/events/manager.ts +385 -0
  104. package/src/events/registry.ts +182 -0
  105. package/src/events/types.ts +124 -0
  106. package/src/frontend/api-routes.ts +65 -23
  107. package/src/frontend/bundler.ts +76 -34
  108. package/src/frontend/console-client.ts +2 -2
  109. package/src/frontend/console-stream.ts +94 -38
  110. package/src/frontend/dev-server.ts +94 -46
  111. package/src/frontend/file-router.ts +61 -19
  112. package/src/frontend/frameworks/index.ts +37 -10
  113. package/src/frontend/frameworks/react.ts +10 -8
  114. package/src/frontend/frameworks/solid.ts +11 -9
  115. package/src/frontend/frameworks/svelte.ts +15 -9
  116. package/src/frontend/frameworks/vue.ts +13 -11
  117. package/src/frontend/hmr-client.ts +12 -10
  118. package/src/frontend/hmr.ts +146 -103
  119. package/src/frontend/index.ts +14 -5
  120. package/src/frontend/islands.ts +41 -22
  121. package/src/frontend/isr.ts +59 -37
  122. package/src/frontend/layout.ts +36 -21
  123. package/src/frontend/ssr/react.ts +74 -27
  124. package/src/frontend/ssr/solid.ts +54 -20
  125. package/src/frontend/ssr/svelte.ts +48 -14
  126. package/src/frontend/ssr/vue.ts +50 -18
  127. package/src/frontend/ssr.ts +83 -39
  128. package/src/frontend/types.ts +91 -56
  129. package/src/health/index.ts +21 -9
  130. package/src/i18n/engine.ts +305 -0
  131. package/src/i18n/index.ts +38 -0
  132. package/src/i18n/loader.ts +218 -0
  133. package/src/i18n/middleware.ts +164 -0
  134. package/src/i18n/negotiator.ts +162 -0
  135. package/src/i18n/types.ts +158 -0
  136. package/src/index.ts +179 -27
  137. package/src/jobs/drivers/memory.ts +315 -0
  138. package/src/jobs/drivers/redis.ts +459 -0
  139. package/src/jobs/index.ts +30 -0
  140. package/src/jobs/queue.ts +281 -0
  141. package/src/jobs/types.ts +295 -0
  142. package/src/jobs/worker.ts +380 -0
  143. package/src/logger/index.ts +1 -3
  144. package/src/logger/transports/index.ts +62 -22
  145. package/src/metrics/index.ts +25 -16
  146. package/src/migrations/index.ts +9 -0
  147. package/src/modules/filters.ts +13 -17
  148. package/src/modules/guards.ts +49 -26
  149. package/src/modules/index.ts +409 -298
  150. package/src/modules/interceptors.ts +58 -20
  151. package/src/modules/lazy.ts +11 -19
  152. package/src/modules/lifecycle.ts +15 -7
  153. package/src/modules/metadata.ts +15 -5
  154. package/src/modules/pipes.ts +94 -72
  155. package/src/notification/channels/base.ts +68 -0
  156. package/src/notification/channels/email.ts +105 -0
  157. package/src/notification/channels/push.ts +104 -0
  158. package/src/notification/channels/sms.ts +105 -0
  159. package/src/notification/channels/whatsapp.ts +104 -0
  160. package/src/notification/index.ts +48 -0
  161. package/src/notification/service.ts +354 -0
  162. package/src/notification/types.ts +344 -0
  163. package/src/observability/__tests__/observability.test.ts +483 -0
  164. package/src/observability/breadcrumbs.ts +114 -0
  165. package/src/observability/index.ts +136 -0
  166. package/src/observability/interceptor.ts +85 -0
  167. package/src/observability/service.ts +303 -0
  168. package/src/observability/trace.ts +37 -0
  169. package/src/observability/types.ts +196 -0
  170. package/src/openapi/__tests__/decorators.test.ts +335 -0
  171. package/src/openapi/__tests__/document-builder.test.ts +285 -0
  172. package/src/openapi/__tests__/route-scanner.test.ts +334 -0
  173. package/src/openapi/__tests__/schema-generator.test.ts +275 -0
  174. package/src/openapi/decorators.ts +328 -0
  175. package/src/openapi/document-builder.ts +274 -0
  176. package/src/openapi/index.ts +112 -0
  177. package/src/openapi/metadata.ts +112 -0
  178. package/src/openapi/route-scanner.ts +289 -0
  179. package/src/openapi/schema-generator.ts +256 -0
  180. package/src/openapi/swagger-module.ts +166 -0
  181. package/src/openapi/types.ts +398 -0
  182. package/src/orm/index.ts +10 -0
  183. package/src/rpc/index.ts +3 -1
  184. package/src/schema/index.ts +9 -0
  185. package/src/security/index.ts +15 -6
  186. package/src/ssg/index.ts +9 -8
  187. package/src/telemetry/index.ts +76 -22
  188. package/src/template/index.ts +7 -0
  189. package/src/templates/engine.ts +224 -0
  190. package/src/templates/index.ts +9 -0
  191. package/src/templates/loader.ts +331 -0
  192. package/src/templates/renderers/markdown.ts +212 -0
  193. package/src/templates/renderers/simple.ts +269 -0
  194. package/src/templates/types.ts +154 -0
  195. package/src/testing/index.ts +100 -27
  196. package/src/types/optional-deps.d.ts +347 -187
  197. package/src/validation/index.ts +92 -2
  198. package/src/validation/schemas.ts +536 -0
  199. package/tests/integration/fullstack.test.ts +4 -4
  200. package/tests/unit/database.test.ts +2 -72
  201. package/tests/unit/env-validation.test.ts +166 -0
  202. package/tests/unit/events.test.ts +910 -0
  203. package/tests/unit/i18n.test.ts +455 -0
  204. package/tests/unit/jobs.test.ts +493 -0
  205. package/tests/unit/notification.test.ts +988 -0
  206. package/tests/unit/observability.test.ts +453 -0
  207. package/tests/unit/orm/builder.test.ts +323 -0
  208. package/tests/unit/orm/casts.test.ts +179 -0
  209. package/tests/unit/orm/compiler.test.ts +220 -0
  210. package/tests/unit/orm/eager-loading.test.ts +285 -0
  211. package/tests/unit/orm/hooks.test.ts +191 -0
  212. package/tests/unit/orm/model.test.ts +373 -0
  213. package/tests/unit/orm/relationships.test.ts +303 -0
  214. package/tests/unit/orm/scopes.test.ts +74 -0
  215. package/tests/unit/templates-simple.test.ts +53 -0
  216. package/tests/unit/templates.test.ts +454 -0
  217. package/tests/unit/validation.test.ts +18 -24
  218. package/tsconfig.json +11 -3
@@ -0,0 +1,105 @@
1
+ /**
2
+ * SMS Channel Service
3
+ *
4
+ * Sends notifications via SMS through various drivers
5
+ * (Twilio, AWS SNS, custom)
6
+ */
7
+
8
+ import type { SMSChannelConfig, SMSMessage } from "../types";
9
+ import { BaseChannelService } from "./base";
10
+
11
+ // ============= SMS Channel Service =============
12
+
13
+ export class SMSChannelService extends BaseChannelService<SMSMessage> {
14
+ readonly name = "sms";
15
+ private config: SMSChannelConfig;
16
+ private sentCount = 0;
17
+ private failureCount = 0;
18
+
19
+ constructor(config: SMSChannelConfig) {
20
+ super();
21
+ this.config = config;
22
+ }
23
+
24
+ /**
25
+ * Validate SMS message structure
26
+ */
27
+ validate(message: unknown): asserts message is SMSMessage {
28
+ if (typeof message !== "object" || message === null) {
29
+ throw new Error("Invalid SMS message: must be an object");
30
+ }
31
+
32
+ const msg = message as Record<string, unknown>;
33
+
34
+ if (msg.channel !== "sms") {
35
+ throw new Error('Invalid SMS message: channel must be "sms"');
36
+ }
37
+
38
+ if (typeof msg.recipient !== "string" || msg.recipient.length === 0) {
39
+ throw new Error(
40
+ "Invalid SMS message: recipient (phone number) is required",
41
+ );
42
+ }
43
+
44
+ if (typeof msg.message !== "string" || msg.message.length === 0) {
45
+ throw new Error("Invalid SMS message: message is required");
46
+ }
47
+
48
+ if (msg.message.length > 160) {
49
+ console.warn(
50
+ `SMS message exceeds 160 characters (${msg.message.length}), will be split`,
51
+ );
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Send SMS message
57
+ */
58
+ async send(message: SMSMessage): Promise<string | undefined> {
59
+ try {
60
+ if (this.config.dryRun) {
61
+ console.log(`[SMSChannel] Would send SMS to: ${message.recipient}`);
62
+ console.log(` Message: ${message.message.substring(0, 50)}...`);
63
+ this.sentCount++;
64
+ return this._generateMessageId();
65
+ }
66
+
67
+ // In a real implementation, delegate to appropriate driver
68
+ // For now, simulate sending
69
+ const messageId = this._generateMessageId();
70
+ console.log(
71
+ `[SMSChannel] SMS sent: ${messageId} to ${message.recipient}`,
72
+ );
73
+ this.sentCount++;
74
+
75
+ return messageId;
76
+ } catch (error) {
77
+ this.failureCount++;
78
+ throw new Error(
79
+ `Failed to send SMS: ${error instanceof Error ? error.message : String(error)}`,
80
+ );
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Get SMS channel metrics
86
+ */
87
+ getMetrics() {
88
+ const total = this.sentCount + this.failureCount;
89
+ return {
90
+ sent: this.sentCount,
91
+ failed: this.failureCount,
92
+ total,
93
+ successRate: total > 0 ? this.sentCount / total : 0,
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Generate a message ID
99
+ */
100
+ private _generateMessageId(): string {
101
+ const timestamp = Date.now();
102
+ const random = Math.random().toString(36).substring(2, 15);
103
+ return `sms_${timestamp}_${random}`;
104
+ }
105
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * WhatsApp Channel Service
3
+ *
4
+ * Sends notifications via WhatsApp through various drivers
5
+ * (Twilio, custom)
6
+ */
7
+
8
+ import type { WhatsAppChannelConfig, WhatsAppMessage } from "../types";
9
+ import { BaseChannelService } from "./base";
10
+
11
+ // ============= WhatsApp Channel Service =============
12
+
13
+ export class WhatsAppChannelService extends BaseChannelService<WhatsAppMessage> {
14
+ readonly name = "whatsapp";
15
+ private config: WhatsAppChannelConfig;
16
+ private sentCount = 0;
17
+ private failureCount = 0;
18
+
19
+ constructor(config: WhatsAppChannelConfig) {
20
+ super();
21
+ this.config = config;
22
+ }
23
+
24
+ /**
25
+ * Validate WhatsApp message structure
26
+ */
27
+ validate(message: unknown): asserts message is WhatsAppMessage {
28
+ if (typeof message !== "object" || message === null) {
29
+ throw new Error("Invalid WhatsApp message: must be an object");
30
+ }
31
+
32
+ const msg = message as Record<string, unknown>;
33
+
34
+ if (msg.channel !== "whatsapp") {
35
+ throw new Error('Invalid WhatsApp message: channel must be "whatsapp"');
36
+ }
37
+
38
+ if (typeof msg.recipient !== "string" || msg.recipient.length === 0) {
39
+ throw new Error(
40
+ "Invalid WhatsApp message: recipient (phone number) is required",
41
+ );
42
+ }
43
+
44
+ if (typeof msg.templateId !== "string" || msg.templateId.length === 0) {
45
+ throw new Error("Invalid WhatsApp message: templateId is required");
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Send WhatsApp message
51
+ */
52
+ async send(message: WhatsAppMessage): Promise<string | undefined> {
53
+ try {
54
+ if (this.config.dryRun) {
55
+ console.log(
56
+ `[WhatsAppChannel] Would send WhatsApp to: ${message.recipient}`,
57
+ );
58
+ console.log(` Template: ${message.templateId}`);
59
+ console.log(
60
+ ` Parameters: ${JSON.stringify(message.parameters || {})}`,
61
+ );
62
+ this.sentCount++;
63
+ return this._generateMessageId();
64
+ }
65
+
66
+ // In a real implementation, delegate to appropriate driver
67
+ // For now, simulate sending
68
+ const messageId = this._generateMessageId();
69
+ console.log(
70
+ `[WhatsAppChannel] WhatsApp sent: ${messageId} to ${message.recipient}`,
71
+ );
72
+ this.sentCount++;
73
+
74
+ return messageId;
75
+ } catch (error) {
76
+ this.failureCount++;
77
+ throw new Error(
78
+ `Failed to send WhatsApp: ${error instanceof Error ? error.message : String(error)}`,
79
+ );
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Get WhatsApp channel metrics
85
+ */
86
+ getMetrics() {
87
+ const total = this.sentCount + this.failureCount;
88
+ return {
89
+ sent: this.sentCount,
90
+ failed: this.failureCount,
91
+ total,
92
+ successRate: total > 0 ? this.sentCount / total : 0,
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Generate a message ID
98
+ */
99
+ private _generateMessageId(): string {
100
+ const timestamp = Date.now();
101
+ const random = Math.random().toString(36).substring(2, 15);
102
+ return `wa_${timestamp}_${random}`;
103
+ }
104
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Notification Module
3
+ *
4
+ * Multi-channel notification system with support for email, SMS, WhatsApp,
5
+ * push notifications, and custom channels. Uses registry pattern for
6
+ * dynamic channel registration.
7
+ */
8
+
9
+ // Export types
10
+ export type {
11
+ NotificationChannel,
12
+ NotificationMessage,
13
+ EmailRecipient,
14
+ EmailRecipients,
15
+ EmailAttachment,
16
+ EmailMessage,
17
+ SMSMessage,
18
+ WhatsAppMessage,
19
+ PushNotificationMessage,
20
+ BuiltInNotification,
21
+ Notifiable,
22
+ ChannelHealth,
23
+ ChannelService,
24
+ BaseChannelConfig,
25
+ EmailChannelConfig,
26
+ SMSChannelConfig,
27
+ WhatsAppChannelConfig,
28
+ PushChannelConfig,
29
+ ChannelConfig,
30
+ NotificationServiceConfig,
31
+ ChannelMetrics,
32
+ NotificationEvent,
33
+ NotificationEventType,
34
+ NotificationValidationResult,
35
+ StandardSchema,
36
+ } from "./types";
37
+
38
+ // Export service
39
+ export { NotificationService, createNotificationService } from "./service";
40
+
41
+ // Export base channel
42
+ export { BaseChannelService } from "./channels/base";
43
+
44
+ // Export built-in channels
45
+ export { EmailChannelService } from "./channels/email";
46
+ export { SMSChannelService } from "./channels/sms";
47
+ export { WhatsAppChannelService } from "./channels/whatsapp";
48
+ export { PushNotificationChannelService } from "./channels/push";
@@ -0,0 +1,354 @@
1
+ /**
2
+ * Notification Service
3
+ *
4
+ * Core notification service with registry-based channel management.
5
+ * Supports dynamic channel registration and multi-channel notification sending.
6
+ */
7
+
8
+ import type {
9
+ ChannelHealth,
10
+ ChannelMetrics,
11
+ ChannelService,
12
+ Notifiable,
13
+ NotificationChannel,
14
+ NotificationMessage,
15
+ NotificationServiceConfig,
16
+ TemplateRef,
17
+ } from "./types";
18
+ import { isTemplateRef } from "./types";
19
+
20
+ // ============= Notification Service =============
21
+
22
+ export class NotificationService {
23
+ private channels: Map<NotificationChannel, ChannelService> = new Map();
24
+ private config: NotificationServiceConfig;
25
+ private metrics: Map<NotificationChannel, ChannelMetrics> = new Map();
26
+
27
+ constructor(config: NotificationServiceConfig = {}) {
28
+ this.config = config;
29
+ }
30
+
31
+ /**
32
+ * Register a channel service
33
+ * @param service Channel service to register
34
+ * @throws Error if channel is already registered
35
+ */
36
+ registerChannel<T extends NotificationMessage>(
37
+ service: ChannelService<T>,
38
+ ): void {
39
+ if (this.channels.has(service.name)) {
40
+ throw new Error(`Channel already registered: ${service.name}`);
41
+ }
42
+ this.channels.set(service.name, service);
43
+
44
+ // Initialize metrics
45
+ if (this.config.enableMetrics) {
46
+ this.metrics.set(service.name, {
47
+ sent: 0,
48
+ failed: 0,
49
+ successRate: 0,
50
+ avgSendTime: 0,
51
+ totalSendTime: 0,
52
+ updatedAt: new Date(),
53
+ });
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Unregister a channel service
59
+ * @param channelName Name of the channel to unregister
60
+ */
61
+ unregisterChannel(channelName: NotificationChannel): void {
62
+ this.channels.delete(channelName);
63
+ this.metrics.delete(channelName);
64
+ }
65
+
66
+ /**
67
+ * Get a registered channel service
68
+ * @param channelName Name of the channel
69
+ * @returns Channel service or null if not found
70
+ */
71
+ getChannel(channelName: NotificationChannel): ChannelService | null {
72
+ return this.channels.get(channelName) || null;
73
+ }
74
+
75
+ /**
76
+ * Get all registered channel names
77
+ */
78
+ getChannels(): NotificationChannel[] {
79
+ return Array.from(this.channels.keys());
80
+ }
81
+
82
+ /**
83
+ * Check if a channel is registered
84
+ */
85
+ hasChannel(channelName: NotificationChannel): boolean {
86
+ return this.channels.has(channelName);
87
+ }
88
+
89
+ /**
90
+ * Send a notification via specified channel
91
+ * @param message Notification message
92
+ * @returns Message ID or undefined
93
+ * @throws Error if channel not found or message invalid
94
+ */
95
+ async send(message: NotificationMessage): Promise<string | undefined> {
96
+ const startTime = Date.now();
97
+
98
+ try {
99
+ const service = this.channels.get(message.channel);
100
+ if (!service) {
101
+ throw new Error(`Channel not registered: ${message.channel}`);
102
+ }
103
+
104
+ // Resolve template references
105
+ const resolvedMessage = await this._resolveTemplates(message);
106
+
107
+ // Validate message
108
+ service.validate(resolvedMessage);
109
+
110
+ // Send message
111
+ const messageId = await service.send(resolvedMessage);
112
+
113
+ // Update metrics
114
+ if (this.config.enableMetrics) {
115
+ this._updateMetrics(message.channel, true, Date.now() - startTime);
116
+ }
117
+
118
+ return messageId;
119
+ } catch (error) {
120
+ // Update metrics
121
+ if (this.config.enableMetrics) {
122
+ this._updateMetrics(message.channel, false, Date.now() - startTime);
123
+ }
124
+
125
+ throw error;
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Send multiple notifications (potentially different channels)
131
+ * @param messages Array of notification messages
132
+ * @returns Array of message IDs
133
+ */
134
+ async sendBatch(
135
+ messages: NotificationMessage[],
136
+ ): Promise<(string | undefined)[]> {
137
+ const results: (string | undefined)[] = [];
138
+
139
+ for (const message of messages) {
140
+ try {
141
+ const messageId = await this.send(message);
142
+ results.push(messageId);
143
+ } catch (error) {
144
+ results.push(undefined);
145
+ }
146
+ }
147
+
148
+ return results;
149
+ }
150
+
151
+ /**
152
+ * Send a notification from a Notifiable
153
+ * @param notifiable Notifiable instance
154
+ * @param channel Optional specific channel (otherwise uses buildAll or default)
155
+ * @returns Message ID(s)
156
+ */
157
+ async sendNotifiable(
158
+ notifiable: Notifiable,
159
+ channel?: NotificationChannel,
160
+ ): Promise<string | undefined> {
161
+ if (channel) {
162
+ // Send to specific channel
163
+ const message = await notifiable.build(channel);
164
+ return this.send(message);
165
+ }
166
+
167
+ // Try buildAll for multi-channel
168
+ if (notifiable.buildAll) {
169
+ const messages = await notifiable.buildAll();
170
+ const results = await this.sendBatch(
171
+ Object.values(messages).filter(
172
+ (m) => m !== undefined,
173
+ ) as NotificationMessage[],
174
+ );
175
+ return results[0]; // Return first message ID
176
+ }
177
+
178
+ // Fall back to build
179
+ const message = await notifiable.build();
180
+ return this.send(message);
181
+ }
182
+
183
+ /**
184
+ * Queue a notification for async sending (requires job queue integration)
185
+ * @param message Notification message
186
+ * @returns Job ID or undefined
187
+ */
188
+ async queue(message: NotificationMessage): Promise<string | undefined> {
189
+ // This will integrate with JobQueue in Phase 3
190
+ // For now, placeholder
191
+ return undefined;
192
+ }
193
+
194
+ /**
195
+ * Get health status for a channel
196
+ * @param channelName Name of the channel
197
+ */
198
+ async getChannelHealth(
199
+ channelName: NotificationChannel,
200
+ ): Promise<ChannelHealth | null> {
201
+ const service = this.channels.get(channelName);
202
+ if (!service) return null;
203
+
204
+ return (
205
+ service.getHealth?.() || {
206
+ status: "healthy" as const,
207
+ message: "OK",
208
+ checkedAt: new Date(),
209
+ }
210
+ );
211
+ }
212
+
213
+ /**
214
+ * Get health status for all channels
215
+ */
216
+ async getHealthStatus(): Promise<Record<NotificationChannel, ChannelHealth>> {
217
+ const health: Record<NotificationChannel, ChannelHealth> = {};
218
+
219
+ for (const [name] of this.channels) {
220
+ const channelHealth = await this.getChannelHealth(name);
221
+ if (channelHealth) {
222
+ health[name] = channelHealth;
223
+ }
224
+ }
225
+
226
+ return health;
227
+ }
228
+
229
+ /**
230
+ * Get metrics for a specific channel
231
+ * @param channelName Name of the channel
232
+ */
233
+ getChannelMetrics(channelName: NotificationChannel): ChannelMetrics | null {
234
+ return this.metrics.get(channelName) || null;
235
+ }
236
+
237
+ /**
238
+ * Get metrics for all channels
239
+ */
240
+ getAllMetrics(): Record<NotificationChannel, ChannelMetrics> {
241
+ const allMetrics: Record<NotificationChannel, ChannelMetrics> = {};
242
+
243
+ for (const [name, metrics] of this.metrics) {
244
+ allMetrics[name] = metrics;
245
+ }
246
+
247
+ return allMetrics;
248
+ }
249
+
250
+ /**
251
+ * Resolve TemplateRef objects in message fields to rendered strings
252
+ */
253
+ private async _resolveTemplates(
254
+ message: NotificationMessage,
255
+ ): Promise<NotificationMessage> {
256
+ const engine = this.config.templateEngine;
257
+ if (!engine) {
258
+ // If no engine, check that no TemplateRef fields exist
259
+ this._assertNoTemplateRefs(message);
260
+ return message;
261
+ }
262
+
263
+ const channel = message.channel;
264
+ const msg = { ...message } as Record<string, unknown>;
265
+
266
+ const resolveField = async (
267
+ value: unknown,
268
+ defaultFormat: "html" | "text",
269
+ ): Promise<string> => {
270
+ if (!isTemplateRef(value)) return value as string;
271
+ const fmt = value.outputFormat ?? defaultFormat;
272
+ const variant = value.variant ?? engine.getVariantForChannel(channel);
273
+ return engine.render(value.templateId, value.data, {
274
+ variant,
275
+ outputFormat: fmt,
276
+ });
277
+ };
278
+
279
+ // Email: resolve html (→ HTML) and text (→ text)
280
+ if (channel === "email") {
281
+ if (isTemplateRef(msg.html)) {
282
+ msg.html = await resolveField(msg.html, "html");
283
+ }
284
+ if (isTemplateRef(msg.text)) {
285
+ msg.text = await resolveField(msg.text, "text");
286
+ }
287
+ }
288
+ // SMS: resolve message (→ text)
289
+ else if (channel === "sms") {
290
+ if (isTemplateRef(msg.message)) {
291
+ msg.message = await resolveField(msg.message, "text");
292
+ }
293
+ }
294
+ // Push: resolve title and body (→ text)
295
+ else if (channel === "push") {
296
+ if (isTemplateRef(msg.title)) {
297
+ msg.title = await resolveField(msg.title, "text");
298
+ }
299
+ if (isTemplateRef(msg.body)) {
300
+ msg.body = await resolveField(msg.body, "text");
301
+ }
302
+ }
303
+
304
+ return msg as NotificationMessage;
305
+ }
306
+
307
+ /**
308
+ * Assert that no TemplateRef fields exist in the message
309
+ * Throws if a TemplateRef is found but no templateEngine is configured
310
+ */
311
+ private _assertNoTemplateRefs(message: Record<string, unknown>): void {
312
+ for (const [key, value] of Object.entries(message)) {
313
+ if (isTemplateRef(value)) {
314
+ throw new Error(
315
+ `TemplateRef found in field "${key}" but no templateEngine is configured in NotificationService`,
316
+ );
317
+ }
318
+ }
319
+ }
320
+
321
+ /**
322
+ * Update metrics for a channel
323
+ */
324
+ private _updateMetrics(
325
+ channelName: NotificationChannel,
326
+ success: boolean,
327
+ duration: number,
328
+ ): void {
329
+ const metrics = this.metrics.get(channelName);
330
+ if (!metrics) return;
331
+
332
+ if (success) {
333
+ metrics.sent++;
334
+ metrics.totalSendTime += duration;
335
+ } else {
336
+ metrics.failed++;
337
+ }
338
+
339
+ const total = metrics.sent + metrics.failed;
340
+ metrics.successRate = total > 0 ? metrics.sent / total : 0;
341
+ metrics.avgSendTime =
342
+ metrics.sent > 0 ? metrics.totalSendTime / metrics.sent : 0;
343
+ metrics.updatedAt = new Date();
344
+ }
345
+ }
346
+
347
+ /**
348
+ * Factory function to create notification service
349
+ */
350
+ export function createNotificationService(
351
+ config?: NotificationServiceConfig,
352
+ ): NotificationService {
353
+ return new NotificationService(config);
354
+ }