@gravito/signal 3.0.0 → 3.0.3

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.
@@ -2,12 +2,40 @@ import { SESClient, SendRawEmailCommand } from '@aws-sdk/client-ses'
2
2
  import nodemailer from 'nodemailer'
3
3
  import type { Address, Message, Transport } from '../types'
4
4
 
5
+ /**
6
+ * Configuration for AWS SES email transport.
7
+ *
8
+ * @public
9
+ * @since 3.0.0
10
+ */
5
11
  export interface SesConfig {
12
+ /** AWS region (e.g., 'us-east-1') */
6
13
  region: string
14
+ /** AWS access key ID (optional, uses default credentials if not provided) */
7
15
  accessKeyId?: string
16
+ /** AWS secret access key (optional, uses default credentials if not provided) */
8
17
  secretAccessKey?: string
9
18
  }
10
19
 
20
+ /**
21
+ * AWS SES (Simple Email Service) transport.
22
+ *
23
+ * Sends emails using Amazon SES with support for attachments,
24
+ * HTML/text content, and all standard email features.
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * const transport = new SesTransport({
29
+ * region: 'us-east-1',
30
+ * accessKeyId: process.env.AWS_ACCESS_KEY_ID,
31
+ * secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
32
+ * })
33
+ * await transport.send(message)
34
+ * ```
35
+ *
36
+ * @since 3.0.0
37
+ * @public
38
+ */
11
39
  export class SesTransport implements Transport {
12
40
  private transporter: nodemailer.Transporter
13
41
 
@@ -1,20 +1,58 @@
1
1
  import nodemailer from 'nodemailer'
2
2
  import type { Address, Message, Transport } from '../types'
3
3
 
4
+ /**
5
+ * Configuration for SMTP email transport.
6
+ *
7
+ * @public
8
+ * @since 3.0.0
9
+ */
4
10
  export interface SmtpConfig {
11
+ /** SMTP server hostname */
5
12
  host: string
13
+ /** SMTP server port (typically 25, 465, or 587) */
6
14
  port: number
15
+ /** Use TLS/SSL connection (true for port 465) */
7
16
  secure?: boolean
17
+ /** Authentication credentials */
8
18
  auth?: {
19
+ /** SMTP username */
9
20
  user: string
21
+ /** SMTP password */
10
22
  pass: string
11
23
  }
24
+ /** TLS options */
12
25
  tls?: {
26
+ /** Reject unauthorized certificates */
13
27
  rejectUnauthorized?: boolean
28
+ /** Cipher suite */
14
29
  ciphers?: string
15
30
  }
16
31
  }
17
32
 
33
+ /**
34
+ * SMTP email transport.
35
+ *
36
+ * Sends emails using standard SMTP protocol with support for
37
+ * authentication, TLS/SSL, attachments, and all email features.
38
+ *
39
+ * @example
40
+ * ```typescript
41
+ * const transport = new SmtpTransport({
42
+ * host: 'smtp.gmail.com',
43
+ * port: 587,
44
+ * secure: false,
45
+ * auth: {
46
+ * user: 'user@gmail.com',
47
+ * pass: 'app-password'
48
+ * }
49
+ * })
50
+ * await transport.send(message)
51
+ * ```
52
+ *
53
+ * @since 3.0.0
54
+ * @public
55
+ */
18
56
  export class SmtpTransport implements Transport {
19
57
  private transporter: nodemailer.Transporter
20
58
 
@@ -1,8 +1,16 @@
1
1
  import type { Message } from '../types'
2
2
 
3
+ /**
4
+ * Interface for email transport mechanisms (SMTP, SES, etc.).
5
+ *
6
+ * @public
7
+ * @since 3.0.0
8
+ */
3
9
  export interface Transport {
4
10
  /**
5
- * Send the given message
11
+ * Send the given message using the underlying transport.
12
+ *
13
+ * @param message - The finalized message to send.
6
14
  */
7
15
  send(message: Message): Promise<void>
8
16
  }
package/src/types.ts CHANGED
@@ -2,79 +2,137 @@ import type { GravitoContext } from '@gravito/core'
2
2
  import type { Transport } from './transports/Transport'
3
3
  export type { Transport }
4
4
 
5
+ /**
6
+ * Representation of an email address with optional display name.
7
+ *
8
+ * @public
9
+ * @since 3.0.0
10
+ */
5
11
  export interface Address {
12
+ /** The display name of the recipient/sender, e.g., "John Doe". */
6
13
  name?: string
14
+ /** The actual email address string. */
7
15
  address: string
8
16
  }
9
17
 
18
+ /**
19
+ * Configuration for an email attachment.
20
+ *
21
+ * @public
22
+ * @since 3.0.0
23
+ */
10
24
  export interface Attachment {
25
+ /** The filename of the attachment. */
11
26
  filename: string
27
+ /** The content of the attachment as a string or Buffer. */
12
28
  content: string | Buffer // Buffer for Node.js
29
+ /** Optional MIME type of the content. */
13
30
  contentType?: string
31
+ /** Optional Content-ID for referencing within HTML content (inline images). */
14
32
  cid?: string // Content-ID for inline images
33
+ /** Optional content encoding. */
15
34
  encoding?: string
16
35
  }
17
36
 
37
+ /**
38
+ * The envelope containing metadata for an email message.
39
+ *
40
+ * Used during the construction phase of a Mailable.
41
+ *
42
+ * @public
43
+ * @since 3.0.0
44
+ */
18
45
  export interface Envelope {
46
+ /** The sender's address. */
19
47
  from?: Address | undefined
48
+ /** Primary recipients. */
20
49
  to?: Address[] | undefined
50
+ /** Carbon copy recipients. */
21
51
  cc?: Address[] | undefined
52
+ /** Blind carbon copy recipients. */
22
53
  bcc?: Address[] | undefined
54
+ /** Reply-to address. */
23
55
  replyTo?: Address | undefined
56
+ /** Email subject line. */
24
57
  subject?: string | undefined
58
+ /** Importance level of the email. */
25
59
  priority?: 'high' | 'normal' | 'low' | undefined
60
+ /** List of file attachments. */
26
61
  attachments?: Attachment[] | undefined
27
62
  }
28
63
 
64
+ /**
65
+ * A fully finalized email message ready to be sent by a transport.
66
+ *
67
+ * Requires mandatory fields that were optional in the Envelope.
68
+ *
69
+ * @public
70
+ * @since 3.0.0
71
+ */
29
72
  export interface Message extends Envelope {
30
- from: Address // From is required in finalized message
31
- to: Address[] // To is required in finalized message
73
+ /** The mandatory sender's address. */
74
+ from: Address
75
+ /** At least one recipient is required. */
76
+ to: Address[]
77
+ /** Mandatory subject. */
32
78
  subject: string
33
- html: string // The rendered HTML content
34
- text?: string // The rendered plain text content
79
+ /** The rendered HTML body content. */
80
+ html: string
81
+ /** Optional rendered plain text body content. */
82
+ text?: string
83
+ /** Custom SMTP headers. */
35
84
  headers?: Record<string, string>
36
85
  }
37
86
 
87
+ /**
88
+ * Global configuration options for OrbitSignal and Mailable instances.
89
+ *
90
+ * @public
91
+ * @since 3.0.0
92
+ */
38
93
  export interface MailConfig {
39
94
  /**
40
- * Default sender address
95
+ * Default sender address used if not specified in the Mailable.
41
96
  */
42
97
  from?: Address
43
98
 
44
99
  /**
45
- * The transport mechanism used to send emails
100
+ * The transport mechanism used to send emails (e.g., SMTP, SES, Log).
46
101
  */
47
102
  transport?: Transport
48
103
 
49
104
  /**
50
- * Enable development mode (intercepts emails)
105
+ * Enable development mode.
106
+ * When true, emails are intercepted by the DevMailbox instead of being sent.
51
107
  */
52
108
  devMode?: boolean | undefined
53
109
 
54
110
  /**
55
- * Directory where email templates are located (for OrbitPrism)
111
+ * Directory where email templates are located for use with OrbitPrism.
56
112
  * Default: src/emails
57
113
  */
58
114
  viewsDir?: string | undefined
59
115
 
60
116
  /**
61
- * URL prefix for Dev UI
117
+ * URL prefix for the Mail Dev UI.
62
118
  * Default: /__mail
63
119
  */
64
120
  devUiPrefix?: string | undefined
65
121
 
66
122
  /**
67
- * Allow Dev UI in production. Default: false.
123
+ * Whether to allow access to the Mail Dev UI in production environments.
124
+ * @default false
68
125
  */
69
126
  devUiAllowInProduction?: boolean | undefined
70
127
 
71
128
  /**
72
- * Gate access to Dev UI (required in production unless allowInProduction is true).
129
+ * Authorization gate for the Mail Dev UI.
130
+ * Should return true to allow access to the UI.
73
131
  */
74
132
  devUiGate?: ((ctx: GravitoContext) => boolean | Promise<boolean>) | undefined
75
133
 
76
134
  /**
77
- * Translation function for i18n support
135
+ * Translation function for internationalization within emails.
78
136
  */
79
137
  translator?:
80
138
  | ((key: string, replace?: Record<string, unknown>, locale?: string) => string)
@@ -1,61 +1,68 @@
1
- import { describe, expect, it, mock } from 'bun:test'
2
- import { DevMailbox } from '../src/dev/DevMailbox'
1
+ import { describe, expect, it } from 'bun:test'
3
2
  import { Mailable } from '../src/Mailable'
4
- import { OrbitSignal } from '../src/OrbitSignal'
5
- import { MemoryTransport } from '../src/transports/MemoryTransport'
6
3
 
7
- class DemoMail extends Mailable {
4
+ class TestMailable extends Mailable {
8
5
  build() {
9
- return this.from('from@example.com')
10
- .to('to@example.com')
11
- .cc('cc@example.com')
12
- .bcc('bcc@example.com')
13
- .replyTo('reply@example.com')
14
- .subject('Hello')
15
- .emailPriority('high')
16
- .html('<p>Hello World</p>')
6
+ return this
17
7
  }
18
8
  }
19
9
 
20
- describe('Mailable', () => {
21
- it('builds envelopes and renders content', async () => {
22
- const mail = new DemoMail()
23
- const envelope = await mail.buildEnvelope({
24
- from: { address: 'default@example.com' },
25
- translator: (key: string) => `t:${key}`,
26
- })
27
-
28
- expect(envelope.from?.address).toBe('from@example.com')
29
- expect(envelope.cc?.[0]?.address).toBe('cc@example.com')
30
- expect(envelope.bcc?.[0]?.address).toBe('bcc@example.com')
31
- expect(envelope.replyTo?.address).toBe('reply@example.com')
32
-
33
- const content = await mail.renderContent()
34
- expect(content.html).toContain('<p>Hello World</p>')
35
- expect(content.text).toBe('Hello World')
36
- expect(mail.t('hello')).toBe('t:hello')
37
- })
38
-
39
- it('queues mailables via OrbitSignal', async () => {
40
- const queueMock = mock(async () => {})
41
- const orbit = new OrbitSignal({
42
- transport: new MemoryTransport(new DevMailbox()),
43
- })
44
-
45
- // 模擬容器中的隊列服務
46
- ;(orbit as any).core = {
47
- container: {
48
- make: (key: string) => {
49
- if (key === 'queue') {
50
- return { push: queueMock }
51
- }
52
- return null
53
- },
54
- },
55
- }
56
-
57
- const mail = new DemoMail()
58
- await orbit.queue(mail)
59
- expect(queueMock).toHaveBeenCalled()
10
+ describe('Mailable Extras', () => {
11
+ it('should handle attachments', () => {
12
+ const mail = new TestMailable()
13
+ mail.attach({ filename: 'test.txt', content: 'hello' })
14
+ expect((mail as any).envelope.attachments).toHaveLength(1)
15
+ expect((mail as any).envelope.attachments[0].filename).toBe('test.txt')
16
+ })
17
+
18
+ it('should set view logic', () => {
19
+ const mail = new TestMailable()
20
+ mail.view('emails/welcome', { user: 'Carl' })
21
+ expect((mail as any).renderer).toBeDefined()
22
+ expect((mail as any).renderData.user).toBe('Carl')
23
+ })
24
+
25
+ it('should handle queue options', () => {
26
+ const mail = new TestMailable()
27
+ mail.onQueue('low').onConnection('redis').delay(60).withPriority('high')
28
+
29
+ expect(mail.queueName).toBe('low')
30
+ expect(mail.connectionName).toBe('redis')
31
+ expect(mail.delaySeconds).toBe(60)
32
+ expect(mail.priority).toBe('high')
33
+ })
34
+
35
+ it('should handle locale', () => {
36
+ const mail = new TestMailable()
37
+ mail.locale('zh-TW')
38
+ expect((mail as any).currentLocale).toBe('zh-TW')
39
+ })
40
+
41
+ it('should support i18n helper', () => {
42
+ const mail = new TestMailable()
43
+ mail.setTranslator((key) => key.toUpperCase())
44
+
45
+ expect(mail.t('hello')).toBe('HELLO')
46
+ })
47
+
48
+ it('should safely fail queue() if app not available', async () => {
49
+ const mail = new TestMailable()
50
+ // We are not mocking @gravito/core, so it should hit the catch block and log warning
51
+ // We just ensure it doesn't throw
52
+ await expect(mail.queue()).resolves.toBeUndefined()
53
+ })
54
+
55
+ it('should support React renderer builder', async () => {
56
+ const mail = new TestMailable()
57
+ // Mock import mechanics? Hard to test dynamic import in this unit test without mocking module system.
58
+ // Instead we test the builder method sets the resolver.
59
+ mail.react('MyComponent', { prop: 1 })
60
+ expect((mail as any).rendererResolver).toBeDefined()
61
+ })
62
+
63
+ it('should support Vue renderer builder', async () => {
64
+ const mail = new TestMailable()
65
+ mail.vue('MyComponent', { prop: 1 })
66
+ expect((mail as any).rendererResolver).toBeDefined()
60
67
  })
61
68
  })
@@ -1,13 +0,0 @@
1
- export {}
2
-
3
- // Module augmentation for GravitoVariables (new abstraction)
4
- declare module '@gravito/core' {
5
- interface GravitoVariables {
6
- /** Mail service for sending emails */
7
- mail?: {
8
- // Use any for params to break circularity in dts generation if any
9
- send: (mailable: any) => Promise<void>
10
- queue: (mailable: any) => Promise<void>
11
- }
12
- }
13
- }