@develit-services/notification 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src.mjs CHANGED
@@ -1,555 +1,3 @@
1
- import { useResult, createInternalError, base, uuidv4, develitWorker, cloudflareQueue, action } from '@develit-io/backend-sdk';
2
- import z$1, { z } from 'zod';
3
- import { z as z$2 } from 'zod/v4';
4
- import twilio from 'twilio';
5
- import { sqliteTable, text } from 'drizzle-orm/sqlite-core';
6
- import 'drizzle-orm';
7
- import { WorkerEntrypoint } from 'cloudflare:workers';
8
- import { drizzle } from 'drizzle-orm/d1';
9
-
10
- const iContactSchema = z.union([
11
- z.string(),
12
- z.object({
13
- email: z.string(),
14
- name: z.union([z.string(), z.undefined()])
15
- })
16
- ]);
17
- const iEmailSchema = z.object({
18
- to: z.union([iContactSchema, z.array(iContactSchema)]),
19
- replyTo: z.union([iContactSchema, z.array(iContactSchema)]).optional(),
20
- cc: z.union([iContactSchema, z.array(iContactSchema)]).optional(),
21
- bcc: z.union([iContactSchema, z.array(iContactSchema)]).optional(),
22
- from: iContactSchema.optional(),
23
- subject: z.string(),
24
- text: z.string().optional(),
25
- html: z.string().optional(),
26
- templateId: z.number().optional(),
27
- templateVariables: z.record(z.string(), z.string()).optional()
28
- });
29
-
30
- class IEmailConnector {
31
- static providerName;
32
- API_KEY;
33
- SMTP_HOST;
34
- SENDER;
35
- constructor({
36
- API_KEY,
37
- SMTP_HOST,
38
- SENDER
39
- }) {
40
- this.API_KEY = API_KEY;
41
- this.SMTP_HOST = SMTP_HOST;
42
- this.SENDER = SENDER;
43
- }
44
- }
45
-
46
- class EcomailConnector extends IEmailConnector {
47
- static providerName = "ecomail";
48
- constructor({
49
- API_KEY,
50
- SMTP_HOST,
51
- SENDER
52
- }) {
53
- super({ API_KEY, SMTP_HOST, SENDER });
54
- }
55
- async sendEmail(email) {
56
- if (email.templateVariables) {
57
- for (const [key, value] of Object.entries(email.templateVariables)) {
58
- if (typeof value === "string" && String(value).includes("localhost") && key in email.templateVariables) {
59
- email.templateVariables[key] = String(value).replace("localhost", "origin");
60
- }
61
- }
62
- }
63
- const emEmail = this.convertEmail(email);
64
- const uri = emEmail.message.template_id ? "https://api2.ecomailapp.cz/transactional/send-template" : "https://api2.ecomailapp.cz/transactional/send-message";
65
- const [data, error] = await useResult(
66
- fetch(uri, {
67
- method: "POST",
68
- headers: {
69
- "Content-Type": "application/json",
70
- key: this.API_KEY
71
- },
72
- body: JSON.stringify(emEmail)
73
- })
74
- );
75
- if (error) throw error;
76
- return data;
77
- }
78
- convertEmail(email) {
79
- const toContacts = this.convertContacts(email.to);
80
- const ccContacts = email.cc ? this.convertContacts(email.cc) : [];
81
- const bccContacts = email.bcc ? this.convertContacts(email.bcc) : [];
82
- const replyTo = email.replyTo ? this.convertContacts(email.replyTo)[0] : void 0;
83
- const from = this.convertContact(email.from || this.SENDER);
84
- const subject = email.subject;
85
- const textAttachments = email.text ? [{ name: "plain", type: "text/plain", content: email.text }] : [];
86
- const htmlAttachments = email.html ? [{ type: "text/html", content: email.html, name: "email_body.html" }] : [];
87
- const attachments = [...textAttachments, ...htmlAttachments];
88
- const contacts = toContacts.flatMap((to) => {
89
- const entries = [];
90
- if (ccContacts.length > 0) {
91
- ccContacts.forEach((cc) => {
92
- entries.push({ email: cc?.email, name: cc?.name, cc: cc?.email });
93
- });
94
- }
95
- if (bccContacts.length > 0) {
96
- bccContacts.forEach((bcc) => {
97
- entries.push({ email: bcc?.email, name: bcc?.name, bcc: bcc?.email });
98
- });
99
- }
100
- return entries.length > 0 ? entries : [to];
101
- });
102
- return {
103
- message: {
104
- from_email: from?.email,
105
- from_name: from.name || "",
106
- subject,
107
- attachments,
108
- to: contacts,
109
- text: email.text,
110
- html: email.html,
111
- reply_to: replyTo?.email,
112
- template_id: email.templateId,
113
- global_merge_vars: email.templateVariables ? Object.keys(email.templateVariables).map((key) => ({
114
- name: key,
115
- content: email.templateVariables[key]
116
- })) : null
117
- }
118
- };
119
- }
120
- convertContacts(contacts) {
121
- if (!contacts) {
122
- return [];
123
- }
124
- const contactArray = Array.isArray(contacts) ? contacts : [contacts];
125
- return contactArray.map(this.convertContact);
126
- }
127
- convertContact(contact) {
128
- if (typeof contact === "string") {
129
- return { email: contact, name: void 0 };
130
- }
131
- return { email: contact?.email, name: contact?.name };
132
- }
133
- }
134
-
135
- const sendEmailInputSchema = z$1.object({
136
- email: iEmailSchema,
137
- metadata: z$1.object({
138
- userAgent: z$1.string().optional(),
139
- ip: z$1.ipv4().or(z$1.ipv6()).optional(),
140
- initiator: z$1.object({
141
- service: z$1.string(),
142
- userId: z$1.string().optional()
143
- })
144
- })
145
- });
146
-
147
- const sendSlackInputSchema = z$2.object({
148
- slack: z$2.object({
149
- message: z$2.string()
150
- }),
151
- metadata: z$2.object({
152
- userAgent: z$2.string().optional(),
153
- ip: z$2.ipv4().or(z$2.ipv6()).optional(),
154
- initiator: z$2.object({
155
- service: z$2.string(),
156
- userId: z$2.string().optional()
157
- })
158
- })
159
- });
160
-
161
- const sendSmsInputSchema = z.object({
162
- sms: z.object({
163
- message: z.string(),
164
- to: z.string()
165
- }),
166
- metadata: z.object({
167
- userAgent: z.string().optional(),
168
- ip: z.ipv4().or(z.ipv6()).optional(),
169
- initiator: z.object({
170
- service: z.string(),
171
- userId: z.string().optional()
172
- })
173
- })
174
- });
175
-
176
- class ISmsConnector {
177
- static providerName;
178
- ACCOUNT_ID;
179
- AUTH_TOKEN;
180
- SERVICE_ID;
181
- constructor({
182
- ACCOUNT_ID,
183
- AUTH_TOKEN,
184
- SERVICE_ID
185
- }) {
186
- this.ACCOUNT_ID = ACCOUNT_ID;
187
- this.AUTH_TOKEN = AUTH_TOKEN;
188
- this.SERVICE_ID = SERVICE_ID;
189
- }
190
- }
191
-
192
- class TwilioConnector extends ISmsConnector {
193
- static providerName = "twilio";
194
- twilioClient;
195
- constructor({
196
- ACCOUNT_ID,
197
- AUTH_TOKEN,
198
- SERVICE_ID
199
- }) {
200
- super({ ACCOUNT_ID, AUTH_TOKEN, SERVICE_ID });
201
- this.twilioClient = twilio(ACCOUNT_ID, AUTH_TOKEN);
202
- }
203
- async sendSms(sms) {
204
- const message = await this.twilioClient.messages.create({
205
- body: sms.message,
206
- messagingServiceSid: this.SERVICE_ID,
207
- to: sms.to
208
- });
209
- if (message.errorMessage)
210
- return createInternalError(null, {
211
- message: message.errorMessage,
212
- status: message.errorCode
213
- });
214
- }
215
- }
216
-
217
- const auditLog = sqliteTable("audit_log", {
218
- ...base,
219
- event: text("event").$type().notNull(),
220
- ip: text("ip"),
221
- userAgent: text("user_agent"),
222
- description: text("description"),
223
- initiatorService: text("initiator_service").notNull(),
224
- initiatorUserId: text("initiator_user_id")
225
- });
226
-
227
- const schema = {
228
- __proto__: null,
229
- auditLog: auditLog
230
- };
231
-
232
- const tables = schema;
233
-
234
- const initiateEmailConnector = async (provider, apiKey, smtpHost, sender) => {
235
- const connector = [EcomailConnector].find(
236
- (conn) => conn.providerName === provider
237
- );
238
- if (!connector)
239
- throw createInternalError(null, {
240
- message: "Unsupported email provider",
241
- status: 404
242
- });
243
- return new connector({ API_KEY: apiKey, SMTP_HOST: smtpHost, SENDER: sender });
244
- };
245
-
246
- const createAuditLogCommand = async ({
247
- db,
248
- auditLog: {
249
- event,
250
- description,
251
- initiatorService,
252
- initiatorUserId,
253
- ip,
254
- userAgent
255
- }
256
- }) => {
257
- const command = db.insert(tables.auditLog).values({
258
- id: uuidv4(),
259
- createdAt: /* @__PURE__ */ new Date(),
260
- event,
261
- description,
262
- ip,
263
- userAgent,
264
- initiatorService,
265
- initiatorUserId
266
- });
267
- return {
268
- command
269
- };
270
- };
271
-
272
- const initiateSmsConnector = async (provider, accountId, authToken, serviceId) => {
273
- const connector = [TwilioConnector].find(
274
- (conn) => conn.providerName === provider
275
- );
276
- if (!connector)
277
- throw createInternalError(null, {
278
- message: "Unsupported sms provider",
279
- status: 404
280
- });
281
- return new connector({
282
- ACCOUNT_ID: accountId,
283
- AUTH_TOKEN: authToken,
284
- SERVICE_ID: serviceId
285
- });
286
- };
287
-
288
- class SlackConnector {
289
- webhooks;
290
- constructor(webhooks) {
291
- this.webhooks = webhooks;
292
- }
293
- async sendNotificationToAllSlack(message) {
294
- const controller = new AbortController();
295
- const timeoutId = setTimeout(() => controller.abort(), 3e3);
296
- for (const webhook of this.webhooks) {
297
- let response = await fetch(webhook, {
298
- method: "POST",
299
- body: JSON.stringify({ text: message }),
300
- headers: { "Content-Type": "application/json" },
301
- signal: controller.signal
302
- });
303
- clearTimeout(timeoutId);
304
- if (!response.ok) {
305
- throw new Error("Failed sending Slack notification to " + message);
306
- }
307
- }
308
- }
309
- }
310
-
311
- class NotificationServiceBase extends develitWorker(
312
- WorkerEntrypoint
313
- ) {
314
- name;
315
- slackConnector = new SlackConnector(this.env.SLACK_WEBHOOKS);
316
- emailConnector;
317
- smsConnector;
318
- db;
319
- constructor(ctx, env) {
320
- super(ctx, env);
321
- this.name = "notification-service";
322
- this.db = drizzle(this.env.NOTIFICATION_D1, { schema: tables });
323
- }
324
- @cloudflareQueue({ baseDelay: 60 })
325
- async queue(batch) {
326
- for (const message of batch.messages) {
327
- this.logInput({ message });
328
- let notificationAction;
329
- const { type, metadata, payload } = message.body;
330
- if (type === "email") {
331
- const [emailConnector, error2] = await useResult(
332
- initiateEmailConnector(
333
- this.env.EMAIL_PROVIDER,
334
- this.env.EMAIL_API_KEY,
335
- this.env.EMAIL_SMTP_HOST,
336
- this.env.EMAIL_SENDER
337
- )
338
- );
339
- if (error2) {
340
- this.logError({ error: error2 });
341
- message.retry();
342
- continue;
343
- }
344
- this.emailConnector = emailConnector;
345
- }
346
- if (type === "sms") {
347
- const [smsConnector, error2] = await useResult(
348
- initiateSmsConnector(
349
- this.env.SMS_PROVIDER,
350
- this.env.SMS_ACCOUNT_ID,
351
- this.env.SMS_AUTH_TOKEN,
352
- this.env.SMS_SERVICE_ID
353
- )
354
- );
355
- if (error2) {
356
- this.logError({ error: error2 });
357
- message.retry();
358
- continue;
359
- }
360
- this.smsConnector = smsConnector;
361
- }
362
- switch (type) {
363
- case "email":
364
- notificationAction = async () => this._sendEmail({
365
- email: payload.email,
366
- metadata
367
- });
368
- break;
369
- case "sms":
370
- notificationAction = async () => this._sendSms({
371
- sms: payload.sms,
372
- metadata
373
- });
374
- break;
375
- case "pushNotification":
376
- notificationAction = async () => this._sendPushNotification();
377
- break;
378
- case "slack":
379
- notificationAction = async () => this.sendSlackNotification({
380
- slack: message.body.payload.slack,
381
- metadata
382
- });
383
- break;
384
- default:
385
- this.logError({ error: `Unknown notification type: ${type}` });
386
- message.retry();
387
- continue;
388
- }
389
- const { error, message: errorMessage } = await notificationAction();
390
- if (error) {
391
- this.logError({ error: errorMessage });
392
- message.retry();
393
- continue;
394
- }
395
- message.ack();
396
- }
397
- }
398
- @action("private-send-email")
399
- async _sendEmail(input) {
400
- return this.handleAction(
401
- { data: input, schema: sendEmailInputSchema },
402
- { successMessage: "Email sent." },
403
- async (params) => {
404
- const {
405
- email,
406
- metadata: {
407
- ip,
408
- userAgent,
409
- initiator: { service, userId }
410
- }
411
- } = params;
412
- if (!this.emailConnector)
413
- this.emailConnector = await initiateEmailConnector(
414
- this.env.EMAIL_PROVIDER,
415
- this.env.EMAIL_API_KEY,
416
- this.env.EMAIL_SMTP_HOST,
417
- this.env.EMAIL_SENDER
418
- );
419
- const response = await this.emailConnector.sendEmail(email);
420
- if (!response?.ok) {
421
- throw createInternalError(null, {
422
- message: `Could not send email: ${JSON.stringify(await response?.json())}`,
423
- status: 500
424
- });
425
- }
426
- const { command } = await createAuditLogCommand({
427
- db: this.db,
428
- auditLog: {
429
- id: uuidv4(),
430
- event: "EMAIL",
431
- ip,
432
- initiatorService: service,
433
- initiatorUserId: userId,
434
- userAgent,
435
- description: JSON.stringify(input)
436
- }
437
- });
438
- await this.db.batch([command]);
439
- return {};
440
- }
441
- );
442
- }
443
- @action("private-send-sms")
444
- async _sendSms(input) {
445
- return this.handleAction(
446
- { data: input, schema: sendSmsInputSchema },
447
- { successMessage: "Sms sent." },
448
- async (params) => {
449
- const {
450
- sms: { message, to },
451
- metadata: {
452
- ip,
453
- userAgent,
454
- initiator: { service, userId }
455
- }
456
- } = params;
457
- await this.smsConnector.sendSms({ message, to });
458
- const { command } = await createAuditLogCommand({
459
- db: this.db,
460
- auditLog: {
461
- id: uuidv4(),
462
- event: "SMS",
463
- ip,
464
- userAgent,
465
- initiatorService: service,
466
- initiatorUserId: userId,
467
- description: JSON.stringify(input)
468
- }
469
- });
470
- await this.db.batch([command]);
471
- return {};
472
- }
473
- );
474
- }
475
- @action("public-send-email")
476
- async sendEmail(input) {
477
- return this.handleAction(
478
- { data: input, schema: sendEmailInputSchema },
479
- { successMessage: "Email sent." },
480
- async (params) => {
481
- const { email, metadata } = params;
482
- await this.pushToQueue(
483
- this.env.NOTIFICATIONS_QUEUE,
484
- {
485
- type: "email",
486
- payload: {
487
- email
488
- },
489
- metadata
490
- }
491
- );
492
- return {};
493
- }
494
- );
495
- }
496
- @action("public-send-sms")
497
- async sendSms(input) {
498
- return this.handleAction(
499
- { data: input, schema: sendSmsInputSchema },
500
- { successMessage: "SMS sent." },
501
- async (params) => {
502
- const {
503
- sms: { message, to },
504
- metadata
505
- } = params;
506
- await this.pushToQueue(
507
- this.env.NOTIFICATIONS_QUEUE,
508
- {
509
- type: "sms",
510
- payload: {
511
- sms: {
512
- message,
513
- to
514
- }
515
- },
516
- metadata
517
- }
518
- );
519
- return {};
520
- }
521
- );
522
- }
523
- @action("send-push-notification")
524
- async _sendPushNotification() {
525
- this.logInput({});
526
- this.logError({ error: "Method not implemented." });
527
- throw new Error("Method not implemented.");
528
- }
529
- @action("send-slack-notification")
530
- async sendSlackNotification(input) {
531
- return this.handleAction(
532
- {
533
- data: input,
534
- schema: sendSlackInputSchema
535
- },
536
- { successMessage: "Slack sent." },
537
- async (params) => {
538
- const { slack } = params;
539
- await this.slackConnector.sendNotificationToAllSlack(slack.message);
540
- return {};
541
- }
542
- );
543
- }
544
- }
545
- function defineNotificationService() {
546
- return class NotificationService extends NotificationServiceBase {
547
- constructor(ctx, env) {
548
- super(ctx, env);
549
- }
550
- };
551
- }
552
-
553
1
  function defineNotificationServiceWrangler(config) {
554
2
  const { name, envs } = config;
555
3
  const base = {
@@ -620,6 +68,4 @@ function defineNotificationServiceWrangler(config) {
620
68
  return base;
621
69
  }
622
70
 
623
- const NotificationService = defineNotificationService();
624
-
625
- export { NotificationService as default, defineNotificationService, defineNotificationServiceWrangler };
71
+ export { defineNotificationServiceWrangler };