@astralibx/email-rule-engine 6.0.0 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -15
- package/dist/index.d.mts +7 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +79 -25
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +79 -25
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -71,7 +71,18 @@ function createEmailTemplateSchema(platformValues, audienceValues, categoryValue
|
|
|
71
71
|
textBody: String,
|
|
72
72
|
subjects: { type: [{ type: String }], required: true, validate: [(v) => v.length >= 1, "At least one subject is required"] },
|
|
73
73
|
bodies: { type: [{ type: String }], required: true, validate: [(v) => v.length >= 1, "At least one body is required"] },
|
|
74
|
-
|
|
74
|
+
preheaders: [{ type: String }],
|
|
75
|
+
fields: {
|
|
76
|
+
type: Schema.Types.Mixed,
|
|
77
|
+
default: {},
|
|
78
|
+
validate: {
|
|
79
|
+
validator: (v) => {
|
|
80
|
+
if (!v || typeof v !== "object") return true;
|
|
81
|
+
return Object.values(v).every((val) => typeof val === "string");
|
|
82
|
+
},
|
|
83
|
+
message: "All field values must be strings"
|
|
84
|
+
}
|
|
85
|
+
},
|
|
75
86
|
variables: [{ type: String }],
|
|
76
87
|
version: { type: Number, default: 1 },
|
|
77
88
|
isActive: { type: Boolean, default: true, index: true }
|
|
@@ -106,6 +117,7 @@ function createEmailTemplateSchema(platformValues, audienceValues, categoryValue
|
|
|
106
117
|
textBody: input.textBody,
|
|
107
118
|
subjects: input.subjects,
|
|
108
119
|
bodies: input.bodies,
|
|
120
|
+
preheaders: input.preheaders || [],
|
|
109
121
|
fields: input.fields || {},
|
|
110
122
|
variables: input.variables || [],
|
|
111
123
|
version: 1,
|
|
@@ -216,6 +228,7 @@ function createEmailRuleSendSchema(collectionPrefix) {
|
|
|
216
228
|
subject: { type: String },
|
|
217
229
|
subjectIndex: { type: Number },
|
|
218
230
|
bodyIndex: { type: Number },
|
|
231
|
+
preheaderIndex: { type: Number },
|
|
219
232
|
failureReason: { type: String }
|
|
220
233
|
},
|
|
221
234
|
{
|
|
@@ -440,7 +453,7 @@ var TemplateRenderService = class {
|
|
|
440
453
|
const textBodyFn = textBody ? Handlebars.compile(textBody, { strict: true }) : void 0;
|
|
441
454
|
return { subjectFn, bodyFn, textBodyFn };
|
|
442
455
|
}
|
|
443
|
-
compileBatchVariants(subjects, bodies, textBody) {
|
|
456
|
+
compileBatchVariants(subjects, bodies, textBody, preheaders) {
|
|
444
457
|
const subjectFns = subjects.map((s) => Handlebars.compile(s, { strict: true }));
|
|
445
458
|
const bodyFns = bodies.map((b) => {
|
|
446
459
|
const mjmlSource = wrapInMjml(b);
|
|
@@ -448,7 +461,8 @@ var TemplateRenderService = class {
|
|
|
448
461
|
return Handlebars.compile(htmlWithHandlebars, { strict: true });
|
|
449
462
|
});
|
|
450
463
|
const textBodyFn = textBody ? Handlebars.compile(textBody, { strict: true }) : void 0;
|
|
451
|
-
|
|
464
|
+
const preheaderFns = preheaders && preheaders.length > 0 ? preheaders.map((p) => Handlebars.compile(p, { strict: true })) : void 0;
|
|
465
|
+
return { subjectFns, bodyFns, textBodyFn, preheaderFns };
|
|
452
466
|
}
|
|
453
467
|
renderFromCompiled(compiled, data) {
|
|
454
468
|
const subject = compiled.subjectFn(data);
|
|
@@ -560,6 +574,7 @@ var UPDATEABLE_FIELDS = /* @__PURE__ */ new Set([
|
|
|
560
574
|
"textBody",
|
|
561
575
|
"subjects",
|
|
562
576
|
"bodies",
|
|
577
|
+
"preheaders",
|
|
563
578
|
"variables",
|
|
564
579
|
"isActive",
|
|
565
580
|
"fields"
|
|
@@ -602,7 +617,7 @@ var TemplateService = class {
|
|
|
602
617
|
throw new TemplateSyntaxError(`Template validation failed: ${validation.errors.join("; ")}`, validation.errors);
|
|
603
618
|
}
|
|
604
619
|
}
|
|
605
|
-
const allContent = [...subjects, ...bodies, input.textBody || ""].join(" ");
|
|
620
|
+
const allContent = [...subjects, ...bodies, ...input.preheaders || [], input.textBody || ""].join(" ");
|
|
606
621
|
const variables = input.variables || this.renderService.extractVariables(allContent);
|
|
607
622
|
return this.EmailTemplate.createTemplate({
|
|
608
623
|
...input,
|
|
@@ -630,11 +645,12 @@ var TemplateService = class {
|
|
|
630
645
|
}
|
|
631
646
|
}
|
|
632
647
|
}
|
|
633
|
-
if (input.textBody || input.subjects || input.bodies) {
|
|
648
|
+
if (input.textBody || input.subjects || input.bodies || input.preheaders) {
|
|
634
649
|
const subjects = input.subjects ?? template.subjects;
|
|
635
650
|
const bodies = input.bodies ?? template.bodies;
|
|
651
|
+
const preheaders = input.preheaders ?? template.preheaders ?? [];
|
|
636
652
|
const textBody = input.textBody ?? template.textBody;
|
|
637
|
-
const allContent = [...subjects, ...bodies, textBody || ""].join(" ");
|
|
653
|
+
const allContent = [...subjects, ...bodies, ...preheaders, textBody || ""].join(" ");
|
|
638
654
|
input.variables = this.renderService.extractVariables(allContent);
|
|
639
655
|
}
|
|
640
656
|
const setFields = {};
|
|
@@ -644,7 +660,7 @@ var TemplateService = class {
|
|
|
644
660
|
}
|
|
645
661
|
}
|
|
646
662
|
const update = { $set: setFields };
|
|
647
|
-
if (input.textBody || input.subjects || input.bodies) {
|
|
663
|
+
if (input.textBody || input.subjects || input.bodies || input.preheaders) {
|
|
648
664
|
update["$inc"] = { version: 1 };
|
|
649
665
|
}
|
|
650
666
|
return this.EmailTemplate.findByIdAndUpdate(
|
|
@@ -896,6 +912,21 @@ var RedisLock = class {
|
|
|
896
912
|
var MS_PER_DAY = 864e5;
|
|
897
913
|
var DEFAULT_LOCK_TTL_MS = 30 * 60 * 1e3;
|
|
898
914
|
var IDENTIFIER_CHUNK_SIZE = 50;
|
|
915
|
+
function getLocalDate(date, timezone) {
|
|
916
|
+
if (!timezone) return date;
|
|
917
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
918
|
+
timeZone: timezone,
|
|
919
|
+
year: "numeric",
|
|
920
|
+
month: "2-digit",
|
|
921
|
+
day: "2-digit",
|
|
922
|
+
hour: "2-digit",
|
|
923
|
+
minute: "2-digit",
|
|
924
|
+
second: "2-digit",
|
|
925
|
+
hour12: false
|
|
926
|
+
}).formatToParts(date);
|
|
927
|
+
const get = (type) => parts.find((p) => p.type === type)?.value || "0";
|
|
928
|
+
return /* @__PURE__ */ new Date(`${get("year")}-${get("month")}-${get("day")}T${get("hour")}:${get("minute")}:${get("second")}`);
|
|
929
|
+
}
|
|
899
930
|
async function processInChunks(items, fn, chunkSize) {
|
|
900
931
|
const results = [];
|
|
901
932
|
for (let i = 0; i < items.length; i += chunkSize) {
|
|
@@ -969,9 +1000,18 @@ var RuleRunnerService = class {
|
|
|
969
1000
|
const throttleConfig = await this.EmailThrottleConfig.getConfig();
|
|
970
1001
|
const allActiveRules = await this.EmailRule.findActive();
|
|
971
1002
|
const now = /* @__PURE__ */ new Date();
|
|
1003
|
+
const tz = this.config.options?.sendWindow?.timezone;
|
|
972
1004
|
const activeRules = allActiveRules.filter((rule) => {
|
|
973
|
-
if (rule.validFrom
|
|
974
|
-
|
|
1005
|
+
if (rule.validFrom) {
|
|
1006
|
+
const localNow = getLocalDate(now, tz);
|
|
1007
|
+
const localValidFrom = getLocalDate(new Date(rule.validFrom), tz);
|
|
1008
|
+
if (localNow < localValidFrom) return false;
|
|
1009
|
+
}
|
|
1010
|
+
if (rule.validTill) {
|
|
1011
|
+
const localNow = getLocalDate(now, tz);
|
|
1012
|
+
const localValidTill = getLocalDate(new Date(rule.validTill), tz);
|
|
1013
|
+
if (localNow > localValidTill) return false;
|
|
1014
|
+
}
|
|
975
1015
|
return true;
|
|
976
1016
|
});
|
|
977
1017
|
this.config.hooks?.onRunStart?.({ rulesCount: activeRules.length, triggeredBy });
|
|
@@ -1128,10 +1168,12 @@ var RuleRunnerService = class {
|
|
|
1128
1168
|
sendMap.set(uid, send);
|
|
1129
1169
|
}
|
|
1130
1170
|
}
|
|
1171
|
+
const preheaders = template.preheaders || [];
|
|
1131
1172
|
const compiledVariants = this.templateRenderer.compileBatchVariants(
|
|
1132
1173
|
template.subjects,
|
|
1133
1174
|
template.bodies,
|
|
1134
|
-
template.textBody
|
|
1175
|
+
template.textBody,
|
|
1176
|
+
preheaders
|
|
1135
1177
|
);
|
|
1136
1178
|
let totalProcessed = 0;
|
|
1137
1179
|
for (let i = 0; i < emailsToProcess.length; i++) {
|
|
@@ -1187,6 +1229,15 @@ var RuleRunnerService = class {
|
|
|
1187
1229
|
let finalHtml = renderedHtml;
|
|
1188
1230
|
let finalText = renderedText;
|
|
1189
1231
|
let finalSubject = renderedSubject;
|
|
1232
|
+
let pi;
|
|
1233
|
+
if (compiledVariants.preheaderFns && compiledVariants.preheaderFns.length > 0) {
|
|
1234
|
+
pi = Math.floor(Math.random() * compiledVariants.preheaderFns.length);
|
|
1235
|
+
const renderedPreheader = compiledVariants.preheaderFns[pi](templateData);
|
|
1236
|
+
if (renderedPreheader) {
|
|
1237
|
+
const preheaderHtml = `<div style="display:none;font-size:1px;color:#ffffff;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;">${renderedPreheader}</div>`;
|
|
1238
|
+
finalHtml = finalHtml.replace(/(<body[^>]*>)/i, `$1${preheaderHtml}`);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1190
1241
|
if (this.config.hooks?.beforeSend) {
|
|
1191
1242
|
try {
|
|
1192
1243
|
const modified = await this.config.hooks.beforeSend({
|
|
@@ -1229,7 +1280,7 @@ var RuleRunnerService = class {
|
|
|
1229
1280
|
dedupKey,
|
|
1230
1281
|
identifier.id,
|
|
1231
1282
|
void 0,
|
|
1232
|
-
{ status: "sent", accountId: agentSelection.accountId, subject: finalSubject, subjectIndex: si, bodyIndex: bi }
|
|
1283
|
+
{ status: "sent", accountId: agentSelection.accountId, subject: finalSubject, subjectIndex: si, bodyIndex: bi, preheaderIndex: pi }
|
|
1233
1284
|
);
|
|
1234
1285
|
const current = throttleMap.get(dedupKey) || { today: 0, thisWeek: 0, lastSentDate: null };
|
|
1235
1286
|
throttleMap.set(dedupKey, {
|
|
@@ -1262,24 +1313,16 @@ var RuleRunnerService = class {
|
|
|
1262
1313
|
$inc: { totalSent: stats.sent, totalSkipped: stats.skipped }
|
|
1263
1314
|
});
|
|
1264
1315
|
if (rule.sendOnce) {
|
|
1265
|
-
const allIdentifiers = rule.target.identifiers;
|
|
1316
|
+
const allIdentifiers = rule.target.identifiers || [];
|
|
1317
|
+
const totalIdentifiers = new Set(allIdentifiers.map((e) => e.toLowerCase().trim()).filter(Boolean)).size;
|
|
1266
1318
|
const sends = await this.EmailRuleSend.find({
|
|
1267
1319
|
ruleId: rule._id
|
|
1268
1320
|
}).lean();
|
|
1269
1321
|
const sentOrProcessedIds = new Set(
|
|
1270
1322
|
sends.filter((s) => s.status !== "throttled").map((s) => String(s.userId || s.emailIdentifierId))
|
|
1271
1323
|
);
|
|
1272
|
-
let pendingCount = 0;
|
|
1273
|
-
for (const email of allIdentifiers) {
|
|
1274
|
-
const identifier = identifierMap.get(email.toLowerCase().trim());
|
|
1275
|
-
if (!identifier) continue;
|
|
1276
|
-
if (!sentOrProcessedIds.has(String(identifier.id))) {
|
|
1277
|
-
pendingCount++;
|
|
1278
|
-
}
|
|
1279
|
-
}
|
|
1280
1324
|
const throttledCount = sends.filter((s) => s.status === "throttled").length;
|
|
1281
|
-
|
|
1282
|
-
if (pendingCount === 0) {
|
|
1325
|
+
if (sentOrProcessedIds.size >= totalIdentifiers && throttledCount === 0) {
|
|
1283
1326
|
await this.EmailRule.findByIdAndUpdate(rule._id, { $set: { isActive: false } });
|
|
1284
1327
|
this.logger.info(`Rule '${rule.name}' auto-disabled \u2014 all identifiers processed`);
|
|
1285
1328
|
}
|
|
@@ -1324,10 +1367,12 @@ var RuleRunnerService = class {
|
|
|
1324
1367
|
identifierMap.set(result.email, { id: result.id, contactId: result.contactId });
|
|
1325
1368
|
}
|
|
1326
1369
|
}
|
|
1370
|
+
const preheadersQ = template.preheaders || [];
|
|
1327
1371
|
const compiledVariants = this.templateRenderer.compileBatchVariants(
|
|
1328
1372
|
template.subjects,
|
|
1329
1373
|
template.bodies,
|
|
1330
|
-
template.textBody
|
|
1374
|
+
template.textBody,
|
|
1375
|
+
preheadersQ
|
|
1331
1376
|
);
|
|
1332
1377
|
const ruleId = rule._id.toString();
|
|
1333
1378
|
const templateId = rule.templateId.toString();
|
|
@@ -1390,6 +1435,15 @@ var RuleRunnerService = class {
|
|
|
1390
1435
|
let finalHtml = renderedHtml;
|
|
1391
1436
|
let finalText = renderedText;
|
|
1392
1437
|
let finalSubject = renderedSubject;
|
|
1438
|
+
let pi;
|
|
1439
|
+
if (compiledVariants.preheaderFns && compiledVariants.preheaderFns.length > 0) {
|
|
1440
|
+
pi = Math.floor(Math.random() * compiledVariants.preheaderFns.length);
|
|
1441
|
+
const renderedPreheader = compiledVariants.preheaderFns[pi](templateData);
|
|
1442
|
+
if (renderedPreheader) {
|
|
1443
|
+
const preheaderHtml = `<div style="display:none;font-size:1px;color:#ffffff;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;">${renderedPreheader}</div>`;
|
|
1444
|
+
finalHtml = finalHtml.replace(/(<body[^>]*>)/i, `$1${preheaderHtml}`);
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1393
1447
|
if (this.config.hooks?.beforeSend) {
|
|
1394
1448
|
try {
|
|
1395
1449
|
const modified = await this.config.hooks.beforeSend({
|
|
@@ -1432,7 +1486,7 @@ var RuleRunnerService = class {
|
|
|
1432
1486
|
userId,
|
|
1433
1487
|
identifier.id,
|
|
1434
1488
|
void 0,
|
|
1435
|
-
{ status: "sent", accountId: agentSelection.accountId, subject: finalSubject, subjectIndex: si, bodyIndex: bi }
|
|
1489
|
+
{ status: "sent", accountId: agentSelection.accountId, subject: finalSubject, subjectIndex: si, bodyIndex: bi, preheaderIndex: pi }
|
|
1436
1490
|
);
|
|
1437
1491
|
const current = throttleMap.get(userId) || { today: 0, thisWeek: 0, lastSentDate: null };
|
|
1438
1492
|
throttleMap.set(userId, {
|
|
@@ -1661,7 +1715,7 @@ function createTemplateController(templateService, options) {
|
|
|
1661
1715
|
}
|
|
1662
1716
|
async function create(req, res) {
|
|
1663
1717
|
try {
|
|
1664
|
-
const { name, subjects, bodies, category, audience, platform } = req.body;
|
|
1718
|
+
const { name, subjects, bodies, category, audience, platform, preheaders } = req.body;
|
|
1665
1719
|
if (!name || !subjects || subjects.length === 0 || !bodies || bodies.length === 0 || !category || !audience || !platform) {
|
|
1666
1720
|
return res.status(400).json({ success: false, error: "name, subjects, bodies, category, audience, and platform are required" });
|
|
1667
1721
|
}
|