@flusys/nestjs-email 9.1.2 → 9.2.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 +7 -0
- package/controllers/email-config.controller.d.ts +1 -0
- package/controllers/email-template.controller.d.ts +1 -0
- package/fesm/184.js +181 -65
- package/fesm/261.js +2 -2
- package/fesm/index.js +4 -4
- package/fesm/modules/index.js +2 -2
- package/fesm/providers/index.js +78 -30
- package/package.json +3 -3
- package/providers/email-factory.service.d.ts +5 -1
- package/services/email-provider-config.service.d.ts +4 -2
- package/services/email-template.service.d.ts +6 -3
- package/utils/company-ownership.util.d.ts +3 -0
package/README.md
CHANGED
|
@@ -207,6 +207,13 @@ await this.emailSend.sendEmail({
|
|
|
207
207
|
|
|
208
208
|
`EmailModule` provides and exports `EMAIL_ADAPTER` (`nestjs-shared`), implemented by `EmailAdapter`: `send({ to, cc?, bcc?, replyTo?, emailConfigId?, subject, html?, text? }, user)` and `sendTemplate({ ...recipients, templateSlug, variables? }, user)`, both answering `{ success, messageId?, error? }`. It resolves the request-scoped `EmailSendService` per call, so singletons can inject it; a feature package injects it `@Optional()` and never imports this package (the entity builder's **Send email** flow step uses it).
|
|
209
209
|
|
|
210
|
+
## 4. Caching and Company Scope
|
|
211
|
+
|
|
212
|
+
- **Email configs are never cached** in the shared cache (`CACHE_INSTANCE`): rows carry SMTP passwords and API keys. Templates are cached (`findById` / `getAll`) under the `emailTemplate` stamp, which every insert / update / delete moves after commit on every instance.
|
|
213
|
+
- **The send path reads the database**: `sendEmail` / `sendTemplateEmail` / `sendTestEmail` load the config and the template (by id or slug) straight from the repository, so an edited template or config is used on the next send on every instance.
|
|
214
|
+
- **Provider clients** (`EmailFactoryService`) are reused per provider + SHA-256 of the full config, credentials included, so an edited config gets a new client everywhere and only identical credentials share one. Concurrent first sends share one pending client; a failed initialization is dropped so the next send retries. Clients idle for 30 minutes, or beyond the 50 most recently used, are dropped and `close()`d (SMTP transports). `clearCache()` closes every client.
|
|
215
|
+
- **Company scope** (`enableCompanyFeature: true`): `getById` / `getAll` only return rows of the caller's company (the company is part of the cache key), and `update` / `delete` reject ids of another company with `entity.belongs.another.company`.
|
|
216
|
+
|
|
210
217
|
## License
|
|
211
218
|
|
|
212
219
|
MIT © FLUSYS
|
|
@@ -4,6 +4,7 @@ declare const EmailConfigController_base: abstract new (service: EmailProviderCo
|
|
|
4
4
|
readonly enabledEndpoints: import("@flusys/nestjs-shared").ApiEndpoint[] | "all";
|
|
5
5
|
service: EmailProviderConfigService;
|
|
6
6
|
isEnabled(endpoint: import("@flusys/nestjs-shared").ApiEndpoint): boolean;
|
|
7
|
+
assertEnabled(endpoint: import("@flusys/nestjs-shared").ApiEndpoint): void;
|
|
7
8
|
insert(addDto: CreateEmailConfigDto, user: import("@flusys/nestjs-shared").ILoggedUserInfo | null): Promise<import("@flusys/nestjs-shared").SingleResponseDto<EmailConfigResponseDto>>;
|
|
8
9
|
insertMany(addDto: CreateEmailConfigDto[], user: import("@flusys/nestjs-shared").ILoggedUserInfo | null): Promise<import("@flusys/nestjs-shared").BulkResponseDto<EmailConfigResponseDto>>;
|
|
9
10
|
getById(id: string, body: import("@flusys/nestjs-shared").GetByIdBodyDto, user: import("@flusys/nestjs-shared").ILoggedUserInfo | null): Promise<import("@flusys/nestjs-shared").SingleResponseDto<EmailConfigResponseDto>>;
|
|
@@ -7,6 +7,7 @@ declare const EmailTemplateController_base: abstract new (service: EmailTemplate
|
|
|
7
7
|
readonly enabledEndpoints: import("@flusys/nestjs-shared").ApiEndpoint[] | "all";
|
|
8
8
|
service: EmailTemplateService;
|
|
9
9
|
isEnabled(endpoint: import("@flusys/nestjs-shared").ApiEndpoint): boolean;
|
|
10
|
+
assertEnabled(endpoint: import("@flusys/nestjs-shared").ApiEndpoint): void;
|
|
10
11
|
insert(addDto: CreateEmailTemplateDto, user: ILoggedUserInfo | null): Promise<SingleResponseDto<EmailTemplateResponseDto>>;
|
|
11
12
|
insertMany(addDto: CreateEmailTemplateDto[], user: ILoggedUserInfo | null): Promise<import("@flusys/nestjs-shared").BulkResponseDto<EmailTemplateResponseDto>>;
|
|
12
13
|
getById(id: string, body: import("@flusys/nestjs-shared").GetByIdBodyDto, user: ILoggedUserInfo | null): Promise<SingleResponseDto<EmailTemplateResponseDto>>;
|
package/fesm/184.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Identity } from "@flusys/nestjs-shared/entities";
|
|
2
2
|
import { createHash } from "crypto";
|
|
3
|
-
import { SYSTEM_MESSAGES } from "@flusys/nestjs-shared/constants";
|
|
4
3
|
import * as __rspack_external__flusys_nestjs_shared_393d2cae from "@flusys/nestjs-shared";
|
|
5
4
|
import * as __rspack_external__flusys_nestjs_shared_classes_6076a329 from "@flusys/nestjs-shared/classes";
|
|
5
|
+
import * as __rspack_external__flusys_nestjs_shared_constants_81f33d5b from "@flusys/nestjs-shared/constants";
|
|
6
6
|
import * as __rspack_external__flusys_nestjs_shared_modules_24702bee from "@flusys/nestjs-shared/modules";
|
|
7
7
|
import * as __rspack_external__flusys_nestjs_shared_utils_9125c1f1 from "@flusys/nestjs-shared/utils";
|
|
8
8
|
import * as __rspack_external__nestjs_common_a04939da from "@nestjs/common";
|
|
@@ -464,7 +464,7 @@ function getEmailEntitiesByConfig(enableCompanyFeature) {
|
|
|
464
464
|
|
|
465
465
|
|
|
466
466
|
},
|
|
467
|
-
|
|
467
|
+
652(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
|
|
468
468
|
|
|
469
469
|
// EXPORTS
|
|
470
470
|
__webpack_require__.d(__webpack_exports__, {
|
|
@@ -527,13 +527,21 @@ function _ts_metadata(metadataKey, metadataValue) {
|
|
|
527
527
|
|
|
528
528
|
|
|
529
529
|
|
|
530
|
+
const MAX_CACHED_PROVIDERS = 50;
|
|
531
|
+
const PROVIDER_IDLE_TTL_MS = 30 * 60 * 1000;
|
|
530
532
|
class EmailFactoryService {
|
|
531
533
|
providerCache = new Map();
|
|
532
534
|
// ─── Public API ─────────────────────────────────────────────────────────────
|
|
533
535
|
async createProvider(config) {
|
|
534
536
|
const cacheKey = this.generateCacheKey(config);
|
|
537
|
+
const now = Date.now();
|
|
535
538
|
const cached = this.providerCache.get(cacheKey);
|
|
536
|
-
if (cached)
|
|
539
|
+
if (cached) {
|
|
540
|
+
cached.lastUsed = now;
|
|
541
|
+
this.providerCache.delete(cacheKey);
|
|
542
|
+
this.providerCache.set(cacheKey, cached);
|
|
543
|
+
return cached.provider;
|
|
544
|
+
}
|
|
537
545
|
const ProviderClass = EmailProviderRegistry.get(config.provider);
|
|
538
546
|
if (!ProviderClass) {
|
|
539
547
|
throw new common_.NotFoundException({
|
|
@@ -545,10 +553,32 @@ class EmailFactoryService {
|
|
|
545
553
|
}
|
|
546
554
|
});
|
|
547
555
|
}
|
|
556
|
+
// The pending promise is cached so concurrent first sends share one client (one SMTP transport)
|
|
557
|
+
const entry = {
|
|
558
|
+
provider: this.instantiate(ProviderClass, config),
|
|
559
|
+
lastUsed: now
|
|
560
|
+
};
|
|
561
|
+
this.providerCache.set(cacheKey, entry);
|
|
562
|
+
entry.provider.catch(()=>{
|
|
563
|
+
if (this.providerCache.get(cacheKey) === entry) this.providerCache.delete(cacheKey);
|
|
564
|
+
});
|
|
565
|
+
this.evict(now);
|
|
566
|
+
return entry.provider;
|
|
567
|
+
}
|
|
568
|
+
async clearCache() {
|
|
569
|
+
const entries = Array.from(this.providerCache.values());
|
|
570
|
+
this.providerCache.clear();
|
|
571
|
+
await Promise.allSettled(entries.map((entry)=>this.closeEntry(entry)));
|
|
572
|
+
}
|
|
573
|
+
// ─── Lifecycle ──────────────────────────────────────────────────────────────
|
|
574
|
+
async onModuleDestroy() {
|
|
575
|
+
await this.clearCache();
|
|
576
|
+
}
|
|
577
|
+
// ─── Private Helpers ────────────────────────────────────────────────────────
|
|
578
|
+
async instantiate(ProviderClass, config) {
|
|
548
579
|
try {
|
|
549
580
|
const instance = new ProviderClass();
|
|
550
581
|
await instance.initialize?.(config.config);
|
|
551
|
-
this.providerCache.set(cacheKey, instance);
|
|
552
582
|
return instance;
|
|
553
583
|
} catch (error) {
|
|
554
584
|
throw new common_.InternalServerErrorException({
|
|
@@ -561,27 +591,36 @@ class EmailFactoryService {
|
|
|
561
591
|
});
|
|
562
592
|
}
|
|
563
593
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
generateCacheKey(config) {
|
|
572
|
-
const sortedKeys = Object.keys(config.config || {}).sort();
|
|
573
|
-
const configString = JSON.stringify(config.config, sortedKeys);
|
|
574
|
-
const hash = createHash('sha256').update(configString).digest('hex').substring(0, 16);
|
|
575
|
-
return `${config.provider}-${hash}`;
|
|
594
|
+
evict(now) {
|
|
595
|
+
for (const [key, entry] of this.providerCache){
|
|
596
|
+
const overCapacity = this.providerCache.size > MAX_CACHED_PROVIDERS;
|
|
597
|
+
if (!overCapacity && now - entry.lastUsed < PROVIDER_IDLE_TTL_MS) continue;
|
|
598
|
+
this.providerCache.delete(key);
|
|
599
|
+
void this.closeEntry(entry);
|
|
600
|
+
}
|
|
576
601
|
}
|
|
577
|
-
async
|
|
578
|
-
if (!provider.close) return;
|
|
602
|
+
async closeEntry(entry) {
|
|
579
603
|
try {
|
|
580
|
-
await provider
|
|
604
|
+
const provider = await entry.provider;
|
|
605
|
+
await provider.close?.();
|
|
581
606
|
} catch {
|
|
582
|
-
//
|
|
607
|
+
// A client that failed to initialize or to close has nothing left to release
|
|
583
608
|
}
|
|
584
609
|
}
|
|
610
|
+
generateCacheKey(config) {
|
|
611
|
+
const hash = createHash('sha256').update(this.stableStringify(config.config ?? {})).digest('hex');
|
|
612
|
+
return `${config.provider}:${hash}`;
|
|
613
|
+
}
|
|
614
|
+
stableStringify(value) {
|
|
615
|
+
if (value === null || value === undefined) return 'null';
|
|
616
|
+
if (Array.isArray(value)) return `[${value.map((item)=>this.stableStringify(item)).join(',')}]`;
|
|
617
|
+
if (typeof value === 'object') {
|
|
618
|
+
const record = value;
|
|
619
|
+
const entries = Object.keys(record).filter((key)=>record[key] !== undefined).sort().map((key)=>`${JSON.stringify(key)}:${this.stableStringify(record[key])}`);
|
|
620
|
+
return `{${entries.join(',')}}`;
|
|
621
|
+
}
|
|
622
|
+
return JSON.stringify(value);
|
|
623
|
+
}
|
|
585
624
|
extractErrorMessage(error) {
|
|
586
625
|
return error instanceof Error ? error.message : 'Unknown error';
|
|
587
626
|
}
|
|
@@ -677,7 +716,8 @@ class SmtpProvider {
|
|
|
677
716
|
}
|
|
678
717
|
// ─── Private Helpers ────────────────────────────────────────────────────────
|
|
679
718
|
formatFrom(from, fromName) {
|
|
680
|
-
|
|
719
|
+
if (!from) return undefined;
|
|
720
|
+
return fromName ? `"${fromName.replace(/["\\]/g, '\\$&')}" <${from}>` : from;
|
|
681
721
|
}
|
|
682
722
|
joinAddresses(addresses) {
|
|
683
723
|
return Array.isArray(addresses) ? addresses.join(', ') : addresses;
|
|
@@ -701,8 +741,8 @@ class SmtpProvider {
|
|
|
701
741
|
}
|
|
702
742
|
}
|
|
703
743
|
|
|
704
|
-
|
|
705
|
-
|
|
744
|
+
// EXTERNAL MODULE: external "@flusys/nestjs-shared/constants"
|
|
745
|
+
var constants_ = __webpack_require__(140);
|
|
706
746
|
// EXTERNAL MODULE: external "@flusys/nestjs-shared/utils"
|
|
707
747
|
var utils_ = __webpack_require__(216);
|
|
708
748
|
;// CONCATENATED MODULE: ./projects/nestjs-email/src/providers/sendgrid-provider.ts
|
|
@@ -717,12 +757,13 @@ class SendGridProvider {
|
|
|
717
757
|
try {
|
|
718
758
|
// Optional peer dependency - loaded at runtime so bundlers don't statically resolve it
|
|
719
759
|
const sgMailModule = await (0,utils_.loadOptionalModule)('@sendgrid/mail');
|
|
720
|
-
|
|
760
|
+
const MailService = sgMailModule.MailService ?? sgMailModule.default?.MailService;
|
|
761
|
+
this.sgMail = MailService ? new MailService() : sgMailModule.default || sgMailModule;
|
|
721
762
|
this.sgMail.setApiKey(config.apiKey);
|
|
722
763
|
} catch {
|
|
723
764
|
throw new common_.InternalServerErrorException({
|
|
724
765
|
message: 'SendGrid initialization failed. Install: npm install @sendgrid/mail',
|
|
725
|
-
messageKey: SYSTEM_MESSAGES.SDK_NOT_INSTALLED,
|
|
766
|
+
messageKey: constants_.SYSTEM_MESSAGES.SDK_NOT_INSTALLED,
|
|
726
767
|
messageVariables: {
|
|
727
768
|
sdk: 'sendgrid'
|
|
728
769
|
}
|
|
@@ -737,7 +778,8 @@ class SendGridProvider {
|
|
|
737
778
|
try {
|
|
738
779
|
const msg = this.buildMessage(options, true);
|
|
739
780
|
const [response] = await this.sgMail.send(msg);
|
|
740
|
-
const
|
|
781
|
+
const headers = response?.headers ?? {};
|
|
782
|
+
const messageId = headers['x-message-id'] || headers['X-Message-Id'];
|
|
741
783
|
return {
|
|
742
784
|
success: true,
|
|
743
785
|
messageId
|
|
@@ -778,7 +820,7 @@ class SendGridProvider {
|
|
|
778
820
|
buildMessage(options, includeExtras) {
|
|
779
821
|
const msg = {
|
|
780
822
|
to: this.toArray(options.to),
|
|
781
|
-
from: options.fromName ? {
|
|
823
|
+
from: options.fromName && options.from ? {
|
|
782
824
|
email: options.from,
|
|
783
825
|
name: options.fromName
|
|
784
826
|
} : options.from,
|
|
@@ -839,7 +881,7 @@ class MailgunProvider {
|
|
|
839
881
|
} catch {
|
|
840
882
|
throw new common_.InternalServerErrorException({
|
|
841
883
|
message: 'Mailgun initialization failed. Install: npm install mailgun.js form-data',
|
|
842
|
-
messageKey: SYSTEM_MESSAGES.SDK_NOT_INSTALLED,
|
|
884
|
+
messageKey: constants_.SYSTEM_MESSAGES.SDK_NOT_INSTALLED,
|
|
843
885
|
messageVariables: {
|
|
844
886
|
sdk: 'mailgun'
|
|
845
887
|
}
|
|
@@ -885,7 +927,7 @@ class MailgunProvider {
|
|
|
885
927
|
// ─── Private Helpers ────────────────────────────────────────────────────────
|
|
886
928
|
buildMessageData(options) {
|
|
887
929
|
const data = {
|
|
888
|
-
from: options.fromName ?
|
|
930
|
+
from: options.fromName && options.from ? `"${options.fromName.replace(/["\\]/g, '\\$&')}" <${options.from}>` : options.from,
|
|
889
931
|
to: this.joinAddresses(options.to),
|
|
890
932
|
subject: options.subject
|
|
891
933
|
};
|
|
@@ -1075,11 +1117,12 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
1075
1117
|
/* import */ var _flusys_nestjs_shared_utils__rspack_import_2 = __webpack_require__(216);
|
|
1076
1118
|
/* import */ var _nestjs_common__rspack_import_3 = __webpack_require__(157);
|
|
1077
1119
|
/* import */ var typeorm__rspack_import_4 = __webpack_require__(158);
|
|
1078
|
-
/* import */ var
|
|
1079
|
-
/* import */ var
|
|
1120
|
+
/* import */ var _config_index_js__rspack_import_9 = __webpack_require__(399);
|
|
1121
|
+
/* import */ var _config_index_js__rspack_import_10 = __webpack_require__(887);
|
|
1080
1122
|
/* import */ var _entities_index_js__rspack_import_5 = __webpack_require__(454);
|
|
1081
|
-
/* import */ var
|
|
1082
|
-
/* import */ var
|
|
1123
|
+
/* import */ var _utils_company_ownership_util_js__rspack_import_6 = __webpack_require__(561);
|
|
1124
|
+
/* import */ var _email_config_service_js__rspack_import_7 = __webpack_require__(843);
|
|
1125
|
+
/* import */ var _email_datasource_provider_js__rspack_import_8 = __webpack_require__(522);
|
|
1083
1126
|
function _ts_decorate(decorators, target, key, desc) {
|
|
1084
1127
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
1085
1128
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
|
|
@@ -1112,18 +1155,25 @@ function _ts_param(paramIndex, decorator) {
|
|
|
1112
1155
|
|
|
1113
1156
|
|
|
1114
1157
|
|
|
1158
|
+
|
|
1115
1159
|
class EmailProviderConfigService extends _flusys_nestjs_shared_classes__rspack_import_0.ApiService {
|
|
1116
1160
|
cacheManager;
|
|
1117
1161
|
utilsService;
|
|
1118
1162
|
emailConfig;
|
|
1119
1163
|
constructor(cacheManager, utilsService, emailConfig, dataSourceProvider){
|
|
1120
|
-
super(
|
|
1164
|
+
super(_config_index_js__rspack_import_9/* .EMAIL_EVENT_ENTITIES.CONFIG */.y.CONFIG, cacheManager, utilsService, EmailProviderConfigService.name, // Rows carry provider credentials (SMTP password, API keys) - keep them out of the shared cache
|
|
1165
|
+
false, (/* inlined export .EMAIL_EVENT_MODULE */"email"), dataSourceProvider, emailConfig.isCompanyFeatureEnabled() ? _entities_index_js__rspack_import_5/* .EmailConfigWithCompany */.Ed : _entities_index_js__rspack_import_5/* .EmailConfig */.pG), this.cacheManager = cacheManager, this.utilsService = utilsService, this.emailConfig = emailConfig;
|
|
1166
|
+
}
|
|
1167
|
+
async beforeUpdateOperation(dto, user) {
|
|
1168
|
+
await this.assertInUserCompany((Array.isArray(dto) ? dto : [
|
|
1169
|
+
dto
|
|
1170
|
+
]).map((d)=>d.id), user);
|
|
1121
1171
|
}
|
|
1122
|
-
async beforeDeleteOperation(dto,
|
|
1172
|
+
async beforeDeleteOperation(dto, user, _queryRunner) {
|
|
1123
1173
|
const ids = Array.isArray(dto.id) ? dto.id : [
|
|
1124
1174
|
dto.id
|
|
1125
1175
|
];
|
|
1126
|
-
await this.
|
|
1176
|
+
await this.assertInUserCompany(ids, user);
|
|
1127
1177
|
const defaultConfigs = await this.repository.find({
|
|
1128
1178
|
where: {
|
|
1129
1179
|
id: (0,typeorm__rspack_import_4.In)(ids),
|
|
@@ -1135,7 +1185,7 @@ class EmailProviderConfigService extends _flusys_nestjs_shared_classes__rspack_i
|
|
|
1135
1185
|
const names = defaultConfigs.map((c)=>c.name).join(', ');
|
|
1136
1186
|
throw new _nestjs_common__rspack_import_3.ForbiddenException({
|
|
1137
1187
|
message: `Cannot delete the default email config(s): ${names}. Set another config as default first.`,
|
|
1138
|
-
messageKey:
|
|
1188
|
+
messageKey: _config_index_js__rspack_import_10/* .EMAIL_CONFIG_MESSAGES.DEFAULT_DELETE_FORBIDDEN */.T6.DEFAULT_DELETE_FORBIDDEN,
|
|
1139
1189
|
messageVariables: {
|
|
1140
1190
|
names
|
|
1141
1191
|
}
|
|
@@ -1143,15 +1193,14 @@ class EmailProviderConfigService extends _flusys_nestjs_shared_classes__rspack_i
|
|
|
1143
1193
|
}
|
|
1144
1194
|
}
|
|
1145
1195
|
async convertSingleDtoToEntity(dto, user) {
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
};
|
|
1196
|
+
await this.ensureDataSourceRepository();
|
|
1197
|
+
const entity = await super.convertSingleDtoToEntity(dto, user);
|
|
1149
1198
|
if (this.emailConfig.isCompanyFeatureEnabled()) {
|
|
1150
1199
|
entity.companyId = user?.companyId ?? null;
|
|
1151
1200
|
}
|
|
1152
1201
|
return entity;
|
|
1153
1202
|
}
|
|
1154
|
-
async getSelectQuery(query,
|
|
1203
|
+
async getSelectQuery(query, user, select) {
|
|
1155
1204
|
if (!select?.length) {
|
|
1156
1205
|
select = [
|
|
1157
1206
|
'id',
|
|
@@ -1168,6 +1217,10 @@ class EmailProviderConfigService extends _flusys_nestjs_shared_classes__rspack_i
|
|
|
1168
1217
|
if (this.emailConfig.isCompanyFeatureEnabled()) select.push('companyId');
|
|
1169
1218
|
}
|
|
1170
1219
|
query.select(select.map((field)=>`${this.entityName}.${field}`));
|
|
1220
|
+
(0,_flusys_nestjs_shared_utils__rspack_import_2.applyCompanyFilter)(query, {
|
|
1221
|
+
isCompanyFeatureEnabled: this.emailConfig.isCompanyFeatureEnabled(),
|
|
1222
|
+
entityAlias: this.entityName
|
|
1223
|
+
}, user);
|
|
1171
1224
|
return {
|
|
1172
1225
|
query,
|
|
1173
1226
|
isRaw: false
|
|
@@ -1175,10 +1228,6 @@ class EmailProviderConfigService extends _flusys_nestjs_shared_classes__rspack_i
|
|
|
1175
1228
|
}
|
|
1176
1229
|
async getExtraManipulateQuery(query, filterDto, user) {
|
|
1177
1230
|
const result = await super.getExtraManipulateQuery(query, filterDto, user);
|
|
1178
|
-
(0,_flusys_nestjs_shared_utils__rspack_import_2.applyCompanyFilter)(query, {
|
|
1179
|
-
isCompanyFeatureEnabled: this.emailConfig.isCompanyFeatureEnabled(),
|
|
1180
|
-
entityAlias: 'emailConfig'
|
|
1181
|
-
}, user);
|
|
1182
1231
|
query.orderBy(`${this.entityName}.createdAt`, 'DESC');
|
|
1183
1232
|
return result;
|
|
1184
1233
|
}
|
|
@@ -1211,6 +1260,10 @@ class EmailProviderConfigService extends _flusys_nestjs_shared_classes__rspack_i
|
|
|
1211
1260
|
}
|
|
1212
1261
|
});
|
|
1213
1262
|
}
|
|
1263
|
+
async assertInUserCompany(ids, user) {
|
|
1264
|
+
await this.ensureDataSourceRepository();
|
|
1265
|
+
await (0,_utils_company_ownership_util_js__rspack_import_6/* .assertRowsInUserCompany */.p)(this.repository, ids.filter(Boolean), user, this.emailConfig.isCompanyFeatureEnabled(), 'Email configuration');
|
|
1266
|
+
}
|
|
1214
1267
|
}
|
|
1215
1268
|
EmailProviderConfigService = _ts_decorate([
|
|
1216
1269
|
(0,_nestjs_common__rspack_import_3.Injectable)({
|
|
@@ -1218,14 +1271,14 @@ EmailProviderConfigService = _ts_decorate([
|
|
|
1218
1271
|
}),
|
|
1219
1272
|
_ts_param(0, (0,_nestjs_common__rspack_import_3.Inject)('CACHE_INSTANCE')),
|
|
1220
1273
|
_ts_param(1, (0,_nestjs_common__rspack_import_3.Inject)(_flusys_nestjs_shared_modules__rspack_import_1.UtilsService)),
|
|
1221
|
-
_ts_param(2, (0,_nestjs_common__rspack_import_3.Inject)(
|
|
1222
|
-
_ts_param(3, (0,_nestjs_common__rspack_import_3.Inject)(
|
|
1274
|
+
_ts_param(2, (0,_nestjs_common__rspack_import_3.Inject)(_email_config_service_js__rspack_import_7/* .EmailConfigService */.m)),
|
|
1275
|
+
_ts_param(3, (0,_nestjs_common__rspack_import_3.Inject)(_email_datasource_provider_js__rspack_import_8/* .EmailDataSourceProvider */.Z)),
|
|
1223
1276
|
_ts_metadata("design:type", Function),
|
|
1224
1277
|
_ts_metadata("design:paramtypes", [
|
|
1225
1278
|
typeof _flusys_nestjs_shared_classes__rspack_import_0.HybridCache === "undefined" ? Object : _flusys_nestjs_shared_classes__rspack_import_0.HybridCache,
|
|
1226
1279
|
typeof _flusys_nestjs_shared_modules__rspack_import_1.UtilsService === "undefined" ? Object : _flusys_nestjs_shared_modules__rspack_import_1.UtilsService,
|
|
1227
|
-
typeof
|
|
1228
|
-
typeof
|
|
1280
|
+
typeof _email_config_service_js__rspack_import_7/* .EmailConfigService */.m === "undefined" ? Object : _email_config_service_js__rspack_import_7/* .EmailConfigService */.m,
|
|
1281
|
+
typeof _email_datasource_provider_js__rspack_import_8/* .EmailDataSourceProvider */.Z === "undefined" ? Object : _email_datasource_provider_js__rspack_import_8/* .EmailDataSourceProvider */.Z
|
|
1229
1282
|
])
|
|
1230
1283
|
], EmailProviderConfigService);
|
|
1231
1284
|
|
|
@@ -1244,7 +1297,7 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
1244
1297
|
/* import */ var _config_index_js__rspack_import_10 = __webpack_require__(399);
|
|
1245
1298
|
/* import */ var _email_config_service_js__rspack_import_4 = __webpack_require__(843);
|
|
1246
1299
|
/* import */ var _dtos_index_js__rspack_import_5 = __webpack_require__(684);
|
|
1247
|
-
/* import */ var _providers_index_js__rspack_import_6 = __webpack_require__(
|
|
1300
|
+
/* import */ var _providers_index_js__rspack_import_6 = __webpack_require__(652);
|
|
1248
1301
|
/* import */ var _email_provider_config_service_js__rspack_import_7 = __webpack_require__(105);
|
|
1249
1302
|
/* import */ var _email_template_service_js__rspack_import_8 = __webpack_require__(759);
|
|
1250
1303
|
function _ts_decorate(decorators, target, key, desc) {
|
|
@@ -1413,13 +1466,13 @@ class EmailSendService {
|
|
|
1413
1466
|
messageKey: _config_index_js__rspack_import_9/* .EMAIL_SEND_MESSAGES.TEMPLATE_NOT_FOUND */.dP.TEMPLATE_NOT_FOUND
|
|
1414
1467
|
});
|
|
1415
1468
|
}
|
|
1469
|
+
(0,_flusys_nestjs_shared_utils__rspack_import_2.validateCompanyOwnership)(template, user, this.emailConfigService.isCompanyFeatureEnabled(), 'Email template');
|
|
1416
1470
|
if (!template.isActive) {
|
|
1417
1471
|
throw new _nestjs_common__rspack_import_3.BadRequestException({
|
|
1418
1472
|
message: 'Email template is inactive',
|
|
1419
1473
|
messageKey: _config_index_js__rspack_import_9/* .EMAIL_SEND_MESSAGES.TEMPLATE_INACTIVE */.dP.TEMPLATE_INACTIVE
|
|
1420
1474
|
});
|
|
1421
1475
|
}
|
|
1422
|
-
(0,_flusys_nestjs_shared_utils__rspack_import_2.validateCompanyOwnership)(template, user, this.emailConfigService.isCompanyFeatureEnabled(), 'Email template');
|
|
1423
1476
|
return template;
|
|
1424
1477
|
}
|
|
1425
1478
|
buildTemplateContent(template, variables) {
|
|
@@ -1456,7 +1509,11 @@ class EmailSendService {
|
|
|
1456
1509
|
if (!variables || Object.keys(variables).length === 0) return content;
|
|
1457
1510
|
const safeVars = escapeHtml ? (0,_flusys_nestjs_shared_utils__rspack_import_2.escapeHtmlVariables)(variables) : variables;
|
|
1458
1511
|
return content.replace(VARIABLE_PATTERN, (match, varName)=>{
|
|
1459
|
-
|
|
1512
|
+
if (!Object.hasOwn(safeVars, varName)) return match;
|
|
1513
|
+
const value = safeVars[varName];
|
|
1514
|
+
if (value === undefined) return match;
|
|
1515
|
+
if (value === null) return '';
|
|
1516
|
+
return typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
1460
1517
|
});
|
|
1461
1518
|
}
|
|
1462
1519
|
buildTestEmailHtml(safeConfig) {
|
|
@@ -1559,10 +1616,11 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
1559
1616
|
/* import */ var _flusys_nestjs_shared_modules__rspack_import_1 = __webpack_require__(236);
|
|
1560
1617
|
/* import */ var _flusys_nestjs_shared_utils__rspack_import_2 = __webpack_require__(216);
|
|
1561
1618
|
/* import */ var _nestjs_common__rspack_import_3 = __webpack_require__(157);
|
|
1562
|
-
/* import */ var
|
|
1619
|
+
/* import */ var _config_index_js__rspack_import_8 = __webpack_require__(399);
|
|
1563
1620
|
/* import */ var _entities_index_js__rspack_import_4 = __webpack_require__(454);
|
|
1564
|
-
/* import */ var
|
|
1565
|
-
/* import */ var
|
|
1621
|
+
/* import */ var _utils_company_ownership_util_js__rspack_import_5 = __webpack_require__(561);
|
|
1622
|
+
/* import */ var _email_config_service_js__rspack_import_6 = __webpack_require__(843);
|
|
1623
|
+
/* import */ var _email_datasource_provider_js__rspack_import_7 = __webpack_require__(522);
|
|
1566
1624
|
function _ts_decorate(decorators, target, key, desc) {
|
|
1567
1625
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
1568
1626
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
|
|
@@ -1594,6 +1652,7 @@ function _ts_param(paramIndex, decorator) {
|
|
|
1594
1652
|
|
|
1595
1653
|
|
|
1596
1654
|
|
|
1655
|
+
|
|
1597
1656
|
const DEFAULT_SELECT_FIELDS = [
|
|
1598
1657
|
'id',
|
|
1599
1658
|
'name',
|
|
@@ -1614,7 +1673,17 @@ class EmailTemplateService extends _flusys_nestjs_shared_classes__rspack_import_
|
|
|
1614
1673
|
utilsService;
|
|
1615
1674
|
emailConfig;
|
|
1616
1675
|
constructor(cacheManager, utilsService, emailConfig, dataSourceProvider){
|
|
1617
|
-
super(
|
|
1676
|
+
super(_config_index_js__rspack_import_8/* .EMAIL_EVENT_ENTITIES.TEMPLATE */.y.TEMPLATE, cacheManager, utilsService, EmailTemplateService.name, true, (/* inlined export .EMAIL_EVENT_MODULE */"email"), dataSourceProvider, emailConfig.isCompanyFeatureEnabled() ? _entities_index_js__rspack_import_4/* .EmailTemplateWithCompany */.U7 : _entities_index_js__rspack_import_4/* .EmailTemplate */.Fy), this.cacheManager = cacheManager, this.utilsService = utilsService, this.emailConfig = emailConfig;
|
|
1677
|
+
}
|
|
1678
|
+
async beforeUpdateOperation(dto, user) {
|
|
1679
|
+
await this.assertInUserCompany((Array.isArray(dto) ? dto : [
|
|
1680
|
+
dto
|
|
1681
|
+
]).map((d)=>d.id), user);
|
|
1682
|
+
}
|
|
1683
|
+
async beforeDeleteOperation(dto, user, _queryRunner) {
|
|
1684
|
+
await this.assertInUserCompany(Array.isArray(dto.id) ? dto.id : [
|
|
1685
|
+
dto.id
|
|
1686
|
+
], user);
|
|
1618
1687
|
}
|
|
1619
1688
|
async convertSingleDtoToEntity(dto, user) {
|
|
1620
1689
|
const entity = await super.convertSingleDtoToEntity(dto, user);
|
|
@@ -1624,7 +1693,7 @@ class EmailTemplateService extends _flusys_nestjs_shared_classes__rspack_import_
|
|
|
1624
1693
|
}
|
|
1625
1694
|
return entity;
|
|
1626
1695
|
}
|
|
1627
|
-
async getSelectQuery(query,
|
|
1696
|
+
async getSelectQuery(query, user, select) {
|
|
1628
1697
|
if (!select?.length) {
|
|
1629
1698
|
select = [
|
|
1630
1699
|
...DEFAULT_SELECT_FIELDS
|
|
@@ -1632,6 +1701,10 @@ class EmailTemplateService extends _flusys_nestjs_shared_classes__rspack_import_
|
|
|
1632
1701
|
if (this.emailConfig.isCompanyFeatureEnabled()) select.push('companyId');
|
|
1633
1702
|
}
|
|
1634
1703
|
query.select(select.map((field)=>`${this.entityName}.${field}`));
|
|
1704
|
+
(0,_flusys_nestjs_shared_utils__rspack_import_2.applyCompanyFilter)(query, {
|
|
1705
|
+
isCompanyFeatureEnabled: this.emailConfig.isCompanyFeatureEnabled(),
|
|
1706
|
+
entityAlias: this.entityName
|
|
1707
|
+
}, user);
|
|
1635
1708
|
return {
|
|
1636
1709
|
query,
|
|
1637
1710
|
isRaw: false
|
|
@@ -1639,10 +1712,6 @@ class EmailTemplateService extends _flusys_nestjs_shared_classes__rspack_import_
|
|
|
1639
1712
|
}
|
|
1640
1713
|
async getExtraManipulateQuery(query, filterDto, user) {
|
|
1641
1714
|
const result = await super.getExtraManipulateQuery(query, filterDto, user);
|
|
1642
|
-
(0,_flusys_nestjs_shared_utils__rspack_import_2.applyCompanyFilter)(query, {
|
|
1643
|
-
isCompanyFeatureEnabled: this.emailConfig.isCompanyFeatureEnabled(),
|
|
1644
|
-
entityAlias: 'emailTemplate'
|
|
1645
|
-
}, user);
|
|
1646
1715
|
query.orderBy(`${this.entityName}.createdAt`, 'DESC');
|
|
1647
1716
|
return result;
|
|
1648
1717
|
}
|
|
@@ -1666,6 +1735,10 @@ class EmailTemplateService extends _flusys_nestjs_shared_classes__rspack_import_
|
|
|
1666
1735
|
});
|
|
1667
1736
|
}
|
|
1668
1737
|
// ─── Private Helpers ────────────────────────────────────────────────────────
|
|
1738
|
+
async assertInUserCompany(ids, user) {
|
|
1739
|
+
await this.ensureDataSourceRepository();
|
|
1740
|
+
await (0,_utils_company_ownership_util_js__rspack_import_5/* .assertRowsInUserCompany */.p)(this.repository, ids.filter(Boolean), user, this.emailConfig.isCompanyFeatureEnabled(), 'Email template');
|
|
1741
|
+
}
|
|
1669
1742
|
async incrementSchemaVersionIfChanged(dto, entity) {
|
|
1670
1743
|
const updateDto = dto;
|
|
1671
1744
|
if (!updateDto.id || !dto.schema) return;
|
|
@@ -1686,14 +1759,14 @@ EmailTemplateService = _ts_decorate([
|
|
|
1686
1759
|
}),
|
|
1687
1760
|
_ts_param(0, (0,_nestjs_common__rspack_import_3.Inject)('CACHE_INSTANCE')),
|
|
1688
1761
|
_ts_param(1, (0,_nestjs_common__rspack_import_3.Inject)(_flusys_nestjs_shared_modules__rspack_import_1.UtilsService)),
|
|
1689
|
-
_ts_param(2, (0,_nestjs_common__rspack_import_3.Inject)(
|
|
1690
|
-
_ts_param(3, (0,_nestjs_common__rspack_import_3.Inject)(
|
|
1762
|
+
_ts_param(2, (0,_nestjs_common__rspack_import_3.Inject)(_email_config_service_js__rspack_import_6/* .EmailConfigService */.m)),
|
|
1763
|
+
_ts_param(3, (0,_nestjs_common__rspack_import_3.Inject)(_email_datasource_provider_js__rspack_import_7/* .EmailDataSourceProvider */.Z)),
|
|
1691
1764
|
_ts_metadata("design:type", Function),
|
|
1692
1765
|
_ts_metadata("design:paramtypes", [
|
|
1693
1766
|
typeof _flusys_nestjs_shared_classes__rspack_import_0.HybridCache === "undefined" ? Object : _flusys_nestjs_shared_classes__rspack_import_0.HybridCache,
|
|
1694
1767
|
typeof _flusys_nestjs_shared_modules__rspack_import_1.UtilsService === "undefined" ? Object : _flusys_nestjs_shared_modules__rspack_import_1.UtilsService,
|
|
1695
|
-
typeof
|
|
1696
|
-
typeof
|
|
1768
|
+
typeof _email_config_service_js__rspack_import_6/* .EmailConfigService */.m === "undefined" ? Object : _email_config_service_js__rspack_import_6/* .EmailConfigService */.m,
|
|
1769
|
+
typeof _email_datasource_provider_js__rspack_import_7/* .EmailDataSourceProvider */.Z === "undefined" ? Object : _email_datasource_provider_js__rspack_import_7/* .EmailDataSourceProvider */.Z
|
|
1697
1770
|
])
|
|
1698
1771
|
], EmailTemplateService);
|
|
1699
1772
|
|
|
@@ -1702,6 +1775,43 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
1702
1775
|
});
|
|
1703
1776
|
|
|
1704
1777
|
|
|
1778
|
+
},
|
|
1779
|
+
561(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
|
|
1780
|
+
/* import */ var _flusys_nestjs_shared_constants__rspack_import_0 = __webpack_require__(140);
|
|
1781
|
+
/* import */ var _nestjs_common__rspack_import_1 = __webpack_require__(157);
|
|
1782
|
+
/* import */ var typeorm__rspack_import_2 = __webpack_require__(158);
|
|
1783
|
+
|
|
1784
|
+
|
|
1785
|
+
|
|
1786
|
+
/**
|
|
1787
|
+
* ApiService write paths load rows by id alone, so a caller could update or delete another company's
|
|
1788
|
+
* row by its id. Every row must belong to the caller's company - a caller without a company may only
|
|
1789
|
+
* touch rows without one.
|
|
1790
|
+
*/ async function assertRowsInUserCompany(repository, ids, user, isCompanyFeatureEnabled, entityName) {
|
|
1791
|
+
if (!isCompanyFeatureEnabled || !user || !ids.length) return;
|
|
1792
|
+
const rows = await repository.find({
|
|
1793
|
+
where: {
|
|
1794
|
+
id: (0,typeorm__rspack_import_2.In)(ids)
|
|
1795
|
+
},
|
|
1796
|
+
withDeleted: true
|
|
1797
|
+
});
|
|
1798
|
+
const userCompanyId = user.companyId ?? null;
|
|
1799
|
+
if (rows.some((row)=>(row.companyId ?? null) !== userCompanyId)) {
|
|
1800
|
+
throw new _nestjs_common__rspack_import_1.ForbiddenException({
|
|
1801
|
+
message: `${entityName} belongs to another company`,
|
|
1802
|
+
messageKey: _flusys_nestjs_shared_constants__rspack_import_0.AUTH_MESSAGES.ENTITY_BELOG_ANOTHER_COMPANY,
|
|
1803
|
+
messageVariables: {
|
|
1804
|
+
entity: entityName
|
|
1805
|
+
}
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
__webpack_require__.d(__webpack_exports__, {
|
|
1811
|
+
p: () => (assertRowsInUserCompany)
|
|
1812
|
+
});
|
|
1813
|
+
|
|
1814
|
+
|
|
1705
1815
|
},
|
|
1706
1816
|
422(module) {
|
|
1707
1817
|
|
|
@@ -1714,6 +1824,12 @@ module.exports = __rspack_external__flusys_nestjs_shared_393d2cae;
|
|
|
1714
1824
|
module.exports = __rspack_external__flusys_nestjs_shared_classes_6076a329;
|
|
1715
1825
|
|
|
1716
1826
|
|
|
1827
|
+
},
|
|
1828
|
+
140(module) {
|
|
1829
|
+
|
|
1830
|
+
module.exports = __rspack_external__flusys_nestjs_shared_constants_81f33d5b;
|
|
1831
|
+
|
|
1832
|
+
|
|
1717
1833
|
},
|
|
1718
1834
|
236(module) {
|
|
1719
1835
|
|
package/fesm/261.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { IdentityResponseDto } from "@flusys/nestjs-shared/dtos";
|
|
2
2
|
import { Expose, Type } from "class-transformer";
|
|
3
|
-
import { IsArray, IsBoolean, IsEmail, IsEnum, IsNotEmpty, IsObject, IsOptional, IsString, IsUUID, MaxLength, ValidateNested, ValidatorConstraint, registerDecorator } from "class-validator";
|
|
3
|
+
import { IsArray, IsBoolean, IsEmail, IsEnum, IsNotEmpty, IsObject, IsOptional, IsString, IsUUID, MaxLength, ValidateIf, ValidateNested, ValidatorConstraint, registerDecorator } from "class-validator";
|
|
4
4
|
import * as __rspack_external__nestjs_swagger_7cd450b5 from "@nestjs/swagger";
|
|
5
5
|
export const __rspack_esm_id = 261;
|
|
6
6
|
export const __rspack_esm_ids = [261];
|
|
@@ -692,7 +692,7 @@ email_send_dto_ts_decorate([
|
|
|
692
692
|
description: 'Template ID (required if templateSlug not provided)'
|
|
693
693
|
}),
|
|
694
694
|
IsUUID(),
|
|
695
|
-
|
|
695
|
+
ValidateIf((dto)=>dto.templateId != null || !dto.templateSlug),
|
|
696
696
|
RequireTemplateIdOrSlug({
|
|
697
697
|
message: 'Either templateId or templateSlug must be provided'
|
|
698
698
|
}),
|
package/fesm/index.js
CHANGED
|
@@ -552,8 +552,8 @@ var config = __webpack_require__(249);
|
|
|
552
552
|
var dtos = __webpack_require__(684);
|
|
553
553
|
// EXTERNAL MODULE: ./projects/nestjs-email/src/enums/index.ts
|
|
554
554
|
var enums = __webpack_require__(315);
|
|
555
|
-
// EXTERNAL MODULE: ./projects/nestjs-email/src/providers/index.ts +
|
|
556
|
-
var providers = __webpack_require__(
|
|
555
|
+
// EXTERNAL MODULE: ./projects/nestjs-email/src/providers/index.ts + 6 modules
|
|
556
|
+
var providers = __webpack_require__(652);
|
|
557
557
|
// EXTERNAL MODULE: external "@flusys/nestjs-shared/utils"
|
|
558
558
|
var utils_ = __webpack_require__(216);
|
|
559
559
|
;// CONCATENATED MODULE: ./projects/nestjs-email/src/utils/email-templates.util.ts
|
|
@@ -654,8 +654,8 @@ var email_constants = __webpack_require__(966);
|
|
|
654
654
|
var controllers = __webpack_require__(574);
|
|
655
655
|
// EXTERNAL MODULE: ./projects/nestjs-email/src/enums/email-provider-type.enum.ts
|
|
656
656
|
var email_provider_type_enum = __webpack_require__(823);
|
|
657
|
-
// EXTERNAL MODULE: ./projects/nestjs-email/src/providers/index.ts +
|
|
658
|
-
var src_providers = __webpack_require__(
|
|
657
|
+
// EXTERNAL MODULE: ./projects/nestjs-email/src/providers/index.ts + 6 modules
|
|
658
|
+
var src_providers = __webpack_require__(652);
|
|
659
659
|
// EXTERNAL MODULE: ./projects/nestjs-email/src/services/index.ts
|
|
660
660
|
var services = __webpack_require__(95);
|
|
661
661
|
;// CONCATENATED MODULE: ./projects/nestjs-email/src/modules/email.module.ts
|
package/fesm/modules/index.js
CHANGED
|
@@ -473,8 +473,8 @@ var email_constants = __webpack_require__(966);
|
|
|
473
473
|
var controllers = __webpack_require__(574);
|
|
474
474
|
// EXTERNAL MODULE: ./projects/nestjs-email/src/enums/email-provider-type.enum.ts
|
|
475
475
|
var email_provider_type_enum = __webpack_require__(823);
|
|
476
|
-
// EXTERNAL MODULE: ./projects/nestjs-email/src/providers/index.ts +
|
|
477
|
-
var src_providers = __webpack_require__(
|
|
476
|
+
// EXTERNAL MODULE: ./projects/nestjs-email/src/providers/index.ts + 6 modules
|
|
477
|
+
var src_providers = __webpack_require__(652);
|
|
478
478
|
// EXTERNAL MODULE: ./projects/nestjs-email/src/services/index.ts
|
|
479
479
|
var services = __webpack_require__(95);
|
|
480
480
|
;// CONCATENATED MODULE: ./projects/nestjs-email/src/modules/email.module.ts
|
package/fesm/providers/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "crypto";
|
|
2
|
-
import { SYSTEM_MESSAGES } from "@flusys/nestjs-shared/constants";
|
|
3
2
|
import * as __rspack_external__flusys_nestjs_shared_393d2cae from "@flusys/nestjs-shared";
|
|
3
|
+
import * as __rspack_external__flusys_nestjs_shared_constants_81f33d5b from "@flusys/nestjs-shared/constants";
|
|
4
4
|
import * as __rspack_external__flusys_nestjs_shared_utils_9125c1f1 from "@flusys/nestjs-shared/utils";
|
|
5
5
|
import * as __rspack_external__nestjs_common_a04939da from "@nestjs/common";
|
|
6
6
|
export const __rspack_esm_id = 467;
|
|
@@ -35,7 +35,7 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
},
|
|
38
|
-
|
|
38
|
+
652(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
|
|
39
39
|
|
|
40
40
|
// EXPORTS
|
|
41
41
|
__webpack_require__.d(__webpack_exports__, {
|
|
@@ -98,13 +98,21 @@ function _ts_metadata(metadataKey, metadataValue) {
|
|
|
98
98
|
|
|
99
99
|
|
|
100
100
|
|
|
101
|
+
const MAX_CACHED_PROVIDERS = 50;
|
|
102
|
+
const PROVIDER_IDLE_TTL_MS = 30 * 60 * 1000;
|
|
101
103
|
class EmailFactoryService {
|
|
102
104
|
providerCache = new Map();
|
|
103
105
|
// ─── Public API ─────────────────────────────────────────────────────────────
|
|
104
106
|
async createProvider(config) {
|
|
105
107
|
const cacheKey = this.generateCacheKey(config);
|
|
108
|
+
const now = Date.now();
|
|
106
109
|
const cached = this.providerCache.get(cacheKey);
|
|
107
|
-
if (cached)
|
|
110
|
+
if (cached) {
|
|
111
|
+
cached.lastUsed = now;
|
|
112
|
+
this.providerCache.delete(cacheKey);
|
|
113
|
+
this.providerCache.set(cacheKey, cached);
|
|
114
|
+
return cached.provider;
|
|
115
|
+
}
|
|
108
116
|
const ProviderClass = EmailProviderRegistry.get(config.provider);
|
|
109
117
|
if (!ProviderClass) {
|
|
110
118
|
throw new common_.NotFoundException({
|
|
@@ -116,10 +124,32 @@ class EmailFactoryService {
|
|
|
116
124
|
}
|
|
117
125
|
});
|
|
118
126
|
}
|
|
127
|
+
// The pending promise is cached so concurrent first sends share one client (one SMTP transport)
|
|
128
|
+
const entry = {
|
|
129
|
+
provider: this.instantiate(ProviderClass, config),
|
|
130
|
+
lastUsed: now
|
|
131
|
+
};
|
|
132
|
+
this.providerCache.set(cacheKey, entry);
|
|
133
|
+
entry.provider.catch(()=>{
|
|
134
|
+
if (this.providerCache.get(cacheKey) === entry) this.providerCache.delete(cacheKey);
|
|
135
|
+
});
|
|
136
|
+
this.evict(now);
|
|
137
|
+
return entry.provider;
|
|
138
|
+
}
|
|
139
|
+
async clearCache() {
|
|
140
|
+
const entries = Array.from(this.providerCache.values());
|
|
141
|
+
this.providerCache.clear();
|
|
142
|
+
await Promise.allSettled(entries.map((entry)=>this.closeEntry(entry)));
|
|
143
|
+
}
|
|
144
|
+
// ─── Lifecycle ──────────────────────────────────────────────────────────────
|
|
145
|
+
async onModuleDestroy() {
|
|
146
|
+
await this.clearCache();
|
|
147
|
+
}
|
|
148
|
+
// ─── Private Helpers ────────────────────────────────────────────────────────
|
|
149
|
+
async instantiate(ProviderClass, config) {
|
|
119
150
|
try {
|
|
120
151
|
const instance = new ProviderClass();
|
|
121
152
|
await instance.initialize?.(config.config);
|
|
122
|
-
this.providerCache.set(cacheKey, instance);
|
|
123
153
|
return instance;
|
|
124
154
|
} catch (error) {
|
|
125
155
|
throw new common_.InternalServerErrorException({
|
|
@@ -132,26 +162,35 @@ class EmailFactoryService {
|
|
|
132
162
|
});
|
|
133
163
|
}
|
|
134
164
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
generateCacheKey(config) {
|
|
143
|
-
const sortedKeys = Object.keys(config.config || {}).sort();
|
|
144
|
-
const configString = JSON.stringify(config.config, sortedKeys);
|
|
145
|
-
const hash = createHash('sha256').update(configString).digest('hex').substring(0, 16);
|
|
146
|
-
return `${config.provider}-${hash}`;
|
|
165
|
+
evict(now) {
|
|
166
|
+
for (const [key, entry] of this.providerCache){
|
|
167
|
+
const overCapacity = this.providerCache.size > MAX_CACHED_PROVIDERS;
|
|
168
|
+
if (!overCapacity && now - entry.lastUsed < PROVIDER_IDLE_TTL_MS) continue;
|
|
169
|
+
this.providerCache.delete(key);
|
|
170
|
+
void this.closeEntry(entry);
|
|
171
|
+
}
|
|
147
172
|
}
|
|
148
|
-
async
|
|
149
|
-
if (!provider.close) return;
|
|
173
|
+
async closeEntry(entry) {
|
|
150
174
|
try {
|
|
151
|
-
await provider
|
|
175
|
+
const provider = await entry.provider;
|
|
176
|
+
await provider.close?.();
|
|
152
177
|
} catch {
|
|
153
|
-
//
|
|
178
|
+
// A client that failed to initialize or to close has nothing left to release
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
generateCacheKey(config) {
|
|
182
|
+
const hash = createHash('sha256').update(this.stableStringify(config.config ?? {})).digest('hex');
|
|
183
|
+
return `${config.provider}:${hash}`;
|
|
184
|
+
}
|
|
185
|
+
stableStringify(value) {
|
|
186
|
+
if (value === null || value === undefined) return 'null';
|
|
187
|
+
if (Array.isArray(value)) return `[${value.map((item)=>this.stableStringify(item)).join(',')}]`;
|
|
188
|
+
if (typeof value === 'object') {
|
|
189
|
+
const record = value;
|
|
190
|
+
const entries = Object.keys(record).filter((key)=>record[key] !== undefined).sort().map((key)=>`${JSON.stringify(key)}:${this.stableStringify(record[key])}`);
|
|
191
|
+
return `{${entries.join(',')}}`;
|
|
154
192
|
}
|
|
193
|
+
return JSON.stringify(value);
|
|
155
194
|
}
|
|
156
195
|
extractErrorMessage(error) {
|
|
157
196
|
return error instanceof Error ? error.message : 'Unknown error';
|
|
@@ -248,7 +287,8 @@ class SmtpProvider {
|
|
|
248
287
|
}
|
|
249
288
|
// ─── Private Helpers ────────────────────────────────────────────────────────
|
|
250
289
|
formatFrom(from, fromName) {
|
|
251
|
-
|
|
290
|
+
if (!from) return undefined;
|
|
291
|
+
return fromName ? `"${fromName.replace(/["\\]/g, '\\$&')}" <${from}>` : from;
|
|
252
292
|
}
|
|
253
293
|
joinAddresses(addresses) {
|
|
254
294
|
return Array.isArray(addresses) ? addresses.join(', ') : addresses;
|
|
@@ -272,8 +312,8 @@ class SmtpProvider {
|
|
|
272
312
|
}
|
|
273
313
|
}
|
|
274
314
|
|
|
275
|
-
|
|
276
|
-
|
|
315
|
+
// EXTERNAL MODULE: external "@flusys/nestjs-shared/constants"
|
|
316
|
+
var constants_ = __webpack_require__(140);
|
|
277
317
|
// EXTERNAL MODULE: external "@flusys/nestjs-shared/utils"
|
|
278
318
|
var utils_ = __webpack_require__(216);
|
|
279
319
|
;// CONCATENATED MODULE: ./projects/nestjs-email/src/providers/sendgrid-provider.ts
|
|
@@ -288,12 +328,13 @@ class SendGridProvider {
|
|
|
288
328
|
try {
|
|
289
329
|
// Optional peer dependency - loaded at runtime so bundlers don't statically resolve it
|
|
290
330
|
const sgMailModule = await (0,utils_.loadOptionalModule)('@sendgrid/mail');
|
|
291
|
-
|
|
331
|
+
const MailService = sgMailModule.MailService ?? sgMailModule.default?.MailService;
|
|
332
|
+
this.sgMail = MailService ? new MailService() : sgMailModule.default || sgMailModule;
|
|
292
333
|
this.sgMail.setApiKey(config.apiKey);
|
|
293
334
|
} catch {
|
|
294
335
|
throw new common_.InternalServerErrorException({
|
|
295
336
|
message: 'SendGrid initialization failed. Install: npm install @sendgrid/mail',
|
|
296
|
-
messageKey: SYSTEM_MESSAGES.SDK_NOT_INSTALLED,
|
|
337
|
+
messageKey: constants_.SYSTEM_MESSAGES.SDK_NOT_INSTALLED,
|
|
297
338
|
messageVariables: {
|
|
298
339
|
sdk: 'sendgrid'
|
|
299
340
|
}
|
|
@@ -308,7 +349,8 @@ class SendGridProvider {
|
|
|
308
349
|
try {
|
|
309
350
|
const msg = this.buildMessage(options, true);
|
|
310
351
|
const [response] = await this.sgMail.send(msg);
|
|
311
|
-
const
|
|
352
|
+
const headers = response?.headers ?? {};
|
|
353
|
+
const messageId = headers['x-message-id'] || headers['X-Message-Id'];
|
|
312
354
|
return {
|
|
313
355
|
success: true,
|
|
314
356
|
messageId
|
|
@@ -349,7 +391,7 @@ class SendGridProvider {
|
|
|
349
391
|
buildMessage(options, includeExtras) {
|
|
350
392
|
const msg = {
|
|
351
393
|
to: this.toArray(options.to),
|
|
352
|
-
from: options.fromName ? {
|
|
394
|
+
from: options.fromName && options.from ? {
|
|
353
395
|
email: options.from,
|
|
354
396
|
name: options.fromName
|
|
355
397
|
} : options.from,
|
|
@@ -410,7 +452,7 @@ class MailgunProvider {
|
|
|
410
452
|
} catch {
|
|
411
453
|
throw new common_.InternalServerErrorException({
|
|
412
454
|
message: 'Mailgun initialization failed. Install: npm install mailgun.js form-data',
|
|
413
|
-
messageKey: SYSTEM_MESSAGES.SDK_NOT_INSTALLED,
|
|
455
|
+
messageKey: constants_.SYSTEM_MESSAGES.SDK_NOT_INSTALLED,
|
|
414
456
|
messageVariables: {
|
|
415
457
|
sdk: 'mailgun'
|
|
416
458
|
}
|
|
@@ -456,7 +498,7 @@ class MailgunProvider {
|
|
|
456
498
|
// ─── Private Helpers ────────────────────────────────────────────────────────
|
|
457
499
|
buildMessageData(options) {
|
|
458
500
|
const data = {
|
|
459
|
-
from: options.fromName ?
|
|
501
|
+
from: options.fromName && options.from ? `"${options.fromName.replace(/["\\]/g, '\\$&')}" <${options.from}>` : options.from,
|
|
460
502
|
to: this.joinAddresses(options.to),
|
|
461
503
|
subject: options.subject
|
|
462
504
|
};
|
|
@@ -496,6 +538,12 @@ class MailgunProvider {
|
|
|
496
538
|
module.exports = __rspack_external__flusys_nestjs_shared_393d2cae;
|
|
497
539
|
|
|
498
540
|
|
|
541
|
+
},
|
|
542
|
+
140(module) {
|
|
543
|
+
|
|
544
|
+
module.exports = __rspack_external__flusys_nestjs_shared_constants_81f33d5b;
|
|
545
|
+
|
|
546
|
+
|
|
499
547
|
},
|
|
500
548
|
216(module) {
|
|
501
549
|
|
|
@@ -515,7 +563,7 @@ import { __webpack_require__ } from '../runtime.js';
|
|
|
515
563
|
var __webpack_exec__ = function(moduleId) { return __webpack_require__(__webpack_require__.s = moduleId); }
|
|
516
564
|
import * as __rspack_chunk_1 from './index.js';
|
|
517
565
|
__webpack_require__.C(__rspack_chunk_1);
|
|
518
|
-
var __webpack_exports__ = __webpack_exec__(
|
|
566
|
+
var __webpack_exports__ = __webpack_exec__(652);var __webpack_exports__EmailFactoryService = __webpack_exports__.c;
|
|
519
567
|
var __webpack_exports__EmailProviderRegistry = __webpack_exports__.xC;
|
|
520
568
|
var __webpack_exports__MailgunProvider = __webpack_exports__.Xp;
|
|
521
569
|
var __webpack_exports__SendGridProvider = __webpack_exports__.Iw;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flusys/nestjs-email",
|
|
3
|
-
"version": "9.1
|
|
3
|
+
"version": "9.2.1",
|
|
4
4
|
"description": "Modular email package with SMTP, SendGrid, and Mailgun providers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./fesm/index.js",
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
}
|
|
99
99
|
},
|
|
100
100
|
"dependencies": {
|
|
101
|
-
"@flusys/nestjs-core": "9.1
|
|
102
|
-
"@flusys/nestjs-shared": "9.1
|
|
101
|
+
"@flusys/nestjs-core": "9.2.1",
|
|
102
|
+
"@flusys/nestjs-shared": "9.2.1"
|
|
103
103
|
}
|
|
104
104
|
}
|
|
@@ -3,8 +3,12 @@ import { type IEmailProvider, type IEmailProviderConfig } from '../interfaces/in
|
|
|
3
3
|
export declare class EmailFactoryService implements OnModuleDestroy {
|
|
4
4
|
private readonly providerCache;
|
|
5
5
|
createProvider(config: IEmailProviderConfig): Promise<IEmailProvider>;
|
|
6
|
+
clearCache(): Promise<void>;
|
|
6
7
|
onModuleDestroy(): Promise<void>;
|
|
8
|
+
private instantiate;
|
|
9
|
+
private evict;
|
|
10
|
+
private closeEntry;
|
|
7
11
|
private generateCacheKey;
|
|
8
|
-
private
|
|
12
|
+
private stableStringify;
|
|
9
13
|
private extractErrorMessage;
|
|
10
14
|
}
|
|
@@ -13,9 +13,10 @@ export declare class EmailProviderConfigService extends ApiService<CreateEmailCo
|
|
|
13
13
|
protected utilsService: UtilsService;
|
|
14
14
|
private readonly emailConfig;
|
|
15
15
|
constructor(cacheManager: HybridCache, utilsService: UtilsService, emailConfig: EmailConfigService, dataSourceProvider: EmailDataSourceProvider);
|
|
16
|
-
protected
|
|
16
|
+
protected beforeUpdateOperation(dto: UpdateEmailConfigDto | UpdateEmailConfigDto[], user: ILoggedUserInfo | null): Promise<void>;
|
|
17
|
+
protected beforeDeleteOperation(dto: DeleteDto, user: ILoggedUserInfo | null, _queryRunner: QueryRunner): Promise<void>;
|
|
17
18
|
convertSingleDtoToEntity(dto: CreateEmailConfigDto | UpdateEmailConfigDto, user: ILoggedUserInfo | null): Promise<EmailConfigBase>;
|
|
18
|
-
getSelectQuery(query: SelectQueryBuilder<EmailConfigBase>,
|
|
19
|
+
getSelectQuery(query: SelectQueryBuilder<EmailConfigBase>, user: ILoggedUserInfo | null, select?: string[]): Promise<{
|
|
19
20
|
query: SelectQueryBuilder<EmailConfig>;
|
|
20
21
|
isRaw: boolean;
|
|
21
22
|
}>;
|
|
@@ -25,4 +26,5 @@ export declare class EmailProviderConfigService extends ApiService<CreateEmailCo
|
|
|
25
26
|
}>;
|
|
26
27
|
findByIdDirect(id: string): Promise<EmailConfigBase | null>;
|
|
27
28
|
getDefaultConfig(user?: ILoggedUserInfo): Promise<EmailConfigBase | null>;
|
|
29
|
+
private assertInUserCompany;
|
|
28
30
|
}
|
|
@@ -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 { type ILoggedUserInfo } from '@flusys/nestjs-shared/interfaces';
|
|
4
4
|
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
5
|
-
import { SelectQueryBuilder } from 'typeorm';
|
|
5
|
+
import { type QueryRunner, SelectQueryBuilder } from 'typeorm';
|
|
6
6
|
import { CreateEmailTemplateDto, UpdateEmailTemplateDto } from '../dtos/index.js';
|
|
7
7
|
import { EmailTemplate, EmailTemplateBase } from '../entities/index.js';
|
|
8
8
|
import { type IEmailTemplate } from '../interfaces/index.js';
|
|
@@ -13,8 +13,10 @@ export declare class EmailTemplateService extends ApiService<CreateEmailTemplate
|
|
|
13
13
|
protected utilsService: UtilsService;
|
|
14
14
|
private readonly emailConfig;
|
|
15
15
|
constructor(cacheManager: HybridCache, utilsService: UtilsService, emailConfig: EmailConfigService, dataSourceProvider: EmailDataSourceProvider);
|
|
16
|
+
protected beforeUpdateOperation(dto: UpdateEmailTemplateDto | UpdateEmailTemplateDto[], user: ILoggedUserInfo | null): Promise<void>;
|
|
17
|
+
protected beforeDeleteOperation(dto: DeleteDto, user: ILoggedUserInfo | null, _queryRunner: QueryRunner): Promise<void>;
|
|
16
18
|
convertSingleDtoToEntity(dto: CreateEmailTemplateDto | UpdateEmailTemplateDto, user: ILoggedUserInfo | null): Promise<EmailTemplateBase>;
|
|
17
|
-
getSelectQuery(query: SelectQueryBuilder<EmailTemplateBase>,
|
|
19
|
+
getSelectQuery(query: SelectQueryBuilder<EmailTemplateBase>, user: ILoggedUserInfo | null, select?: string[]): Promise<{
|
|
18
20
|
query: SelectQueryBuilder<EmailTemplate>;
|
|
19
21
|
isRaw: boolean;
|
|
20
22
|
}>;
|
|
@@ -24,5 +26,6 @@ export declare class EmailTemplateService extends ApiService<CreateEmailTemplate
|
|
|
24
26
|
}>;
|
|
25
27
|
findByIdDirect(id: string): Promise<EmailTemplateBase | null>;
|
|
26
28
|
findBySlug(slug: string, user?: ILoggedUserInfo): Promise<EmailTemplateBase | null>;
|
|
29
|
+
private assertInUserCompany;
|
|
27
30
|
private incrementSchemaVersionIfChanged;
|
|
28
31
|
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type ILoggedUserInfo } from '@flusys/nestjs-shared/interfaces';
|
|
2
|
+
import { type ObjectLiteral, type Repository } from 'typeorm';
|
|
3
|
+
export declare function assertRowsInUserCompany<T extends ObjectLiteral>(repository: Repository<T>, ids: string[], user: ILoggedUserInfo | null | undefined, isCompanyFeatureEnabled: boolean, entityName: string): Promise<void>;
|