@tc-libs/email-sender 5.2.1 → 5.3.1
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 +32 -0
- package/package.json +10 -6
- package/src/constants/email-outbox.constant.d.ts +13 -0
- package/src/constants/email-outbox.constant.js +23 -0
- package/src/constants/email-outbox.constant.js.map +1 -0
- package/src/email-sender.module.js +21 -4
- package/src/email-sender.module.js.map +1 -1
- package/src/email-sender.module.options.d.ts +1 -0
- package/src/index.d.ts +6 -0
- package/src/index.js +8 -0
- package/src/index.js.map +1 -1
- package/src/interfaces/email-outbox.interface.d.ts +13 -0
- package/src/interfaces/email-outbox.interface.js +3 -0
- package/src/interfaces/email-outbox.interface.js.map +1 -0
- package/src/repository/email-outbox.repository.module.d.ts +2 -0
- package/src/repository/email-outbox.repository.module.js +27 -0
- package/src/repository/email-outbox.repository.module.js.map +1 -0
- package/src/repository/entities/email-outbox.entity.d.ts +30 -0
- package/src/repository/entities/email-outbox.entity.js +76 -0
- package/src/repository/entities/email-outbox.entity.js.map +1 -0
- package/src/repository/repositories/email-outbox.repository.d.ts +25 -0
- package/src/repository/repositories/email-outbox.repository.js +59 -0
- package/src/repository/repositories/email-outbox.repository.js.map +1 -0
- package/src/services/email-outbox.service.d.ts +20 -0
- package/src/services/email-outbox.service.js +96 -0
- package/src/services/email-outbox.service.js.map +1 -0
- package/src/tasks/email-outbox.task.d.ts +19 -0
- package/src/tasks/email-outbox.task.js +52 -0
- package/src/tasks/email-outbox.task.js.map +1 -0
- package/src/utils/email-outbox.util.d.ts +4 -0
- package/src/utils/email-outbox.util.js +17 -0
- package/src/utils/email-outbox.util.js.map +1 -0
package/README.md
CHANGED
|
@@ -77,6 +77,38 @@ await this.emailSenderService.sendTemplate({
|
|
|
77
77
|
|
|
78
78
|
Il package assume l'esistenza di template email persistiti nella collection dedicata, non solo file `.html` locali.
|
|
79
79
|
|
|
80
|
+
## Outbox (invio transazionale)
|
|
81
|
+
|
|
82
|
+
`EmailOutboxService`, registrato da `EmailSenderModule`, separa l'evento dall'invio:
|
|
83
|
+
|
|
84
|
+
- `enqueue(messaggio, { idempotencyKey, session })` scrive nella collection `emailoutboxes`. Con la `session` della transazione dell'evento, l'email esiste solo se l'evento viene confermato. La stessa chiave non crea una seconda email, nemmeno dentro la transazione: si usa un upsert, che non fallisce con un duplicate key;
|
|
85
|
+
- `processDue(now?)` prende in carico le email dovute una alla volta, con un update atomico, e le invia con `sendTemplate`. In caso d'errore ritenta dopo 1, 2, 4, 8 minuti (al massimo un'ora); dopo 5 tentativi l'email resta `failed` e parte una `notifyError` senza destinatari né variabili. Gli errori salvati hanno gli indirizzi oscurati;
|
|
86
|
+
- l'invio è "almeno una volta": un nodo che cade fra l'invio e la conferma fa ripartire l'email dopo 10 minuti.
|
|
87
|
+
|
|
88
|
+
Le variabili si salvano su MongoDB: devono essere dati semplici (niente documenti Mongoose, metodi o stream). Gli allegati non sono ammessi: un'email con allegato resta su `sendTemplate`.
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
await this.emailOutbox.enqueue(
|
|
92
|
+
{
|
|
93
|
+
template: 'user_email_confirm',
|
|
94
|
+
lang: 'fr',
|
|
95
|
+
to: [user.email],
|
|
96
|
+
variables: { firstName: user.firstName, link },
|
|
97
|
+
},
|
|
98
|
+
{ idempotencyKey: `signup:${code}`, session },
|
|
99
|
+
);
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Task di invio
|
|
103
|
+
|
|
104
|
+
`EmailOutboxTask` (codice `email-outbox`, ogni minuto) estende `AbstractCronTask` di `@tc-libs/task`: lock distribuito per `app.name` e nessuna transazione intorno all'invio, che è un effetto esterno. L'app:
|
|
105
|
+
|
|
106
|
+
- passa `app.name` nelle opzioni di `EmailSenderModule`;
|
|
107
|
+
- registra `EmailOutboxTask` fra i provider del router dei job, che importa `TaskModule` e `RetryPolicyModule` con `ScheduleModule` attivo;
|
|
108
|
+
- crea e attiva il task per ambiente: i task di `@tc-libs/task` nascono inattivi, e finché non lo è nessuna email in coda parte.
|
|
109
|
+
|
|
110
|
+
Nei test, con lo scheduler spento, si chiama `processDue()` direttamente.
|
|
111
|
+
|
|
80
112
|
## Sviluppo
|
|
81
113
|
|
|
82
114
|
```bash
|
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tc-libs/email-sender",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.3.1",
|
|
4
4
|
"dependencies": {
|
|
5
|
-
"@tc-libs/app-cache": "5.
|
|
6
|
-
"@tc-libs/database": "5.
|
|
7
|
-
"@tc-libs/errors": "5.
|
|
8
|
-
"@tc-libs/
|
|
9
|
-
"@tc-libs/
|
|
5
|
+
"@tc-libs/app-cache": "5.3.1",
|
|
6
|
+
"@tc-libs/database": "5.3.1",
|
|
7
|
+
"@tc-libs/errors": "5.3.1",
|
|
8
|
+
"@tc-libs/logger": "5.3.1",
|
|
9
|
+
"@tc-libs/pagination": "5.3.1",
|
|
10
|
+
"@tc-libs/retry-policy": "5.3.1",
|
|
11
|
+
"@tc-libs/service": "5.3.1",
|
|
12
|
+
"@tc-libs/task": "5.3.1",
|
|
10
13
|
"axios": "1.8.4",
|
|
11
14
|
"ejs": "3.1.10",
|
|
12
15
|
"mailgun-js": "^0.22.0",
|
|
@@ -18,6 +21,7 @@
|
|
|
18
21
|
"peerDependencies": {
|
|
19
22
|
"@nestjs/common": "^11.0.0 || ^12.0.0",
|
|
20
23
|
"@nestjs/mongoose": "^11.0.0 || ^12.0.0",
|
|
24
|
+
"@nestjs/schedule": "^5.0.0 || ^12.0.0",
|
|
21
25
|
"class-validator": "^0.14.0",
|
|
22
26
|
"mongoose": "^8.0.0 || ^9.0.0"
|
|
23
27
|
},
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare enum EMAIL_OUTBOX_STATUS {
|
|
2
|
+
PENDING = "pending",
|
|
3
|
+
SENDING = "sending",
|
|
4
|
+
SENT = "sent",
|
|
5
|
+
FAILED = "failed"
|
|
6
|
+
}
|
|
7
|
+
export declare const EMAIL_OUTBOX_TASK_CODE = "email-outbox";
|
|
8
|
+
export declare const EMAIL_OUTBOX_TASK_STALE_MS: number;
|
|
9
|
+
export declare const EMAIL_OUTBOX_BATCH_SIZE = 20;
|
|
10
|
+
export declare const EMAIL_OUTBOX_MAX_ATTEMPTS = 5;
|
|
11
|
+
export declare const EMAIL_OUTBOX_BACKOFF_BASE_MS: number;
|
|
12
|
+
export declare const EMAIL_OUTBOX_BACKOFF_MAX_MS: number;
|
|
13
|
+
export declare const EMAIL_OUTBOX_CLAIM_TIMEOUT_MS: number;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EMAIL_OUTBOX_CLAIM_TIMEOUT_MS = exports.EMAIL_OUTBOX_BACKOFF_MAX_MS = exports.EMAIL_OUTBOX_BACKOFF_BASE_MS = exports.EMAIL_OUTBOX_MAX_ATTEMPTS = exports.EMAIL_OUTBOX_BATCH_SIZE = exports.EMAIL_OUTBOX_TASK_STALE_MS = exports.EMAIL_OUTBOX_TASK_CODE = exports.EMAIL_OUTBOX_STATUS = void 0;
|
|
4
|
+
var EMAIL_OUTBOX_STATUS;
|
|
5
|
+
(function (EMAIL_OUTBOX_STATUS) {
|
|
6
|
+
EMAIL_OUTBOX_STATUS["PENDING"] = "pending";
|
|
7
|
+
// Presa in carico da un invio: se resta così oltre EMAIL_OUTBOX_CLAIM_TIMEOUT_MS il nodo è caduto e si riprova.
|
|
8
|
+
EMAIL_OUTBOX_STATUS["SENDING"] = "sending";
|
|
9
|
+
EMAIL_OUTBOX_STATUS["SENT"] = "sent";
|
|
10
|
+
EMAIL_OUTBOX_STATUS["FAILED"] = "failed";
|
|
11
|
+
})(EMAIL_OUTBOX_STATUS || (exports.EMAIL_OUTBOX_STATUS = EMAIL_OUTBOX_STATUS = {}));
|
|
12
|
+
exports.EMAIL_OUTBOX_TASK_CODE = 'email-outbox';
|
|
13
|
+
// Lock del task: ben oltre la durata di un giro (EMAIL_OUTBOX_BATCH_SIZE invii con il timeout HTTP di Mailgun).
|
|
14
|
+
exports.EMAIL_OUTBOX_TASK_STALE_MS = 10 * 60 * 1000;
|
|
15
|
+
exports.EMAIL_OUTBOX_BATCH_SIZE = 20;
|
|
16
|
+
// Dopo l'ultimo tentativo fallito l'email resta FAILED, con una notifica senza dati personali.
|
|
17
|
+
exports.EMAIL_OUTBOX_MAX_ATTEMPTS = 5;
|
|
18
|
+
// Attesa prima del tentativo n: 1, 2, 4, 8 minuti..., al massimo un'ora.
|
|
19
|
+
exports.EMAIL_OUTBOX_BACKOFF_BASE_MS = 60 * 1000;
|
|
20
|
+
exports.EMAIL_OUTBOX_BACKOFF_MAX_MS = 60 * 60 * 1000;
|
|
21
|
+
// Un'email SENDING da più di così è rimasta orfana (nodo caduto durante l'invio) e torna disponibile.
|
|
22
|
+
exports.EMAIL_OUTBOX_CLAIM_TIMEOUT_MS = 10 * 60 * 1000;
|
|
23
|
+
//# sourceMappingURL=email-outbox.constant.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-outbox.constant.js","sourceRoot":"","sources":["../../../../../packages/email-sender/src/constants/email-outbox.constant.ts"],"names":[],"mappings":";;;AAAA,IAAY,mBAMX;AAND,WAAY,mBAAmB;IAC7B,0CAAmB,CAAA;IACnB,gHAAgH;IAChH,0CAAmB,CAAA;IACnB,oCAAa,CAAA;IACb,wCAAiB,CAAA;AACnB,CAAC,EANW,mBAAmB,mCAAnB,mBAAmB,QAM9B;AAEY,QAAA,sBAAsB,GAAG,cAAc,CAAC;AAErD,gHAAgH;AACnG,QAAA,0BAA0B,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAE5C,QAAA,uBAAuB,GAAG,EAAE,CAAC;AAE1C,+FAA+F;AAClF,QAAA,yBAAyB,GAAG,CAAC,CAAC;AAE3C,yEAAyE;AAC5D,QAAA,4BAA4B,GAAG,EAAE,GAAG,IAAI,CAAC;AACzC,QAAA,2BAA2B,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAE1D,sGAAsG;AACzF,QAAA,6BAA6B,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC"}
|
|
@@ -3,24 +3,36 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.EmailSenderModule = void 0;
|
|
4
4
|
const email_sender_module_options_1 = require("./email-sender.module.options");
|
|
5
5
|
const email_sender_service_1 = require("./services/email-sender.service");
|
|
6
|
+
const email_outbox_service_1 = require("./services/email-outbox.service");
|
|
6
7
|
const email_repository_module_1 = require("./repository/email.repository.module");
|
|
8
|
+
const email_outbox_repository_module_1 = require("./repository/email-outbox.repository.module");
|
|
9
|
+
// EmailOutboxTask non è fra i provider: l'app lo registra nel router dei job, dove c'è TaskModule.
|
|
7
10
|
class EmailSenderModule {
|
|
8
11
|
static register(options, isGlobal) {
|
|
9
12
|
return {
|
|
10
13
|
module: EmailSenderModule,
|
|
11
|
-
imports: [email_repository_module_1.EmailRepositoryModule],
|
|
14
|
+
imports: [email_repository_module_1.EmailRepositoryModule, email_outbox_repository_module_1.EmailOutboxRepositoryModule],
|
|
12
15
|
providers: [
|
|
13
16
|
{ provide: email_sender_module_options_1.EMAIL_SENDER_CONFIG_OPTIONS, useValue: options },
|
|
14
17
|
email_sender_service_1.EmailSenderService,
|
|
18
|
+
email_outbox_service_1.EmailOutboxService,
|
|
19
|
+
],
|
|
20
|
+
exports: [
|
|
21
|
+
email_sender_module_options_1.EMAIL_SENDER_CONFIG_OPTIONS,
|
|
22
|
+
email_sender_service_1.EmailSenderService,
|
|
23
|
+
email_outbox_service_1.EmailOutboxService,
|
|
15
24
|
],
|
|
16
|
-
exports: [email_sender_service_1.EmailSenderService],
|
|
17
25
|
global: isGlobal,
|
|
18
26
|
};
|
|
19
27
|
}
|
|
20
28
|
static registerAsync(options, isGlobal) {
|
|
21
29
|
return {
|
|
22
30
|
module: EmailSenderModule,
|
|
23
|
-
imports: [
|
|
31
|
+
imports: [
|
|
32
|
+
...(options.imports || []),
|
|
33
|
+
email_repository_module_1.EmailRepositoryModule,
|
|
34
|
+
email_outbox_repository_module_1.EmailOutboxRepositoryModule,
|
|
35
|
+
],
|
|
24
36
|
providers: [
|
|
25
37
|
{
|
|
26
38
|
provide: email_sender_module_options_1.EMAIL_SENDER_CONFIG_OPTIONS,
|
|
@@ -28,8 +40,13 @@ class EmailSenderModule {
|
|
|
28
40
|
inject: options.inject,
|
|
29
41
|
},
|
|
30
42
|
email_sender_service_1.EmailSenderService,
|
|
43
|
+
email_outbox_service_1.EmailOutboxService,
|
|
44
|
+
],
|
|
45
|
+
exports: [
|
|
46
|
+
email_sender_module_options_1.EMAIL_SENDER_CONFIG_OPTIONS,
|
|
47
|
+
email_sender_service_1.EmailSenderService,
|
|
48
|
+
email_outbox_service_1.EmailOutboxService,
|
|
31
49
|
],
|
|
32
|
-
exports: [email_sender_service_1.EmailSenderService],
|
|
33
50
|
global: isGlobal,
|
|
34
51
|
};
|
|
35
52
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"email-sender.module.js","sourceRoot":"","sources":["../../../../packages/email-sender/src/email-sender.module.ts"],"names":[],"mappings":";;;AACA,+EAGuC;AACvC,0EAAqE;AACrE,kFAA6E;
|
|
1
|
+
{"version":3,"file":"email-sender.module.js","sourceRoot":"","sources":["../../../../packages/email-sender/src/email-sender.module.ts"],"names":[],"mappings":";;;AACA,+EAGuC;AACvC,0EAAqE;AACrE,0EAAqE;AACrE,kFAA6E;AAC7E,gGAA0F;AAE1F,mGAAmG;AACnG,MAAa,iBAAiB;IAC5B,MAAM,CAAC,QAAQ,CACb,OAA2B,EAC3B,QAAkB;QAElB,OAAO;YACL,MAAM,EAAE,iBAAiB;YACzB,OAAO,EAAE,CAAC,+CAAqB,EAAE,4DAA2B,CAAC;YAC7D,SAAS,EAAE;gBACT,EAAE,OAAO,EAAE,yDAA2B,EAAE,QAAQ,EAAE,OAAO,EAAE;gBAC3D,yCAAkB;gBAClB,yCAAkB;aACnB;YACD,OAAO,EAAE;gBACP,yDAA2B;gBAC3B,yCAAkB;gBAClB,yCAAkB;aACnB;YACD,MAAM,EAAE,QAAQ;SACjB,CAAC;IACJ,CAAC;IAED,MAAM,CAAC,aAAa,CAClB,OAAgC,EAChC,QAAkB;QAElB,OAAO;YACL,MAAM,EAAE,iBAAiB;YACzB,OAAO,EAAE;gBACP,GAAG,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC;gBAC1B,+CAAqB;gBACrB,4DAA2B;aAC5B;YACD,SAAS,EAAE;gBACT;oBACE,OAAO,EAAE,yDAA2B;oBACpC,UAAU,EAAE,OAAO,CAAC,UAAU;oBAC9B,MAAM,EAAE,OAAO,CAAC,MAAM;iBACvB;gBACD,yCAAkB;gBAClB,yCAAkB;aACnB;YACD,OAAO,EAAE;gBACP,yDAA2B;gBAC3B,yCAAkB;gBAClB,yCAAkB;aACnB;YACD,MAAM,EAAE,QAAQ;SACjB,CAAC;IACJ,CAAC;CACF;AAlDD,8CAkDC"}
|
package/src/index.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
export * from './email-sender.module';
|
|
2
|
+
export { EMAIL_SENDER_CONFIG_OPTIONS, EmailSenderOptions, } from './email-sender.module.options';
|
|
2
3
|
export * from './interfaces/email-sender.type';
|
|
4
|
+
export * from './interfaces/email-outbox.interface';
|
|
5
|
+
export * from './constants/email-outbox.constant';
|
|
3
6
|
export * from './services/email-sender.service';
|
|
7
|
+
export * from './services/email-outbox.service';
|
|
8
|
+
export * from './tasks/email-outbox.task';
|
|
4
9
|
export * from './dtos/email.dto';
|
|
5
10
|
export * from './repository/entities/email.entity';
|
|
11
|
+
export * from './repository/entities/email-outbox.entity';
|
package/src/index.js
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EMAIL_SENDER_CONFIG_OPTIONS = void 0;
|
|
3
4
|
const tslib_1 = require("tslib");
|
|
4
5
|
tslib_1.__exportStar(require("./email-sender.module"), exports);
|
|
6
|
+
var email_sender_module_options_1 = require("./email-sender.module.options");
|
|
7
|
+
Object.defineProperty(exports, "EMAIL_SENDER_CONFIG_OPTIONS", { enumerable: true, get: function () { return email_sender_module_options_1.EMAIL_SENDER_CONFIG_OPTIONS; } });
|
|
5
8
|
tslib_1.__exportStar(require("./interfaces/email-sender.type"), exports);
|
|
9
|
+
tslib_1.__exportStar(require("./interfaces/email-outbox.interface"), exports);
|
|
10
|
+
tslib_1.__exportStar(require("./constants/email-outbox.constant"), exports);
|
|
6
11
|
tslib_1.__exportStar(require("./services/email-sender.service"), exports);
|
|
12
|
+
tslib_1.__exportStar(require("./services/email-outbox.service"), exports);
|
|
13
|
+
tslib_1.__exportStar(require("./tasks/email-outbox.task"), exports);
|
|
7
14
|
tslib_1.__exportStar(require("./dtos/email.dto"), exports);
|
|
8
15
|
tslib_1.__exportStar(require("./repository/entities/email.entity"), exports);
|
|
16
|
+
tslib_1.__exportStar(require("./repository/entities/email-outbox.entity"), exports);
|
|
9
17
|
//# sourceMappingURL=index.js.map
|
package/src/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../packages/email-sender/src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../packages/email-sender/src/index.ts"],"names":[],"mappings":";;;;AAAA,gEAAsC;AACtC,6EAGuC;AAFrC,0IAAA,2BAA2B,OAAA;AAG7B,yEAA+C;AAC/C,8EAAoD;AACpD,4EAAkD;AAClD,0EAAgD;AAChD,0EAAgD;AAChD,oEAA0C;AAC1C,2DAAiC;AACjC,6EAAmD;AACnD,oFAA0D"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ClientSession } from 'mongoose';
|
|
2
|
+
export interface IEmailOutboxMessage {
|
|
3
|
+
template: string;
|
|
4
|
+
lang: string;
|
|
5
|
+
to: string[];
|
|
6
|
+
cc?: string[];
|
|
7
|
+
bcc?: string[];
|
|
8
|
+
variables?: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
export interface IEmailOutboxEnqueueOptions {
|
|
11
|
+
idempotencyKey: string;
|
|
12
|
+
session?: ClientSession;
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-outbox.interface.js","sourceRoot":"","sources":["../../../../../packages/email-sender/src/interfaces/email-outbox.interface.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EmailOutboxRepositoryModule = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const common_1 = require("@nestjs/common");
|
|
6
|
+
const mongoose_1 = require("@nestjs/mongoose");
|
|
7
|
+
const email_outbox_entity_1 = require("./entities/email-outbox.entity");
|
|
8
|
+
const email_outbox_repository_1 = require("./repositories/email-outbox.repository");
|
|
9
|
+
let EmailOutboxRepositoryModule = class EmailOutboxRepositoryModule {
|
|
10
|
+
};
|
|
11
|
+
exports.EmailOutboxRepositoryModule = EmailOutboxRepositoryModule;
|
|
12
|
+
exports.EmailOutboxRepositoryModule = EmailOutboxRepositoryModule = tslib_1.__decorate([
|
|
13
|
+
(0, common_1.Module)({
|
|
14
|
+
providers: [email_outbox_repository_1.EmailOutboxRepository],
|
|
15
|
+
exports: [email_outbox_repository_1.EmailOutboxRepository],
|
|
16
|
+
controllers: [],
|
|
17
|
+
imports: [
|
|
18
|
+
mongoose_1.MongooseModule.forFeature([
|
|
19
|
+
{
|
|
20
|
+
name: email_outbox_entity_1.EmailOutboxEntity.name,
|
|
21
|
+
schema: email_outbox_entity_1.EmailOutboxSchema,
|
|
22
|
+
},
|
|
23
|
+
]),
|
|
24
|
+
],
|
|
25
|
+
})
|
|
26
|
+
], EmailOutboxRepositoryModule);
|
|
27
|
+
//# sourceMappingURL=email-outbox.repository.module.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-outbox.repository.module.js","sourceRoot":"","sources":["../../../../../packages/email-sender/src/repository/email-outbox.repository.module.ts"],"names":[],"mappings":";;;;AAAA,2CAAwC;AACxC,+CAAkD;AAClD,wEAGwC;AACxC,oFAA+E;AAexE,IAAM,2BAA2B,GAAjC,MAAM,2BAA2B;CAAG,CAAA;AAA9B,kEAA2B;sCAA3B,2BAA2B;IAbvC,IAAA,eAAM,EAAC;QACN,SAAS,EAAE,CAAC,+CAAqB,CAAC;QAClC,OAAO,EAAE,CAAC,+CAAqB,CAAC;QAChC,WAAW,EAAE,EAAE;QACf,OAAO,EAAE;YACP,yBAAc,CAAC,UAAU,CAAC;gBACxB;oBACE,IAAI,EAAE,uCAAiB,CAAC,IAAI;oBAC5B,MAAM,EAAE,uCAAiB;iBAC1B;aACF,CAAC;SACH;KACF,CAAC;GACW,2BAA2B,CAAG"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { DatabaseMongoEntityAbstract } from '@tc-libs/database';
|
|
2
|
+
import { Document, Types } from 'mongoose';
|
|
3
|
+
import { EMAIL_OUTBOX_STATUS } from '../../constants/email-outbox.constant';
|
|
4
|
+
export declare class EmailOutboxEntity extends DatabaseMongoEntityAbstract {
|
|
5
|
+
idempotencyKey: string;
|
|
6
|
+
template: string;
|
|
7
|
+
lang: string;
|
|
8
|
+
to: string[];
|
|
9
|
+
cc?: string[];
|
|
10
|
+
bcc?: string[];
|
|
11
|
+
variables: Record<string, unknown>;
|
|
12
|
+
status: EMAIL_OUTBOX_STATUS;
|
|
13
|
+
attempts: number;
|
|
14
|
+
nextAttemptAt: Date;
|
|
15
|
+
claimedAt?: Date;
|
|
16
|
+
sentAt?: Date;
|
|
17
|
+
lastError?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare const EmailOutboxSchema: import("mongoose").Schema<EmailOutboxEntity, import("mongoose").Model<EmailOutboxEntity, any, any, any, Document<unknown, any, EmailOutboxEntity> & EmailOutboxEntity & Required<{
|
|
20
|
+
_id: Types.ObjectId;
|
|
21
|
+
}> & {
|
|
22
|
+
__v: number;
|
|
23
|
+
}, any>, {}, {}, {}, {}, import("mongoose").DefaultSchemaOptions, EmailOutboxEntity, Document<unknown, {}, import("mongoose").FlatRecord<EmailOutboxEntity>> & import("mongoose").FlatRecord<EmailOutboxEntity> & Required<{
|
|
24
|
+
_id: Types.ObjectId;
|
|
25
|
+
}> & {
|
|
26
|
+
__v: number;
|
|
27
|
+
}>;
|
|
28
|
+
export type EmailOutboxDoc = EmailOutboxEntity & Document<Types.ObjectId> & {
|
|
29
|
+
_id: Types.ObjectId;
|
|
30
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EmailOutboxSchema = exports.EmailOutboxEntity = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const mongoose_1 = require("@nestjs/mongoose");
|
|
6
|
+
const database_1 = require("@tc-libs/database");
|
|
7
|
+
const mongoose_2 = require("mongoose");
|
|
8
|
+
const email_outbox_constant_1 = require("../../constants/email-outbox.constant");
|
|
9
|
+
// Outbox delle email transazionali: l'evento la scrive nella propria transazione, EmailOutboxTask la invia.
|
|
10
|
+
let EmailOutboxEntity = class EmailOutboxEntity extends database_1.DatabaseMongoEntityAbstract {
|
|
11
|
+
};
|
|
12
|
+
exports.EmailOutboxEntity = EmailOutboxEntity;
|
|
13
|
+
tslib_1.__decorate([
|
|
14
|
+
(0, mongoose_1.Prop)({ required: true }),
|
|
15
|
+
tslib_1.__metadata("design:type", String)
|
|
16
|
+
], EmailOutboxEntity.prototype, "idempotencyKey", void 0);
|
|
17
|
+
tslib_1.__decorate([
|
|
18
|
+
(0, mongoose_1.Prop)({ required: true }),
|
|
19
|
+
tslib_1.__metadata("design:type", String)
|
|
20
|
+
], EmailOutboxEntity.prototype, "template", void 0);
|
|
21
|
+
tslib_1.__decorate([
|
|
22
|
+
(0, mongoose_1.Prop)({ required: true }),
|
|
23
|
+
tslib_1.__metadata("design:type", String)
|
|
24
|
+
], EmailOutboxEntity.prototype, "lang", void 0);
|
|
25
|
+
tslib_1.__decorate([
|
|
26
|
+
(0, mongoose_1.Prop)({ required: true, type: [String] }),
|
|
27
|
+
tslib_1.__metadata("design:type", Array)
|
|
28
|
+
], EmailOutboxEntity.prototype, "to", void 0);
|
|
29
|
+
tslib_1.__decorate([
|
|
30
|
+
(0, mongoose_1.Prop)({ type: [String] }),
|
|
31
|
+
tslib_1.__metadata("design:type", Array)
|
|
32
|
+
], EmailOutboxEntity.prototype, "cc", void 0);
|
|
33
|
+
tslib_1.__decorate([
|
|
34
|
+
(0, mongoose_1.Prop)({ type: [String] }),
|
|
35
|
+
tslib_1.__metadata("design:type", Array)
|
|
36
|
+
], EmailOutboxEntity.prototype, "bcc", void 0);
|
|
37
|
+
tslib_1.__decorate([
|
|
38
|
+
(0, mongoose_1.Prop)({ type: mongoose_2.SchemaTypes.Mixed, default: {} }),
|
|
39
|
+
tslib_1.__metadata("design:type", Object)
|
|
40
|
+
], EmailOutboxEntity.prototype, "variables", void 0);
|
|
41
|
+
tslib_1.__decorate([
|
|
42
|
+
(0, mongoose_1.Prop)({
|
|
43
|
+
required: true,
|
|
44
|
+
type: String,
|
|
45
|
+
enum: email_outbox_constant_1.EMAIL_OUTBOX_STATUS,
|
|
46
|
+
default: email_outbox_constant_1.EMAIL_OUTBOX_STATUS.PENDING,
|
|
47
|
+
}),
|
|
48
|
+
tslib_1.__metadata("design:type", String)
|
|
49
|
+
], EmailOutboxEntity.prototype, "status", void 0);
|
|
50
|
+
tslib_1.__decorate([
|
|
51
|
+
(0, mongoose_1.Prop)({ required: true, default: 0 }),
|
|
52
|
+
tslib_1.__metadata("design:type", Number)
|
|
53
|
+
], EmailOutboxEntity.prototype, "attempts", void 0);
|
|
54
|
+
tslib_1.__decorate([
|
|
55
|
+
(0, mongoose_1.Prop)({ required: true, type: Date }),
|
|
56
|
+
tslib_1.__metadata("design:type", Date)
|
|
57
|
+
], EmailOutboxEntity.prototype, "nextAttemptAt", void 0);
|
|
58
|
+
tslib_1.__decorate([
|
|
59
|
+
(0, mongoose_1.Prop)({ type: Date }),
|
|
60
|
+
tslib_1.__metadata("design:type", Date)
|
|
61
|
+
], EmailOutboxEntity.prototype, "claimedAt", void 0);
|
|
62
|
+
tslib_1.__decorate([
|
|
63
|
+
(0, mongoose_1.Prop)({ type: Date }),
|
|
64
|
+
tslib_1.__metadata("design:type", Date)
|
|
65
|
+
], EmailOutboxEntity.prototype, "sentAt", void 0);
|
|
66
|
+
tslib_1.__decorate([
|
|
67
|
+
(0, mongoose_1.Prop)(),
|
|
68
|
+
tslib_1.__metadata("design:type", String)
|
|
69
|
+
], EmailOutboxEntity.prototype, "lastError", void 0);
|
|
70
|
+
exports.EmailOutboxEntity = EmailOutboxEntity = tslib_1.__decorate([
|
|
71
|
+
(0, database_1.DatabaseEntity)({ collection: 'emailoutboxes' })
|
|
72
|
+
], EmailOutboxEntity);
|
|
73
|
+
exports.EmailOutboxSchema = mongoose_1.SchemaFactory.createForClass(EmailOutboxEntity);
|
|
74
|
+
exports.EmailOutboxSchema.index({ idempotencyKey: 1 }, { unique: true });
|
|
75
|
+
exports.EmailOutboxSchema.index({ status: 1, nextAttemptAt: 1 });
|
|
76
|
+
//# sourceMappingURL=email-outbox.entity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-outbox.entity.js","sourceRoot":"","sources":["../../../../../../packages/email-sender/src/repository/entities/email-outbox.entity.ts"],"names":[],"mappings":";;;;AAAA,+CAAuD;AACvD,gDAAgF;AAChF,uCAAwD;AACxD,iFAA4E;AAE5E,4GAA4G;AAErG,IAAM,iBAAiB,GAAvB,MAAM,iBAAkB,SAAQ,sCAA2B;CA6CjE,CAAA;AA7CY,8CAAiB;AAE5B;IADC,IAAA,eAAI,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;yDACF;AAGvB;IADC,IAAA,eAAI,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;mDACR;AAGjB;IADC,IAAA,eAAI,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;+CACZ;AAGb;IADC,IAAA,eAAI,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC;;6CAC5B;AAGb;IADC,IAAA,eAAI,EAAC,EAAE,IAAI,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC;;6CACX;AAGd;IADC,IAAA,eAAI,EAAC,EAAE,IAAI,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC;;8CACV;AAGf;IADC,IAAA,eAAI,EAAC,EAAE,IAAI,EAAE,sBAAW,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;;oDACZ;AAQnC;IANC,IAAA,eAAI,EAAC;QACJ,QAAQ,EAAE,IAAI;QACd,IAAI,EAAE,MAAM;QACZ,IAAI,EAAE,2CAAmB;QACzB,OAAO,EAAE,2CAAmB,CAAC,OAAO;KACrC,CAAC;;iDAC0B;AAG5B;IADC,IAAA,eAAI,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;;mDACpB;AAGjB;IADC,IAAA,eAAI,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;sCACtB,IAAI;wDAAC;AAGpB;IADC,IAAA,eAAI,EAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;sCACT,IAAI;oDAAC;AAGjB;IADC,IAAA,eAAI,EAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;sCACZ,IAAI;iDAAC;AAId;IADC,IAAA,eAAI,GAAE;;oDACY;4BA5CR,iBAAiB;IAD7B,IAAA,yBAAc,EAAC,EAAE,UAAU,EAAE,eAAe,EAAE,CAAC;GACnC,iBAAiB,CA6C7B;AAEY,QAAA,iBAAiB,GAC5B,wBAAa,CAAC,cAAc,CAAC,iBAAiB,CAAC,CAAC;AAElD,yBAAiB,CAAC,KAAK,CAAC,EAAE,cAAc,EAAE,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;AACjE,yBAAiB,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,aAAa,EAAE,CAAC,EAAE,CAAC,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Logger } from '@nestjs/common';
|
|
2
|
+
import { AppCacheService } from '@tc-libs/app-cache';
|
|
3
|
+
import { DatabaseMongoRepositoryAbstract } from '@tc-libs/database';
|
|
4
|
+
import { ClientSession, Connection, Model, Types } from 'mongoose';
|
|
5
|
+
import { EmailOutboxDoc, EmailOutboxEntity } from '../entities/email-outbox.entity';
|
|
6
|
+
export type EmailOutboxClaimed = EmailOutboxEntity & {
|
|
7
|
+
_id: Types.ObjectId;
|
|
8
|
+
};
|
|
9
|
+
export type EmailOutboxInsert = Omit<EmailOutboxEntity, '_id' | 'createdAt' | 'updatedAt'>;
|
|
10
|
+
export declare class EmailOutboxRepository extends DatabaseMongoRepositoryAbstract<EmailOutboxEntity, EmailOutboxDoc> {
|
|
11
|
+
protected readonly _model: Model<EmailOutboxEntity>;
|
|
12
|
+
protected readonly _connection: Connection;
|
|
13
|
+
protected readonly _logger: Logger;
|
|
14
|
+
constructor(_model: Model<EmailOutboxEntity>, _connection: Connection, _cache: AppCacheService);
|
|
15
|
+
/**
|
|
16
|
+
* Upsert con $setOnInsert sulla chiave di idempotenza: una seconda richiesta con la stessa chiave non scrive nulla e,
|
|
17
|
+
* a differenza di un insert, non fallisce con un duplicate key che annullerebbe la transazione dell'evento.
|
|
18
|
+
*/
|
|
19
|
+
enqueueOnce(email: EmailOutboxInsert, session?: ClientSession): Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* Prende in carico la prossima email da inviare, in modo atomico: due nodi non ricevono la stessa. Sono dovute le
|
|
22
|
+
* PENDING con nextAttemptAt passato e le SENDING rimaste orfane da prima di staleBefore.
|
|
23
|
+
*/
|
|
24
|
+
claimNext(now: Date, staleBefore: Date): Promise<EmailOutboxClaimed | null>;
|
|
25
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var EmailOutboxRepository_1;
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.EmailOutboxRepository = void 0;
|
|
5
|
+
const tslib_1 = require("tslib");
|
|
6
|
+
const common_1 = require("@nestjs/common");
|
|
7
|
+
const app_cache_1 = require("@tc-libs/app-cache");
|
|
8
|
+
const database_1 = require("@tc-libs/database");
|
|
9
|
+
const mongoose_1 = require("mongoose");
|
|
10
|
+
const email_outbox_constant_1 = require("../../constants/email-outbox.constant");
|
|
11
|
+
const email_outbox_entity_1 = require("../entities/email-outbox.entity");
|
|
12
|
+
let EmailOutboxRepository = EmailOutboxRepository_1 = class EmailOutboxRepository extends database_1.DatabaseMongoRepositoryAbstract {
|
|
13
|
+
constructor(_model, _connection, _cache) {
|
|
14
|
+
super(_model, _connection, _cache);
|
|
15
|
+
this._model = _model;
|
|
16
|
+
this._connection = _connection;
|
|
17
|
+
this._logger = new common_1.Logger(EmailOutboxRepository_1.name);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Upsert con $setOnInsert sulla chiave di idempotenza: una seconda richiesta con la stessa chiave non scrive nulla e,
|
|
21
|
+
* a differenza di un insert, non fallisce con un duplicate key che annullerebbe la transazione dell'evento.
|
|
22
|
+
*/
|
|
23
|
+
async enqueueOnce(email, session) {
|
|
24
|
+
await this._model
|
|
25
|
+
.updateOne({ idempotencyKey: email.idempotencyKey }, { $setOnInsert: email }, { upsert: true, session })
|
|
26
|
+
.exec();
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Prende in carico la prossima email da inviare, in modo atomico: due nodi non ricevono la stessa. Sono dovute le
|
|
30
|
+
* PENDING con nextAttemptAt passato e le SENDING rimaste orfane da prima di staleBefore.
|
|
31
|
+
*/
|
|
32
|
+
claimNext(now, staleBefore) {
|
|
33
|
+
return this._model
|
|
34
|
+
.findOneAndUpdate({
|
|
35
|
+
$or: [
|
|
36
|
+
{
|
|
37
|
+
status: email_outbox_constant_1.EMAIL_OUTBOX_STATUS.PENDING,
|
|
38
|
+
nextAttemptAt: { $lte: now },
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
status: email_outbox_constant_1.EMAIL_OUTBOX_STATUS.SENDING,
|
|
42
|
+
claimedAt: { $lte: staleBefore },
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
}, { $set: { status: email_outbox_constant_1.EMAIL_OUTBOX_STATUS.SENDING, claimedAt: now } }, { sort: { nextAttemptAt: 1 }, new: true })
|
|
46
|
+
.lean()
|
|
47
|
+
.exec();
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
exports.EmailOutboxRepository = EmailOutboxRepository;
|
|
51
|
+
exports.EmailOutboxRepository = EmailOutboxRepository = EmailOutboxRepository_1 = tslib_1.__decorate([
|
|
52
|
+
(0, common_1.Injectable)(),
|
|
53
|
+
tslib_1.__param(0, (0, database_1.DatabaseModel)(email_outbox_entity_1.EmailOutboxEntity.name)),
|
|
54
|
+
tslib_1.__param(1, (0, database_1.DatabaseConnection)()),
|
|
55
|
+
tslib_1.__metadata("design:paramtypes", [mongoose_1.Model,
|
|
56
|
+
mongoose_1.Connection,
|
|
57
|
+
app_cache_1.AppCacheService])
|
|
58
|
+
], EmailOutboxRepository);
|
|
59
|
+
//# sourceMappingURL=email-outbox.repository.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-outbox.repository.js","sourceRoot":"","sources":["../../../../../../packages/email-sender/src/repository/repositories/email-outbox.repository.ts"],"names":[],"mappings":";;;;;AAAA,2CAAoD;AACpD,kDAAqD;AACrD,gDAI2B;AAC3B,uCAAmE;AACnE,iFAA4E;AAC5E,yEAGyC;AAWlC,IAAM,qBAAqB,6BAA3B,MAAM,qBAAsB,SAAQ,0CAG1C;IAEC,YAEE,MAA4D,EAE5D,WAAmD,EACnD,MAAuB;QAEvB,KAAK,CAAC,MAAM,EAAE,WAAW,EAAE,MAAM,CAAC,CAAC;QALP,WAAM,GAAN,MAAM,CAA0B;QAEhC,gBAAW,GAAX,WAAW,CAAY;QALlC,YAAO,GAAW,IAAI,eAAM,CAAC,uBAAqB,CAAC,IAAI,CAAC,CAAC;IAS5E,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,WAAW,CACf,KAAwB,EACxB,OAAuB;QAEvB,MAAM,IAAI,CAAC,MAAM;aACd,SAAS,CACR,EAAE,cAAc,EAAE,KAAK,CAAC,cAAc,EAAE,EACxC,EAAE,YAAY,EAAE,KAAK,EAAE,EACvB,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,CAC1B;aACA,IAAI,EAAE,CAAC;IACZ,CAAC;IAED;;;OAGG;IACH,SAAS,CAAC,GAAS,EAAE,WAAiB;QACpC,OAAO,IAAI,CAAC,MAAM;aACf,gBAAgB,CACf;YACE,GAAG,EAAE;gBACH;oBACE,MAAM,EAAE,2CAAmB,CAAC,OAAO;oBACnC,aAAa,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE;iBAC7B;gBACD;oBACE,MAAM,EAAE,2CAAmB,CAAC,OAAO;oBACnC,SAAS,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE;iBACjC;aACF;SACF,EACD,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,2CAAmB,CAAC,OAAO,EAAE,SAAS,EAAE,GAAG,EAAE,EAAE,EACjE,EAAE,IAAI,EAAE,EAAE,aAAa,EAAE,CAAC,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,CAC1C;aACA,IAAI,EAAsB;aAC1B,IAAI,EAAE,CAAC;IACZ,CAAC;CACF,CAAA;AAzDY,sDAAqB;gCAArB,qBAAqB;IADjC,IAAA,mBAAU,GAAE;IAOR,mBAAA,IAAA,wBAAa,EAAC,uCAAiB,CAAC,IAAI,CAAC,CAAA;IAErC,mBAAA,IAAA,6BAAkB,GAAE,CAAA;6CADe,gBAAK;QAEA,qBAAU;QAC3C,2BAAe;GAVd,qBAAqB,CAyDjC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { LogFactory } from '@tc-libs/logger';
|
|
2
|
+
import { IEmailOutboxEnqueueOptions, IEmailOutboxMessage } from '../interfaces/email-outbox.interface';
|
|
3
|
+
import { EmailOutboxRepository } from '../repository/repositories/email-outbox.repository';
|
|
4
|
+
import { EmailSenderService } from './email-sender.service';
|
|
5
|
+
/**
|
|
6
|
+
* Outbox delle email transazionali. Gli eventi accodano con `enqueue` nella propria transazione, EmailOutboxTask invia
|
|
7
|
+
* con `processDue`. L'invio è "almeno una volta": un nodo che cade fra l'invio e la conferma fa ripartire l'email.
|
|
8
|
+
*/
|
|
9
|
+
export declare class EmailOutboxService {
|
|
10
|
+
private readonly _outbox;
|
|
11
|
+
private readonly _email;
|
|
12
|
+
private readonly _logger;
|
|
13
|
+
constructor(_outbox: EmailOutboxRepository, _email: EmailSenderService, logFactory: LogFactory);
|
|
14
|
+
/** Accoda un'email; con `session` la scrittura fa parte della transazione dell'evento che la genera. */
|
|
15
|
+
enqueue({ template, lang, to, cc, bcc, variables }: IEmailOutboxMessage, { idempotencyKey, session }: IEmailOutboxEnqueueOptions): Promise<void>;
|
|
16
|
+
/** Invia le email dovute, al massimo EMAIL_OUTBOX_BATCH_SIZE per giro. Restituisce quante ne ha prese in carico. */
|
|
17
|
+
processDue(now?: Date): Promise<number>;
|
|
18
|
+
private _send;
|
|
19
|
+
private _registerFailure;
|
|
20
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var EmailOutboxService_1;
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.EmailOutboxService = void 0;
|
|
5
|
+
const tslib_1 = require("tslib");
|
|
6
|
+
const common_1 = require("@nestjs/common");
|
|
7
|
+
const logger_1 = require("@tc-libs/logger");
|
|
8
|
+
const email_outbox_constant_1 = require("../constants/email-outbox.constant");
|
|
9
|
+
const email_outbox_repository_1 = require("../repository/repositories/email-outbox.repository");
|
|
10
|
+
const email_outbox_util_1 = require("../utils/email-outbox.util");
|
|
11
|
+
const email_sender_service_1 = require("./email-sender.service");
|
|
12
|
+
/**
|
|
13
|
+
* Outbox delle email transazionali. Gli eventi accodano con `enqueue` nella propria transazione, EmailOutboxTask invia
|
|
14
|
+
* con `processDue`. L'invio è "almeno una volta": un nodo che cade fra l'invio e la conferma fa ripartire l'email.
|
|
15
|
+
*/
|
|
16
|
+
let EmailOutboxService = EmailOutboxService_1 = class EmailOutboxService {
|
|
17
|
+
constructor(_outbox, _email, logFactory) {
|
|
18
|
+
this._outbox = _outbox;
|
|
19
|
+
this._email = _email;
|
|
20
|
+
this._logger = logFactory.createLogger(EmailOutboxService_1.name);
|
|
21
|
+
}
|
|
22
|
+
/** Accoda un'email; con `session` la scrittura fa parte della transazione dell'evento che la genera. */
|
|
23
|
+
async enqueue({ template, lang, to, cc, bcc, variables = {} }, { idempotencyKey, session }) {
|
|
24
|
+
await this._outbox.enqueueOnce({
|
|
25
|
+
idempotencyKey,
|
|
26
|
+
template,
|
|
27
|
+
lang,
|
|
28
|
+
to,
|
|
29
|
+
cc,
|
|
30
|
+
bcc,
|
|
31
|
+
variables,
|
|
32
|
+
status: email_outbox_constant_1.EMAIL_OUTBOX_STATUS.PENDING,
|
|
33
|
+
attempts: 0,
|
|
34
|
+
nextAttemptAt: new Date(),
|
|
35
|
+
}, session);
|
|
36
|
+
}
|
|
37
|
+
/** Invia le email dovute, al massimo EMAIL_OUTBOX_BATCH_SIZE per giro. Restituisce quante ne ha prese in carico. */
|
|
38
|
+
async processDue(now = new Date()) {
|
|
39
|
+
const staleBefore = new Date(now.getTime() - email_outbox_constant_1.EMAIL_OUTBOX_CLAIM_TIMEOUT_MS);
|
|
40
|
+
let processed = 0;
|
|
41
|
+
while (processed < email_outbox_constant_1.EMAIL_OUTBOX_BATCH_SIZE) {
|
|
42
|
+
const email = await this._outbox.claimNext(now, staleBefore);
|
|
43
|
+
if (!email)
|
|
44
|
+
break;
|
|
45
|
+
processed++;
|
|
46
|
+
await this._send(email, now);
|
|
47
|
+
}
|
|
48
|
+
return processed;
|
|
49
|
+
}
|
|
50
|
+
async _send(email, now) {
|
|
51
|
+
try {
|
|
52
|
+
await this._email.sendTemplate({
|
|
53
|
+
lang: email.lang,
|
|
54
|
+
template: email.template,
|
|
55
|
+
options: { to: email.to, cc: email.cc, bcc: email.bcc },
|
|
56
|
+
variables: email.variables,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
await this._registerFailure(email, error, now);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
await this._outbox.updateById(email._id, {
|
|
64
|
+
$set: { status: email_outbox_constant_1.EMAIL_OUTBOX_STATUS.SENT, sentAt: new Date() },
|
|
65
|
+
$unset: { claimedAt: '', lastError: '' },
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
async _registerFailure(email, error, now) {
|
|
69
|
+
const attempts = email.attempts + 1;
|
|
70
|
+
const lastError = (0, email_outbox_util_1.redactEmailError)(error);
|
|
71
|
+
const failed = attempts >= email_outbox_constant_1.EMAIL_OUTBOX_MAX_ATTEMPTS;
|
|
72
|
+
await this._outbox.updateById(email._id, {
|
|
73
|
+
$set: {
|
|
74
|
+
attempts,
|
|
75
|
+
lastError,
|
|
76
|
+
status: failed
|
|
77
|
+
? email_outbox_constant_1.EMAIL_OUTBOX_STATUS.FAILED
|
|
78
|
+
: email_outbox_constant_1.EMAIL_OUTBOX_STATUS.PENDING,
|
|
79
|
+
nextAttemptAt: new Date(now.getTime() + (0, email_outbox_util_1.emailOutboxBackoffMs)(attempts)),
|
|
80
|
+
},
|
|
81
|
+
$unset: { claimedAt: '' },
|
|
82
|
+
});
|
|
83
|
+
if (failed) {
|
|
84
|
+
// Solo id, template e motivo: niente destinatari né variabili nella notifica.
|
|
85
|
+
this._logger.notifyError(new Error(`Email outbox ${email._id} (${email.template}) non inviata dopo ${attempts} tentativi: ${lastError}`));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
exports.EmailOutboxService = EmailOutboxService;
|
|
90
|
+
exports.EmailOutboxService = EmailOutboxService = EmailOutboxService_1 = tslib_1.__decorate([
|
|
91
|
+
(0, common_1.Injectable)(),
|
|
92
|
+
tslib_1.__metadata("design:paramtypes", [email_outbox_repository_1.EmailOutboxRepository,
|
|
93
|
+
email_sender_service_1.EmailSenderService,
|
|
94
|
+
logger_1.LogFactory])
|
|
95
|
+
], EmailOutboxService);
|
|
96
|
+
//# sourceMappingURL=email-outbox.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-outbox.service.js","sourceRoot":"","sources":["../../../../../packages/email-sender/src/services/email-outbox.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { LogFactory } from '@tc-libs/logger';
|
|
2
|
+
import { RetryPolicyMongoService } from '@tc-libs/retry-policy';
|
|
3
|
+
import { AbstractCronTask, TaskRunService, TaskService } from '@tc-libs/task';
|
|
4
|
+
import { EmailSenderOptions } from '../email-sender.module.options';
|
|
5
|
+
import { EmailOutboxService } from '../services/email-outbox.service';
|
|
6
|
+
/**
|
|
7
|
+
* Invio dell'outbox ogni minuto, con il lock distribuito di @tc-libs/task per `app.name`. Senza createSession: l'invio
|
|
8
|
+
* è un effetto esterno e un retry di transazione lo ripeterebbe; ogni email passa a SENDING con un update atomico.
|
|
9
|
+
* L'app lo registra fra i provider del proprio router dei job (serve TaskModule) e lo attiva per ambiente.
|
|
10
|
+
*/
|
|
11
|
+
export declare class EmailOutboxTask extends AbstractCronTask {
|
|
12
|
+
private readonly _outbox;
|
|
13
|
+
code: string;
|
|
14
|
+
staleMs: number;
|
|
15
|
+
private readonly _envName;
|
|
16
|
+
constructor(logFactory: LogFactory, taskService: TaskService, taskRunService: TaskRunService, retryPolicy: RetryPolicyMongoService, options: EmailSenderOptions, _outbox: EmailOutboxService);
|
|
17
|
+
handleCron(): Promise<void>;
|
|
18
|
+
protected work(): Promise<void>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var EmailOutboxTask_1;
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.EmailOutboxTask = void 0;
|
|
5
|
+
const tslib_1 = require("tslib");
|
|
6
|
+
const common_1 = require("@nestjs/common");
|
|
7
|
+
const schedule_1 = require("@nestjs/schedule");
|
|
8
|
+
const logger_1 = require("@tc-libs/logger");
|
|
9
|
+
const retry_policy_1 = require("@tc-libs/retry-policy");
|
|
10
|
+
const task_1 = require("@tc-libs/task");
|
|
11
|
+
const email_outbox_constant_1 = require("../constants/email-outbox.constant");
|
|
12
|
+
const email_sender_module_options_1 = require("../email-sender.module.options");
|
|
13
|
+
const email_outbox_service_1 = require("../services/email-outbox.service");
|
|
14
|
+
/**
|
|
15
|
+
* Invio dell'outbox ogni minuto, con il lock distribuito di @tc-libs/task per `app.name`. Senza createSession: l'invio
|
|
16
|
+
* è un effetto esterno e un retry di transazione lo ripeterebbe; ogni email passa a SENDING con un update atomico.
|
|
17
|
+
* L'app lo registra fra i provider del proprio router dei job (serve TaskModule) e lo attiva per ambiente.
|
|
18
|
+
*/
|
|
19
|
+
let EmailOutboxTask = EmailOutboxTask_1 = class EmailOutboxTask extends task_1.AbstractCronTask {
|
|
20
|
+
constructor(logFactory, taskService, taskRunService, retryPolicy, options, _outbox) {
|
|
21
|
+
super(logFactory, EmailOutboxTask_1.name, taskService, taskRunService, retryPolicy);
|
|
22
|
+
this._outbox = _outbox;
|
|
23
|
+
this.code = email_outbox_constant_1.EMAIL_OUTBOX_TASK_CODE;
|
|
24
|
+
this.staleMs = email_outbox_constant_1.EMAIL_OUTBOX_TASK_STALE_MS;
|
|
25
|
+
if (!options.app.name) {
|
|
26
|
+
throw new Error('EmailOutboxTask: app.name mancante nelle opzioni di EmailSenderModule');
|
|
27
|
+
}
|
|
28
|
+
this._envName = options.app.name;
|
|
29
|
+
}
|
|
30
|
+
async handleCron() {
|
|
31
|
+
await this.run(this._envName);
|
|
32
|
+
}
|
|
33
|
+
async work() {
|
|
34
|
+
await this._outbox.processDue();
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
exports.EmailOutboxTask = EmailOutboxTask;
|
|
38
|
+
tslib_1.__decorate([
|
|
39
|
+
(0, schedule_1.Cron)(schedule_1.CronExpression.EVERY_MINUTE, { name: email_outbox_constant_1.EMAIL_OUTBOX_TASK_CODE }),
|
|
40
|
+
tslib_1.__metadata("design:type", Function),
|
|
41
|
+
tslib_1.__metadata("design:paramtypes", []),
|
|
42
|
+
tslib_1.__metadata("design:returntype", Promise)
|
|
43
|
+
], EmailOutboxTask.prototype, "handleCron", null);
|
|
44
|
+
exports.EmailOutboxTask = EmailOutboxTask = EmailOutboxTask_1 = tslib_1.__decorate([
|
|
45
|
+
(0, common_1.Injectable)(),
|
|
46
|
+
tslib_1.__param(4, (0, common_1.Inject)(email_sender_module_options_1.EMAIL_SENDER_CONFIG_OPTIONS)),
|
|
47
|
+
tslib_1.__metadata("design:paramtypes", [logger_1.LogFactory,
|
|
48
|
+
task_1.TaskService,
|
|
49
|
+
task_1.TaskRunService,
|
|
50
|
+
retry_policy_1.RetryPolicyMongoService, Object, email_outbox_service_1.EmailOutboxService])
|
|
51
|
+
], EmailOutboxTask);
|
|
52
|
+
//# sourceMappingURL=email-outbox.task.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-outbox.task.js","sourceRoot":"","sources":["../../../../../packages/email-sender/src/tasks/email-outbox.task.ts"],"names":[],"mappings":";;;;;AAAA,2CAAoD;AACpD,+CAAwD;AACxD,4CAA6C;AAC7C,wDAAgE;AAChE,wCAA8E;AAC9E,8EAG4C;AAC5C,gFAGwC;AACxC,2EAAsE;AAEtE;;;;GAIG;AAEI,IAAM,eAAe,uBAArB,MAAM,eAAgB,SAAQ,uBAAgB;IAKnD,YACE,UAAsB,EACtB,WAAwB,EACxB,cAA8B,EAC9B,WAAoC,EACC,OAA2B,EAC/C,OAA2B;QAE5C,KAAK,CACH,UAAU,EACV,iBAAe,CAAC,IAAI,EACpB,WAAW,EACX,cAAc,EACd,WAAW,CACZ,CAAC;QARe,YAAO,GAAP,OAAO,CAAoB;QAVrC,SAAI,GAAG,8CAAsB,CAAC;QAC9B,YAAO,GAAG,kDAA0B,CAAC;QAkB5C,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;YACtB,MAAM,IAAI,KAAK,CACb,uEAAuE,CACxE,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC;IACnC,CAAC;IAGK,AAAN,KAAK,CAAC,UAAU;QACd,MAAM,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAChC,CAAC;IAEkB,KAAK,CAAC,IAAI;QAC3B,MAAM,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC;IAClC,CAAC;CACF,CAAA;AApCY,0CAAe;AA6BpB;IADL,IAAA,eAAI,EAAC,yBAAc,CAAC,YAAY,EAAE,EAAE,IAAI,EAAE,8CAAsB,EAAE,CAAC;;;;iDAGnE;0BA/BU,eAAe;IAD3B,IAAA,mBAAU,GAAE;IAWR,mBAAA,IAAA,eAAM,EAAC,yDAA2B,CAAC,CAAA;6CAJxB,mBAAU;QACT,kBAAW;QACR,qBAAc;QACjB,sCAAuB,UAEV,yCAAkB;GAXnC,eAAe,CAoC3B"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Attesa prima del prossimo tentativo, dopo `attempts` fallimenti: raddoppia a ogni errore, fino al massimo. */
|
|
2
|
+
export declare function emailOutboxBackoffMs(attempts: number): number;
|
|
3
|
+
/** Errore da salvare o notificare senza dati personali: gli indirizzi email diventano [email]. */
|
|
4
|
+
export declare function redactEmailError(error: unknown): string;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.emailOutboxBackoffMs = emailOutboxBackoffMs;
|
|
4
|
+
exports.redactEmailError = redactEmailError;
|
|
5
|
+
const email_outbox_constant_1 = require("../constants/email-outbox.constant");
|
|
6
|
+
/** Attesa prima del prossimo tentativo, dopo `attempts` fallimenti: raddoppia a ogni errore, fino al massimo. */
|
|
7
|
+
function emailOutboxBackoffMs(attempts) {
|
|
8
|
+
return Math.min(email_outbox_constant_1.EMAIL_OUTBOX_BACKOFF_BASE_MS * 2 ** Math.max(attempts - 1, 0), email_outbox_constant_1.EMAIL_OUTBOX_BACKOFF_MAX_MS);
|
|
9
|
+
}
|
|
10
|
+
/** Errore da salvare o notificare senza dati personali: gli indirizzi email diventano [email]. */
|
|
11
|
+
function redactEmailError(error) {
|
|
12
|
+
const message = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
13
|
+
return message
|
|
14
|
+
.replace(/[^\s@<>"',;:]+@[^\s@<>"',;:]+/g, '[email]')
|
|
15
|
+
.slice(0, 500);
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=email-outbox.util.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-outbox.util.js","sourceRoot":"","sources":["../../../../../packages/email-sender/src/utils/email-outbox.util.ts"],"names":[],"mappings":";;AAMA,oDAKC;AAGD,4CAMC;AApBD,8EAG4C;AAE5C,iHAAiH;AACjH,SAAgB,oBAAoB,CAAC,QAAgB;IACnD,OAAO,IAAI,CAAC,GAAG,CACb,oDAA4B,GAAG,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,QAAQ,GAAG,CAAC,EAAE,CAAC,CAAC,EAC7D,mDAA2B,CAC5B,CAAC;AACJ,CAAC;AAED,kGAAkG;AAClG,SAAgB,gBAAgB,CAAC,KAAc;IAC7C,MAAM,OAAO,GACX,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC7E,OAAO,OAAO;SACX,OAAO,CAAC,gCAAgC,EAAE,SAAS,CAAC;SACpD,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AACnB,CAAC"}
|