fiberx-backend-toolkit 0.0.78 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/code_templates/email_enqueue_code_template.js +2 -0
- package/dist/config/constants.d.ts +1 -0
- package/dist/config/constants.js +2 -1
- package/dist/mailer/processors/email_delivery_processor.d.ts +17 -0
- package/dist/mailer/processors/email_delivery_processor.js +183 -0
- package/dist/mailer/utils/mailer_data_loader_util.js +1 -1
- package/dist/types/mailer_type.d.ts +44 -1
- package/dist/utils/ejs_render_util.d.ts +27 -0
- package/dist/utils/ejs_render_util.js +66 -0
- package/dist/utils/main.d.ts +3 -1
- package/dist/utils/main.js +5 -1
- package/dist/utils/p_limit_util.d.ts +11 -0
- package/dist/utils/p_limit_util.js +40 -0
- package/package.json +5 -1
|
@@ -19,6 +19,7 @@ const GENERATE_NOTIFICATION_CODE_TYPE = (interface_name, db_obj, code) => {
|
|
|
19
19
|
*/
|
|
20
20
|
export interface ${interface_name} {
|
|
21
21
|
${objectToType(db_obj, 8)}
|
|
22
|
+
attachment?s: EmailAttachmentInterface[]
|
|
22
23
|
}
|
|
23
24
|
`;
|
|
24
25
|
};
|
|
@@ -53,6 +54,7 @@ const GENERATE_UTIL_METHOD_CODE = (interface_name, method_name, code) => {
|
|
|
53
54
|
exports.GENERATE_UTIL_METHOD_CODE = GENERATE_UTIL_METHOD_CODE;
|
|
54
55
|
const GENERATE_UTIL_CLASS_CODE = (types_file_name, generated_content) => {
|
|
55
56
|
return `
|
|
57
|
+
import { EmailAttachmentInterface } from "fiberx-backend-toolkit";
|
|
56
58
|
import { EmailEnqueueProcessor } from "fiberx-backend-toolkit/dist/mailer/main";
|
|
57
59
|
import { LoggerUtil } from "fiberx-backend-toolkit/dist/utils/main";
|
|
58
60
|
import * as Types from "@/${types_file_name.replace(".ts", "")}";
|
|
@@ -7,6 +7,7 @@ export declare const MODELS_DIR: string;
|
|
|
7
7
|
export declare const MIGRATIONS_DIR: string;
|
|
8
8
|
export declare const SEEDERS_DIR: string;
|
|
9
9
|
export declare const EMAIL_ENQUEUE_DIR: string;
|
|
10
|
+
export declare const EMAIL_PREVIEW_DIR: string;
|
|
10
11
|
export declare const SEQUELIZE_META_TABLE_NAME = "sequelize_database_tables_meta";
|
|
11
12
|
export declare const SEQUELIZE_SEEDER_META_TABLE_NAME = "sequelize_database_table_seeder_meta";
|
|
12
13
|
export declare const REQUEST_ID_COOKIE_MAX_AGE: number;
|
package/dist/config/constants.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.DEFAULT_CONTENT_CACHE_TTL = exports.DEFAULT_CONTENT_LANG = exports.DEFAULT_CONTENT_URL = exports.CONTENT_LANG_KEY = exports.CONTENT_URL_KEY = exports.CONTENT_CACHE_TTL_KEY = exports.EMAIL_ENQUEUE_UTIL_FILE_NAME = exports.EMAIL_ENQUEUE_TYPES_FILE_NAME = exports.DEFAULT_MAILER_CACHE_TTL = exports.DEFAULT_MAILER_CACHE_KEY = exports.DEFAULT_RBAC_CACHE_TTL = exports.DEFAULT_RBAC_CACHE_KEY = exports.DEFAULT_TOTP_WINDOW = exports.DEFAULT_TOTP_DIGITS = exports.DEFAULT_TOTP_STEP = exports.ALPHABET_CORPUS = exports.CHARACTER_CORPUS = exports.REQUEST_RATE_LIMITTER_OPTIONS = exports.CORS_MAX_AGE_IN_MICRO_SECONDS = exports.CORS_MAX_AGE_IN_SECONDS = exports.CORS_ALLOWED_HEADERS = exports.HEADERS_KEY_NAME = exports.CORS_ALLOWED_METHODS = exports.DEVICE_ID_HEADERS_NAME = exports.DEVICE_ID_COOKIE_NAME = exports.DEVICE_ID_COOKIE_MAX_AGE = exports.REQUEST_ID_HEADERS_NAME = exports.REQUEST_ID_COOKIE_NAME = exports.REQUEST_ID_COOKIE_MAX_AGE = exports.SEQUELIZE_SEEDER_META_TABLE_NAME = exports.SEQUELIZE_META_TABLE_NAME = exports.EMAIL_ENQUEUE_DIR = exports.SEEDERS_DIR = exports.MIGRATIONS_DIR = exports.MODELS_DIR = exports.SCHEMA_SNAPSHOTS_DIR = exports.SCHEMAS_DIR = exports.ENV_VAR_DIR = exports.LOG_DIR = exports.BASE_DIR = void 0;
|
|
6
|
+
exports.DEFAULT_CONTENT_CACHE_TTL = exports.DEFAULT_CONTENT_LANG = exports.DEFAULT_CONTENT_URL = exports.CONTENT_LANG_KEY = exports.CONTENT_URL_KEY = exports.CONTENT_CACHE_TTL_KEY = exports.EMAIL_ENQUEUE_UTIL_FILE_NAME = exports.EMAIL_ENQUEUE_TYPES_FILE_NAME = exports.DEFAULT_MAILER_CACHE_TTL = exports.DEFAULT_MAILER_CACHE_KEY = exports.DEFAULT_RBAC_CACHE_TTL = exports.DEFAULT_RBAC_CACHE_KEY = exports.DEFAULT_TOTP_WINDOW = exports.DEFAULT_TOTP_DIGITS = exports.DEFAULT_TOTP_STEP = exports.ALPHABET_CORPUS = exports.CHARACTER_CORPUS = exports.REQUEST_RATE_LIMITTER_OPTIONS = exports.CORS_MAX_AGE_IN_MICRO_SECONDS = exports.CORS_MAX_AGE_IN_SECONDS = exports.CORS_ALLOWED_HEADERS = exports.HEADERS_KEY_NAME = exports.CORS_ALLOWED_METHODS = exports.DEVICE_ID_HEADERS_NAME = exports.DEVICE_ID_COOKIE_NAME = exports.DEVICE_ID_COOKIE_MAX_AGE = exports.REQUEST_ID_HEADERS_NAME = exports.REQUEST_ID_COOKIE_NAME = exports.REQUEST_ID_COOKIE_MAX_AGE = exports.SEQUELIZE_SEEDER_META_TABLE_NAME = exports.SEQUELIZE_META_TABLE_NAME = exports.EMAIL_PREVIEW_DIR = exports.EMAIL_ENQUEUE_DIR = exports.SEEDERS_DIR = exports.MIGRATIONS_DIR = exports.MODELS_DIR = exports.SCHEMA_SNAPSHOTS_DIR = exports.SCHEMAS_DIR = exports.ENV_VAR_DIR = exports.LOG_DIR = exports.BASE_DIR = void 0;
|
|
7
7
|
const path_1 = __importDefault(require("path"));
|
|
8
8
|
exports.BASE_DIR = process.cwd();
|
|
9
9
|
exports.LOG_DIR = path_1.default.join(exports.BASE_DIR, "logs");
|
|
@@ -14,6 +14,7 @@ exports.MODELS_DIR = path_1.default.join(exports.BASE_DIR, "src/database/models"
|
|
|
14
14
|
exports.MIGRATIONS_DIR = path_1.default.join(exports.BASE_DIR, "src/database/migrations");
|
|
15
15
|
exports.SEEDERS_DIR = path_1.default.join(exports.BASE_DIR, "src/database/seeders");
|
|
16
16
|
exports.EMAIL_ENQUEUE_DIR = path_1.default.join(exports.BASE_DIR, "src/");
|
|
17
|
+
exports.EMAIL_PREVIEW_DIR = path_1.default.join(exports.BASE_DIR, "local_media/email/email_preview");
|
|
17
18
|
exports.SEQUELIZE_META_TABLE_NAME = "sequelize_database_tables_meta";
|
|
18
19
|
exports.SEQUELIZE_SEEDER_META_TABLE_NAME = "sequelize_database_table_seeder_meta";
|
|
19
20
|
exports.REQUEST_ID_COOKIE_MAX_AGE = (1000 * 60 * 60 * 24 * 30); // 7 days
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { EmailDeliveryQueueAdapter, EmailDeliveryOptions } from "../../types/mailer_type";
|
|
2
|
+
declare class EmailDeliveryProcessor<TQueueEntity, TTemplate, TBaseTemplate, TMailerConfig> {
|
|
3
|
+
private static instance;
|
|
4
|
+
readonly name = "email_delivery_processor";
|
|
5
|
+
private readonly logger;
|
|
6
|
+
private readonly adapter;
|
|
7
|
+
private readonly options;
|
|
8
|
+
private constructor();
|
|
9
|
+
private render;
|
|
10
|
+
private renderLocalEmailPreview;
|
|
11
|
+
private sendEmail;
|
|
12
|
+
static initialize<TQueueEntity, TTemplate, TBaseTemplate, TMailerConfig>(adapter: EmailDeliveryQueueAdapter<TQueueEntity, TTemplate, TBaseTemplate, TMailerConfig>, options?: EmailDeliveryOptions): EmailDeliveryProcessor<TQueueEntity, TTemplate, TBaseTemplate, TMailerConfig>;
|
|
13
|
+
static getInstance<TQueueEntity, TTemplate, TBaseTemplate, TMailerConfig>(): EmailDeliveryProcessor<TQueueEntity, TTemplate, TBaseTemplate, TMailerConfig>;
|
|
14
|
+
processSingleRecord(record: TQueueEntity): Promise<void>;
|
|
15
|
+
run(limit?: number): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
export default EmailDeliveryProcessor;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const path_1 = __importDefault(require("path"));
|
|
7
|
+
const nodemailer_1 = __importDefault(require("nodemailer"));
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const main_1 = require("../../utils/main");
|
|
10
|
+
const constants_1 = require("../../config/constants");
|
|
11
|
+
class EmailDeliveryProcessor {
|
|
12
|
+
static instance = null;
|
|
13
|
+
name = "email_delivery_processor";
|
|
14
|
+
logger = new main_1.LoggerUtil(this.name);
|
|
15
|
+
adapter;
|
|
16
|
+
options;
|
|
17
|
+
constructor(adapter, options) {
|
|
18
|
+
this.adapter = adapter;
|
|
19
|
+
this.options = options || {};
|
|
20
|
+
}
|
|
21
|
+
// EMial Render method
|
|
22
|
+
async render(template, payload) {
|
|
23
|
+
try {
|
|
24
|
+
return await main_1.EJSRenderUtil.safeRenderString(template, payload);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
this.logger.error(`Failed to render string with given payload`, { template, payload, error });
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// Render preview in dev mode
|
|
32
|
+
async renderLocalEmailPreview(record, html) {
|
|
33
|
+
try {
|
|
34
|
+
const output_dir = this.options?.email_preview_dir ?? constants_1.EMAIL_PREVIEW_DIR;
|
|
35
|
+
const file_base_name = this.adapter.getLocalEmailPreviewFileBaseName(record);
|
|
36
|
+
const file_name = `${file_base_name}.html`;
|
|
37
|
+
const output_file = path_1.default.join(output_dir, file_name);
|
|
38
|
+
main_1.InputValidatorUtil.dirExists(output_dir, true);
|
|
39
|
+
fs_1.default.writeFileSync(output_file, html, "utf-8");
|
|
40
|
+
this.logger.success(`Local Email Preview saved to ${output_file}`);
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
this.logger.error(`Failed to render local email preview`, { error });
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Send email live
|
|
49
|
+
async sendEmail(record, mailer_config, rendered_subject, rendered_body, attachments = []) {
|
|
50
|
+
const { id: record_id, recipient_email } = this.adapter.getEmailRecordIdAndRecipient(record);
|
|
51
|
+
try {
|
|
52
|
+
this.logger.info(`Sending Rendered Email for Record with ID ${record_id} -> sending to ${recipient_email}`);
|
|
53
|
+
const email_attachments = attachments?.map((attachemnt) => {
|
|
54
|
+
return {
|
|
55
|
+
filename: attachemnt.file_name,
|
|
56
|
+
path: attachemnt.file_path,
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
this.logger.info(`Done preparing ${email_attachments?.length} Email attachments for Record with ID ${record_id}`);
|
|
60
|
+
const { from_email_address, reply_to_email_address, host, port, username, decrypted_password, tls_reject_unauthorized = false } = await this.adapter.getMailerTransporterConfig(record, mailer_config);
|
|
61
|
+
const transporter = nodemailer_1.default.createTransport({
|
|
62
|
+
host,
|
|
63
|
+
port,
|
|
64
|
+
secure: port === 465,
|
|
65
|
+
auth: {
|
|
66
|
+
user: username,
|
|
67
|
+
pass: decrypted_password,
|
|
68
|
+
},
|
|
69
|
+
tls: { rejectUnauthorized: tls_reject_unauthorized }
|
|
70
|
+
});
|
|
71
|
+
this.logger.info(`Mailer transporter Config has been set for Record with ID ${record_id}`);
|
|
72
|
+
await transporter.sendMail({
|
|
73
|
+
from: from_email_address,
|
|
74
|
+
to: recipient_email,
|
|
75
|
+
subject: rendered_subject,
|
|
76
|
+
html: rendered_body,
|
|
77
|
+
replyTo: reply_to_email_address,
|
|
78
|
+
attachments: email_attachments.length ? email_attachments : undefined,
|
|
79
|
+
});
|
|
80
|
+
this.logger.success(`Done sending Rendered Email for Record with ID ${record_id} -> sending to ${recipient_email}`);
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
this.logger.error(`Failed to send email`, { record_id, recipient_email, error });
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// ==============================
|
|
89
|
+
// Singleton
|
|
90
|
+
// ==============================
|
|
91
|
+
static initialize(adapter, options) {
|
|
92
|
+
if (this.instance) {
|
|
93
|
+
throw new Error("EmailDeliveryProcessor already initialized.");
|
|
94
|
+
}
|
|
95
|
+
this.instance = new EmailDeliveryProcessor(adapter, options);
|
|
96
|
+
return this.instance;
|
|
97
|
+
}
|
|
98
|
+
static getInstance() {
|
|
99
|
+
if (!this.instance) {
|
|
100
|
+
throw new Error("EmailDeliveryProcessor not initialized.");
|
|
101
|
+
}
|
|
102
|
+
return this.instance;
|
|
103
|
+
}
|
|
104
|
+
// ==============================
|
|
105
|
+
// Process 1 Record
|
|
106
|
+
// ==============================
|
|
107
|
+
async processSingleRecord(record) {
|
|
108
|
+
const { id: record_id, recipient_email } = this.adapter.getEmailRecordIdAndRecipient(record);
|
|
109
|
+
try {
|
|
110
|
+
this.logger.info(`Processing email queue Record ${record_id}`, { record_id, recipient_email });
|
|
111
|
+
const template = await this.adapter.getEmailRecordTemplate(record);
|
|
112
|
+
if (!template) {
|
|
113
|
+
this.logger.error(`Missing Email Template for Record ${record_id}`, { record_id, recipient_email, template });
|
|
114
|
+
throw new Error(`Missing Email Template for Record ${record_id}`);
|
|
115
|
+
}
|
|
116
|
+
const base_template = await this.adapter.getEmailRecordBaseTemplate(record, template);
|
|
117
|
+
if (!base_template) {
|
|
118
|
+
this.logger.error(`Missing Base Email Template for Record ${record_id}`, { record_id, recipient_email, base_template });
|
|
119
|
+
throw new Error(`Missing Base Email Template for Record ${record_id}`);
|
|
120
|
+
}
|
|
121
|
+
const mailer_config = await this.adapter.getEmailRecordMailerConfig(record, base_template);
|
|
122
|
+
if (!mailer_config) {
|
|
123
|
+
this.logger.error(`Missing Mailer Config for Record ${record_id}`, { record_id, recipient_email, mailer_config });
|
|
124
|
+
throw new Error(`Missing Mailer Config for Record ${record_id}`);
|
|
125
|
+
}
|
|
126
|
+
const subject_value = this.adapter.getEmailRecordSubjectValue(record);
|
|
127
|
+
const body_value = this.adapter.getEmailRecordBodyValue(record);
|
|
128
|
+
const base_email_value = this.adapter.getEmailRecordBaseTemplateBody(record, base_template);
|
|
129
|
+
const email_payload = this.adapter.getEmailRecordPayload(record);
|
|
130
|
+
const rendered_subject = await this.render(subject_value, email_payload);
|
|
131
|
+
const rendered_body = await this.render(body_value, email_payload);
|
|
132
|
+
if (!rendered_subject || !rendered_body) {
|
|
133
|
+
this.logger.error(`Failed to render Email Subject or body for Record ${record_id}`, { rendered_subject, rendered_body });
|
|
134
|
+
throw new Error(`Failed to render Email Subject or body for Record ${record_id}`);
|
|
135
|
+
}
|
|
136
|
+
const full_email_payload = await this.adapter.getFullEmailPayload(record, rendered_subject, rendered_body, email_payload);
|
|
137
|
+
const full_email_render = await this.render(base_email_value, full_email_payload);
|
|
138
|
+
if (!full_email_render) {
|
|
139
|
+
this.logger.error(`Failed to render Full Email for Record ${record_id}`, { full_email_render });
|
|
140
|
+
throw new Error(`Failed to render Full Email for Record ${record_id}`);
|
|
141
|
+
}
|
|
142
|
+
let email_sent = false;
|
|
143
|
+
if (main_1.InputValidatorUtil.isDevelopment()) {
|
|
144
|
+
email_sent = await this.renderLocalEmailPreview(record, full_email_render);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
email_sent = await this.sendEmail(record, mailer_config, rendered_subject, rendered_body, email_payload?.attachments);
|
|
148
|
+
}
|
|
149
|
+
if (!email_sent) {
|
|
150
|
+
this.logger.error(`Failed to Send email for Record ${record_id}`, { email_sent, recipient_email });
|
|
151
|
+
throw new Error(`Failed to Send email for Record ${record_id}`);
|
|
152
|
+
}
|
|
153
|
+
await this.adapter.updateEmailRecordAsCompleted(record);
|
|
154
|
+
this.logger.success(`Completed Processing email queue Record ${record_id}`, { record_id, recipient_email });
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
const message = error?.message ?? "Unknown error";
|
|
158
|
+
await this.adapter.updateEmailRecordAsFailed(record, message);
|
|
159
|
+
this.logger.error(`Failed to Process email queue Record ${record_id}`, { record_id, recipient_email });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// ==============================
|
|
163
|
+
// ENTRY METHOD
|
|
164
|
+
// ==============================
|
|
165
|
+
async run(limit = 50) {
|
|
166
|
+
try {
|
|
167
|
+
this.logger.info("Starting email delivery batch", { limit });
|
|
168
|
+
const records = await this.adapter.fetchRecordsToProcess(limit);
|
|
169
|
+
this.logger.info(`Found ${records.length} queued email(s) to process`);
|
|
170
|
+
const concurrency = this.options?.concurrency ?? 5;
|
|
171
|
+
for (let i = 0; i < records.length; i += concurrency) {
|
|
172
|
+
const batch = records.slice(i, i + concurrency);
|
|
173
|
+
await Promise.all(batch.map(r => this.processSingleRecord(r)));
|
|
174
|
+
}
|
|
175
|
+
this.logger.info(`Done Processing ${records.length} queued email(s)`);
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
this.logger.error(`Failed to run Email Delivery processor`, { error });
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
exports.default = EmailDeliveryProcessor;
|
|
@@ -116,7 +116,7 @@ class MailerDataLoaderUtil {
|
|
|
116
116
|
if (templates_by_notification_code.has(code)) {
|
|
117
117
|
return templates_by_notification_code.get(code) ?? null;
|
|
118
118
|
}
|
|
119
|
-
return await this.provider.
|
|
119
|
+
return await this.provider.fetchEmailContentTemplateByNotificationCode(code);
|
|
120
120
|
}
|
|
121
121
|
// --------------------------
|
|
122
122
|
// Refresh
|
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
export interface EmailAttachmentInterface {
|
|
2
|
+
file_name: string;
|
|
3
|
+
file_path: string;
|
|
4
|
+
content_type: string;
|
|
5
|
+
}
|
|
6
|
+
export interface MailerTransporterConfigInterface {
|
|
7
|
+
from_email_address: string;
|
|
8
|
+
reply_to_email_address: string;
|
|
9
|
+
host: string;
|
|
10
|
+
port: number;
|
|
11
|
+
username: string;
|
|
12
|
+
decrypted_password: string;
|
|
13
|
+
tls_reject_unauthorized: boolean;
|
|
14
|
+
}
|
|
1
15
|
export interface EmailEnqueueGeneratorOptions {
|
|
2
16
|
output_dir: string;
|
|
3
17
|
types_file_name?: string;
|
|
@@ -44,7 +58,7 @@ export interface MailerDataLoaderProvider<TMailerConfig, TBaseTemplate, TNotific
|
|
|
44
58
|
fetchBaseTemplate(): Promise<TBaseTemplate | null>;
|
|
45
59
|
fetchNotificationTypes(): Promise<TNotificationType[]>;
|
|
46
60
|
fetchEmailContentTemplates(): Promise<TEmailContentTempltae[]>;
|
|
47
|
-
|
|
61
|
+
fetchEmailContentTemplateByNotificationCode(code: string): Promise<TEmailContentTempltae | null>;
|
|
48
62
|
getNotificaionCodeFromNotification(notification: TNotificationType): string;
|
|
49
63
|
getNotificaionCodeFromTemplate(email_content_template: TEmailContentTempltae): string;
|
|
50
64
|
}
|
|
@@ -60,3 +74,32 @@ export interface MailerDataLoaderOptionsInterface {
|
|
|
60
74
|
cache_key?: string;
|
|
61
75
|
cache_ttl?: number;
|
|
62
76
|
}
|
|
77
|
+
export interface EmailBasePayloadInterface {
|
|
78
|
+
static_content: Record<string, any>;
|
|
79
|
+
db_content: Record<string, any>;
|
|
80
|
+
attachments?: EmailAttachmentInterface[];
|
|
81
|
+
}
|
|
82
|
+
export interface EmailDeliveryOptions {
|
|
83
|
+
max_attempts?: number;
|
|
84
|
+
email_preview_dir?: string;
|
|
85
|
+
concurrency?: number;
|
|
86
|
+
}
|
|
87
|
+
export interface EmailDeliveryQueueAdapter<TQueueEntity, TTemplate, TBaseTemplate, TMailerConfig> {
|
|
88
|
+
fetchRecordsToProcess(limit: number): Promise<TQueueEntity[]>;
|
|
89
|
+
getEmailRecordIdAndRecipient(record: TQueueEntity): {
|
|
90
|
+
id: string;
|
|
91
|
+
recipient_email: string;
|
|
92
|
+
};
|
|
93
|
+
getEmailRecordTemplate(record: TQueueEntity): Promise<TTemplate | null>;
|
|
94
|
+
getEmailRecordBaseTemplate(record: TQueueEntity, template?: TTemplate): Promise<TBaseTemplate | null>;
|
|
95
|
+
getEmailRecordMailerConfig(record: TQueueEntity, base_template?: TBaseTemplate): Promise<TMailerConfig | null>;
|
|
96
|
+
getEmailRecordSubjectValue(record: TQueueEntity): string;
|
|
97
|
+
getEmailRecordBodyValue(record: TQueueEntity): string;
|
|
98
|
+
getEmailRecordBaseTemplateBody(record: TQueueEntity, base_template: TBaseTemplate): string;
|
|
99
|
+
getEmailRecordPayload(record: TQueueEntity): EmailBasePayloadInterface;
|
|
100
|
+
getLocalEmailPreviewFileBaseName(record: TQueueEntity): string;
|
|
101
|
+
getFullEmailPayload(record: TQueueEntity, subject: string, body: string, payload: EmailBasePayloadInterface): Promise<Record<string, any>>;
|
|
102
|
+
getMailerTransporterConfig(record: TQueueEntity, config: TMailerConfig): Promise<MailerTransporterConfigInterface>;
|
|
103
|
+
updateEmailRecordAsCompleted(record: TQueueEntity): Promise<TQueueEntity>;
|
|
104
|
+
updateEmailRecordAsFailed(record: TQueueEntity, message?: string): Promise<TQueueEntity>;
|
|
105
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Options as EjsOptions } from "ejs";
|
|
2
|
+
declare class EJSRenderUtil {
|
|
3
|
+
/**
|
|
4
|
+
* Render an EJS template string with a payload.
|
|
5
|
+
*
|
|
6
|
+
* @param template_string - Raw EJS string (e.g "<h1><%= name %></h1>")
|
|
7
|
+
* @param payload - Data object injected into template
|
|
8
|
+
* @param options - Optional EJS rendering options
|
|
9
|
+
* @returns Rendered string
|
|
10
|
+
*/
|
|
11
|
+
static renderString<T extends Record<string, any>>(template_string: string, payload: T, options?: EjsOptions): Promise<string>;
|
|
12
|
+
/**
|
|
13
|
+
* Async render from file path.
|
|
14
|
+
*
|
|
15
|
+
* @param file_path - Absolute or relative file path
|
|
16
|
+
* @param payload - Data object injected into template
|
|
17
|
+
* @param options - Optional EJS rendering options
|
|
18
|
+
* @returns Rendered string
|
|
19
|
+
*/
|
|
20
|
+
static renderFile<T extends Record<string, any>>(file_path: string, payload: T, options?: EjsOptions): Promise<string>;
|
|
21
|
+
/**
|
|
22
|
+
* Safe render version (never throws).
|
|
23
|
+
* Returns null instead of throwing error.
|
|
24
|
+
*/
|
|
25
|
+
static safeRenderString<T extends Record<string, any>>(template_string: string, payload: T, options?: EjsOptions): Promise<string | null>;
|
|
26
|
+
}
|
|
27
|
+
export default EJSRenderUtil;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const ejs_1 = __importDefault(require("ejs"));
|
|
7
|
+
class EJSRenderUtil {
|
|
8
|
+
/**
|
|
9
|
+
* Render an EJS template string with a payload.
|
|
10
|
+
*
|
|
11
|
+
* @param template_string - Raw EJS string (e.g "<h1><%= name %></h1>")
|
|
12
|
+
* @param payload - Data object injected into template
|
|
13
|
+
* @param options - Optional EJS rendering options
|
|
14
|
+
* @returns Rendered string
|
|
15
|
+
*/
|
|
16
|
+
static async renderString(template_string, payload, options) {
|
|
17
|
+
if (!template_string || typeof template_string !== "string") {
|
|
18
|
+
throw new Error("Invalid template string provided.");
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
return await ejs_1.default.render(template_string, payload, {
|
|
22
|
+
strict: false,
|
|
23
|
+
...options,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
throw new Error(`EJS string rendering failed: ${error?.message ?? "Unknown error"}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Async render from file path.
|
|
32
|
+
*
|
|
33
|
+
* @param file_path - Absolute or relative file path
|
|
34
|
+
* @param payload - Data object injected into template
|
|
35
|
+
* @param options - Optional EJS rendering options
|
|
36
|
+
* @returns Rendered string
|
|
37
|
+
*/
|
|
38
|
+
static async renderFile(file_path, payload, options) {
|
|
39
|
+
if (!file_path || typeof file_path !== "string") {
|
|
40
|
+
throw new Error("Invalid file path provided.");
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
return await ejs_1.default.renderFile(file_path, payload, {
|
|
44
|
+
async: true,
|
|
45
|
+
strict: false,
|
|
46
|
+
...options,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
throw new Error(`EJS file rendering failed: ${error?.message ?? "Unknown error"}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Safe render version (never throws).
|
|
55
|
+
* Returns null instead of throwing error.
|
|
56
|
+
*/
|
|
57
|
+
static async safeRenderString(template_string, payload, options) {
|
|
58
|
+
try {
|
|
59
|
+
return await this.renderString(template_string, payload, options);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
exports.default = EJSRenderUtil;
|
package/dist/utils/main.d.ts
CHANGED
|
@@ -11,4 +11,6 @@ import EncryptorDecryptorUtil from "./encryptor_decryptor_util";
|
|
|
11
11
|
import TOTPServiceUtil from "./totp_service_util";
|
|
12
12
|
import ContentManagerUtil from "./content_manager_util";
|
|
13
13
|
import FsActionsUtil from "./fs_actions_util";
|
|
14
|
-
|
|
14
|
+
import EJSRenderUtil from "./ejs_render_util";
|
|
15
|
+
import PLimitUtil from "./p_limit_util";
|
|
16
|
+
export { LoggerUtil, InputTransformerUtil, InputValidatorUtil, EnvManagerUtil, SqlFormatterUtil, ServerUtil, SafeExecuteUtil, InMemoryCacheUtil, UUIDGeneratorUtil, EncryptorDecryptorUtil, TOTPServiceUtil, ContentManagerUtil, FsActionsUtil, EJSRenderUtil, PLimitUtil };
|
package/dist/utils/main.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.FsActionsUtil = exports.ContentManagerUtil = exports.TOTPServiceUtil = exports.EncryptorDecryptorUtil = exports.UUIDGeneratorUtil = exports.InMemoryCacheUtil = exports.SafeExecuteUtil = exports.ServerUtil = exports.SqlFormatterUtil = exports.EnvManagerUtil = exports.InputValidatorUtil = exports.InputTransformerUtil = exports.LoggerUtil = void 0;
|
|
6
|
+
exports.PLimitUtil = exports.EJSRenderUtil = exports.FsActionsUtil = exports.ContentManagerUtil = exports.TOTPServiceUtil = exports.EncryptorDecryptorUtil = exports.UUIDGeneratorUtil = exports.InMemoryCacheUtil = exports.SafeExecuteUtil = exports.ServerUtil = exports.SqlFormatterUtil = exports.EnvManagerUtil = exports.InputValidatorUtil = exports.InputTransformerUtil = exports.LoggerUtil = void 0;
|
|
7
7
|
const logger_util_1 = __importDefault(require("./logger_util"));
|
|
8
8
|
exports.LoggerUtil = logger_util_1.default;
|
|
9
9
|
const input_transformer_util_1 = __importDefault(require("./input_transformer_util"));
|
|
@@ -30,3 +30,7 @@ const content_manager_util_1 = __importDefault(require("./content_manager_util")
|
|
|
30
30
|
exports.ContentManagerUtil = content_manager_util_1.default;
|
|
31
31
|
const fs_actions_util_1 = __importDefault(require("./fs_actions_util"));
|
|
32
32
|
exports.FsActionsUtil = fs_actions_util_1.default;
|
|
33
|
+
const ejs_render_util_1 = __importDefault(require("./ejs_render_util"));
|
|
34
|
+
exports.EJSRenderUtil = ejs_render_util_1.default;
|
|
35
|
+
const p_limit_util_1 = __importDefault(require("./p_limit_util"));
|
|
36
|
+
exports.PLimitUtil = p_limit_util_1.default;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
declare class PLimitUtil {
|
|
2
|
+
/**
|
|
3
|
+
* Run an array of async tasks with a concurrency limit.
|
|
4
|
+
*/
|
|
5
|
+
static run<T>(tasks: Array<() => Promise<T>>, concurrency: number): Promise<T[]>;
|
|
6
|
+
/**
|
|
7
|
+
* Convenience method for mapping over items with concurrency limit.
|
|
8
|
+
*/
|
|
9
|
+
static map<T, R>(items: T[], concurrency: number, asyncFn: (item: T, index: number) => Promise<R>): Promise<R[]>;
|
|
10
|
+
}
|
|
11
|
+
export default PLimitUtil;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
class PLimitUtil {
|
|
4
|
+
/**
|
|
5
|
+
* Run an array of async tasks with a concurrency limit.
|
|
6
|
+
*/
|
|
7
|
+
static async run(tasks, concurrency) {
|
|
8
|
+
if (concurrency <= 0) {
|
|
9
|
+
throw new Error("Concurrency must be greater than 0");
|
|
10
|
+
}
|
|
11
|
+
const results = [];
|
|
12
|
+
let current_index = 0;
|
|
13
|
+
const worker = async () => {
|
|
14
|
+
while (true) {
|
|
15
|
+
const index = current_index++;
|
|
16
|
+
if (index >= tasks.length) {
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
results[index] = await tasks[index]();
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
// Fail fast (you can change behavior if needed)
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const workers = Array.from({ length: Math.min(concurrency, tasks.length) }, () => worker());
|
|
29
|
+
await Promise.all(workers);
|
|
30
|
+
return results;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Convenience method for mapping over items with concurrency limit.
|
|
34
|
+
*/
|
|
35
|
+
static async map(items, concurrency, asyncFn) {
|
|
36
|
+
const tasks = items.map((item, index) => () => asyncFn(item, index));
|
|
37
|
+
return this.run(tasks, concurrency);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
exports.default = PLimitUtil;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fiberx-backend-toolkit",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "A TypeScript backend toolkit providing shared domain logic, infrastructure helpers, and utilities for FiberX server-side applications and services.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -23,12 +23,15 @@
|
|
|
23
23
|
"license": "ISC",
|
|
24
24
|
"author": "David Matt-Ojo",
|
|
25
25
|
"dependencies": {
|
|
26
|
+
"@types/nodemailer": "^7.0.11",
|
|
26
27
|
"axios": "^1.11.0",
|
|
27
28
|
"bcrypt": "^6.0.0",
|
|
28
29
|
"dayjs": "^1.11.18",
|
|
30
|
+
"ejs": "^4.0.1",
|
|
29
31
|
"express": "^5.2.1",
|
|
30
32
|
"js-yaml": "^4.1.0",
|
|
31
33
|
"jsonwebtoken": "^9.0.2",
|
|
34
|
+
"nodemailer": "^8.0.1",
|
|
32
35
|
"sequelize": "^6.37.7",
|
|
33
36
|
"sequelize-typescript": "^2.1.6",
|
|
34
37
|
"uuid": "^12.0.0"
|
|
@@ -36,6 +39,7 @@
|
|
|
36
39
|
"devDependencies": {
|
|
37
40
|
"@types/axios": "^0.9.36",
|
|
38
41
|
"@types/bcrypt": "^6.0.0",
|
|
42
|
+
"@types/ejs": "^3.1.5",
|
|
39
43
|
"@types/express": "^5.0.6",
|
|
40
44
|
"@types/js-yaml": "^4.0.9",
|
|
41
45
|
"@types/jsonwebtoken": "^9.0.10",
|