@flusys/nestjs-email 6.4.0 → 7.0.0-beta.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.
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Domain event actions published by the email module, alongside the CRUD
3
+ * lifecycle actions every ApiService emits.
4
+ */ "use strict";
5
+ Object.defineProperty(exports, "__esModule", {
6
+ value: true
7
+ });
8
+ function _export(target, all) {
9
+ for(var name in all)Object.defineProperty(target, name, {
10
+ enumerable: true,
11
+ get: Object.getOwnPropertyDescriptor(all, name).get
12
+ });
13
+ }
14
+ _export(exports, {
15
+ get EMAIL_EVENT_ACTIONS () {
16
+ return EMAIL_EVENT_ACTIONS;
17
+ },
18
+ get EMAIL_EVENT_ENTITIES () {
19
+ return EMAIL_EVENT_ENTITIES;
20
+ },
21
+ get EMAIL_EVENT_MODULE () {
22
+ return EMAIL_EVENT_MODULE;
23
+ }
24
+ });
25
+ const EMAIL_EVENT_ACTIONS = {
26
+ SENT: 'sent',
27
+ FAILED: 'failed'
28
+ };
29
+ const EMAIL_EVENT_ENTITIES = {
30
+ MESSAGE: 'email_message',
31
+ CONFIG: 'emailConfig',
32
+ TEMPLATE: 'emailTemplate'
33
+ };
34
+ const EMAIL_EVENT_MODULE = 'email';
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", {
3
3
  value: true
4
4
  });
5
5
  _export_star(require("./email.constants"), exports);
6
+ _export_star(require("./event-actions"), exports);
6
7
  _export_star(require("./message-keys"), exports);
7
8
  function _export_star(from, to) {
8
9
  Object.keys(from).forEach(function(k) {
@@ -10,6 +10,9 @@ function _export(target, all) {
10
10
  });
11
11
  }
12
12
  _export(exports, {
13
+ get EMAIL_CONFIG_MESSAGES () {
14
+ return EMAIL_CONFIG_MESSAGES;
15
+ },
13
16
  get EMAIL_SEND_MESSAGES () {
14
17
  return EMAIL_SEND_MESSAGES;
15
18
  },
@@ -21,6 +24,9 @@ const EMAIL_TEMPLATE_MESSAGES = {
21
24
  GET_SUCCESS: 'email.template.get.success',
22
25
  NOT_FOUND: 'email.template.not.found'
23
26
  };
27
+ const EMAIL_CONFIG_MESSAGES = {
28
+ DEFAULT_DELETE_FORBIDDEN: 'email.config.default.delete.forbidden'
29
+ };
24
30
  const EMAIL_SEND_MESSAGES = {
25
31
  SUCCESS: 'email.send.success',
26
32
  FAILED: 'email.send.failed',
@@ -78,6 +78,7 @@ let EmailModule = class EmailModule {
78
78
  useValue: options
79
79
  });
80
80
  }
81
+ providers.push((0, _modules.createModuleEventsProvider)('email', _emailconstants.EMAIL_MODULE_OPTIONS));
81
82
  return providers;
82
83
  }
83
84
  static createAsyncProviders(options) {
@@ -12,6 +12,8 @@ const _classes = require("@flusys/nestjs-shared/classes");
12
12
  const _modules = require("@flusys/nestjs-shared/modules");
13
13
  const _utils = require("@flusys/nestjs-shared/utils");
14
14
  const _common = require("@nestjs/common");
15
+ const _typeorm = require("typeorm");
16
+ const _config = require("../config");
15
17
  const _entities = require("../entities");
16
18
  const _emailconfigservice = require("./email-config.service");
17
19
  const _emaildatasourceprovider = require("./email-datasource.provider");
@@ -46,6 +48,29 @@ let EmailProviderConfigService = class EmailProviderConfigService extends _class
46
48
  resolveEntity() {
47
49
  return this.emailConfig.isCompanyFeatureEnabled() ? _entities.EmailConfigWithCompany : _entities.EmailConfig;
48
50
  }
51
+ async beforeDeleteOperation(dto, _user, _queryRunner) {
52
+ const ids = Array.isArray(dto.id) ? dto.id : [
53
+ dto.id
54
+ ];
55
+ await this.ensureDataSourceRepository();
56
+ const defaultConfigs = await this.repository.find({
57
+ where: {
58
+ id: (0, _typeorm.In)(ids),
59
+ isDefault: true,
60
+ deletedAt: (0, _typeorm.IsNull)()
61
+ }
62
+ });
63
+ if (defaultConfigs.length > 0) {
64
+ const names = defaultConfigs.map((c)=>c.name).join(', ');
65
+ throw new _common.ForbiddenException({
66
+ message: `Cannot delete the default email config(s): ${names}. Set another config as default first.`,
67
+ messageKey: _config.EMAIL_CONFIG_MESSAGES.DEFAULT_DELETE_FORBIDDEN,
68
+ messageVariables: {
69
+ names
70
+ }
71
+ });
72
+ }
73
+ }
49
74
  async convertSingleDtoToEntity(dto, user) {
50
75
  const entity = {
51
76
  ...dto
@@ -244,4 +244,22 @@ describe('EmailProviderConfigService', ()=>{
244
244
  });
245
245
  });
246
246
  });
247
+ describe('beforeDeleteOperation', ()=>{
248
+ it('rejects deleting the default email config', async ()=>{
249
+ mockRepo.find.mockResolvedValue([
250
+ buildEmailConfig({
251
+ isDefault: true
252
+ })
253
+ ]);
254
+ await expect(service.beforeDeleteOperation({
255
+ id: 'config-uuid-1'
256
+ }, null, {})).rejects.toThrow('Cannot delete the default email config');
257
+ });
258
+ it('allows deleting a non-default email config', async ()=>{
259
+ mockRepo.find.mockResolvedValue([]);
260
+ await expect(service.beforeDeleteOperation({
261
+ id: 'config-uuid-1'
262
+ }, null, {})).resolves.toBeUndefined();
263
+ });
264
+ });
247
265
  });
@@ -9,10 +9,11 @@ Object.defineProperty(exports, "EmailSendService", {
9
9
  }
10
10
  });
11
11
  const _nestjsshared = require("@flusys/nestjs-shared");
12
- const _config = require("../config");
12
+ const _classes = require("@flusys/nestjs-shared/classes");
13
13
  const _interfaces = require("@flusys/nestjs-shared/interfaces");
14
14
  const _utils = require("@flusys/nestjs-shared/utils");
15
15
  const _common = require("@nestjs/common");
16
+ const _config = require("../config");
16
17
  const _emailconfigservice = require("./email-config.service");
17
18
  const _dtos = require("../dtos");
18
19
  const _providers = require("../providers");
@@ -51,7 +52,7 @@ let EmailSendService = class EmailSendService {
51
52
  async sendEmail(dto, user) {
52
53
  const { provider, config } = await this.getEmailProviderWithConfig(dto.emailConfigId, user);
53
54
  const sender = this.resolveSender(dto, config);
54
- return await provider.sendEmail({
55
+ const result = await provider.sendEmail({
55
56
  to: dto.to,
56
57
  cc: dto.cc,
57
58
  bcc: dto.bcc,
@@ -62,13 +63,17 @@ let EmailSendService = class EmailSendService {
62
63
  replyTo: dto.replyTo,
63
64
  attachments: this.buildAttachments(dto.attachments)
64
65
  });
66
+ await this.publishSendEvent(result, dto.to, user, {
67
+ emailConfigId: config.id
68
+ });
69
+ return result;
65
70
  }
66
71
  async sendTemplateEmail(dto, user) {
67
72
  const template = await this.resolveTemplate(dto, user);
68
73
  const { subject, html, text } = this.buildTemplateContent(template, dto.variables);
69
74
  const { provider, config } = await this.getEmailProviderWithConfig(dto.emailConfigId, user);
70
75
  const sender = this.resolveSender(dto, config);
71
- return await provider.sendEmail({
76
+ const result = await provider.sendEmail({
72
77
  to: dto.to,
73
78
  cc: dto.cc,
74
79
  bcc: dto.bcc,
@@ -79,6 +84,12 @@ let EmailSendService = class EmailSendService {
79
84
  replyTo: dto.replyTo,
80
85
  attachments: this.buildAttachments(dto.attachments)
81
86
  });
87
+ await this.publishSendEvent(result, dto.to, user, {
88
+ emailConfigId: config.id,
89
+ templateId: template.id,
90
+ templateSlug: template.slug
91
+ });
92
+ return result;
82
93
  }
83
94
  async sendTestEmail(emailConfigId, recipient, user) {
84
95
  const { provider, config } = await this.getEmailProviderWithConfig(emailConfigId, user);
@@ -94,6 +105,10 @@ let EmailSendService = class EmailSendService {
94
105
  from: config.fromEmail || undefined,
95
106
  fromName: config.fromName || this.emailConfigService.getDefaultFromName()
96
107
  });
108
+ await this.publishSendEvent(result, recipient, user, {
109
+ emailConfigId: config.id,
110
+ isTest: true
111
+ });
97
112
  return result;
98
113
  }
99
114
  // ─── Private: Provider & Config Resolution ──────────────────────────────────
@@ -137,7 +152,7 @@ let EmailSendService = class EmailSendService {
137
152
  }
138
153
  // ─── Private: Template Resolution ───────────────────────────────────────────
139
154
  async resolveTemplate(dto, user) {
140
- let template = null;
155
+ let template;
141
156
  if (dto.templateId) {
142
157
  template = await this.emailTemplateService.findByIdDirect(dto.templateId);
143
158
  } else if (dto.templateSlug) {
@@ -213,6 +228,27 @@ let EmailSendService = class EmailSendService {
213
228
  </div>
214
229
  `;
215
230
  }
231
+ async publishSendEvent(result, to, user, metadata) {
232
+ const recipients = Array.isArray(to) ? to : [
233
+ to
234
+ ];
235
+ await (0, _classes.publishDomainEvent)({
236
+ module: _config.EMAIL_EVENT_MODULE,
237
+ entity: _config.EMAIL_EVENT_ENTITIES.MESSAGE,
238
+ action: result.success ? _config.EMAIL_EVENT_ACTIONS.SENT : _config.EMAIL_EVENT_ACTIONS.FAILED,
239
+ ids: result.messageId ? [
240
+ result.messageId
241
+ ] : [],
242
+ metadata: {
243
+ ...metadata,
244
+ recipientCount: recipients.length,
245
+ ...result.error ? {
246
+ error: result.error
247
+ } : {}
248
+ },
249
+ user: user ?? null
250
+ });
251
+ }
216
252
  constructor(emailFactory, emailConfigService, emailProviderConfigService, emailTemplateService){
217
253
  _define_property(this, "emailFactory", void 0);
218
254
  _define_property(this, "emailConfigService", void 0);
@@ -8,6 +8,7 @@ const _loggedusermock = require("@test-utils/mocks/logged-user.mock");
8
8
  const _providers = require("../providers");
9
9
  const _emailconfigservice = require("./email-config.service");
10
10
  const _emailproviderconfigservice = require("./email-provider-config.service");
11
+ const _classes = require("@flusys/nestjs-shared/classes");
11
12
  const _emailsendservice = require("./email-send.service");
12
13
  const _emailtemplateservice = require("./email-template.service");
13
14
  function buildConfig(overrides = {}) {
@@ -543,4 +544,71 @@ describe('EmailSendService', ()=>{
543
544
  expect(payload.html).not.toContain('<script>alert(1)</script>');
544
545
  });
545
546
  });
547
+ describe('domain events', ()=>{
548
+ let publish;
549
+ beforeEach(()=>{
550
+ publish = jest.fn().mockResolvedValue(undefined);
551
+ _classes.EventBusRegistry.reset();
552
+ _classes.EventBusRegistry.setBus({
553
+ publish,
554
+ subscribe: jest.fn(),
555
+ close: jest.fn()
556
+ });
557
+ _classes.EventBusRegistry.configureModule('email', {
558
+ enabled: true
559
+ });
560
+ mockEmailProviderConfigService.findByIdDirect.mockResolvedValue(buildConfig());
561
+ });
562
+ afterEach(()=>{
563
+ _classes.EventBusRegistry.reset();
564
+ });
565
+ it('publishes email.email_message.sent for a successful send', async ()=>{
566
+ await service.sendEmail({
567
+ to: 'user@example.com',
568
+ subject: 'Hi',
569
+ emailConfigId: 'config-uuid-1'
570
+ }, (0, _loggedusermock.buildMockUser)());
571
+ expect(publish).toHaveBeenCalledWith(expect.objectContaining({
572
+ name: 'email.email_message.sent',
573
+ payload: expect.objectContaining({
574
+ ids: [
575
+ 'msg-1'
576
+ ],
577
+ metadata: expect.objectContaining({
578
+ recipientCount: 1
579
+ })
580
+ })
581
+ }));
582
+ });
583
+ it('publishes email.email_message.failed when the provider reports a failure', async ()=>{
584
+ mockProvider.sendEmail.mockResolvedValue({
585
+ success: false,
586
+ error: 'smtp refused'
587
+ });
588
+ await service.sendEmail({
589
+ to: 'user@example.com',
590
+ subject: 'Hi',
591
+ emailConfigId: 'config-uuid-1'
592
+ }, (0, _loggedusermock.buildMockUser)());
593
+ expect(publish).toHaveBeenCalledWith(expect.objectContaining({
594
+ name: 'email.email_message.failed',
595
+ payload: expect.objectContaining({
596
+ metadata: expect.objectContaining({
597
+ error: 'smtp refused'
598
+ })
599
+ })
600
+ }));
601
+ });
602
+ it('does not publish when the email module has events disabled', async ()=>{
603
+ _classes.EventBusRegistry.configureModule('email', {
604
+ enabled: false
605
+ });
606
+ await service.sendEmail({
607
+ to: 'user@example.com',
608
+ subject: 'Hi',
609
+ emailConfigId: 'config-uuid-1'
610
+ }, (0, _loggedusermock.buildMockUser)());
611
+ expect(publish).not.toHaveBeenCalled();
612
+ });
613
+ });
546
614
  });
@@ -0,0 +1,10 @@
1
+ export declare const EMAIL_EVENT_ACTIONS: {
2
+ readonly SENT: "sent";
3
+ readonly FAILED: "failed";
4
+ };
5
+ export declare const EMAIL_EVENT_ENTITIES: {
6
+ readonly MESSAGE: "email_message";
7
+ readonly CONFIG: "emailConfig";
8
+ readonly TEMPLATE: "emailTemplate";
9
+ };
10
+ export declare const EMAIL_EVENT_MODULE = "email";
package/config/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from './email.constants';
2
+ export * from './event-actions';
2
3
  export * from './message-keys';
@@ -2,6 +2,9 @@ export declare const EMAIL_TEMPLATE_MESSAGES: {
2
2
  readonly GET_SUCCESS: "email.template.get.success";
3
3
  readonly NOT_FOUND: "email.template.not.found";
4
4
  };
5
+ export declare const EMAIL_CONFIG_MESSAGES: {
6
+ readonly DEFAULT_DELETE_FORBIDDEN: "email.config.default.delete.forbidden";
7
+ };
5
8
  export declare const EMAIL_SEND_MESSAGES: {
6
9
  readonly SUCCESS: "email.send.success";
7
10
  readonly FAILED: "email.send.failed";
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Domain event actions published by the email module, alongside the CRUD
3
+ * lifecycle actions every ApiService emits.
4
+ */ export const EMAIL_EVENT_ACTIONS = {
5
+ SENT: 'sent',
6
+ FAILED: 'failed'
7
+ };
8
+ /** Every entity the email module publishes under, ApiService backed or not */ export const EMAIL_EVENT_ENTITIES = {
9
+ MESSAGE: 'email_message',
10
+ CONFIG: 'emailConfig',
11
+ TEMPLATE: 'emailTemplate'
12
+ };
13
+ export const EMAIL_EVENT_MODULE = 'email';
@@ -1,2 +1,3 @@
1
1
  export * from './email.constants';
2
+ export * from './event-actions';
2
3
  export * from './message-keys';
@@ -3,6 +3,9 @@ export const EMAIL_TEMPLATE_MESSAGES = {
3
3
  GET_SUCCESS: 'email.template.get.success',
4
4
  NOT_FOUND: 'email.template.not.found'
5
5
  };
6
+ export const EMAIL_CONFIG_MESSAGES = {
7
+ DEFAULT_DELETE_FORBIDDEN: 'email.config.default.delete.forbidden'
8
+ };
6
9
  export const EMAIL_SEND_MESSAGES = {
7
10
  SUCCESS: 'email.send.success',
8
11
  FAILED: 'email.send.failed',
@@ -4,7 +4,7 @@ function _ts_decorate(decorators, target, key, desc) {
4
4
  else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  }
7
- import { CacheModule, UtilsModule } from '@flusys/nestjs-shared/modules';
7
+ import { CacheModule, UtilsModule, createModuleEventsProvider } from '@flusys/nestjs-shared/modules';
8
8
  import { Module } from '@nestjs/common';
9
9
  import { EMAIL_MODULE_OPTIONS } from '../config/email.constants';
10
10
  import { EmailConfigController, EmailSendController, EmailTemplateController } from '../controllers';
@@ -68,6 +68,7 @@ export class EmailModule {
68
68
  useValue: options
69
69
  });
70
70
  }
71
+ providers.push(createModuleEventsProvider('email', EMAIL_MODULE_OPTIONS));
71
72
  return providers;
72
73
  }
73
74
  static createAsyncProviders(options) {
@@ -28,7 +28,9 @@ function _ts_param(paramIndex, decorator) {
28
28
  import { HybridCache, ApiService } from '@flusys/nestjs-shared/classes';
29
29
  import { UtilsService } from '@flusys/nestjs-shared/modules';
30
30
  import { applyCompanyFilter, buildCompanyWhereCondition } from '@flusys/nestjs-shared/utils';
31
- import { Inject, Injectable, Scope } from '@nestjs/common';
31
+ import { ForbiddenException, Inject, Injectable, Scope } from '@nestjs/common';
32
+ import { In, IsNull } from 'typeorm';
33
+ import { EMAIL_CONFIG_MESSAGES } from '../config';
32
34
  import { EmailConfig, EmailConfigWithCompany } from '../entities';
33
35
  import { EmailConfigService } from './email-config.service';
34
36
  import { EmailDataSourceProvider } from './email-datasource.provider';
@@ -36,6 +38,29 @@ export class EmailProviderConfigService extends ApiService {
36
38
  resolveEntity() {
37
39
  return this.emailConfig.isCompanyFeatureEnabled() ? EmailConfigWithCompany : EmailConfig;
38
40
  }
41
+ async beforeDeleteOperation(dto, _user, _queryRunner) {
42
+ const ids = Array.isArray(dto.id) ? dto.id : [
43
+ dto.id
44
+ ];
45
+ await this.ensureDataSourceRepository();
46
+ const defaultConfigs = await this.repository.find({
47
+ where: {
48
+ id: In(ids),
49
+ isDefault: true,
50
+ deletedAt: IsNull()
51
+ }
52
+ });
53
+ if (defaultConfigs.length > 0) {
54
+ const names = defaultConfigs.map((c)=>c.name).join(', ');
55
+ throw new ForbiddenException({
56
+ message: `Cannot delete the default email config(s): ${names}. Set another config as default first.`,
57
+ messageKey: EMAIL_CONFIG_MESSAGES.DEFAULT_DELETE_FORBIDDEN,
58
+ messageVariables: {
59
+ names
60
+ }
61
+ });
62
+ }
63
+ }
39
64
  async convertSingleDtoToEntity(dto, user) {
40
65
  const entity = {
41
66
  ...dto
@@ -240,4 +240,22 @@ describe('EmailProviderConfigService', ()=>{
240
240
  });
241
241
  });
242
242
  });
243
+ describe('beforeDeleteOperation', ()=>{
244
+ it('rejects deleting the default email config', async ()=>{
245
+ mockRepo.find.mockResolvedValue([
246
+ buildEmailConfig({
247
+ isDefault: true
248
+ })
249
+ ]);
250
+ await expect(service.beforeDeleteOperation({
251
+ id: 'config-uuid-1'
252
+ }, null, {})).rejects.toThrow('Cannot delete the default email config');
253
+ });
254
+ it('allows deleting a non-default email config', async ()=>{
255
+ mockRepo.find.mockResolvedValue([]);
256
+ await expect(service.beforeDeleteOperation({
257
+ id: 'config-uuid-1'
258
+ }, null, {})).resolves.toBeUndefined();
259
+ });
260
+ });
243
261
  });
@@ -26,10 +26,11 @@ function _ts_param(paramIndex, decorator) {
26
26
  };
27
27
  }
28
28
  import { LogAction } from '@flusys/nestjs-shared';
29
- import { EMAIL_SEND_MESSAGES } from '../config';
29
+ import { publishDomainEvent } from '@flusys/nestjs-shared/classes';
30
30
  import { ILoggedUserInfo } from '@flusys/nestjs-shared/interfaces';
31
31
  import { escapeHtmlVariables, validateCompanyOwnership } from '@flusys/nestjs-shared/utils';
32
32
  import { BadRequestException, Inject, Injectable, NotFoundException, Scope } from '@nestjs/common';
33
+ import { EMAIL_EVENT_ACTIONS, EMAIL_EVENT_ENTITIES, EMAIL_EVENT_MODULE, EMAIL_SEND_MESSAGES } from '../config';
33
34
  import { EmailConfigService } from './email-config.service';
34
35
  import { SendEmailDto, SendTemplateEmailDto } from '../dtos';
35
36
  import { EmailFactoryService } from '../providers';
@@ -41,7 +42,7 @@ export class EmailSendService {
41
42
  async sendEmail(dto, user) {
42
43
  const { provider, config } = await this.getEmailProviderWithConfig(dto.emailConfigId, user);
43
44
  const sender = this.resolveSender(dto, config);
44
- return await provider.sendEmail({
45
+ const result = await provider.sendEmail({
45
46
  to: dto.to,
46
47
  cc: dto.cc,
47
48
  bcc: dto.bcc,
@@ -52,13 +53,17 @@ export class EmailSendService {
52
53
  replyTo: dto.replyTo,
53
54
  attachments: this.buildAttachments(dto.attachments)
54
55
  });
56
+ await this.publishSendEvent(result, dto.to, user, {
57
+ emailConfigId: config.id
58
+ });
59
+ return result;
55
60
  }
56
61
  async sendTemplateEmail(dto, user) {
57
62
  const template = await this.resolveTemplate(dto, user);
58
63
  const { subject, html, text } = this.buildTemplateContent(template, dto.variables);
59
64
  const { provider, config } = await this.getEmailProviderWithConfig(dto.emailConfigId, user);
60
65
  const sender = this.resolveSender(dto, config);
61
- return await provider.sendEmail({
66
+ const result = await provider.sendEmail({
62
67
  to: dto.to,
63
68
  cc: dto.cc,
64
69
  bcc: dto.bcc,
@@ -69,6 +74,12 @@ export class EmailSendService {
69
74
  replyTo: dto.replyTo,
70
75
  attachments: this.buildAttachments(dto.attachments)
71
76
  });
77
+ await this.publishSendEvent(result, dto.to, user, {
78
+ emailConfigId: config.id,
79
+ templateId: template.id,
80
+ templateSlug: template.slug
81
+ });
82
+ return result;
72
83
  }
73
84
  async sendTestEmail(emailConfigId, recipient, user) {
74
85
  const { provider, config } = await this.getEmailProviderWithConfig(emailConfigId, user);
@@ -84,6 +95,10 @@ export class EmailSendService {
84
95
  from: config.fromEmail || undefined,
85
96
  fromName: config.fromName || this.emailConfigService.getDefaultFromName()
86
97
  });
98
+ await this.publishSendEvent(result, recipient, user, {
99
+ emailConfigId: config.id,
100
+ isTest: true
101
+ });
87
102
  return result;
88
103
  }
89
104
  // ─── Private: Provider & Config Resolution ──────────────────────────────────
@@ -127,7 +142,7 @@ export class EmailSendService {
127
142
  }
128
143
  // ─── Private: Template Resolution ───────────────────────────────────────────
129
144
  async resolveTemplate(dto, user) {
130
- let template = null;
145
+ let template;
131
146
  if (dto.templateId) {
132
147
  template = await this.emailTemplateService.findByIdDirect(dto.templateId);
133
148
  } else if (dto.templateSlug) {
@@ -203,6 +218,27 @@ export class EmailSendService {
203
218
  </div>
204
219
  `;
205
220
  }
221
+ async publishSendEvent(result, to, user, metadata) {
222
+ const recipients = Array.isArray(to) ? to : [
223
+ to
224
+ ];
225
+ await publishDomainEvent({
226
+ module: EMAIL_EVENT_MODULE,
227
+ entity: EMAIL_EVENT_ENTITIES.MESSAGE,
228
+ action: result.success ? EMAIL_EVENT_ACTIONS.SENT : EMAIL_EVENT_ACTIONS.FAILED,
229
+ ids: result.messageId ? [
230
+ result.messageId
231
+ ] : [],
232
+ metadata: {
233
+ ...metadata,
234
+ recipientCount: recipients.length,
235
+ ...result.error ? {
236
+ error: result.error
237
+ } : {}
238
+ },
239
+ user: user ?? null
240
+ });
241
+ }
206
242
  constructor(emailFactory, emailConfigService, emailProviderConfigService, emailTemplateService){
207
243
  _define_property(this, "emailFactory", void 0);
208
244
  _define_property(this, "emailConfigService", void 0);
@@ -4,6 +4,7 @@ import { buildMockUser } from '@test-utils/mocks/logged-user.mock';
4
4
  import { EmailFactoryService } from '../providers';
5
5
  import { EmailConfigService } from './email-config.service';
6
6
  import { EmailProviderConfigService } from './email-provider-config.service';
7
+ import { EventBusRegistry } from '@flusys/nestjs-shared/classes';
7
8
  import { EmailSendService } from './email-send.service';
8
9
  import { EmailTemplateService } from './email-template.service';
9
10
  function buildConfig(overrides = {}) {
@@ -539,4 +540,71 @@ describe('EmailSendService', ()=>{
539
540
  expect(payload.html).not.toContain('<script>alert(1)</script>');
540
541
  });
541
542
  });
543
+ describe('domain events', ()=>{
544
+ let publish;
545
+ beforeEach(()=>{
546
+ publish = jest.fn().mockResolvedValue(undefined);
547
+ EventBusRegistry.reset();
548
+ EventBusRegistry.setBus({
549
+ publish,
550
+ subscribe: jest.fn(),
551
+ close: jest.fn()
552
+ });
553
+ EventBusRegistry.configureModule('email', {
554
+ enabled: true
555
+ });
556
+ mockEmailProviderConfigService.findByIdDirect.mockResolvedValue(buildConfig());
557
+ });
558
+ afterEach(()=>{
559
+ EventBusRegistry.reset();
560
+ });
561
+ it('publishes email.email_message.sent for a successful send', async ()=>{
562
+ await service.sendEmail({
563
+ to: 'user@example.com',
564
+ subject: 'Hi',
565
+ emailConfigId: 'config-uuid-1'
566
+ }, buildMockUser());
567
+ expect(publish).toHaveBeenCalledWith(expect.objectContaining({
568
+ name: 'email.email_message.sent',
569
+ payload: expect.objectContaining({
570
+ ids: [
571
+ 'msg-1'
572
+ ],
573
+ metadata: expect.objectContaining({
574
+ recipientCount: 1
575
+ })
576
+ })
577
+ }));
578
+ });
579
+ it('publishes email.email_message.failed when the provider reports a failure', async ()=>{
580
+ mockProvider.sendEmail.mockResolvedValue({
581
+ success: false,
582
+ error: 'smtp refused'
583
+ });
584
+ await service.sendEmail({
585
+ to: 'user@example.com',
586
+ subject: 'Hi',
587
+ emailConfigId: 'config-uuid-1'
588
+ }, buildMockUser());
589
+ expect(publish).toHaveBeenCalledWith(expect.objectContaining({
590
+ name: 'email.email_message.failed',
591
+ payload: expect.objectContaining({
592
+ metadata: expect.objectContaining({
593
+ error: 'smtp refused'
594
+ })
595
+ })
596
+ }));
597
+ });
598
+ it('does not publish when the email module has events disabled', async ()=>{
599
+ EventBusRegistry.configureModule('email', {
600
+ enabled: false
601
+ });
602
+ await service.sendEmail({
603
+ to: 'user@example.com',
604
+ subject: 'Hi',
605
+ emailConfigId: 'config-uuid-1'
606
+ }, buildMockUser());
607
+ expect(publish).not.toHaveBeenCalled();
608
+ });
609
+ });
542
610
  });
@@ -1,9 +1,11 @@
1
1
  import { IBootstrapAppConfig, IDataSourceServiceOptions, IDynamicModuleConfig, IModuleOptionsFactory } from '@flusys/nestjs-core';
2
+ import { IModuleEventsConfig } from '@flusys/nestjs-shared/interfaces';
2
3
  import { ModuleMetadata, Type } from '@nestjs/common';
3
4
  export interface IEmailModuleConfig extends IDataSourceServiceOptions {
4
5
  defaultProvider?: string;
5
6
  rateLimitPerMinute?: number;
6
7
  enableLogging?: boolean;
8
+ events?: IModuleEventsConfig;
7
9
  }
8
10
  export interface EmailModuleOptions extends IDynamicModuleConfig {
9
11
  bootstrapAppConfig?: IBootstrapAppConfig;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flusys/nestjs-email",
3
- "version": "6.4.0",
3
+ "version": "7.0.0-beta.1",
4
4
  "description": "Modular email package with SMTP, SendGrid, and Mailgun providers",
5
5
  "main": "cjs/index.js",
6
6
  "module": "fesm/index.js",
@@ -105,7 +105,7 @@
105
105
  }
106
106
  },
107
107
  "dependencies": {
108
- "@flusys/nestjs-core": "6.4.0",
109
- "@flusys/nestjs-shared": "6.4.0"
108
+ "@flusys/nestjs-core": "7.0.0-beta.1",
109
+ "@flusys/nestjs-shared": "7.0.0-beta.1"
110
110
  }
111
111
  }
@@ -1,8 +1,8 @@
1
1
  import { HybridCache, ApiService } from '@flusys/nestjs-shared/classes';
2
- import { FilterAndPaginationDto } from '@flusys/nestjs-shared/dtos';
2
+ import { DeleteDto, FilterAndPaginationDto } from '@flusys/nestjs-shared/dtos';
3
3
  import { ILoggedUserInfo } from '@flusys/nestjs-shared/interfaces';
4
4
  import { UtilsService } from '@flusys/nestjs-shared/modules';
5
- import { EntityTarget, SelectQueryBuilder } from 'typeorm';
5
+ import { EntityTarget, QueryRunner, SelectQueryBuilder } from 'typeorm';
6
6
  import { CreateEmailConfigDto, UpdateEmailConfigDto } from '../dtos';
7
7
  import { EmailConfig, EmailConfigBase } from '../entities';
8
8
  import { IEmailConfig } from '../interfaces';
@@ -14,6 +14,7 @@ export declare class EmailProviderConfigService extends ApiService<CreateEmailCo
14
14
  private readonly emailConfig;
15
15
  constructor(cacheManager: HybridCache, utilsService: UtilsService, emailConfig: EmailConfigService, dataSourceProvider: EmailDataSourceProvider);
16
16
  protected resolveEntity(): EntityTarget<EmailConfigBase>;
17
+ protected beforeDeleteOperation(dto: DeleteDto, _user: ILoggedUserInfo | null, _queryRunner: QueryRunner): Promise<void>;
17
18
  convertSingleDtoToEntity(dto: CreateEmailConfigDto | UpdateEmailConfigDto, user: ILoggedUserInfo | null): Promise<EmailConfigBase>;
18
19
  getSelectQuery(query: SelectQueryBuilder<EmailConfigBase>, _user: ILoggedUserInfo | null, select?: string[]): Promise<{
19
20
  query: SelectQueryBuilder<EmailConfig>;
@@ -23,4 +23,5 @@ export declare class EmailSendService {
23
23
  private buildAttachments;
24
24
  private interpolateVariables;
25
25
  private buildTestEmailHtml;
26
+ private publishSendEvent;
26
27
  }