@maestro-js/agent-skills 1.0.0-alpha.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.
- package/dist/choosing-an-npm-package/SKILL.md +72 -0
- package/dist/maestro-cli/SKILL.md +257 -0
- package/dist/maestro-packages/SKILL.md +42 -0
- package/dist/maestro-packages/broadcast.md +306 -0
- package/dist/maestro-packages/cache-control.md +337 -0
- package/dist/maestro-packages/cache.md +397 -0
- package/dist/maestro-packages/config.md +203 -0
- package/dist/maestro-packages/context.md +365 -0
- package/dist/maestro-packages/crypt.md +253 -0
- package/dist/maestro-packages/db.md +391 -0
- package/dist/maestro-packages/events.md +295 -0
- package/dist/maestro-packages/exceptions.md +231 -0
- package/dist/maestro-packages/form.md +391 -0
- package/dist/maestro-packages/hash.md +248 -0
- package/dist/maestro-packages/helpers.md +249 -0
- package/dist/maestro-packages/log.md +321 -0
- package/dist/maestro-packages/mail.md +365 -0
- package/dist/maestro-packages/metrics.md +236 -0
- package/dist/maestro-packages/queue.md +284 -0
- package/dist/maestro-packages/rate-limiting.md +206 -0
- package/dist/maestro-packages/react-router-file-routes.md +337 -0
- package/dist/maestro-packages/recurring-jobs.md +353 -0
- package/dist/maestro-packages/sql.md +316 -0
- package/dist/maestro-packages/tracing.md +240 -0
- package/package.json +33 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: log
|
|
3
|
+
description: "Structured logging with pluggable transports and channel-based routing for the Maestro framework. Use when working with @maestro-js/log, adding or modifying log transports, configuring log channels, filtering log messages by level or channel, integrating logging into other packages, or writing tests that capture log output. Key capabilities: create loggers with pluggable transports (console, file, Sentry, stub), route messages through named channels, filter by level and channel, override Node.js console, custom formatters, and the silence transport for suppressing output."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @maestro-js/log
|
|
7
|
+
|
|
8
|
+
Structured logging with pluggable transports and channel-based routing. Messages have a severity level (`emergency`, `error`, `warn`, `info`) and an optional channel for routing to specific transports.
|
|
9
|
+
|
|
10
|
+
## Quick Setup
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { Log } from '@maestro-js/log'
|
|
14
|
+
|
|
15
|
+
// Use the default logger directly (falls back to console if no transports added)
|
|
16
|
+
Log.info('Server started on port', 3000)
|
|
17
|
+
|
|
18
|
+
// Or create an independent logger with transports
|
|
19
|
+
const logger = Log.create({
|
|
20
|
+
transports: [
|
|
21
|
+
Log.transports.console(),
|
|
22
|
+
Log.transports.file({ filename: '/var/log/app.log' })
|
|
23
|
+
]
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
logger.info('Server started on port', 3000)
|
|
27
|
+
logger.error('Connection failed', err)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Architecture
|
|
31
|
+
|
|
32
|
+
The package does NOT use the service-registry Provider pattern. Instead it exports a `Log` object that acts as a default logger facade and a `Log.create()` factory for independent logger instances.
|
|
33
|
+
|
|
34
|
+
### Source Files
|
|
35
|
+
|
|
36
|
+
- `packages/log/src/index.ts` -- Main entry: `Log.create()`, default logger facade, `Log.transports` namespace
|
|
37
|
+
- `packages/log/src/log-types.ts` -- Core types: `LogLevel`, `LogChannel`, `LogMessage`, `LogTransport`, `LogMessageFilter`, `LogFormatter`
|
|
38
|
+
- `packages/log/src/console-transport.ts` -- Console transport with colorized output
|
|
39
|
+
- `packages/log/src/file-transport.ts` -- File transport with streaming writes and `flush()`
|
|
40
|
+
- `packages/log/src/sentry-transport.ts` -- Sentry error/message capture transport
|
|
41
|
+
- `packages/log/src/sentry-breadcrumbs-transport.ts` -- Sentry breadcrumbs transport
|
|
42
|
+
- `packages/log/src/stub-transport.ts` -- In-memory transport for testing
|
|
43
|
+
- `packages/log/src/transport-helpers.ts` -- `parseLogMessageFilter()` utility
|
|
44
|
+
|
|
45
|
+
### Key Types
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
type LogLevel = 'emergency' | 'error' | 'warn' | 'info'
|
|
49
|
+
type LogChannel = string | null
|
|
50
|
+
|
|
51
|
+
interface LogMessage<LogMessageData extends any[] = any[]> {
|
|
52
|
+
data: LogMessageData
|
|
53
|
+
level: LogLevel
|
|
54
|
+
channel: LogChannel
|
|
55
|
+
time: Iso.Instant
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface LogTransport<LogMessageData extends any[] = any[]> {
|
|
59
|
+
write(message: LogMessage<LogMessageData>): void
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type LogMessageFilter<LogMessageData extends any[] = any[]> =
|
|
63
|
+
| { level?: LogLevel | LogLevel[]; channel?: LogChannel | LogChannel[] }
|
|
64
|
+
| ((message: LogMessage<LogMessageData>) => boolean)
|
|
65
|
+
| undefined
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Transports
|
|
69
|
+
|
|
70
|
+
All transports accept an optional `filter` for level/channel-based routing.
|
|
71
|
+
|
|
72
|
+
### Console Transport
|
|
73
|
+
|
|
74
|
+
Colorized output to stdout with configurable formatting.
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
Log.transports.console()
|
|
78
|
+
Log.transports.console({ filter: { level: ['error', 'emergency'] } })
|
|
79
|
+
Log.transports.console({ format: (message) => `${message.level}: ${message.data.join(' ')}\n` })
|
|
80
|
+
// Pass InspectOptions instead of a formatter function
|
|
81
|
+
Log.transports.console({ format: { colors: true, depth: 4 } })
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### File Transport
|
|
85
|
+
|
|
86
|
+
Appends log messages to a file via streaming. Creates the directory if it does not exist. Returns a transport with an additional `flush()` method.
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
const fileTransport = Log.transports.file({
|
|
90
|
+
filename: '/var/log/app.log',
|
|
91
|
+
filter: { level: ['error', 'emergency'] },
|
|
92
|
+
format: (message) => JSON.stringify(message) + '\n'
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
// Ensure all buffered writes are flushed to disk
|
|
96
|
+
await fileTransport.flush()
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Sentry Transport
|
|
100
|
+
|
|
101
|
+
Captures errors and messages to Sentry. Errors at `error`/`emergency` level use `captureException`; other levels use `captureMessage`.
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import * as Sentry from '@sentry/node'
|
|
105
|
+
|
|
106
|
+
Log.transports.sentry({
|
|
107
|
+
sentry: Sentry,
|
|
108
|
+
filter: { level: ['error', 'emergency'] },
|
|
109
|
+
getCaptureContext(message) {
|
|
110
|
+
return {
|
|
111
|
+
tags: { channel: message.channel ?? 'default' },
|
|
112
|
+
extra: {},
|
|
113
|
+
level: message.level === 'emergency' ? 'fatal' : message.level
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Sentry Breadcrumbs Transport
|
|
120
|
+
|
|
121
|
+
Adds log messages as Sentry breadcrumbs for context on future error captures.
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
import * as Sentry from '@sentry/node'
|
|
125
|
+
|
|
126
|
+
Log.transports.sentryBreadcrumbs({
|
|
127
|
+
sentry: Sentry,
|
|
128
|
+
filter: { level: ['info', 'warn'] }
|
|
129
|
+
})
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Stub Transport
|
|
133
|
+
|
|
134
|
+
In-memory transport for testing. Captures messages and provides `listMessages()` and `reset()`.
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
const stub = Log.transports.stub()
|
|
138
|
+
const logger = Log.create({ transports: [stub] })
|
|
139
|
+
|
|
140
|
+
logger.info('test message')
|
|
141
|
+
stub.listMessages() // => [{ data: ['test message'], level: 'info', channel: null, time: '...' }]
|
|
142
|
+
stub.reset()
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Silence Transport
|
|
146
|
+
|
|
147
|
+
Suppresses all output. Use to disable logging entirely.
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
Log.addTransport(Log.transports.silence)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## API Reference
|
|
154
|
+
|
|
155
|
+
### `Log.create(config?)`
|
|
156
|
+
|
|
157
|
+
Create an independent logger instance.
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
const logger = Log.create({
|
|
161
|
+
transports: [Log.transports.console()]
|
|
162
|
+
})
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Default Logger Facade
|
|
166
|
+
|
|
167
|
+
Call logging methods directly on `Log` without creating an instance.
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
Log.addTransport(Log.transports.console())
|
|
171
|
+
Log.info('message', data)
|
|
172
|
+
Log.warn('slow query', { durationMs: 500 })
|
|
173
|
+
Log.error('failed', new Error('timeout'))
|
|
174
|
+
Log.emergency('system down')
|
|
175
|
+
Log.removeTransport(someTransport)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Logging Methods
|
|
179
|
+
|
|
180
|
+
- `info(...data)` -- Log at info level
|
|
181
|
+
- `warn(...data)` -- Log at warn level
|
|
182
|
+
- `error(...data)` -- Log at error level
|
|
183
|
+
- `emergency(...data)` -- Log at emergency level
|
|
184
|
+
- `write(message)` -- Write a raw log message (supply `level`, `data`, and `time` yourself)
|
|
185
|
+
|
|
186
|
+
### Channels
|
|
187
|
+
|
|
188
|
+
Create channel-scoped sub-loggers that tag every message with a channel name. Transports can filter by channel.
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
const logger = Log.create({ transports: [Log.transports.console()] })
|
|
192
|
+
const dbLog = logger.channel('database')
|
|
193
|
+
dbLog.info('Query executed', { table: 'users', durationMs: 12 })
|
|
194
|
+
dbLog.warn('Slow query detected')
|
|
195
|
+
|
|
196
|
+
// Filter transport to only receive database channel messages
|
|
197
|
+
Log.transports.console({ filter: { channel: 'database' } })
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### `Log.overrideNodeJsConsole(logger)`
|
|
201
|
+
|
|
202
|
+
Replace global `console.log`, `console.warn`, `console.error`, `console.info`, and `console.debug` with methods from the provided logger. Returns a restore function.
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
const restore = Log.overrideNodeJsConsole(logger)
|
|
206
|
+
console.log('now goes through logger')
|
|
207
|
+
restore() // restores original console methods
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Filtering
|
|
211
|
+
|
|
212
|
+
Transports accept a `filter` option that controls which messages reach them.
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
// Filter by level
|
|
216
|
+
Log.transports.console({ filter: { level: 'error' } })
|
|
217
|
+
Log.transports.console({ filter: { level: ['error', 'emergency'] } })
|
|
218
|
+
|
|
219
|
+
// Filter by channel
|
|
220
|
+
Log.transports.console({ filter: { channel: 'database' } })
|
|
221
|
+
|
|
222
|
+
// Filter by both level and channel
|
|
223
|
+
Log.transports.console({ filter: { level: 'error', channel: 'database' } })
|
|
224
|
+
|
|
225
|
+
// Custom filter function
|
|
226
|
+
Log.transports.console({
|
|
227
|
+
filter: (message) => message.level === 'error' && message.data.some((d) => d instanceof Error)
|
|
228
|
+
})
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Fallback Behavior
|
|
232
|
+
|
|
233
|
+
When a logger has no transports registered, it falls back to a default `ConsoleTransport()`. Add `Log.transports.silence` to explicitly suppress all output.
|
|
234
|
+
|
|
235
|
+
## Custom Transports
|
|
236
|
+
|
|
237
|
+
Implement the `LogTransport` interface.
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
const customTransport: Log.Transport = {
|
|
241
|
+
write(message) {
|
|
242
|
+
// message.data, message.level, message.channel, message.time
|
|
243
|
+
fetch('https://log-service.example.com', {
|
|
244
|
+
method: 'POST',
|
|
245
|
+
body: JSON.stringify(message)
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Testing
|
|
252
|
+
|
|
253
|
+
Run tests:
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
pnpm --filter @maestro-js/log test
|
|
257
|
+
cd packages/log && npx beartest ./tests/log.test.ts
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Use `StubTransport` to capture and assert on log output:
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
import { test } from 'beartest-js'
|
|
264
|
+
import { expect } from 'expect'
|
|
265
|
+
import { Log } from '@maestro-js/log'
|
|
266
|
+
|
|
267
|
+
test('should capture log messages', () => {
|
|
268
|
+
const stub = Log.transports.stub()
|
|
269
|
+
const logger = Log.create({ transports: [stub] })
|
|
270
|
+
|
|
271
|
+
logger.info('test')
|
|
272
|
+
|
|
273
|
+
const messages = stub.listMessages()
|
|
274
|
+
expect(messages).toHaveLength(1)
|
|
275
|
+
expect(messages[0].level).toBe('info')
|
|
276
|
+
expect(messages[0].data).toEqual(['test'])
|
|
277
|
+
|
|
278
|
+
stub.reset()
|
|
279
|
+
})
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Use inline transports for targeted assertions:
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
test('should write to transport', () => {
|
|
286
|
+
const messages: Log.Message[] = []
|
|
287
|
+
const log = Log.create()
|
|
288
|
+
log.addTransport({ write(message) { messages.push(message) } })
|
|
289
|
+
|
|
290
|
+
log.info('log')
|
|
291
|
+
expect(messages).toHaveLength(1)
|
|
292
|
+
})
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## Cross-Package Integration
|
|
296
|
+
|
|
297
|
+
Log is used widely across Maestro packages as the standard logging mechanism:
|
|
298
|
+
|
|
299
|
+
- **queue, recurring-jobs, mail** -- Log job processing, email sending, and scheduling events
|
|
300
|
+
- **db** -- Log queries and connection events
|
|
301
|
+
- **cache, events, broadcast** -- Log cache operations and event dispatching
|
|
302
|
+
- **metrics, exceptions** -- Log metric collection and exception handling
|
|
303
|
+
|
|
304
|
+
Typical integration pattern in other packages:
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
import { Log } from '@maestro-js/log'
|
|
308
|
+
|
|
309
|
+
const log = Log.create()
|
|
310
|
+
// or use Log.info(), Log.error() directly via the default facade
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Context Integration
|
|
314
|
+
|
|
315
|
+
Combine with `@maestro-js/context` (AsyncLocalStorage) for request-scoped logging by passing contextual data through log message data arguments.
|
|
316
|
+
|
|
317
|
+
## Dependencies
|
|
318
|
+
|
|
319
|
+
- `colorette` -- Terminal color support for console transport
|
|
320
|
+
- `iso-fns2` -- ISO timestamp types (`Iso.Instant`)
|
|
321
|
+
- `@sentry/types` (devDependency) -- Sentry type definitions
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mail
|
|
3
|
+
description: "Driver-based email sending with queued delivery, scheduled sending, and mailable templates for the @maestro-js/mail package. Use when working with @maestro-js/mail. Covers creating mail services with SendGrid/SES/MailTrap/Stub drivers, sending emails immediately or via queue, scheduling future delivery, building reusable mailable templates, processing webhook events, generating ICS calendar attachments, and testing with the stub driver."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @maestro-js/mail
|
|
7
|
+
|
|
8
|
+
Driver-based email sending with queued delivery, scheduled sending, and reusable mailable templates. Follows the Maestro Provider pattern with pluggable drivers (SendGrid, SES, MailTrap, Stub).
|
|
9
|
+
|
|
10
|
+
## Quick Setup
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { Mail } from '@maestro-js/mail'
|
|
14
|
+
import { Queue } from '@maestro-js/queue'
|
|
15
|
+
import { Log } from '@maestro-js/log'
|
|
16
|
+
|
|
17
|
+
// Create a logger for mail records
|
|
18
|
+
const mailLogger = Log.createLogger<[Mail.LogMessage]>({ transports: [Log.transports.console()] })
|
|
19
|
+
|
|
20
|
+
// Create and register the mail service
|
|
21
|
+
const mailService = Mail.Provider.create({
|
|
22
|
+
driver: Mail.drivers.sendGrid(sendGridClient),
|
|
23
|
+
name: 'default',
|
|
24
|
+
globalFrom: { email: 'noreply@example.com', name: 'MyApp' },
|
|
25
|
+
logger: mailLogger,
|
|
26
|
+
queueService: Queue.provider('default')
|
|
27
|
+
})
|
|
28
|
+
Mail.Provider.register('default', mailService)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Drivers
|
|
32
|
+
|
|
33
|
+
### SendGrid
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import sgMail from '@sendgrid/mail'
|
|
37
|
+
sgMail.setApiKey(process.env.SENDGRID_API_KEY)
|
|
38
|
+
const driver = Mail.drivers.sendGrid(sgMail)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The SendGrid driver accepts the `@sendgrid/mail` client. It passes `envelope.extras` as `customArgs` and supports webhook event normalization via `driver.toMailEvent(sgEvent)`.
|
|
42
|
+
|
|
43
|
+
### SES
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { SESv2Client, SendRawEmailCommand } from '@aws-sdk/client-sesv2'
|
|
47
|
+
const sesClient = new SESv2Client({ region: 'us-east-1' })
|
|
48
|
+
const driver = Mail.drivers.ses(sesClient, 'my-configuration-set')
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The SES driver requires a client adapter with a `send()` method and a configuration set name. It uses `nodemailer/MailComposer` to build raw MIME messages, sends per-recipient with retry (5 attempts, 3s delay), and passes `envelope.extras` as SES message tags. Supports webhook event normalization via `driver.toMailEvent(sesEvent)`.
|
|
52
|
+
|
|
53
|
+
### MailTrap
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { MailtrapClient } from 'mailtrap'
|
|
57
|
+
const mailtrapClient = new MailtrapClient({ token: process.env.MAILTRAP_TOKEN })
|
|
58
|
+
const driver = Mail.drivers.mailTrap(mailtrapClient)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The MailTrap driver retries up to 10 attempts with 10s delay. It passes `envelope.extras` as `custom_variables`. Supports webhook event normalization via `driver.toMailEvent(mtEvent)`.
|
|
62
|
+
|
|
63
|
+
### Stub
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
const driver = Mail.drivers.stub()
|
|
67
|
+
// Or with logging:
|
|
68
|
+
const driver = Mail.drivers.stub({ logger: myLogger })
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The Stub driver returns success with random UUIDs as message IDs. Use for testing and development. Optionally accepts a logger to record sent envelopes.
|
|
72
|
+
|
|
73
|
+
## API Reference
|
|
74
|
+
|
|
75
|
+
### Mail.Provider.create(config)
|
|
76
|
+
|
|
77
|
+
Create a mail service instance. Config fields:
|
|
78
|
+
|
|
79
|
+
| Field | Type | Description |
|
|
80
|
+
|---|---|---|
|
|
81
|
+
| `driver` | `Mail.Driver` | The mail driver instance |
|
|
82
|
+
| `name` | `string` | Instance name, used for queue naming (`Maestro.Mail.{name}`) |
|
|
83
|
+
| `globalFrom` | `Mail.Address` | Default sender, overridden by `envelope.from` |
|
|
84
|
+
| `logger` | `Log.Logger<[Mail.LogMessage]>` | Logger for envelope and event records |
|
|
85
|
+
| `queueService` | `Queue.QueueService` | Queue service for async delivery |
|
|
86
|
+
| `onMessageSendFailure?` | `(envelope, error) => unknown` | Optional callback on send failure |
|
|
87
|
+
|
|
88
|
+
### Mail.Provider.register(name, service)
|
|
89
|
+
|
|
90
|
+
Register a service instance in the named registry.
|
|
91
|
+
|
|
92
|
+
### Mail.send(envelope)
|
|
93
|
+
|
|
94
|
+
Send an email immediately. Returns `{ success: boolean, messages: { id: string, email: string }[] }`. Addresses are trimmed, lowercased, and deduplicated automatically.
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
await Mail.send({
|
|
98
|
+
to: 'alice@example.com',
|
|
99
|
+
subject: 'Order Confirmed',
|
|
100
|
+
content: { html: '<h1>Thank you!</h1>' }
|
|
101
|
+
})
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Mail.queue(envelope)
|
|
105
|
+
|
|
106
|
+
Enqueue an email for immediate async processing by the queue worker. Does not block the current request.
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
Mail.queue({
|
|
110
|
+
to: 'bob@example.com',
|
|
111
|
+
subject: 'Password Reset',
|
|
112
|
+
content: { html: '<p>Click the link to reset your password.</p>' }
|
|
113
|
+
})
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Mail.later(envelope, scheduledDate)
|
|
117
|
+
|
|
118
|
+
Enqueue an email for delivery at a future date. The `scheduledDate` is an `Iso.Instant`.
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
Mail.later(
|
|
122
|
+
{ to: 'user@example.com', subject: 'Reminder', content: { text: 'Your trial ends tomorrow.' } },
|
|
123
|
+
'2025-01-15T09:00:00Z' as Iso.Instant
|
|
124
|
+
)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Mail.provider(key)
|
|
128
|
+
|
|
129
|
+
Resolve a named mail service instance. Returns `{ send, queue, later }`.
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
const transactional = Mail.provider('transactional')
|
|
133
|
+
await transactional.send({ to: '...', subject: '...', content: { html: '...' } })
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Mail.mailable(name, renderFn)
|
|
137
|
+
|
|
138
|
+
Register a reusable mailable template. The render function receives args and returns an `Envelope` or `Mail.EMPTY_ENVELOPE` to skip sending.
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
const OrderConfirmation = Mail.mailable('OrderConfirmation', async (args: { orderId: string }) => {
|
|
142
|
+
const order = await db.query('SELECT * FROM orders WHERE id = ?', [args.orderId])
|
|
143
|
+
return {
|
|
144
|
+
to: order.email,
|
|
145
|
+
subject: `Order #${order.id} Confirmed`,
|
|
146
|
+
content: { html: `<h1>Thank you, ${order.name}!</h1>` }
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
#### Rendered Mailable Methods
|
|
152
|
+
|
|
153
|
+
After calling `.render(args)`, the returned object provides:
|
|
154
|
+
|
|
155
|
+
- **`send()`** -- Send immediately
|
|
156
|
+
- **`queue()`** -- Enqueue for async processing
|
|
157
|
+
- **`later(scheduledDate)`** -- Schedule for future delivery
|
|
158
|
+
- **`content()`** -- Retrieve the generated content without sending
|
|
159
|
+
- **`provider(key)`** -- Target a specific named mail service
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
// Send immediately
|
|
163
|
+
await OrderConfirmation.render({ orderId: '1042' }).send()
|
|
164
|
+
|
|
165
|
+
// Queue for async processing
|
|
166
|
+
await OrderConfirmation.render({ orderId: '1042' }).queue()
|
|
167
|
+
|
|
168
|
+
// Schedule for later
|
|
169
|
+
await OrderConfirmation.render({ orderId: '1042' }).later('2025-06-01T12:00:00Z' as Iso.Instant)
|
|
170
|
+
|
|
171
|
+
// Get content without sending
|
|
172
|
+
const content = await OrderConfirmation.render({ orderId: '1042' }).content()
|
|
173
|
+
|
|
174
|
+
// Use a specific provider
|
|
175
|
+
await OrderConfirmation.render({ orderId: '1042' }).provider('transactional').send()
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Mail.EMPTY_ENVELOPE
|
|
179
|
+
|
|
180
|
+
Return from a mailable render function to skip sending. Useful for conditional emails.
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
const WeeklyDigest = Mail.mailable('WeeklyDigest', async (args: { userId: string }) => {
|
|
184
|
+
const updates = await getUpdates(args.userId)
|
|
185
|
+
if (updates.length === 0) return Mail.EMPTY_ENVELOPE
|
|
186
|
+
return { to: '...', subject: 'Weekly Digest', content: { html: '...' } }
|
|
187
|
+
})
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Mail.attachments.ics(options)
|
|
191
|
+
|
|
192
|
+
Generate an ICS calendar attachment from structured options.
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
const attachment = Mail.attachments.ics({
|
|
196
|
+
title: 'Team Standup',
|
|
197
|
+
start: '2025-03-01T10:00:00Z' as Iso.Instant,
|
|
198
|
+
end: 'PT1H' as Iso.Duration, // 1-hour duration
|
|
199
|
+
location: 'Conference Room A',
|
|
200
|
+
organizer: { name: 'Alice', email: 'alice@example.com' },
|
|
201
|
+
attendees: [{ name: 'Bob', email: 'bob@example.com', rsvp: true }],
|
|
202
|
+
status: 'CONFIRMED',
|
|
203
|
+
alarms: [{ action: 'display', description: 'Standup in 15 min', trigger: '-PT15M' as Iso.Duration }]
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
await Mail.send({
|
|
207
|
+
to: 'bob@example.com',
|
|
208
|
+
subject: 'Team Standup Invite',
|
|
209
|
+
content: { html: '<p>You are invited to standup.</p>' },
|
|
210
|
+
attachments: [attachment]
|
|
211
|
+
})
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
ICS options support `start` and `end` as `Iso.Instant`, `Iso.ZonedDateTime`, or `Iso.DateTime`. The `end` field also accepts `Iso.Duration`. Additional fields: `description`, `geo`, `url`, `busyStatus`, `categories`, `uid`, `method`, `recurrenceRule`, `sequence`, `calName`, `classification`, `created`, `lastModified`, `htmlContent`, `alarms`.
|
|
215
|
+
|
|
216
|
+
## Envelope Type
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
interface Envelope {
|
|
220
|
+
to: Mail.Address | Mail.Address[]
|
|
221
|
+
from?: Mail.Address // overrides globalFrom
|
|
222
|
+
cc?: Mail.Address | Mail.Address[]
|
|
223
|
+
bcc?: Mail.Address | Mail.Address[]
|
|
224
|
+
attachments?: Mail.Attachment[]
|
|
225
|
+
subject: string | null
|
|
226
|
+
content: { text: string } | { html: string }
|
|
227
|
+
headers?: {
|
|
228
|
+
listUnsubscribe?: string
|
|
229
|
+
listUnsubscribePost?: string
|
|
230
|
+
replyTo?: Mail.Address
|
|
231
|
+
}
|
|
232
|
+
category?: string | null
|
|
233
|
+
extras?: Record<string, string | null>
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
The `Mail.Address` type is either a plain email string or `{ email: string, name?: string }`.
|
|
238
|
+
|
|
239
|
+
## Common Patterns
|
|
240
|
+
|
|
241
|
+
### Sending with Attachments
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
await Mail.send({
|
|
245
|
+
to: 'user@example.com',
|
|
246
|
+
subject: 'Your Invoice',
|
|
247
|
+
content: { html: '<p>Please find your invoice attached.</p>' },
|
|
248
|
+
attachments: [{
|
|
249
|
+
content: Buffer.from(pdfData).toString('base64'),
|
|
250
|
+
filename: 'invoice.pdf',
|
|
251
|
+
type: 'application/pdf',
|
|
252
|
+
disposition: 'attachment'
|
|
253
|
+
}]
|
|
254
|
+
})
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Unsubscribe Headers
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
await Mail.send({
|
|
261
|
+
to: 'subscriber@example.com',
|
|
262
|
+
subject: 'Newsletter',
|
|
263
|
+
content: { html: '...' },
|
|
264
|
+
headers: {
|
|
265
|
+
listUnsubscribe: '<https://example.com/unsubscribe?id=123>',
|
|
266
|
+
listUnsubscribePost: 'List-Unsubscribe=One-Click'
|
|
267
|
+
},
|
|
268
|
+
category: 'newsletter'
|
|
269
|
+
})
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Processing Webhook Events
|
|
273
|
+
|
|
274
|
+
SendGrid, SES, and MailTrap drivers expose `toMailEvent()` to normalize provider-specific webhook payloads into a unified `Mail.Event` type.
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
// SendGrid webhook handler
|
|
278
|
+
const sgDriver = Mail.drivers.sendGrid(sgMail) as SendGridMailDriver
|
|
279
|
+
const event = sgDriver.toMailEvent(rawSendGridEvent) // returns MailEvent | null
|
|
280
|
+
|
|
281
|
+
// SES webhook handler
|
|
282
|
+
const sesDriver = Mail.drivers.ses(sesClient, 'config-set') as SesMailDriver
|
|
283
|
+
const event = sesDriver.toMailEvent(rawSesEvent) // returns MailEvent | null
|
|
284
|
+
|
|
285
|
+
// MailTrap webhook handler
|
|
286
|
+
const mtDriver = Mail.drivers.mailTrap(mtClient) as MailTrapMailDriver
|
|
287
|
+
const event = mtDriver.toMailEvent(rawMailTrapEvent) // returns MailEvent
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
The normalized `Mail.Event` includes: `eventId`, `event` (delivery, bounce, softBounce, open, click, unsubscribe, spamComplaint, reject, suspension), `email`, `messageId`, `timestamp`, `category`, `extras`, `driverName`.
|
|
291
|
+
|
|
292
|
+
### Conditional Mailable with EMPTY_ENVELOPE
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
const InactivityReminder = Mail.mailable('InactivityReminder', async (args: { userId: string }) => {
|
|
296
|
+
const user = await getUser(args.userId)
|
|
297
|
+
if (user.lastActiveAt > thirtyDaysAgo) return Mail.EMPTY_ENVELOPE
|
|
298
|
+
return {
|
|
299
|
+
to: user.email,
|
|
300
|
+
subject: 'We miss you!',
|
|
301
|
+
content: { html: `<p>Hi ${user.name}, it has been a while...</p>` }
|
|
302
|
+
}
|
|
303
|
+
})
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Error Handling with onMessageSendFailure
|
|
307
|
+
|
|
308
|
+
```ts
|
|
309
|
+
Mail.Provider.create({
|
|
310
|
+
driver: Mail.drivers.sendGrid(sgMail),
|
|
311
|
+
name: 'default',
|
|
312
|
+
globalFrom: { email: 'noreply@example.com' },
|
|
313
|
+
logger: mailLogger,
|
|
314
|
+
queueService: Queue.provider('default'),
|
|
315
|
+
onMessageSendFailure(envelope, error) {
|
|
316
|
+
Log.error({ message: 'Mail send failed', to: envelope.to, error })
|
|
317
|
+
}
|
|
318
|
+
})
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
## Testing
|
|
322
|
+
|
|
323
|
+
Use the Stub driver in tests to avoid real email delivery.
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
const stubDriver = Mail.drivers.stub()
|
|
327
|
+
|
|
328
|
+
const mailService = Mail.Provider.create({
|
|
329
|
+
driver: stubDriver,
|
|
330
|
+
name: 'default',
|
|
331
|
+
globalFrom: { email: 'test@example.com' },
|
|
332
|
+
logger: testLogger,
|
|
333
|
+
queueService: testQueueService
|
|
334
|
+
})
|
|
335
|
+
Mail.Provider.register('default', mailService)
|
|
336
|
+
|
|
337
|
+
// Emails go through the full pipeline but are not delivered
|
|
338
|
+
await Mail.send({ to: 'user@example.com', subject: 'Test', content: { text: 'Hello' } })
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
To inspect sent envelopes, pass a logger to the stub driver:
|
|
342
|
+
|
|
343
|
+
```ts
|
|
344
|
+
const sent: Mail.Envelope[] = []
|
|
345
|
+
const stubDriver = Mail.drivers.stub({
|
|
346
|
+
logger: { info: (envelope) => sent.push(envelope) } as any
|
|
347
|
+
})
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
## Cross-Package Integration
|
|
351
|
+
|
|
352
|
+
- **@maestro-js/service-registry** -- Provides the Provider pattern (`create`, `register`, `provider`, facade)
|
|
353
|
+
- **@maestro-js/log** -- Used for structured mail logging (envelope records and webhook events)
|
|
354
|
+
- **@maestro-js/queue** -- Powers `Mail.queue()` and `Mail.later()` for async and scheduled delivery. Queue names follow the pattern `Maestro.Mail.{name}` and `Maestro.Mailable.{name}`
|
|
355
|
+
- **@maestro-js/helpers** -- Used by SES and MailTrap drivers for retry logic (`Helpers.retry()`)
|
|
356
|
+
|
|
357
|
+
## Key Source Files
|
|
358
|
+
|
|
359
|
+
- `packages/mail/src/index.ts` -- Main module, service creation, mailable system, facade export
|
|
360
|
+
- `packages/mail/src/mail-types.ts` -- Type definitions (Envelope, Address, Content, Attachment, Event, Driver)
|
|
361
|
+
- `packages/mail/src/send-grid-mail-driver.ts` -- SendGrid driver with webhook event mapping
|
|
362
|
+
- `packages/mail/src/ses-mail-driver.ts` -- SES driver with raw MIME composition and retry
|
|
363
|
+
- `packages/mail/src/mail-trap-mail-driver.ts` -- MailTrap driver with retry and webhook events
|
|
364
|
+
- `packages/mail/src/stub-mail-driver.ts` -- Stub driver for testing
|
|
365
|
+
- `packages/mail/src/ics-attachment.ts` -- ICS calendar attachment generator
|