@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.
- package/README.md +136 -16
- package/dist/cli/{index.js → bin.js} +3036 -1421
- package/dist/container/index.js +250 -0
- package/dist/context/index.js +219 -0
- package/dist/database/index.js +493 -0
- package/dist/frontend/index.js +7697 -0
- package/dist/health/index.js +364 -0
- package/dist/i18n/index.js +345 -0
- package/dist/index.js +11043 -6482
- package/dist/jobs/index.js +819 -0
- package/dist/lock/index.js +367 -0
- package/dist/logger/index.js +281 -0
- package/dist/metrics/index.js +289 -0
- package/dist/middleware/index.js +77 -0
- package/dist/migrations/index.js +571 -0
- package/dist/modules/index.js +3346 -0
- package/dist/notification/index.js +484 -0
- package/dist/observability/index.js +331 -0
- package/dist/openapi/index.js +776 -0
- package/dist/orm/index.js +1356 -0
- package/dist/router/index.js +886 -0
- package/dist/rpc/index.js +691 -0
- package/dist/schema/index.js +400 -0
- package/dist/telemetry/index.js +595 -0
- package/dist/template/index.js +640 -0
- package/dist/templates/index.js +640 -0
- package/dist/testing/index.js +1111 -0
- package/dist/types/index.js +60 -0
- package/package.json +121 -27
- package/src/cache/index.ts +2 -1
- package/src/cli/bin.ts +2 -2
- package/src/cli/commands/build.ts +183 -165
- package/src/cli/commands/dev.ts +96 -89
- package/src/cli/commands/generate.ts +142 -111
- package/src/cli/commands/help.ts +20 -16
- package/src/cli/commands/index.ts +3 -6
- package/src/cli/commands/migration.ts +124 -105
- package/src/cli/commands/new.ts +392 -438
- package/src/cli/commands/start.ts +81 -79
- package/src/cli/core/args.ts +68 -50
- package/src/cli/core/console.ts +89 -95
- package/src/cli/core/index.ts +4 -4
- package/src/cli/core/prompt.ts +65 -62
- package/src/cli/core/spinner.ts +23 -20
- package/src/cli/index.ts +46 -38
- package/src/cli/templates/database/index.ts +61 -0
- package/src/cli/templates/database/mysql.ts +14 -0
- package/src/cli/templates/database/none.ts +16 -0
- package/src/cli/templates/database/postgresql.ts +14 -0
- package/src/cli/templates/database/sqlite.ts +14 -0
- package/src/cli/templates/deploy.ts +29 -26
- package/src/cli/templates/docker.ts +41 -30
- package/src/cli/templates/frontend/index.ts +63 -0
- package/src/cli/templates/frontend/none.ts +17 -0
- package/src/cli/templates/frontend/react.ts +140 -0
- package/src/cli/templates/frontend/solid.ts +134 -0
- package/src/cli/templates/frontend/svelte.ts +131 -0
- package/src/cli/templates/frontend/vue.ts +130 -0
- package/src/cli/templates/generators/index.ts +339 -0
- package/src/cli/templates/generators/types.ts +56 -0
- package/src/cli/templates/index.ts +35 -2
- package/src/cli/templates/project/api.ts +81 -0
- package/src/cli/templates/project/default.ts +140 -0
- package/src/cli/templates/project/fullstack.ts +111 -0
- package/src/cli/templates/project/index.ts +95 -0
- package/src/cli/templates/project/minimal.ts +45 -0
- package/src/cli/templates/project/types.ts +94 -0
- package/src/cli/templates/project/website.ts +263 -0
- package/src/cli/utils/fs.ts +55 -41
- package/src/cli/utils/index.ts +3 -2
- package/src/cli/utils/strings.ts +47 -33
- package/src/cli/utils/version.ts +47 -0
- package/src/config/env-validation.ts +100 -0
- package/src/config/env.ts +169 -41
- package/src/config/index.ts +28 -20
- package/src/config/loader.ts +25 -16
- package/src/config/merge.ts +21 -10
- package/src/config/types.ts +545 -25
- package/src/config/validation.ts +215 -7
- package/src/container/forward-ref.ts +22 -22
- package/src/container/index.ts +34 -12
- package/src/context/index.ts +11 -1
- package/src/database/index.ts +7 -190
- package/src/database/orm/builder.ts +457 -0
- package/src/database/orm/casts/index.ts +130 -0
- package/src/database/orm/casts/types.ts +25 -0
- package/src/database/orm/compiler.ts +304 -0
- package/src/database/orm/hooks/index.ts +114 -0
- package/src/database/orm/index.ts +61 -0
- package/src/database/orm/model-registry.ts +59 -0
- package/src/database/orm/model.ts +821 -0
- package/src/database/orm/relationships/base.ts +146 -0
- package/src/database/orm/relationships/belongs-to-many.ts +179 -0
- package/src/database/orm/relationships/belongs-to.ts +56 -0
- package/src/database/orm/relationships/has-many.ts +45 -0
- package/src/database/orm/relationships/has-one.ts +41 -0
- package/src/database/orm/relationships/index.ts +11 -0
- package/src/database/orm/scopes/index.ts +55 -0
- package/src/events/__tests__/event-system.test.ts +235 -0
- package/src/events/config.ts +238 -0
- package/src/events/example-usage.ts +185 -0
- package/src/events/index.ts +278 -0
- package/src/events/manager.ts +385 -0
- package/src/events/registry.ts +182 -0
- package/src/events/types.ts +124 -0
- package/src/frontend/api-routes.ts +65 -23
- package/src/frontend/bundler.ts +76 -34
- package/src/frontend/console-client.ts +2 -2
- package/src/frontend/console-stream.ts +94 -38
- package/src/frontend/dev-server.ts +94 -46
- package/src/frontend/file-router.ts +61 -19
- package/src/frontend/frameworks/index.ts +37 -10
- package/src/frontend/frameworks/react.ts +10 -8
- package/src/frontend/frameworks/solid.ts +11 -9
- package/src/frontend/frameworks/svelte.ts +15 -9
- package/src/frontend/frameworks/vue.ts +13 -11
- package/src/frontend/hmr-client.ts +12 -10
- package/src/frontend/hmr.ts +146 -103
- package/src/frontend/index.ts +14 -5
- package/src/frontend/islands.ts +41 -22
- package/src/frontend/isr.ts +59 -37
- package/src/frontend/layout.ts +36 -21
- package/src/frontend/ssr/react.ts +74 -27
- package/src/frontend/ssr/solid.ts +54 -20
- package/src/frontend/ssr/svelte.ts +48 -14
- package/src/frontend/ssr/vue.ts +50 -18
- package/src/frontend/ssr.ts +83 -39
- package/src/frontend/types.ts +91 -56
- package/src/health/index.ts +21 -9
- package/src/i18n/engine.ts +305 -0
- package/src/i18n/index.ts +38 -0
- package/src/i18n/loader.ts +218 -0
- package/src/i18n/middleware.ts +164 -0
- package/src/i18n/negotiator.ts +162 -0
- package/src/i18n/types.ts +158 -0
- package/src/index.ts +179 -27
- package/src/jobs/drivers/memory.ts +315 -0
- package/src/jobs/drivers/redis.ts +459 -0
- package/src/jobs/index.ts +30 -0
- package/src/jobs/queue.ts +281 -0
- package/src/jobs/types.ts +295 -0
- package/src/jobs/worker.ts +380 -0
- package/src/logger/index.ts +1 -3
- package/src/logger/transports/index.ts +62 -22
- package/src/metrics/index.ts +25 -16
- package/src/migrations/index.ts +9 -0
- package/src/modules/filters.ts +13 -17
- package/src/modules/guards.ts +49 -26
- package/src/modules/index.ts +409 -298
- package/src/modules/interceptors.ts +58 -20
- package/src/modules/lazy.ts +11 -19
- package/src/modules/lifecycle.ts +15 -7
- package/src/modules/metadata.ts +15 -5
- package/src/modules/pipes.ts +94 -72
- package/src/notification/channels/base.ts +68 -0
- package/src/notification/channels/email.ts +105 -0
- package/src/notification/channels/push.ts +104 -0
- package/src/notification/channels/sms.ts +105 -0
- package/src/notification/channels/whatsapp.ts +104 -0
- package/src/notification/index.ts +48 -0
- package/src/notification/service.ts +354 -0
- package/src/notification/types.ts +344 -0
- package/src/observability/__tests__/observability.test.ts +483 -0
- package/src/observability/breadcrumbs.ts +114 -0
- package/src/observability/index.ts +136 -0
- package/src/observability/interceptor.ts +85 -0
- package/src/observability/service.ts +303 -0
- package/src/observability/trace.ts +37 -0
- package/src/observability/types.ts +196 -0
- package/src/openapi/__tests__/decorators.test.ts +335 -0
- package/src/openapi/__tests__/document-builder.test.ts +285 -0
- package/src/openapi/__tests__/route-scanner.test.ts +334 -0
- package/src/openapi/__tests__/schema-generator.test.ts +275 -0
- package/src/openapi/decorators.ts +328 -0
- package/src/openapi/document-builder.ts +274 -0
- package/src/openapi/index.ts +112 -0
- package/src/openapi/metadata.ts +112 -0
- package/src/openapi/route-scanner.ts +289 -0
- package/src/openapi/schema-generator.ts +256 -0
- package/src/openapi/swagger-module.ts +166 -0
- package/src/openapi/types.ts +398 -0
- package/src/orm/index.ts +10 -0
- package/src/rpc/index.ts +3 -1
- package/src/schema/index.ts +9 -0
- package/src/security/index.ts +15 -6
- package/src/ssg/index.ts +9 -8
- package/src/telemetry/index.ts +76 -22
- package/src/template/index.ts +7 -0
- package/src/templates/engine.ts +224 -0
- package/src/templates/index.ts +9 -0
- package/src/templates/loader.ts +331 -0
- package/src/templates/renderers/markdown.ts +212 -0
- package/src/templates/renderers/simple.ts +269 -0
- package/src/templates/types.ts +154 -0
- package/src/testing/index.ts +100 -27
- package/src/types/optional-deps.d.ts +347 -187
- package/src/validation/index.ts +92 -2
- package/src/validation/schemas.ts +536 -0
- package/tests/integration/fullstack.test.ts +4 -4
- package/tests/unit/database.test.ts +2 -72
- package/tests/unit/env-validation.test.ts +166 -0
- package/tests/unit/events.test.ts +910 -0
- package/tests/unit/i18n.test.ts +455 -0
- package/tests/unit/jobs.test.ts +493 -0
- package/tests/unit/notification.test.ts +988 -0
- package/tests/unit/observability.test.ts +453 -0
- package/tests/unit/orm/builder.test.ts +323 -0
- package/tests/unit/orm/casts.test.ts +179 -0
- package/tests/unit/orm/compiler.test.ts +220 -0
- package/tests/unit/orm/eager-loading.test.ts +285 -0
- package/tests/unit/orm/hooks.test.ts +191 -0
- package/tests/unit/orm/model.test.ts +373 -0
- package/tests/unit/orm/relationships.test.ts +303 -0
- package/tests/unit/orm/scopes.test.ts +74 -0
- package/tests/unit/templates-simple.test.ts +53 -0
- package/tests/unit/templates.test.ts +454 -0
- package/tests/unit/validation.test.ts +18 -24
- package/tsconfig.json +11 -3
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __export = (target, all) => {
|
|
4
|
+
for (var name in all)
|
|
5
|
+
__defProp(target, name, {
|
|
6
|
+
get: all[name],
|
|
7
|
+
enumerable: true,
|
|
8
|
+
configurable: true,
|
|
9
|
+
set: (newValue) => all[name] = () => newValue
|
|
10
|
+
});
|
|
11
|
+
};
|
|
12
|
+
var __legacyDecorateClassTS = function(decorators, target, key, desc) {
|
|
13
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
14
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
|
|
15
|
+
r = Reflect.decorate(decorators, target, key, desc);
|
|
16
|
+
else
|
|
17
|
+
for (var i = decorators.length - 1;i >= 0; i--)
|
|
18
|
+
if (d = decorators[i])
|
|
19
|
+
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
20
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
21
|
+
};
|
|
22
|
+
var __legacyMetadataTS = (k, v) => {
|
|
23
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function")
|
|
24
|
+
return Reflect.metadata(k, v);
|
|
25
|
+
};
|
|
26
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
27
|
+
var __require = import.meta.require;
|
|
28
|
+
|
|
29
|
+
// src/notification/types.ts
|
|
30
|
+
function isTemplateRef(value) {
|
|
31
|
+
return typeof value === "object" && value !== null && "templateId" in value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/notification/service.ts
|
|
35
|
+
class NotificationService {
|
|
36
|
+
channels = new Map;
|
|
37
|
+
config;
|
|
38
|
+
metrics = new Map;
|
|
39
|
+
constructor(config = {}) {
|
|
40
|
+
this.config = config;
|
|
41
|
+
}
|
|
42
|
+
registerChannel(service) {
|
|
43
|
+
if (this.channels.has(service.name)) {
|
|
44
|
+
throw new Error(`Channel already registered: ${service.name}`);
|
|
45
|
+
}
|
|
46
|
+
this.channels.set(service.name, service);
|
|
47
|
+
if (this.config.enableMetrics) {
|
|
48
|
+
this.metrics.set(service.name, {
|
|
49
|
+
sent: 0,
|
|
50
|
+
failed: 0,
|
|
51
|
+
successRate: 0,
|
|
52
|
+
avgSendTime: 0,
|
|
53
|
+
totalSendTime: 0,
|
|
54
|
+
updatedAt: new Date
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
unregisterChannel(channelName) {
|
|
59
|
+
this.channels.delete(channelName);
|
|
60
|
+
this.metrics.delete(channelName);
|
|
61
|
+
}
|
|
62
|
+
getChannel(channelName) {
|
|
63
|
+
return this.channels.get(channelName) || null;
|
|
64
|
+
}
|
|
65
|
+
getChannels() {
|
|
66
|
+
return Array.from(this.channels.keys());
|
|
67
|
+
}
|
|
68
|
+
hasChannel(channelName) {
|
|
69
|
+
return this.channels.has(channelName);
|
|
70
|
+
}
|
|
71
|
+
async send(message) {
|
|
72
|
+
const startTime = Date.now();
|
|
73
|
+
try {
|
|
74
|
+
const service = this.channels.get(message.channel);
|
|
75
|
+
if (!service) {
|
|
76
|
+
throw new Error(`Channel not registered: ${message.channel}`);
|
|
77
|
+
}
|
|
78
|
+
const resolvedMessage = await this._resolveTemplates(message);
|
|
79
|
+
service.validate(resolvedMessage);
|
|
80
|
+
const messageId = await service.send(resolvedMessage);
|
|
81
|
+
if (this.config.enableMetrics) {
|
|
82
|
+
this._updateMetrics(message.channel, true, Date.now() - startTime);
|
|
83
|
+
}
|
|
84
|
+
return messageId;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (this.config.enableMetrics) {
|
|
87
|
+
this._updateMetrics(message.channel, false, Date.now() - startTime);
|
|
88
|
+
}
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async sendBatch(messages) {
|
|
93
|
+
const results = [];
|
|
94
|
+
for (const message of messages) {
|
|
95
|
+
try {
|
|
96
|
+
const messageId = await this.send(message);
|
|
97
|
+
results.push(messageId);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
results.push(undefined);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return results;
|
|
103
|
+
}
|
|
104
|
+
async sendNotifiable(notifiable, channel) {
|
|
105
|
+
if (channel) {
|
|
106
|
+
const message2 = await notifiable.build(channel);
|
|
107
|
+
return this.send(message2);
|
|
108
|
+
}
|
|
109
|
+
if (notifiable.buildAll) {
|
|
110
|
+
const messages = await notifiable.buildAll();
|
|
111
|
+
const results = await this.sendBatch(Object.values(messages).filter((m) => m !== undefined));
|
|
112
|
+
return results[0];
|
|
113
|
+
}
|
|
114
|
+
const message = await notifiable.build();
|
|
115
|
+
return this.send(message);
|
|
116
|
+
}
|
|
117
|
+
async queue(message) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
async getChannelHealth(channelName) {
|
|
121
|
+
const service = this.channels.get(channelName);
|
|
122
|
+
if (!service)
|
|
123
|
+
return null;
|
|
124
|
+
return service.getHealth?.() || {
|
|
125
|
+
status: "healthy",
|
|
126
|
+
message: "OK",
|
|
127
|
+
checkedAt: new Date
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
async getHealthStatus() {
|
|
131
|
+
const health = {};
|
|
132
|
+
for (const [name] of this.channels) {
|
|
133
|
+
const channelHealth = await this.getChannelHealth(name);
|
|
134
|
+
if (channelHealth) {
|
|
135
|
+
health[name] = channelHealth;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return health;
|
|
139
|
+
}
|
|
140
|
+
getChannelMetrics(channelName) {
|
|
141
|
+
return this.metrics.get(channelName) || null;
|
|
142
|
+
}
|
|
143
|
+
getAllMetrics() {
|
|
144
|
+
const allMetrics = {};
|
|
145
|
+
for (const [name, metrics] of this.metrics) {
|
|
146
|
+
allMetrics[name] = metrics;
|
|
147
|
+
}
|
|
148
|
+
return allMetrics;
|
|
149
|
+
}
|
|
150
|
+
async _resolveTemplates(message) {
|
|
151
|
+
const engine = this.config.templateEngine;
|
|
152
|
+
if (!engine) {
|
|
153
|
+
this._assertNoTemplateRefs(message);
|
|
154
|
+
return message;
|
|
155
|
+
}
|
|
156
|
+
const channel = message.channel;
|
|
157
|
+
const msg = { ...message };
|
|
158
|
+
const resolveField = async (value, defaultFormat) => {
|
|
159
|
+
if (!isTemplateRef(value))
|
|
160
|
+
return value;
|
|
161
|
+
const fmt = value.outputFormat ?? defaultFormat;
|
|
162
|
+
const variant = value.variant ?? engine.getVariantForChannel(channel);
|
|
163
|
+
return engine.render(value.templateId, value.data, {
|
|
164
|
+
variant,
|
|
165
|
+
outputFormat: fmt
|
|
166
|
+
});
|
|
167
|
+
};
|
|
168
|
+
if (channel === "email") {
|
|
169
|
+
if (isTemplateRef(msg.html)) {
|
|
170
|
+
msg.html = await resolveField(msg.html, "html");
|
|
171
|
+
}
|
|
172
|
+
if (isTemplateRef(msg.text)) {
|
|
173
|
+
msg.text = await resolveField(msg.text, "text");
|
|
174
|
+
}
|
|
175
|
+
} else if (channel === "sms") {
|
|
176
|
+
if (isTemplateRef(msg.message)) {
|
|
177
|
+
msg.message = await resolveField(msg.message, "text");
|
|
178
|
+
}
|
|
179
|
+
} else if (channel === "push") {
|
|
180
|
+
if (isTemplateRef(msg.title)) {
|
|
181
|
+
msg.title = await resolveField(msg.title, "text");
|
|
182
|
+
}
|
|
183
|
+
if (isTemplateRef(msg.body)) {
|
|
184
|
+
msg.body = await resolveField(msg.body, "text");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return msg;
|
|
188
|
+
}
|
|
189
|
+
_assertNoTemplateRefs(message) {
|
|
190
|
+
for (const [key, value] of Object.entries(message)) {
|
|
191
|
+
if (isTemplateRef(value)) {
|
|
192
|
+
throw new Error(`TemplateRef found in field "${key}" but no templateEngine is configured in NotificationService`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
_updateMetrics(channelName, success, duration) {
|
|
197
|
+
const metrics = this.metrics.get(channelName);
|
|
198
|
+
if (!metrics)
|
|
199
|
+
return;
|
|
200
|
+
if (success) {
|
|
201
|
+
metrics.sent++;
|
|
202
|
+
metrics.totalSendTime += duration;
|
|
203
|
+
} else {
|
|
204
|
+
metrics.failed++;
|
|
205
|
+
}
|
|
206
|
+
const total = metrics.sent + metrics.failed;
|
|
207
|
+
metrics.successRate = total > 0 ? metrics.sent / total : 0;
|
|
208
|
+
metrics.avgSendTime = metrics.sent > 0 ? metrics.totalSendTime / metrics.sent : 0;
|
|
209
|
+
metrics.updatedAt = new Date;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function createNotificationService(config) {
|
|
213
|
+
return new NotificationService(config);
|
|
214
|
+
}
|
|
215
|
+
// src/notification/channels/base.ts
|
|
216
|
+
class BaseChannelService {
|
|
217
|
+
configSchema;
|
|
218
|
+
async getHealth() {
|
|
219
|
+
return {
|
|
220
|
+
status: "healthy",
|
|
221
|
+
message: "Channel is operational",
|
|
222
|
+
checkedAt: new Date
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
validateField(value, expectedType, fieldName) {
|
|
226
|
+
const actualType = typeof value;
|
|
227
|
+
if (actualType !== expectedType) {
|
|
228
|
+
throw new Error(`Invalid ${this.name} message: ${fieldName} must be ${expectedType}, got ${actualType}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
validateRequired(value, fieldName) {
|
|
232
|
+
if (value === undefined || value === null) {
|
|
233
|
+
throw new Error(`Invalid ${this.name} message: ${fieldName} is required`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// src/notification/channels/email.ts
|
|
238
|
+
class EmailChannelService extends BaseChannelService {
|
|
239
|
+
name = "email";
|
|
240
|
+
config;
|
|
241
|
+
sentCount = 0;
|
|
242
|
+
failureCount = 0;
|
|
243
|
+
constructor(config) {
|
|
244
|
+
super();
|
|
245
|
+
this.config = config;
|
|
246
|
+
}
|
|
247
|
+
validate(message) {
|
|
248
|
+
if (typeof message !== "object" || message === null) {
|
|
249
|
+
throw new Error("Invalid email message: must be an object");
|
|
250
|
+
}
|
|
251
|
+
const msg = message;
|
|
252
|
+
if (msg.channel !== "email") {
|
|
253
|
+
throw new Error('Invalid email message: channel must be "email"');
|
|
254
|
+
}
|
|
255
|
+
if (typeof msg.recipient !== "string" || !msg.recipient.includes("@")) {
|
|
256
|
+
throw new Error("Invalid email message: recipient must be a valid email");
|
|
257
|
+
}
|
|
258
|
+
if (typeof msg.subject !== "string" || msg.subject.length === 0) {
|
|
259
|
+
throw new Error("Invalid email message: subject is required");
|
|
260
|
+
}
|
|
261
|
+
if (!msg.html && !msg.text) {
|
|
262
|
+
throw new Error("Invalid email message: either html or text must be provided");
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
async send(message) {
|
|
266
|
+
try {
|
|
267
|
+
if (this.config.dryRun) {
|
|
268
|
+
console.log(`[EmailChannel] Would send email to: ${message.recipient}`);
|
|
269
|
+
console.log(` Subject: ${message.subject}`);
|
|
270
|
+
this.sentCount++;
|
|
271
|
+
return this._generateMessageId();
|
|
272
|
+
}
|
|
273
|
+
const messageId = this._generateMessageId();
|
|
274
|
+
console.log(`[EmailChannel] Email sent: ${messageId} to ${message.recipient}`);
|
|
275
|
+
this.sentCount++;
|
|
276
|
+
return messageId;
|
|
277
|
+
} catch (error) {
|
|
278
|
+
this.failureCount++;
|
|
279
|
+
throw new Error(`Failed to send email: ${error instanceof Error ? error.message : String(error)}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
getMetrics() {
|
|
283
|
+
const total = this.sentCount + this.failureCount;
|
|
284
|
+
return {
|
|
285
|
+
sent: this.sentCount,
|
|
286
|
+
failed: this.failureCount,
|
|
287
|
+
total,
|
|
288
|
+
successRate: total > 0 ? this.sentCount / total : 0
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
_generateMessageId() {
|
|
292
|
+
const timestamp = Date.now();
|
|
293
|
+
const random = Math.random().toString(36).substring(2, 15);
|
|
294
|
+
return `${timestamp}.${random}@bueno.local`;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// src/notification/channels/sms.ts
|
|
298
|
+
class SMSChannelService extends BaseChannelService {
|
|
299
|
+
name = "sms";
|
|
300
|
+
config;
|
|
301
|
+
sentCount = 0;
|
|
302
|
+
failureCount = 0;
|
|
303
|
+
constructor(config) {
|
|
304
|
+
super();
|
|
305
|
+
this.config = config;
|
|
306
|
+
}
|
|
307
|
+
validate(message) {
|
|
308
|
+
if (typeof message !== "object" || message === null) {
|
|
309
|
+
throw new Error("Invalid SMS message: must be an object");
|
|
310
|
+
}
|
|
311
|
+
const msg = message;
|
|
312
|
+
if (msg.channel !== "sms") {
|
|
313
|
+
throw new Error('Invalid SMS message: channel must be "sms"');
|
|
314
|
+
}
|
|
315
|
+
if (typeof msg.recipient !== "string" || msg.recipient.length === 0) {
|
|
316
|
+
throw new Error("Invalid SMS message: recipient (phone number) is required");
|
|
317
|
+
}
|
|
318
|
+
if (typeof msg.message !== "string" || msg.message.length === 0) {
|
|
319
|
+
throw new Error("Invalid SMS message: message is required");
|
|
320
|
+
}
|
|
321
|
+
if (msg.message.length > 160) {
|
|
322
|
+
console.warn(`SMS message exceeds 160 characters (${msg.message.length}), will be split`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
async send(message) {
|
|
326
|
+
try {
|
|
327
|
+
if (this.config.dryRun) {
|
|
328
|
+
console.log(`[SMSChannel] Would send SMS to: ${message.recipient}`);
|
|
329
|
+
console.log(` Message: ${message.message.substring(0, 50)}...`);
|
|
330
|
+
this.sentCount++;
|
|
331
|
+
return this._generateMessageId();
|
|
332
|
+
}
|
|
333
|
+
const messageId = this._generateMessageId();
|
|
334
|
+
console.log(`[SMSChannel] SMS sent: ${messageId} to ${message.recipient}`);
|
|
335
|
+
this.sentCount++;
|
|
336
|
+
return messageId;
|
|
337
|
+
} catch (error) {
|
|
338
|
+
this.failureCount++;
|
|
339
|
+
throw new Error(`Failed to send SMS: ${error instanceof Error ? error.message : String(error)}`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
getMetrics() {
|
|
343
|
+
const total = this.sentCount + this.failureCount;
|
|
344
|
+
return {
|
|
345
|
+
sent: this.sentCount,
|
|
346
|
+
failed: this.failureCount,
|
|
347
|
+
total,
|
|
348
|
+
successRate: total > 0 ? this.sentCount / total : 0
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
_generateMessageId() {
|
|
352
|
+
const timestamp = Date.now();
|
|
353
|
+
const random = Math.random().toString(36).substring(2, 15);
|
|
354
|
+
return `sms_${timestamp}_${random}`;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
// src/notification/channels/whatsapp.ts
|
|
358
|
+
class WhatsAppChannelService extends BaseChannelService {
|
|
359
|
+
name = "whatsapp";
|
|
360
|
+
config;
|
|
361
|
+
sentCount = 0;
|
|
362
|
+
failureCount = 0;
|
|
363
|
+
constructor(config) {
|
|
364
|
+
super();
|
|
365
|
+
this.config = config;
|
|
366
|
+
}
|
|
367
|
+
validate(message) {
|
|
368
|
+
if (typeof message !== "object" || message === null) {
|
|
369
|
+
throw new Error("Invalid WhatsApp message: must be an object");
|
|
370
|
+
}
|
|
371
|
+
const msg = message;
|
|
372
|
+
if (msg.channel !== "whatsapp") {
|
|
373
|
+
throw new Error('Invalid WhatsApp message: channel must be "whatsapp"');
|
|
374
|
+
}
|
|
375
|
+
if (typeof msg.recipient !== "string" || msg.recipient.length === 0) {
|
|
376
|
+
throw new Error("Invalid WhatsApp message: recipient (phone number) is required");
|
|
377
|
+
}
|
|
378
|
+
if (typeof msg.templateId !== "string" || msg.templateId.length === 0) {
|
|
379
|
+
throw new Error("Invalid WhatsApp message: templateId is required");
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
async send(message) {
|
|
383
|
+
try {
|
|
384
|
+
if (this.config.dryRun) {
|
|
385
|
+
console.log(`[WhatsAppChannel] Would send WhatsApp to: ${message.recipient}`);
|
|
386
|
+
console.log(` Template: ${message.templateId}`);
|
|
387
|
+
console.log(` Parameters: ${JSON.stringify(message.parameters || {})}`);
|
|
388
|
+
this.sentCount++;
|
|
389
|
+
return this._generateMessageId();
|
|
390
|
+
}
|
|
391
|
+
const messageId = this._generateMessageId();
|
|
392
|
+
console.log(`[WhatsAppChannel] WhatsApp sent: ${messageId} to ${message.recipient}`);
|
|
393
|
+
this.sentCount++;
|
|
394
|
+
return messageId;
|
|
395
|
+
} catch (error) {
|
|
396
|
+
this.failureCount++;
|
|
397
|
+
throw new Error(`Failed to send WhatsApp: ${error instanceof Error ? error.message : String(error)}`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
getMetrics() {
|
|
401
|
+
const total = this.sentCount + this.failureCount;
|
|
402
|
+
return {
|
|
403
|
+
sent: this.sentCount,
|
|
404
|
+
failed: this.failureCount,
|
|
405
|
+
total,
|
|
406
|
+
successRate: total > 0 ? this.sentCount / total : 0
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
_generateMessageId() {
|
|
410
|
+
const timestamp = Date.now();
|
|
411
|
+
const random = Math.random().toString(36).substring(2, 15);
|
|
412
|
+
return `wa_${timestamp}_${random}`;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
// src/notification/channels/push.ts
|
|
416
|
+
class PushNotificationChannelService extends BaseChannelService {
|
|
417
|
+
name = "push";
|
|
418
|
+
config;
|
|
419
|
+
sentCount = 0;
|
|
420
|
+
failureCount = 0;
|
|
421
|
+
constructor(config) {
|
|
422
|
+
super();
|
|
423
|
+
this.config = config;
|
|
424
|
+
}
|
|
425
|
+
validate(message) {
|
|
426
|
+
if (typeof message !== "object" || message === null) {
|
|
427
|
+
throw new Error("Invalid push message: must be an object");
|
|
428
|
+
}
|
|
429
|
+
const msg = message;
|
|
430
|
+
if (msg.channel !== "push") {
|
|
431
|
+
throw new Error('Invalid push message: channel must be "push"');
|
|
432
|
+
}
|
|
433
|
+
if (typeof msg.recipient !== "string" || msg.recipient.length === 0) {
|
|
434
|
+
throw new Error("Invalid push message: recipient (device token) is required");
|
|
435
|
+
}
|
|
436
|
+
if (typeof msg.title !== "string" || msg.title.length === 0) {
|
|
437
|
+
throw new Error("Invalid push message: title is required");
|
|
438
|
+
}
|
|
439
|
+
if (typeof msg.body !== "string" || msg.body.length === 0) {
|
|
440
|
+
throw new Error("Invalid push message: body is required");
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
async send(message) {
|
|
444
|
+
try {
|
|
445
|
+
if (this.config.dryRun) {
|
|
446
|
+
console.log(`[PushChannel] Would send push to: ${message.recipient}`);
|
|
447
|
+
console.log(` Title: ${message.title}`);
|
|
448
|
+
console.log(` Body: ${message.body}`);
|
|
449
|
+
this.sentCount++;
|
|
450
|
+
return this._generateMessageId();
|
|
451
|
+
}
|
|
452
|
+
const messageId = this._generateMessageId();
|
|
453
|
+
console.log(`[PushChannel] Push sent: ${messageId} to ${message.recipient}`);
|
|
454
|
+
this.sentCount++;
|
|
455
|
+
return messageId;
|
|
456
|
+
} catch (error) {
|
|
457
|
+
this.failureCount++;
|
|
458
|
+
throw new Error(`Failed to send push: ${error instanceof Error ? error.message : String(error)}`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
getMetrics() {
|
|
462
|
+
const total = this.sentCount + this.failureCount;
|
|
463
|
+
return {
|
|
464
|
+
sent: this.sentCount,
|
|
465
|
+
failed: this.failureCount,
|
|
466
|
+
total,
|
|
467
|
+
successRate: total > 0 ? this.sentCount / total : 0
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
_generateMessageId() {
|
|
471
|
+
const timestamp = Date.now();
|
|
472
|
+
const random = Math.random().toString(36).substring(2, 15);
|
|
473
|
+
return `push_${timestamp}_${random}`;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
export {
|
|
477
|
+
createNotificationService,
|
|
478
|
+
WhatsAppChannelService,
|
|
479
|
+
SMSChannelService,
|
|
480
|
+
PushNotificationChannelService,
|
|
481
|
+
NotificationService,
|
|
482
|
+
EmailChannelService,
|
|
483
|
+
BaseChannelService
|
|
484
|
+
};
|