@getstrata/core 0.5.42 → 0.5.43

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.
@@ -2153,6 +2153,45 @@ class Blueprint {
2153
2153
  });
2154
2154
  }
2155
2155
  }
2156
+ // ../../src/core/database/schema/driver.ts
2157
+ function normalizeConnectionName(connection) {
2158
+ const normalized = connection.trim().toLowerCase();
2159
+ if (normalized === "pgsql" || normalized === "postgres" || normalized === "postgresql") {
2160
+ return "pgsql";
2161
+ }
2162
+ if (normalized === "mysql" || normalized === "mariadb") {
2163
+ return "mysql";
2164
+ }
2165
+ if (normalized === "sqlite") {
2166
+ return "sqlite";
2167
+ }
2168
+ throw new Error(`Unsupported DB_CONNECTION: ${connection}`);
2169
+ }
2170
+ function resolveDriverFromUrl(url) {
2171
+ const normalized = url.trim().toLowerCase();
2172
+ if (normalized.startsWith("postgres://") || normalized.startsWith("postgresql://") || normalized.startsWith("postgres:")) {
2173
+ return "pgsql";
2174
+ }
2175
+ if (normalized.startsWith("mysql://") || normalized.startsWith("mysql:")) {
2176
+ return "mysql";
2177
+ }
2178
+ if (normalized.startsWith("sqlite:")) {
2179
+ return "sqlite";
2180
+ }
2181
+ return null;
2182
+ }
2183
+ function resolveDatabaseDriver(options = {}) {
2184
+ const connection = options.connection ?? process.env.DB_CONNECTION;
2185
+ if (connection) {
2186
+ return normalizeConnectionName(connection);
2187
+ }
2188
+ const url = options.url ?? process.env.DATABASE_URL ?? "";
2189
+ const fromUrl = resolveDriverFromUrl(url);
2190
+ if (fromUrl) {
2191
+ return fromUrl;
2192
+ }
2193
+ return "pgsql";
2194
+ }
2156
2195
  // ../../src/core/database/schema/errors.ts
2157
2196
  class UnsupportedSchemaFeatureError extends Error {
2158
2197
  constructor(feature, driver) {
@@ -2497,6 +2536,25 @@ class SchemaBuilder {
2497
2536
  }
2498
2537
  }
2499
2538
  }
2539
+
2540
+ class Schema {
2541
+ static builder(driver) {
2542
+ return new SchemaBuilder(driver ?? resolveDatabaseDriver());
2543
+ }
2544
+ static async run(db, driver, callback) {
2545
+ const schema = Schema.builder(driver);
2546
+ await callback(schema);
2547
+ await schema.execute(db);
2548
+ }
2549
+ }
2550
+ function createSchemaBuilder(db, driver) {
2551
+ const builder = Schema.builder(driver);
2552
+ return Object.assign(builder, {
2553
+ async commit() {
2554
+ await builder.execute(db);
2555
+ }
2556
+ });
2557
+ }
2500
2558
  // ../../src/core/database/table.ts
2501
2559
  function defineTable(definition) {
2502
2560
  return definition;
@@ -0,0 +1,208 @@
1
+ // @bun
2
+ // ../../src/core/mail/mailer.ts
3
+ function resolveSmtpConfig() {
4
+ const host = process.env.MAIL_HOST?.trim();
5
+ if (!host) {
6
+ throw new Error('MAIL_DRIVER="smtp" requires MAIL_HOST to be set.');
7
+ }
8
+ const from = process.env.MAIL_FROM?.trim();
9
+ if (!from) {
10
+ throw new Error('MAIL_DRIVER="smtp" requires MAIL_FROM to be set.');
11
+ }
12
+ const port = Number.parseInt(process.env.MAIL_PORT ?? "587", 10);
13
+ if (!Number.isInteger(port) || port <= 0) {
14
+ throw new Error('Environment variable "MAIL_PORT" must be a positive integer.');
15
+ }
16
+ return {
17
+ host,
18
+ port,
19
+ from,
20
+ secure: (process.env.MAIL_SECURE ?? "false") === "true",
21
+ ...process.env.MAIL_USERNAME?.trim() ? { username: process.env.MAIL_USERNAME.trim() } : {},
22
+ ...process.env.MAIL_PASSWORD?.trim() ? { password: process.env.MAIL_PASSWORD.trim() } : {}
23
+ };
24
+ }
25
+ function encodeBase64(value) {
26
+ return Buffer.from(value, "utf8").toString("base64");
27
+ }
28
+ function parseSmtpResponses(buffer) {
29
+ const responses = [];
30
+ let remainder = buffer;
31
+ while (remainder.includes(`\r
32
+ `)) {
33
+ const index = remainder.indexOf(`\r
34
+ `);
35
+ const line = remainder.slice(0, index);
36
+ remainder = remainder.slice(index + 2);
37
+ if (line.length >= 4 && line[3] === "-") {
38
+ continue;
39
+ }
40
+ responses.push(line);
41
+ }
42
+ return { responses, remainder };
43
+ }
44
+ async function waitForSmtpResponse(readResponse, expectedCodes) {
45
+ const response = await readResponse();
46
+ const code = response.slice(0, 3);
47
+ if (!expectedCodes.includes(code)) {
48
+ throw new Error(`Unexpected SMTP response: ${response}`);
49
+ }
50
+ return response;
51
+ }
52
+ async function openSmtpConnection(config) {
53
+ let buffer = "";
54
+ const waiters = [];
55
+ const readResponse = () => new Promise((resolve, reject) => {
56
+ const parsed = parseSmtpResponses(buffer);
57
+ if (parsed.responses.length > 0) {
58
+ buffer = parsed.remainder;
59
+ resolve(parsed.responses.shift());
60
+ return;
61
+ }
62
+ waiters.push({ resolve, reject });
63
+ });
64
+ const socket = await Bun.connect({
65
+ hostname: config.host,
66
+ port: config.port,
67
+ socket: {
68
+ open() {},
69
+ data(_socket, chunk) {
70
+ buffer += Buffer.from(chunk).toString("utf8");
71
+ const parsed = parseSmtpResponses(buffer);
72
+ buffer = parsed.remainder;
73
+ while (parsed.responses.length > 0 && waiters.length > 0) {
74
+ const response = parsed.responses.shift();
75
+ waiters.shift()?.resolve(response);
76
+ }
77
+ },
78
+ error(_socket, error) {
79
+ const pending = waiters.splice(0);
80
+ for (const waiter of pending) {
81
+ waiter.reject(error instanceof Error ? error : new Error(String(error)));
82
+ }
83
+ }
84
+ }
85
+ });
86
+ return { socket, readResponse };
87
+ }
88
+ async function defaultSmtpTransport(config, message) {
89
+ const { socket, readResponse } = await openSmtpConnection(config);
90
+ try {
91
+ await waitForSmtpResponse(readResponse, ["220"]);
92
+ await socket.write(`EHLO workhub.local\r
93
+ `);
94
+ await waitForSmtpResponse(readResponse, ["250"]);
95
+ if (config.username && config.password) {
96
+ await socket.write(`AUTH LOGIN\r
97
+ `);
98
+ await waitForSmtpResponse(readResponse, ["334"]);
99
+ await socket.write(`${encodeBase64(config.username)}\r
100
+ `);
101
+ await waitForSmtpResponse(readResponse, ["334"]);
102
+ await socket.write(`${encodeBase64(config.password)}\r
103
+ `);
104
+ await waitForSmtpResponse(readResponse, ["235"]);
105
+ }
106
+ await socket.write(`MAIL FROM:<${config.from}>\r
107
+ `);
108
+ await waitForSmtpResponse(readResponse, ["250"]);
109
+ await socket.write(`RCPT TO:<${message.to}>\r
110
+ `);
111
+ await waitForSmtpResponse(readResponse, ["250", "251"]);
112
+ await socket.write(`DATA\r
113
+ `);
114
+ await waitForSmtpResponse(readResponse, ["354"]);
115
+ const payload = buildSmtpPayload(config.from, message);
116
+ await socket.write(payload);
117
+ await waitForSmtpResponse(readResponse, ["250"]);
118
+ await socket.write(`QUIT\r
119
+ `);
120
+ await waitForSmtpResponse(readResponse, ["221"]);
121
+ } finally {
122
+ socket.end();
123
+ }
124
+ }
125
+ function buildSmtpPayload(from, message) {
126
+ const headers = [
127
+ `From: ${from}`,
128
+ `To: ${message.to}`,
129
+ `Subject: ${message.subject}`,
130
+ "MIME-Version: 1.0"
131
+ ];
132
+ if (message.html) {
133
+ const boundary = `strata-${Date.now().toString(36)}`;
134
+ headers.push(`Content-Type: multipart/alternative; boundary="${boundary}"`);
135
+ const parts = [
136
+ `--${boundary}`,
137
+ "Content-Type: text/plain; charset=utf-8",
138
+ "",
139
+ message.body,
140
+ `--${boundary}`,
141
+ "Content-Type: text/html; charset=utf-8",
142
+ "",
143
+ message.html,
144
+ `--${boundary}--`,
145
+ ""
146
+ ];
147
+ return [...headers, "", ...parts, ".", ""].join(`\r
148
+ `);
149
+ }
150
+ headers.push("Content-Type: text/plain; charset=utf-8");
151
+ return [...headers, "", message.body, ".", ""].join(`\r
152
+ `);
153
+ }
154
+
155
+ class LogMailDriver {
156
+ async send(message) {
157
+ console.log(JSON.stringify({
158
+ level: "info",
159
+ channel: "mail",
160
+ to: message.to,
161
+ subject: message.subject,
162
+ body: message.body,
163
+ ...message.html ? { html: message.html } : {}
164
+ }));
165
+ }
166
+ }
167
+
168
+ class SmtpMailDriver {
169
+ config;
170
+ transport;
171
+ constructor(config, transport = defaultSmtpTransport) {
172
+ this.config = config;
173
+ this.transport = transport;
174
+ }
175
+ send(message) {
176
+ return this.transport(this.config, message);
177
+ }
178
+ }
179
+
180
+ class Mailer {
181
+ driver;
182
+ constructor(driver) {
183
+ this.driver = driver;
184
+ }
185
+ send(message) {
186
+ return this.driver.send(message);
187
+ }
188
+ }
189
+ function createMailDriver() {
190
+ const driver = process.env.MAIL_DRIVER ?? "log";
191
+ if (driver === "smtp") {
192
+ return new SmtpMailDriver(resolveSmtpConfig());
193
+ }
194
+ return new LogMailDriver;
195
+ }
196
+ var appMailer = new Mailer(createMailDriver());
197
+ function mailer() {
198
+ return appMailer;
199
+ }
200
+ export {
201
+ resolveSmtpConfig,
202
+ mailer,
203
+ createMailDriver,
204
+ buildSmtpPayload,
205
+ SmtpMailDriver,
206
+ Mailer,
207
+ LogMailDriver
208
+ };
@@ -0,0 +1,63 @@
1
+ // @bun
2
+ // ../../src/core/mail/markdownMail.ts
3
+ function escapeHtml(value) {
4
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
5
+ }
6
+ function stripMarkdown(markdown) {
7
+ return markdown.replace(/^#{1,6}\s+/gm, "").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/\*([^*]+)\*/g, "$1").replace(/`([^`]+)`/g, "$1").replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)").replace(/^[-*]\s+/gm, "\u2022 ").replace(/```[\s\S]*?```/g, "").replace(/\n{3,}/g, `
8
+
9
+ `).trim();
10
+ }
11
+ function markdownToHtml(markdown) {
12
+ const escaped = markdown.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
13
+ return escaped.replace(/^### (.+)$/gm, "<h3>$1</h3>").replace(/^## (.+)$/gm, "<h2>$1</h2>").replace(/^# (.+)$/gm, "<h1>$1</h1>").replace(/`([^`]+)`/g, "<code>$1</code>").replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>").replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>').replace(/^[-*]\s+(.+)$/gm, "<li>$1</li>").replace(/(<li>[\s\S]*?<\/li>\n?)+/g, (block) => `<ul>${block}</ul>`).replace(/```(\w+)?\n([\s\S]*?)```/g, "<pre><code>$2</code></pre>").split(/\n\n+/).map((block) => {
14
+ if (block.startsWith("<")) {
15
+ return block;
16
+ }
17
+ return `<p>${block.replace(/\n/g, " ")}</p>`;
18
+ }).join(`
19
+ `);
20
+ }
21
+ function wrapMarkdownMailLayout(bodyHtml, options = {}) {
22
+ const title = escapeHtml(options.title ?? "GetStrata");
23
+ const preview = escapeHtml(options.preview ?? "");
24
+ const footer = escapeHtml(options.footer ?? "Sent by GetStrata");
25
+ return `<!DOCTYPE html>
26
+ <html lang="en">
27
+ <head>
28
+ <meta charset="utf-8">
29
+ <meta name="viewport" content="width=device-width, initial-scale=1">
30
+ <title>${title}</title>
31
+ <style>
32
+ body { font-family: system-ui, sans-serif; line-height: 1.5; color: #111827; background: #f9fafb; margin: 0; padding: 24px; }
33
+ .container { max-width: 640px; margin: 0 auto; background: #ffffff; border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden; }
34
+ .header { padding: 20px 24px; border-bottom: 1px solid #e5e7eb; font-weight: 600; }
35
+ .content { padding: 24px; }
36
+ .footer { padding: 16px 24px; border-top: 1px solid #e5e7eb; color: #6b7280; font-size: 14px; }
37
+ a { color: #2563eb; }
38
+ code { background: #f3f4f6; padding: 2px 4px; border-radius: 4px; }
39
+ pre { background: #111827; color: #f9fafb; padding: 12px; border-radius: 6px; overflow-x: auto; }
40
+ </style>
41
+ </head>
42
+ <body>
43
+ ${preview ? `<span style="display:none;max-height:0;overflow:hidden;">${preview}</span>` : ""}
44
+ <div class="container">
45
+ <div class="header">${title}</div>
46
+ <div class="content">${bodyHtml}</div>
47
+ <div class="footer">${footer}</div>
48
+ </div>
49
+ </body>
50
+ </html>`;
51
+ }
52
+ function renderMarkdownMail(markdown, options = {}) {
53
+ const bodyHtml = markdownToHtml(markdown.trim());
54
+ const html = wrapMarkdownMailLayout(bodyHtml, options);
55
+ const text = stripMarkdown(markdown);
56
+ return { html, text };
57
+ }
58
+ export {
59
+ wrapMarkdownMailLayout,
60
+ stripMarkdown,
61
+ renderMarkdownMail,
62
+ markdownToHtml
63
+ };
@@ -0,0 +1,78 @@
1
+ // @bun
2
+ // ../../src/core/mail/markdownMail.ts
3
+ function escapeHtml(value) {
4
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
5
+ }
6
+ function stripMarkdown(markdown) {
7
+ return markdown.replace(/^#{1,6}\s+/gm, "").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/\*([^*]+)\*/g, "$1").replace(/`([^`]+)`/g, "$1").replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)").replace(/^[-*]\s+/gm, "\u2022 ").replace(/```[\s\S]*?```/g, "").replace(/\n{3,}/g, `
8
+
9
+ `).trim();
10
+ }
11
+ function markdownToHtml(markdown) {
12
+ const escaped = markdown.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
13
+ return escaped.replace(/^### (.+)$/gm, "<h3>$1</h3>").replace(/^## (.+)$/gm, "<h2>$1</h2>").replace(/^# (.+)$/gm, "<h1>$1</h1>").replace(/`([^`]+)`/g, "<code>$1</code>").replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>").replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>').replace(/^[-*]\s+(.+)$/gm, "<li>$1</li>").replace(/(<li>[\s\S]*?<\/li>\n?)+/g, (block) => `<ul>${block}</ul>`).replace(/```(\w+)?\n([\s\S]*?)```/g, "<pre><code>$2</code></pre>").split(/\n\n+/).map((block) => {
14
+ if (block.startsWith("<")) {
15
+ return block;
16
+ }
17
+ return `<p>${block.replace(/\n/g, " ")}</p>`;
18
+ }).join(`
19
+ `);
20
+ }
21
+ function wrapMarkdownMailLayout(bodyHtml, options = {}) {
22
+ const title = escapeHtml(options.title ?? "GetStrata");
23
+ const preview = escapeHtml(options.preview ?? "");
24
+ const footer = escapeHtml(options.footer ?? "Sent by GetStrata");
25
+ return `<!DOCTYPE html>
26
+ <html lang="en">
27
+ <head>
28
+ <meta charset="utf-8">
29
+ <meta name="viewport" content="width=device-width, initial-scale=1">
30
+ <title>${title}</title>
31
+ <style>
32
+ body { font-family: system-ui, sans-serif; line-height: 1.5; color: #111827; background: #f9fafb; margin: 0; padding: 24px; }
33
+ .container { max-width: 640px; margin: 0 auto; background: #ffffff; border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden; }
34
+ .header { padding: 20px 24px; border-bottom: 1px solid #e5e7eb; font-weight: 600; }
35
+ .content { padding: 24px; }
36
+ .footer { padding: 16px 24px; border-top: 1px solid #e5e7eb; color: #6b7280; font-size: 14px; }
37
+ a { color: #2563eb; }
38
+ code { background: #f3f4f6; padding: 2px 4px; border-radius: 4px; }
39
+ pre { background: #111827; color: #f9fafb; padding: 12px; border-radius: 6px; overflow-x: auto; }
40
+ </style>
41
+ </head>
42
+ <body>
43
+ ${preview ? `<span style="display:none;max-height:0;overflow:hidden;">${preview}</span>` : ""}
44
+ <div class="container">
45
+ <div class="header">${title}</div>
46
+ <div class="content">${bodyHtml}</div>
47
+ <div class="footer">${footer}</div>
48
+ </div>
49
+ </body>
50
+ </html>`;
51
+ }
52
+ function renderMarkdownMail(markdown, options = {}) {
53
+ const bodyHtml = markdownToHtml(markdown.trim());
54
+ const html = wrapMarkdownMailLayout(bodyHtml, options);
55
+ const text = stripMarkdown(markdown);
56
+ return { html, text };
57
+ }
58
+
59
+ // ../../src/core/mail/markdownMailable.ts
60
+ function buildMarkdownMailMessage(input) {
61
+ const rendered = renderMarkdownMail(input.markdown, {
62
+ title: input.layout?.title ?? input.subject,
63
+ ...input.layout
64
+ });
65
+ return {
66
+ to: input.to,
67
+ subject: input.subject,
68
+ body: rendered.text,
69
+ html: rendered.html
70
+ };
71
+ }
72
+ async function sendMarkdownMail(mailer, input) {
73
+ await mailer.send(buildMarkdownMailMessage(input));
74
+ }
75
+ export {
76
+ sendMarkdownMail,
77
+ buildMarkdownMailMessage
78
+ };
@@ -0,0 +1,152 @@
1
+ // @bun
2
+ // ../../src/core/mail/markdownMail.ts
3
+ function escapeHtml(value) {
4
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
5
+ }
6
+ function stripMarkdown(markdown) {
7
+ return markdown.replace(/^#{1,6}\s+/gm, "").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/\*([^*]+)\*/g, "$1").replace(/`([^`]+)`/g, "$1").replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)").replace(/^[-*]\s+/gm, "\u2022 ").replace(/```[\s\S]*?```/g, "").replace(/\n{3,}/g, `
8
+
9
+ `).trim();
10
+ }
11
+ function markdownToHtml(markdown) {
12
+ const escaped = markdown.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
13
+ return escaped.replace(/^### (.+)$/gm, "<h3>$1</h3>").replace(/^## (.+)$/gm, "<h2>$1</h2>").replace(/^# (.+)$/gm, "<h1>$1</h1>").replace(/`([^`]+)`/g, "<code>$1</code>").replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>").replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>').replace(/^[-*]\s+(.+)$/gm, "<li>$1</li>").replace(/(<li>[\s\S]*?<\/li>\n?)+/g, (block) => `<ul>${block}</ul>`).replace(/```(\w+)?\n([\s\S]*?)```/g, "<pre><code>$2</code></pre>").split(/\n\n+/).map((block) => {
14
+ if (block.startsWith("<")) {
15
+ return block;
16
+ }
17
+ return `<p>${block.replace(/\n/g, " ")}</p>`;
18
+ }).join(`
19
+ `);
20
+ }
21
+ function wrapMarkdownMailLayout(bodyHtml, options = {}) {
22
+ const title = escapeHtml(options.title ?? "GetStrata");
23
+ const preview = escapeHtml(options.preview ?? "");
24
+ const footer = escapeHtml(options.footer ?? "Sent by GetStrata");
25
+ return `<!DOCTYPE html>
26
+ <html lang="en">
27
+ <head>
28
+ <meta charset="utf-8">
29
+ <meta name="viewport" content="width=device-width, initial-scale=1">
30
+ <title>${title}</title>
31
+ <style>
32
+ body { font-family: system-ui, sans-serif; line-height: 1.5; color: #111827; background: #f9fafb; margin: 0; padding: 24px; }
33
+ .container { max-width: 640px; margin: 0 auto; background: #ffffff; border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden; }
34
+ .header { padding: 20px 24px; border-bottom: 1px solid #e5e7eb; font-weight: 600; }
35
+ .content { padding: 24px; }
36
+ .footer { padding: 16px 24px; border-top: 1px solid #e5e7eb; color: #6b7280; font-size: 14px; }
37
+ a { color: #2563eb; }
38
+ code { background: #f3f4f6; padding: 2px 4px; border-radius: 4px; }
39
+ pre { background: #111827; color: #f9fafb; padding: 12px; border-radius: 6px; overflow-x: auto; }
40
+ </style>
41
+ </head>
42
+ <body>
43
+ ${preview ? `<span style="display:none;max-height:0;overflow:hidden;">${preview}</span>` : ""}
44
+ <div class="container">
45
+ <div class="header">${title}</div>
46
+ <div class="content">${bodyHtml}</div>
47
+ <div class="footer">${footer}</div>
48
+ </div>
49
+ </body>
50
+ </html>`;
51
+ }
52
+ function renderMarkdownMail(markdown, options = {}) {
53
+ const bodyHtml = markdownToHtml(markdown.trim());
54
+ const html = wrapMarkdownMailLayout(bodyHtml, options);
55
+ const text = stripMarkdown(markdown);
56
+ return { html, text };
57
+ }
58
+
59
+ // ../../src/core/mail/markdownMailable.ts
60
+ function buildMarkdownMailMessage(input) {
61
+ const rendered = renderMarkdownMail(input.markdown, {
62
+ title: input.layout?.title ?? input.subject,
63
+ ...input.layout
64
+ });
65
+ return {
66
+ to: input.to,
67
+ subject: input.subject,
68
+ body: rendered.text,
69
+ html: rendered.html
70
+ };
71
+ }
72
+ async function sendMarkdownMail(mailer, input) {
73
+ await mailer.send(buildMarkdownMailMessage(input));
74
+ }
75
+
76
+ // ../../src/core/notifications/dispatcher.ts
77
+ class NotificationDispatcher {
78
+ mailer;
79
+ databaseStore;
80
+ constructor(mailer, databaseStore = null) {
81
+ this.mailer = mailer;
82
+ this.databaseStore = databaseStore;
83
+ }
84
+ async send(notifiable, notification) {
85
+ for (const channel of notification.via(notifiable)) {
86
+ if (channel === "mail") {
87
+ await this.sendMail(notifiable, notification);
88
+ continue;
89
+ }
90
+ if (channel === "database") {
91
+ await this.sendDatabase(notifiable, notification);
92
+ }
93
+ }
94
+ }
95
+ async sendMail(notifiable, notification) {
96
+ const routed = notifiable.routeNotificationFor("mail");
97
+ if (routed === null) {
98
+ return;
99
+ }
100
+ const message = notification.toMail(notifiable);
101
+ if (!message) {
102
+ return;
103
+ }
104
+ if (message.markdown) {
105
+ await this.mailer.send(buildMarkdownMailMessage({
106
+ to: String(routed),
107
+ subject: message.subject,
108
+ markdown: message.markdown
109
+ }));
110
+ return;
111
+ }
112
+ await this.mailer.send({
113
+ to: String(routed),
114
+ subject: message.subject,
115
+ body: message.body ?? "",
116
+ ...message.html ? { html: message.html } : {}
117
+ });
118
+ }
119
+ async sendDatabase(notifiable, notification) {
120
+ if (!this.databaseStore) {
121
+ return;
122
+ }
123
+ const payload = notification.toDatabase(notifiable);
124
+ if (!payload) {
125
+ return;
126
+ }
127
+ await this.databaseStore.create({
128
+ userId: Number(notifiable.getNotificationKey()),
129
+ ...payload
130
+ });
131
+ }
132
+ }
133
+ function createNotificationDispatcher(mailer, databaseStore) {
134
+ return new NotificationDispatcher(mailer, databaseStore ?? null);
135
+ }
136
+ // ../../src/core/notifications/notification.ts
137
+ class Notification {
138
+ via(_notifiable) {
139
+ throw new Error("Notification subclasses must implement via().");
140
+ }
141
+ toMail(_notifiable) {
142
+ return null;
143
+ }
144
+ toDatabase(_notifiable) {
145
+ return null;
146
+ }
147
+ }
148
+ export {
149
+ createNotificationDispatcher,
150
+ NotificationDispatcher,
151
+ Notification
152
+ };