tina4-nodejs 3.0.0-rc.2 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/BENCHMARK_REPORT.md +248 -86
  2. package/CARBONAH.md +4 -4
  3. package/CLAUDE.md +16 -1
  4. package/COMPARISON.md +58 -46
  5. package/README.md +60 -6
  6. package/package.json +2 -1
  7. package/packages/cli/src/bin.ts +8 -0
  8. package/packages/cli/src/commands/generate.ts +237 -0
  9. package/packages/core/gallery/queue/meta.json +1 -1
  10. package/packages/core/gallery/queue/src/lib/queueDb.ts +32 -0
  11. package/packages/core/gallery/queue/src/routes/api/gallery/queue/consume/post.ts +28 -0
  12. package/packages/core/gallery/queue/src/routes/api/gallery/queue/fail/post.ts +28 -0
  13. package/packages/core/gallery/queue/src/routes/api/gallery/queue/produce/post.ts +20 -10
  14. package/packages/core/gallery/queue/src/routes/api/gallery/queue/retry/post.ts +25 -0
  15. package/packages/core/gallery/queue/src/routes/api/gallery/queue/status/get.ts +36 -6
  16. package/packages/core/gallery/queue/src/routes/gallery/queue/get.ts +160 -0
  17. package/packages/core/src/cache.ts +402 -10
  18. package/packages/core/src/index.ts +5 -2
  19. package/packages/core/src/messenger.ts +118 -36
  20. package/packages/core/src/queue.ts +172 -92
  21. package/packages/core/src/response.ts +46 -0
  22. package/packages/core/src/router.ts +94 -1
  23. package/packages/core/src/server.ts +66 -7
  24. package/packages/core/src/types.ts +20 -1
  25. package/packages/core/src/websocketConnection.ts +16 -0
  26. package/packages/frond/src/engine.ts +184 -6
  27. package/packages/orm/src/baseModel.ts +274 -20
  28. package/packages/orm/src/cachedDatabase.ts +180 -0
  29. package/packages/orm/src/index.ts +4 -0
  30. package/packages/orm/src/model.ts +1 -0
  31. package/packages/orm/src/types.ts +75 -0
@@ -4,10 +4,24 @@
4
4
  * Sends email via raw SMTP socket communication (net/tls).
5
5
  * No nodemailer, no external dependencies.
6
6
  *
7
- * import { Messenger, createMessenger } from "@tina4/core";
7
+ * Unified .env-driven configuration with constructor override.
8
+ * Priority: constructor params > .env (TINA4_MAIL_* with SMTP_* fallback) > sensible defaults
8
9
  *
9
- * const messenger = createMessenger();
10
- * await messenger.send({ to: "alice@example.com", subject: "Hello", body: "Hi there" });
10
+ * // .env
11
+ * // TINA4_MAIL_HOST=smtp.gmail.com
12
+ * // TINA4_MAIL_PORT=587
13
+ * // TINA4_MAIL_USERNAME=user@gmail.com
14
+ * // TINA4_MAIL_PASSWORD=app-password
15
+ * // TINA4_MAIL_FROM=noreply@myapp.com
16
+ * // TINA4_MAIL_ENCRYPTION=tls
17
+ * // TINA4_MAIL_IMAP_HOST=imap.gmail.com
18
+ * // TINA4_MAIL_IMAP_PORT=993
19
+ *
20
+ * import { Messenger } from "@tina4/core";
21
+ *
22
+ * const mail = new Messenger(); // reads from .env
23
+ * const mail = new Messenger({ host: "smtp.office365.com", port: 587 }); // override
24
+ * await mail.send({ to: "user@test.com", subject: "Welcome", body: "<h1>Hello!</h1>", html: true, text: "Hello!" });
11
25
  */
12
26
  import net from "node:net";
13
27
  import tls from "node:tls";
@@ -46,6 +60,8 @@ interface MessengerOptions {
46
60
  password?: string;
47
61
  fromAddress?: string;
48
62
  fromName?: string;
63
+ encryption?: string;
64
+ /** @deprecated Use encryption instead */
49
65
  useTls?: boolean;
50
66
  imapHost?: string;
51
67
  imapPort?: number;
@@ -80,8 +96,9 @@ interface SendOptions {
80
96
  subject: string;
81
97
  body: string;
82
98
  html?: boolean;
83
- cc?: string[];
84
- bcc?: string[];
99
+ text?: string;
100
+ cc?: string | string[];
101
+ bcc?: string | string[];
85
102
  replyTo?: string;
86
103
  attachments?: string[];
87
104
  headers?: Record<string, string>;
@@ -152,13 +169,16 @@ function buildMimeMessage(options: {
152
169
  subject: string;
153
170
  body: string;
154
171
  html: boolean;
172
+ text?: string;
155
173
  replyTo?: string;
156
174
  attachments?: string[];
157
175
  headers?: Record<string, string>;
158
176
  messageId: string;
159
177
  }): string {
160
178
  const boundary = `----=_Tina4_${Date.now()}_${Math.random().toString(36).substring(2)}`;
179
+ const altBoundary = `----=_Tina4Alt_${Date.now()}_${Math.random().toString(36).substring(2)}`;
161
180
  const hasAttachments = options.attachments && options.attachments.length > 0;
181
+ const hasTextAlt = options.text !== undefined && options.html;
162
182
  const lines: string[] = [];
163
183
 
164
184
  // Headers
@@ -190,34 +210,34 @@ function buildMimeMessage(options: {
190
210
  lines.push(`Content-Type: multipart/mixed; boundary="${boundary}"`);
191
211
  lines.push("");
192
212
  lines.push(`--${boundary}`);
193
- }
194
213
 
195
- // Body part
196
- if (options.html) {
197
- if (hasAttachments) {
198
- lines.push("Content-Type: text/html; charset=UTF-8");
214
+ // Body part (with optional text alternative)
215
+ if (hasTextAlt) {
216
+ lines.push(`Content-Type: multipart/alternative; boundary="${altBoundary}"`);
217
+ lines.push("");
218
+ lines.push(`--${altBoundary}`);
219
+ lines.push("Content-Type: text/plain; charset=UTF-8");
199
220
  lines.push("Content-Transfer-Encoding: 7bit");
200
221
  lines.push("");
201
- } else {
202
- lines.push("Content-Type: text/html; charset=UTF-8");
222
+ lines.push(options.text!);
203
223
  lines.push("");
204
- }
205
- } else {
206
- if (hasAttachments) {
207
- lines.push("Content-Type: text/plain; charset=UTF-8");
224
+ lines.push(`--${altBoundary}`);
225
+ lines.push("Content-Type: text/html; charset=UTF-8");
208
226
  lines.push("Content-Transfer-Encoding: 7bit");
209
227
  lines.push("");
228
+ lines.push(options.body);
229
+ lines.push("");
230
+ lines.push(`--${altBoundary}--`);
210
231
  } else {
211
- lines.push("Content-Type: text/plain; charset=UTF-8");
232
+ const contentType = options.html ? "text/html" : "text/plain";
233
+ lines.push(`Content-Type: ${contentType}; charset=UTF-8`);
234
+ lines.push("Content-Transfer-Encoding: 7bit");
212
235
  lines.push("");
236
+ lines.push(options.body);
213
237
  }
214
- }
215
-
216
- lines.push(options.body);
217
238
 
218
- // Attachments
219
- if (hasAttachments && options.attachments) {
220
- for (const filePath of options.attachments) {
239
+ // Attachments
240
+ for (const filePath of options.attachments!) {
221
241
  const fileName = basename(filePath);
222
242
  const fileData = readFileSync(filePath);
223
243
  const base64Data = fileData.toString("base64");
@@ -237,6 +257,29 @@ function buildMimeMessage(options: {
237
257
 
238
258
  lines.push("");
239
259
  lines.push(`--${boundary}--`);
260
+ } else if (hasTextAlt) {
261
+ // Text alternative without attachments
262
+ lines.push(`Content-Type: multipart/alternative; boundary="${altBoundary}"`);
263
+ lines.push("");
264
+ lines.push(`--${altBoundary}`);
265
+ lines.push("Content-Type: text/plain; charset=UTF-8");
266
+ lines.push("Content-Transfer-Encoding: 7bit");
267
+ lines.push("");
268
+ lines.push(options.text!);
269
+ lines.push("");
270
+ lines.push(`--${altBoundary}`);
271
+ lines.push("Content-Type: text/html; charset=UTF-8");
272
+ lines.push("Content-Transfer-Encoding: 7bit");
273
+ lines.push("");
274
+ lines.push(options.body);
275
+ lines.push("");
276
+ lines.push(`--${altBoundary}--`);
277
+ } else {
278
+ // Simple message
279
+ const contentType = options.html ? "text/html" : "text/plain";
280
+ lines.push(`Content-Type: ${contentType}; charset=UTF-8`);
281
+ lines.push("");
282
+ lines.push(options.body);
240
283
  }
241
284
 
242
285
  return lines.join("\r\n");
@@ -251,6 +294,7 @@ export class Messenger {
251
294
  private password: string;
252
295
  private fromAddress: string;
253
296
  private fromName: string;
297
+ private encryption: string;
254
298
  private useTls: boolean;
255
299
  private imapHost: string;
256
300
  private imapPort: number;
@@ -258,17 +302,54 @@ export class Messenger {
258
302
  private imapPass: string;
259
303
 
260
304
  constructor(options?: MessengerOptions) {
261
- this.host = options?.host ?? process.env.SMTP_HOST ?? "localhost";
262
- this.port = options?.port ?? parseInt(process.env.SMTP_PORT ?? "587", 10);
263
- this.username = options?.username ?? process.env.SMTP_USERNAME ?? "";
264
- this.password = options?.password ?? process.env.SMTP_PASSWORD ?? "";
265
- this.fromAddress = options?.fromAddress ?? process.env.SMTP_FROM ?? "";
266
- this.fromName = options?.fromName ?? process.env.SMTP_FROM_NAME ?? "";
267
- this.useTls = options?.useTls ?? (process.env.SMTP_USE_TLS !== "false");
268
- this.imapHost = options?.imapHost ?? process.env.IMAP_HOST ?? "";
269
- this.imapPort = options?.imapPort ?? parseInt(process.env.IMAP_PORT ?? "993", 10);
270
- this.imapUser = options?.imapUser ?? process.env.IMAP_USER ?? this.username;
271
- this.imapPass = options?.imapPass ?? process.env.IMAP_PASS ?? this.password;
305
+ // Priority: constructor > TINA4_MAIL_* > SMTP_* > sensible default
306
+ this.host = options?.host
307
+ ?? process.env.TINA4_MAIL_HOST
308
+ ?? process.env.SMTP_HOST
309
+ ?? "localhost";
310
+ this.port = options?.port
311
+ ?? parseInt(process.env.TINA4_MAIL_PORT ?? process.env.SMTP_PORT ?? "587", 10);
312
+ this.username = options?.username
313
+ ?? process.env.TINA4_MAIL_USERNAME
314
+ ?? process.env.SMTP_USERNAME
315
+ ?? "";
316
+ this.password = options?.password
317
+ ?? process.env.TINA4_MAIL_PASSWORD
318
+ ?? process.env.SMTP_PASSWORD
319
+ ?? "";
320
+ this.fromAddress = options?.fromAddress
321
+ ?? process.env.TINA4_MAIL_FROM
322
+ ?? process.env.SMTP_FROM
323
+ ?? (this.username || "noreply@localhost");
324
+ this.fromName = options?.fromName
325
+ ?? process.env.TINA4_MAIL_FROM_NAME
326
+ ?? process.env.SMTP_FROM_NAME
327
+ ?? "";
328
+
329
+ // Encryption: constructor > .env > backward-compat useTls > default "tls"
330
+ const envEncryption = options?.encryption
331
+ ?? process.env.TINA4_MAIL_ENCRYPTION;
332
+ if (envEncryption) {
333
+ this.encryption = envEncryption.toLowerCase();
334
+ } else if (options?.useTls !== undefined) {
335
+ this.encryption = options.useTls ? "tls" : "none";
336
+ } else {
337
+ this.encryption = "tls";
338
+ }
339
+ this.useTls = ["tls", "starttls"].includes(this.encryption);
340
+
341
+ this.imapHost = options?.imapHost
342
+ ?? process.env.TINA4_MAIL_IMAP_HOST
343
+ ?? process.env.IMAP_HOST
344
+ ?? "";
345
+ this.imapPort = options?.imapPort
346
+ ?? parseInt(process.env.TINA4_MAIL_IMAP_PORT ?? process.env.IMAP_PORT ?? "993", 10);
347
+ this.imapUser = options?.imapUser
348
+ ?? process.env.IMAP_USER
349
+ ?? this.username;
350
+ this.imapPass = options?.imapPass
351
+ ?? process.env.IMAP_PASS
352
+ ?? this.password;
272
353
  }
273
354
 
274
355
  /**
@@ -276,8 +357,8 @@ export class Messenger {
276
357
  */
277
358
  async send(options: SendOptions): Promise<SendResult> {
278
359
  const toList = Array.isArray(options.to) ? options.to : [options.to];
279
- const ccList = options.cc ?? [];
280
- const bccList = options.bcc ?? [];
360
+ const ccList = Array.isArray(options.cc) ? options.cc : (options.cc ? [options.cc] : []);
361
+ const bccList = Array.isArray(options.bcc) ? options.bcc : (options.bcc ? [options.bcc] : []);
281
362
  const allRecipients = [...toList, ...ccList, ...bccList];
282
363
  const messageId = `${randomUUID()}@${this.host}`;
283
364
 
@@ -401,6 +482,7 @@ export class Messenger {
401
482
  subject: options.subject,
402
483
  body: options.body,
403
484
  html: options.html ?? false,
485
+ text: options.text,
404
486
  replyTo: options.replyTo,
405
487
  attachments: options.attachments,
406
488
  headers: options.headers,