@venizia/ignis-docs 0.2.0 → 0.2.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.
- package/README.md +14 -14
- package/content/best-practices/api-usage-examples.md +39 -19
- package/content/best-practices/architectural-patterns.md +22 -11
- package/content/best-practices/architecture-decisions.md +29 -16
- package/content/best-practices/code-style-standards/advanced-patterns.md +43 -31
- package/content/best-practices/code-style-standards/constants-configuration.md +19 -4
- package/content/best-practices/code-style-standards/control-flow.md +33 -1
- package/content/best-practices/code-style-standards/documentation.md +28 -8
- package/content/best-practices/code-style-standards/function-patterns.md +15 -4
- package/content/best-practices/code-style-standards/index.md +7 -2
- package/content/best-practices/code-style-standards/naming-conventions.md +26 -2
- package/content/best-practices/code-style-standards/route-definitions.md +9 -5
- package/content/best-practices/code-style-standards/tooling.md +14 -1
- package/content/best-practices/code-style-standards/type-safety.md +39 -6
- package/content/best-practices/common-pitfalls.md +59 -35
- package/content/best-practices/contribution-workflow.md +6 -2
- package/content/best-practices/data-modeling.md +78 -62
- package/content/best-practices/deployment-strategies.md +56 -19
- package/content/best-practices/error-handling.md +182 -93
- package/content/best-practices/index.md +6 -0
- package/content/best-practices/performance-optimization.md +26 -13
- package/content/best-practices/security-guidelines.md +35 -9
- package/content/best-practices/testing-strategies.md +7 -2
- package/content/best-practices/troubleshooting-tips.md +27 -17
- package/content/extensions/components/api-reference.md +107 -322
- package/content/extensions/components/authentication/api.md +454 -603
- package/content/extensions/components/authentication/errors.md +121 -498
- package/content/extensions/components/authentication/index.md +88 -801
- package/content/extensions/components/authentication/usage.md +207 -956
- package/content/extensions/components/authorization/api.md +736 -656
- package/content/extensions/components/authorization/errors.md +168 -206
- package/content/extensions/components/authorization/index.md +82 -797
- package/content/extensions/components/authorization/usage.md +194 -527
- package/content/extensions/components/health-check.md +71 -243
- package/content/extensions/components/mail/api.md +504 -287
- package/content/extensions/components/mail/errors.md +73 -61
- package/content/extensions/components/mail/index.md +96 -467
- package/content/extensions/components/mail/usage.md +130 -172
- package/content/extensions/components/request-tracker.md +66 -173
- package/content/extensions/components/socket-io/api.md +195 -13
- package/content/extensions/components/socket-io/errors.md +3 -3
- package/content/extensions/components/socket-io/index.md +50 -337
- package/content/extensions/components/socket-io/usage.md +143 -26
- package/content/extensions/components/static-asset/api.md +410 -142
- package/content/extensions/components/static-asset/errors.md +110 -53
- package/content/extensions/components/static-asset/index.md +79 -608
- package/content/extensions/components/static-asset/usage.md +180 -300
- package/content/extensions/components/websocket/api.md +275 -399
- package/content/extensions/components/websocket/errors.md +47 -56
- package/content/extensions/components/websocket/index.md +74 -407
- package/content/extensions/components/websocket/usage.md +110 -341
- package/content/extensions/helpers/cron/index.md +51 -160
- package/content/extensions/helpers/crypto/index.md +62 -483
- package/content/extensions/helpers/crypto/reference.md +456 -0
- package/content/extensions/helpers/env/index.md +60 -178
- package/content/extensions/helpers/error/index.md +221 -207
- package/content/extensions/helpers/inversion/index.md +65 -556
- package/content/extensions/helpers/inversion/reference.md +522 -0
- package/content/extensions/helpers/kafka/admin.md +20 -1
- package/content/extensions/helpers/kafka/compile-binary.md +41 -35
- package/content/extensions/helpers/kafka/consumer.md +54 -20
- package/content/extensions/helpers/kafka/examples.md +21 -16
- package/content/extensions/helpers/kafka/index.md +80 -610
- package/content/extensions/helpers/kafka/producer.md +134 -5
- package/content/extensions/helpers/kafka/schema-registry.md +45 -70
- package/content/extensions/helpers/logger/hf-logger.md +193 -0
- package/content/extensions/helpers/logger/index.md +64 -563
- package/content/extensions/helpers/logger/pino.md +85 -0
- package/content/extensions/helpers/logger/reference.md +746 -0
- package/content/extensions/helpers/network/api.md +241 -195
- package/content/extensions/helpers/network/index.md +72 -530
- package/content/extensions/helpers/queue/index.md +72 -900
- package/content/extensions/helpers/queue/reference.md +467 -0
- package/content/extensions/helpers/redis/index.md +73 -645
- package/content/extensions/helpers/redis/reference.md +727 -0
- package/content/extensions/helpers/secrets/index.md +66 -0
- package/content/extensions/helpers/socket-io/api.md +305 -203
- package/content/extensions/helpers/socket-io/index.md +66 -432
- package/content/extensions/helpers/storage/api.md +564 -462
- package/content/extensions/helpers/storage/index.md +77 -573
- package/content/extensions/helpers/types/index.md +66 -499
- package/content/extensions/helpers/types/reference.md +650 -0
- package/content/extensions/helpers/uid/index.md +58 -227
- package/content/extensions/helpers/websocket/api.md +329 -216
- package/content/extensions/helpers/websocket/index.md +65 -503
- package/content/extensions/helpers/worker-thread/index.md +58 -396
- package/content/extensions/helpers/worker-thread/reference.md +428 -0
- package/content/guides/core-concepts/persistent/models.md +1 -1
- package/content/guides/core-concepts/persistent/search-typesense.md +3 -3
- package/content/guides/core-concepts/persistent/transactions.md +1 -1
- package/content/guides/core-concepts/secrets-vault.md +177 -0
- package/content/guides/core-concepts/services.md +1 -1
- package/content/guides/migrations/redis-helpers-migration.md +1 -1
- package/content/guides/migrations/unified-connectors-migration.md +2 -2
- package/content/guides/tutorials/ecommerce-api.md +3 -8
- package/content/references/base/application.md +1 -1
- package/content/references/base/connectors.md +79 -136
- package/content/references/base/datasources-reference.md +599 -0
- package/content/references/base/datasources.md +84 -444
- package/content/references/base/dependency-injection.md +17 -39
- package/content/references/base/filter-system/application-usage.md +69 -121
- package/content/references/base/filter-system/array-operators.md +12 -0
- package/content/references/base/filter-system/comparison-operators.md +12 -0
- package/content/references/base/filter-system/default-filter.md +136 -348
- package/content/references/base/filter-system/fields-order-pagination.md +38 -16
- package/content/references/base/filter-system/index.md +106 -257
- package/content/references/base/filter-system/json-filtering.md +12 -2
- package/content/references/base/filter-system/list-operators.md +16 -2
- package/content/references/base/filter-system/logical-operators.md +13 -0
- package/content/references/base/filter-system/null-operators.md +13 -0
- package/content/references/base/filter-system/pattern-matching.md +12 -0
- package/content/references/base/filter-system/quick-reference.md +11 -2
- package/content/references/base/filter-system/range-operators.md +12 -0
- package/content/references/base/filter-system/tips.md +70 -133
- package/content/references/base/filter-system/use-cases.md +156 -233
- package/content/references/base/middlewares.md +35 -21
- package/content/references/base/models-reference.md +886 -0
- package/content/references/base/models.md +80 -1452
- package/content/references/base/repositories/advanced.md +156 -192
- package/content/references/base/repositories/index.md +77 -650
- package/content/references/base/repositories/mixins.md +22 -18
- package/content/references/base/repositories/relations.md +123 -171
- package/content/references/base/repositories/soft-deletable.md +58 -56
- package/content/references/base/secrets.md +263 -0
- package/content/references/base/services.md +2 -2
- package/content/references/configuration/environment-variables.md +48 -4
- package/content/references/configuration/index.md +49 -31
- package/content/references/quick-reference.md +3 -16
- package/content/references/utilities/crypto.md +35 -76
- package/content/references/utilities/date.md +33 -73
- package/content/references/utilities/index.md +1 -1
- package/content/references/utilities/jsx-reference.md +298 -0
- package/content/references/utilities/jsx.md +82 -525
- package/content/references/utilities/module.md +29 -62
- package/content/references/utilities/parse.md +34 -64
- package/content/references/utilities/performance.md +33 -58
- package/content/references/utilities/promise.md +28 -62
- package/content/references/utilities/request.md +57 -218
- package/content/references/utilities/schema.md +43 -137
- package/content/references/utilities/statuses-reference.md +361 -0
- package/content/references/utilities/statuses.md +63 -667
- package/package.json +8 -8
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# Mail -- Usage & Examples
|
|
2
2
|
|
|
3
|
-
> Practical examples for sending emails, using templates, queue executors, verification generators
|
|
3
|
+
> Practical examples for sending emails, using templates, queue executors, and verification generators.
|
|
4
4
|
|
|
5
|
-
## Sending
|
|
5
|
+
## Sending emails
|
|
6
6
|
|
|
7
|
-
Inject `IMailService` via the `MailKeys.MAIL_SERVICE` binding key
|
|
7
|
+
Inject `IMailService` via the `MailKeys.MAIL_SERVICE` binding key from any service.
|
|
8
8
|
|
|
9
|
-
**
|
|
9
|
+
**Send a single email:**
|
|
10
10
|
|
|
11
11
|
```typescript
|
|
12
12
|
import { BaseService, inject } from '@venizia/ignis';
|
|
@@ -39,7 +39,10 @@ export class UserService extends BaseService {
|
|
|
39
39
|
}
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
**
|
|
42
|
+
- **Default `from`.** If `message.from` is omitted, `send()` fills it from `MailKeys.MAIL_OPTIONS`. With `fromName` set, the default renders as `"fromName" <from>`; with neither `from` nor `fromName`, it falls back to `MailDefaults.FALLBACK_FROM` (`noreply@example.com`).
|
|
43
|
+
- **Transport errors never throw here.** The built-in Nodemailer and Mailgun transports catch their own errors and return `{ success: false, error }`. `send()` only throws `SEND_FAILED` for a custom transport that throws instead of returning a failed result.
|
|
44
|
+
|
|
45
|
+
**Send a batch of emails:**
|
|
43
46
|
|
|
44
47
|
```typescript
|
|
45
48
|
async sendBulkNotifications(users: Array<{ email: string; name: string }>) {
|
|
@@ -64,31 +67,35 @@ async sendBulkNotifications(users: Array<{ email: string; name: string }>) {
|
|
|
64
67
|
}
|
|
65
68
|
```
|
|
66
69
|
|
|
67
|
-
**
|
|
70
|
+
- **Concurrency defaults to `MailDefaults.BATCH_CONCURRENCY` (`5`).** `sendBatch()` runs every message through `send()` with `executePromiseWithLimit()`.
|
|
71
|
+
- **One bad message never aborts the batch.** A `send()` that throws is caught per-message and converted to `{ success: false, error }`; only a failure of the batch operation itself throws `BATCH_SEND_FAILED`.
|
|
72
|
+
|
|
73
|
+
### Message validation
|
|
68
74
|
|
|
69
|
-
|
|
75
|
+
`MailService.validateMessage()` runs before every send and throws immediately -- before the transport is ever called -- if any of these hold:
|
|
70
76
|
|
|
71
|
-
| Condition | Error
|
|
77
|
+
| Condition | Error code | Message |
|
|
72
78
|
|-----------|-----------|---------|
|
|
73
|
-
| `to` is missing or empty array | `MailErrorCodes.INVALID_RECIPIENT` | `Recipient email address is required` |
|
|
79
|
+
| `to` is missing or an empty array | `MailErrorCodes.INVALID_RECIPIENT` | `Recipient email address is required` |
|
|
74
80
|
| `subject` is missing | `MailErrorCodes.INVALID_CONFIGURATION` | `Email subject is required` |
|
|
75
81
|
| Both `text` and `html` are missing | `MailErrorCodes.INVALID_CONFIGURATION` | `Email must have either text or html content` |
|
|
76
82
|
|
|
77
83
|
```typescript
|
|
78
|
-
//
|
|
84
|
+
// Throws before reaching the transport
|
|
79
85
|
await mailService.send({
|
|
80
86
|
to: 'user@example.com',
|
|
81
87
|
subject: '', // Empty subject triggers validation error
|
|
82
88
|
html: '<p>Hello</p>',
|
|
83
89
|
});
|
|
84
|
-
// Error: { statusCode: 400,
|
|
90
|
+
// Error: { statusCode: 400, message: 'Email subject is required',
|
|
91
|
+
// normalized: { code: 'core.mail.invalid_configuration', args: {}, text: 'Email subject is required' } }
|
|
85
92
|
```
|
|
86
93
|
|
|
87
|
-
## Template
|
|
94
|
+
## Template engine
|
|
88
95
|
|
|
89
|
-
###
|
|
96
|
+
### Register and send a template
|
|
90
97
|
|
|
91
|
-
Inject both `IMailTemplateEngine` and `IMailService`
|
|
98
|
+
Inject both `IMailTemplateEngine` and `IMailService`. `sendTemplate()` renders through the engine and sends via `IMailService` -- it never bypasses `send()`.
|
|
92
99
|
|
|
93
100
|
```typescript
|
|
94
101
|
import { BaseService, inject } from '@venizia/ignis';
|
|
@@ -106,7 +113,6 @@ export class NotificationService extends BaseService {
|
|
|
106
113
|
}
|
|
107
114
|
|
|
108
115
|
registerTemplates() {
|
|
109
|
-
// Register a welcome email template
|
|
110
116
|
this.templateEngine.registerTemplate({
|
|
111
117
|
name: 'welcome-email',
|
|
112
118
|
content: `
|
|
@@ -126,72 +132,33 @@ export class NotificationService extends BaseService {
|
|
|
126
132
|
}
|
|
127
133
|
|
|
128
134
|
async sendWelcomeEmail(userEmail: string, userName: string, verificationCode: string) {
|
|
129
|
-
|
|
135
|
+
return this.mailService.sendTemplate({
|
|
130
136
|
templateName: 'welcome-email',
|
|
131
|
-
data: {
|
|
132
|
-
userName,
|
|
133
|
-
verificationCode,
|
|
134
|
-
appName: 'My Application',
|
|
135
|
-
},
|
|
137
|
+
data: { userName, verificationCode, appName: 'My Application' },
|
|
136
138
|
recipients: userEmail,
|
|
137
139
|
options: {
|
|
138
|
-
|
|
139
|
-
attachments: [
|
|
140
|
-
{
|
|
141
|
-
filename: 'logo.png',
|
|
142
|
-
path: '/path/to/logo.png',
|
|
143
|
-
cid: 'logo',
|
|
144
|
-
},
|
|
145
|
-
],
|
|
140
|
+
attachments: [{ filename: 'logo.png', path: '/path/to/logo.png', cid: 'logo' }],
|
|
146
141
|
},
|
|
147
142
|
});
|
|
148
|
-
|
|
149
|
-
return result;
|
|
150
143
|
}
|
|
151
144
|
}
|
|
152
145
|
```
|
|
153
146
|
|
|
154
|
-
|
|
147
|
+
- **Subject resolution order.** `options.subject` (explicit override) beats the template's own `subject` (rendered through the same engine) beats the literal fallback `'No Subject'`.
|
|
148
|
+
- **`sendTemplate()` requires the template engine binding.** It throws `INVALID_CONFIGURATION` ("Template engine not configured") if `MailKeys.MAIL_TEMPLATE_ENGINE` was never injected -- the constructor parameter is `isOptional: true`, so a service that skips it degrades silently until the first `sendTemplate()` call.
|
|
155
149
|
|
|
156
|
-
|
|
150
|
+
### How rendering works
|
|
157
151
|
|
|
158
|
-
|
|
152
|
+
`TemplateEngineService` keeps templates in an in-memory `Map<string, ITemplate>` and substitutes <code v-pre>{{variable}}</code> placeholders with regex `/\{\{(\s*[\w.]+\s*)\}\}/g`.
|
|
159
153
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
4. Otherwise, the value is converted to string via `String(value)`
|
|
154
|
+
- **Nested lookup.** A key is trimmed, then resolved by splitting on `.` and walking the data object (`user.profile.name`).
|
|
155
|
+
- **Missing values are preserved, not blanked.** If a resolved value is `undefined` or `null`, the original <code v-pre>{{placeholder}}</code> text stays in the output and a warning is logged -- it is never replaced with an empty string.
|
|
156
|
+
- **String coercion.** A resolved value is converted with `String(value)`.
|
|
164
157
|
|
|
165
158
|
> [!IMPORTANT]
|
|
166
|
-
> Missing template variables are **not** replaced with empty strings.
|
|
167
|
-
|
|
168
|
-
**Template Features:**
|
|
169
|
-
|
|
170
|
-
- Simple <code v-pre>{{variable}}</code> syntax (no loops or conditionals)
|
|
171
|
-
- Nested object access via dot notation: <code v-pre>{{user.profile.name}}</code>
|
|
172
|
-
- Subject line templating (subjects are rendered through the same engine)
|
|
173
|
-
- HTML and plain text support
|
|
174
|
-
- Validation before rendering (optional, throws on missing keys)
|
|
175
|
-
- In-memory template registry (`Map<string, ITemplate>`)
|
|
176
|
-
- Template metadata (subject, description via `ITemplate`)
|
|
177
|
-
- Missing placeholders preserved as-is (not replaced with empty strings)
|
|
178
|
-
- `clearTemplates()` to reset the entire registry
|
|
159
|
+
> Missing template variables are **not** replaced with empty strings. This makes debugging easier -- you can see which variables were not resolved directly in the rendered output.
|
|
179
160
|
|
|
180
|
-
###
|
|
181
|
-
|
|
182
|
-
`validateTemplateData()` extracts all unique placeholder keys from a template string and checks if each key resolves to a non-null, non-undefined value in the data object. It returns:
|
|
183
|
-
|
|
184
|
-
```typescript
|
|
185
|
-
{
|
|
186
|
-
isValid: boolean; // true if all placeholders have values
|
|
187
|
-
missingKeys: string[]; // placeholder names missing from data
|
|
188
|
-
allKeys: string[]; // all unique placeholder names found
|
|
189
|
-
}
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
When `requireValidate: true` is passed to `render()` or `renderSimpleTemplate()`, validation runs first and throws with `MailErrorCodes.INVALID_CONFIGURATION` if any keys are missing.
|
|
193
|
-
|
|
194
|
-
Template validation example:
|
|
161
|
+
### Validate template data before sending
|
|
195
162
|
|
|
196
163
|
```typescript
|
|
197
164
|
const template = '<h1>Hello {{userName}}, your code is {{code}}</h1>';
|
|
@@ -200,23 +167,32 @@ const data = { userName: 'John' }; // Missing 'code'
|
|
|
200
167
|
const validation = this.templateEngine.validateTemplateData({ template, data });
|
|
201
168
|
|
|
202
169
|
if (!validation.isValid) {
|
|
203
|
-
console.error('Missing template variables:', validation.missingKeys);
|
|
204
|
-
// Output: ['code']
|
|
170
|
+
console.error('Missing template variables:', validation.missingKeys); // ['code']
|
|
205
171
|
}
|
|
206
172
|
|
|
207
|
-
//
|
|
173
|
+
// Or throw at render time instead of checking manually
|
|
208
174
|
try {
|
|
209
175
|
const html = this.templateEngine.render({
|
|
210
176
|
templateData: template,
|
|
211
177
|
data,
|
|
212
|
-
requireValidate: true, // Throws
|
|
178
|
+
requireValidate: true, // Throws INVALID_CONFIGURATION if any placeholder is missing
|
|
213
179
|
});
|
|
214
180
|
} catch (error) {
|
|
215
181
|
console.error('Template rendering failed:', error.message);
|
|
216
182
|
}
|
|
217
183
|
```
|
|
218
184
|
|
|
219
|
-
|
|
185
|
+
`validateTemplateData()` extracts every unique placeholder key from the template and reports:
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
{
|
|
189
|
+
isValid: boolean; // true if all placeholders resolve to a non-null value
|
|
190
|
+
missingKeys: string[]; // placeholder names missing from data
|
|
191
|
+
allKeys: string[]; // every unique placeholder name found
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Sync templates from a database
|
|
220
196
|
|
|
221
197
|
```typescript
|
|
222
198
|
async syncTemplatesFromDatabase() {
|
|
@@ -229,99 +205,112 @@ async syncTemplatesFromDatabase() {
|
|
|
229
205
|
});
|
|
230
206
|
|
|
231
207
|
const templateConfigs = await configRepository.find({
|
|
232
|
-
filter: {
|
|
233
|
-
where: {
|
|
234
|
-
code: { inq: ['MAIL_TEMPLATE_WELCOME', 'MAIL_TEMPLATE_VERIFICATION'] },
|
|
235
|
-
},
|
|
236
|
-
},
|
|
208
|
+
filter: { where: { code: { inq: ['MAIL_TEMPLATE_WELCOME', 'MAIL_TEMPLATE_VERIFICATION'] } } },
|
|
237
209
|
});
|
|
238
210
|
|
|
239
211
|
templateConfigs.forEach(config => {
|
|
240
212
|
templateEngine.registerTemplate({
|
|
241
213
|
name: config.code,
|
|
242
214
|
content: config.jValue.content,
|
|
243
|
-
options: {
|
|
244
|
-
subject: config.jValue.subject,
|
|
245
|
-
description: config.jValue.description,
|
|
246
|
-
},
|
|
215
|
+
options: { subject: config.jValue.subject, description: config.jValue.description },
|
|
247
216
|
});
|
|
248
217
|
this.logger.info('[syncTemplates] Registered template: %s', config.code);
|
|
249
218
|
});
|
|
250
219
|
}
|
|
251
220
|
```
|
|
252
221
|
|
|
253
|
-
## Queue
|
|
222
|
+
## Queue executors
|
|
223
|
+
|
|
224
|
+
`IMailQueueExecutor` is a separate subsystem from `MailService` -- it only exposes `enqueueVerificationEmail()` and `setProcessor()`, and never calls `send()` on its own. You provide the processor function (typically one that wraps `mailService.send()`); the executor's job is timing, retry, and delivery guarantees around calling it.
|
|
254
225
|
|
|
255
|
-
|
|
226
|
+
| Executor | Class | Backing |
|
|
227
|
+
|----------|-------|---------|
|
|
228
|
+
| `direct` | `DirectMailExecutorHelper` | None -- calls the processor immediately |
|
|
229
|
+
| `internal-queue` | `InternalQueueMailExecutorHelper` | In-memory `SequentialQueueHelper` |
|
|
230
|
+
| `bullmq` | `BullMQMailExecutorHelper` | Redis, via `BullMQHelper` |
|
|
256
231
|
|
|
257
|
-
|
|
232
|
+
### Direct executor
|
|
258
233
|
|
|
259
|
-
|
|
234
|
+
Calls the processor immediately, with no queueing. Returns `{ queued: false, ... }`. Throws `Processor not set. Call setProcessor() first.` if `enqueueVerificationEmail()` runs before `setProcessor()`. Use it for development or when a caller needs a synchronous result.
|
|
260
235
|
|
|
261
|
-
|
|
236
|
+
### Internal queue executor
|
|
262
237
|
|
|
263
|
-
-
|
|
264
|
-
- Supports delayed jobs via `setTimeout` (stored in a `delayedJobs` Map)
|
|
265
|
-
- Retry logic: on failure, retries up to `options.attempts` (default 3) with configurable backoff
|
|
266
|
-
- Backoff calculation: `exponential` uses `delay * 2^(attempt-1)`, `fixed` uses the raw delay, no backoff config defaults to 1000ms
|
|
267
|
-
- Does not persist jobs across restarts
|
|
268
|
-
- Logs queue state changes and individual job lifecycle events
|
|
238
|
+
In-memory, single-instance, backed by `SequentialQueueHelper` from `@venizia/ignis-helpers` with `autoDispatch: true`.
|
|
269
239
|
|
|
270
|
-
|
|
240
|
+
- Job IDs follow `job_<counter>_<timestamp>`.
|
|
241
|
+
- A `delay` option schedules the enqueue itself via `setTimeout`, tracked in a `delayedJobs` map.
|
|
242
|
+
- On failure (a thrown error, or the processor returning `{ success: false }`), it retries up to `options.attempts` (default `3`) with backoff: `exponential` is `delay * 2^(attempt - 1)`, `fixed` is the raw delay, and no `backoff` config at all defaults to `1000ms`.
|
|
243
|
+
- Does not persist jobs across restarts. `close()` clears every pending delayed/retry timer.
|
|
271
244
|
|
|
272
|
-
|
|
245
|
+
### BullMQ executor
|
|
273
246
|
|
|
274
|
-
- Job persistence
|
|
275
|
-
- Distributed worker support
|
|
276
|
-
- Configurable retry strategies (exponential by default, with 1000ms base delay)
|
|
277
|
-
- Job prioritization
|
|
278
|
-
- Delayed job execution
|
|
279
|
-
- Job progress tracking via worker callbacks
|
|
280
|
-
- `removeOnComplete: true`, `removeOnFail: false` (failed jobs retained for debugging)
|
|
247
|
+
Redis-backed, distributed, backed by `BullMQHelper`. Job persistence, worker concurrency, prioritization, and delayed execution come from BullMQ itself. `removeOnComplete: true`, `removeOnFail: false` (failed jobs stay for debugging). Default enqueue options: `attempts: 3`, `backoff: { type: 'exponential', delay: 1000 }`.
|
|
281
248
|
|
|
282
|
-
**Mode
|
|
249
|
+
**Mode gates what the executor can do:**
|
|
283
250
|
|
|
284
|
-
| Mode | Queue
|
|
285
|
-
|
|
286
|
-
| `'queue-only'` | Yes | No (
|
|
251
|
+
| Mode | Queue created | Workers created | Can enqueue | Can process |
|
|
252
|
+
|------|----------------|------------------|-------------|-------------|
|
|
253
|
+
| `'queue-only'` | Yes | No (`setProcessor()` skips worker creation) | Yes -- **without** calling `setProcessor()` first | No |
|
|
287
254
|
| `'worker-only'` | No | Yes | No (throws) | Yes |
|
|
288
|
-
| `'both'` | Yes | Yes | Yes | Yes |
|
|
255
|
+
| `'both'` | Yes | Yes | Yes (requires `setProcessor()` first) | Yes |
|
|
289
256
|
|
|
290
|
-
|
|
257
|
+
> [!IMPORTANT]
|
|
258
|
+
> `'queue-only'` mode is the one exception to "call `setProcessor()` before you enqueue" -- `enqueueVerificationEmail()` only requires a processor when the mode is *not* `queue-only`. A producer instance can enqueue jobs a separate `worker-only` instance later processes.
|
|
291
259
|
|
|
292
|
-
|
|
260
|
+
**Dynamic worker management** -- get the bound instance and manage workers at runtime, no restart required:
|
|
293
261
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
262
|
+
```typescript
|
|
263
|
+
const executor = this.application.get<BullMQMailExecutorHelper>({
|
|
264
|
+
key: MailKeys.MAIL_QUEUE_EXECUTOR_INSTANCE,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
executor.addWorker({ workerIdentifier: 'mail-queue-worker-extra', concurrency: 10, lockDuration: 60000 });
|
|
297
268
|
|
|
298
|
-
|
|
269
|
+
executor.getWorkerCount(); // e.g. 2
|
|
270
|
+
executor.getMode(); // e.g. 'both'
|
|
299
271
|
|
|
300
|
-
|
|
272
|
+
await executor.removeWorker(1); // remove by array index
|
|
273
|
+
await executor.clearWorkers(); // close and remove every worker
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
`setProcessor()` on the BullMQ executor is `async` and takes an optional second argument for worker configuration -- it clears all existing workers before creating new ones:
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
await executor.setProcessor(
|
|
280
|
+
async (email: string) => {
|
|
281
|
+
// your processing logic
|
|
282
|
+
return { success: true, message: 'Sent', expiresInMinutes: 10 };
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
numberOfWorkers: 3, // default: 1
|
|
286
|
+
concurrencyPerWorker: 10, // default: 5
|
|
287
|
+
lockDuration: 60000, // job lock duration in ms, default: 30000
|
|
288
|
+
},
|
|
289
|
+
);
|
|
290
|
+
```
|
|
301
291
|
|
|
302
|
-
|
|
292
|
+
## Verification generators
|
|
303
293
|
|
|
304
|
-
|
|
294
|
+
`MailComponent` binds three generators, all **transient** (a fresh instance per resolution, since none is registered with `.setScope('singleton')`):
|
|
305
295
|
|
|
306
|
-
|
|
296
|
+
| Generator | Implements | Behavior |
|
|
297
|
+
|-----------|-----------|----------|
|
|
298
|
+
| `NumericCodeGenerator` | `IVerificationCodeGenerator` | `crypto.randomInt(0, 10^length)`, zero-padded via `padStart()` -- e.g. code `42` at length `6` becomes `"000042"` |
|
|
299
|
+
| `RandomTokenGenerator` | `IVerificationTokenGenerator` | `crypto.randomBytes(bytes).toString('base64url')` -- 32 bytes produces a 43-character string, URL-safe, no padding |
|
|
300
|
+
| `DefaultVerificationDataGenerator` | `IVerificationDataGenerator` | Composes both generators via `@inject`, producing a full `IVerificationData` with separate expiries |
|
|
307
301
|
|
|
308
|
-
|
|
309
|
-
- A short numeric code for manual entry (SMS, email)
|
|
310
|
-
- A long random base64url token for URL-based verification
|
|
311
|
-
- Separate expiry times: code uses `getExpiryTime(minutes)`, token uses `getExpiryTimeInHours(hours)`
|
|
312
|
-
- Generation timestamps in ISO 8601 format
|
|
313
|
-
- Attempt counter (set to 0 initially)
|
|
314
|
-
- `lastCodeSentAt` set to `now`
|
|
302
|
+
`DefaultVerificationDataGenerator.generateVerificationData()` returns:
|
|
315
303
|
|
|
316
|
-
|
|
304
|
+
- A short numeric code (manual entry: SMS, email)
|
|
305
|
+
- A long base64url token (URL-based verification links)
|
|
306
|
+
- Separate expiries: code via `getExpiryTime(minutes)`, token via `getExpiryTimeInHours(hours)`
|
|
307
|
+
- ISO 8601 generation timestamps, `codeAttempts: 0`, `lastCodeSentAt` set to now
|
|
308
|
+
|
|
309
|
+
**End-to-end verification flow:**
|
|
317
310
|
|
|
318
311
|
```typescript
|
|
319
312
|
import { BaseService, inject } from '@venizia/ignis';
|
|
320
|
-
import {
|
|
321
|
-
MailKeys,
|
|
322
|
-
type IMailService,
|
|
323
|
-
type IVerificationDataGenerator,
|
|
324
|
-
} from '@venizia/ignis/mail';
|
|
313
|
+
import { MailKeys, type IMailService, type IVerificationDataGenerator } from '@venizia/ignis/mail';
|
|
325
314
|
|
|
326
315
|
export class AuthService extends BaseService {
|
|
327
316
|
constructor(
|
|
@@ -334,18 +323,15 @@ export class AuthService extends BaseService {
|
|
|
334
323
|
}
|
|
335
324
|
|
|
336
325
|
async sendVerificationEmail(userEmail: string) {
|
|
337
|
-
// Generate verification code and token
|
|
338
326
|
const verificationData = this.verificationGenerator.generateVerificationData({
|
|
339
|
-
codeLength: 6,
|
|
340
|
-
tokenBytes: 32,
|
|
341
|
-
codeExpiryMinutes: 10,
|
|
342
|
-
tokenExpiryHours: 24,
|
|
327
|
+
codeLength: 6,
|
|
328
|
+
tokenBytes: 32,
|
|
329
|
+
codeExpiryMinutes: 10,
|
|
330
|
+
tokenExpiryHours: 24,
|
|
343
331
|
});
|
|
344
332
|
|
|
345
|
-
//
|
|
346
|
-
// await this.saveVerificationData(userEmail, verificationData);
|
|
333
|
+
// Persist verificationData to your own user/verification table here.
|
|
347
334
|
|
|
348
|
-
// Send verification email
|
|
349
335
|
const result = await this.mailService.send({
|
|
350
336
|
to: userEmail,
|
|
351
337
|
subject: 'Email Verification',
|
|
@@ -362,43 +348,15 @@ export class AuthService extends BaseService {
|
|
|
362
348
|
}
|
|
363
349
|
```
|
|
364
350
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
```typescript
|
|
368
|
-
const verificationData = this.verificationGenerator.generateVerificationData({
|
|
369
|
-
codeLength: 6, // 6-digit code
|
|
370
|
-
tokenBytes: 32, // 32-byte token -> 43-char base64url string
|
|
371
|
-
codeExpiryMinutes: 10, // Code expires in 10 minutes
|
|
372
|
-
tokenExpiryHours: 24, // Token expires in 24 hours
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
// Store in database
|
|
376
|
-
await this.userRepo.update({
|
|
377
|
-
where: { id: userId },
|
|
378
|
-
data: {
|
|
379
|
-
verificationCode: verificationData.verificationCode,
|
|
380
|
-
verificationCodeExpiresAt: new Date(verificationData.codeExpiresAt),
|
|
381
|
-
verificationToken: verificationData.verificationToken,
|
|
382
|
-
verificationTokenExpiresAt: new Date(verificationData.tokenExpiresAt),
|
|
383
|
-
},
|
|
384
|
-
});
|
|
385
|
-
```
|
|
386
|
-
|
|
387
|
-
## Security Note
|
|
351
|
+
## Logging and credentials
|
|
388
352
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
```typescript
|
|
392
|
-
this.logger.for(this.createAndBindInstances.name).info('Mail Options: %j', mailOptions);
|
|
393
|
-
```
|
|
353
|
+
`MailComponent.createAndBindInstances()` logs only `mailOptions.provider` and `queueExecutorConfig.type` at `info` level -- by design, never the full config objects, so SMTP passwords, OAuth2 secrets, API keys, and Redis passwords never reach a log sink through the component itself.
|
|
394
354
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
- Uses a log pipeline that redacts sensitive fields
|
|
398
|
-
- Strips credential fields before binding the options
|
|
355
|
+
> [!WARNING]
|
|
356
|
+
> That guarantee only covers what `MailComponent` logs internally. If your own wrapper component or bootstrap code logs the `TMailOptions` or `IMailQueueExecutorConfig` object directly (for example, while debugging a binding), you reintroduce the leak yourself -- log individual safe fields (`provider`, `type`) instead of the whole object.
|
|
399
357
|
|
|
400
|
-
## See
|
|
358
|
+
## See also
|
|
401
359
|
|
|
402
|
-
- [Setup & Configuration](./) --
|
|
403
|
-
- [API Reference](./api) --
|
|
404
|
-
- [Error Reference](./errors) --
|
|
360
|
+
- [Setup & Configuration](./) -- quick reference, setup, configuration, and binding keys
|
|
361
|
+
- [API Reference](./api) -- architecture, interfaces, and internals
|
|
362
|
+
- [Error Reference](./errors) -- error codes and troubleshooting
|