@astralibx/email-rule-engine 12.7.3 → 12.9.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 +3 -0
- package/dist/index.cjs +301 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +91 -12
- package/dist/index.d.ts +91 -12
- package/dist/index.mjs +298 -46
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -11,6 +11,26 @@ import { Queue, Worker } from 'bullmq';
|
|
|
11
11
|
|
|
12
12
|
// src/schemas/template.schema.ts
|
|
13
13
|
|
|
14
|
+
// src/constants/field-types.ts
|
|
15
|
+
var FIELD_TYPE = {
|
|
16
|
+
String: "string",
|
|
17
|
+
Number: "number",
|
|
18
|
+
Boolean: "boolean",
|
|
19
|
+
Date: "date",
|
|
20
|
+
ObjectId: "objectId",
|
|
21
|
+
Array: "array",
|
|
22
|
+
Object: "object"
|
|
23
|
+
};
|
|
24
|
+
var TYPE_OPERATORS = {
|
|
25
|
+
string: ["eq", "neq", "contains", "in", "not_in", "exists", "not_exists"],
|
|
26
|
+
number: ["eq", "neq", "gt", "gte", "lt", "lte", "in", "not_in", "exists", "not_exists"],
|
|
27
|
+
boolean: ["eq", "neq", "exists", "not_exists"],
|
|
28
|
+
date: ["eq", "neq", "gt", "gte", "lt", "lte", "exists", "not_exists"],
|
|
29
|
+
objectId: ["eq", "neq", "in", "not_in", "exists", "not_exists"],
|
|
30
|
+
array: ["contains", "in", "not_in", "exists", "not_exists"],
|
|
31
|
+
object: ["exists", "not_exists"]
|
|
32
|
+
};
|
|
33
|
+
|
|
14
34
|
// src/constants/index.ts
|
|
15
35
|
var TEMPLATE_CATEGORY = {
|
|
16
36
|
Onboarding: "onboarding",
|
|
@@ -166,7 +186,8 @@ function createEmailRuleSchema(platformValues, audienceValues, collectionPrefix)
|
|
|
166
186
|
...platformValues ? { enum: platformValues } : {}
|
|
167
187
|
},
|
|
168
188
|
conditions: [RuleConditionSchema],
|
|
169
|
-
identifiers: [{ type: String }]
|
|
189
|
+
identifiers: [{ type: String }],
|
|
190
|
+
collection: { type: String }
|
|
170
191
|
}, { _id: false });
|
|
171
192
|
const RuleRunStatsSchema = new Schema({
|
|
172
193
|
matched: { type: Number, default: 0 },
|
|
@@ -257,9 +278,9 @@ function createEmailRuleSendSchema(collectionPrefix) {
|
|
|
257
278
|
const schema = new Schema(
|
|
258
279
|
{
|
|
259
280
|
ruleId: { type: Schema.Types.ObjectId, ref: "EmailRule", required: true },
|
|
260
|
-
userId: { type:
|
|
261
|
-
emailIdentifierId: { type:
|
|
262
|
-
messageId: { type:
|
|
281
|
+
userId: { type: String, required: true },
|
|
282
|
+
emailIdentifierId: { type: String },
|
|
283
|
+
messageId: { type: String },
|
|
263
284
|
sentAt: { type: Date, required: true, default: () => /* @__PURE__ */ new Date() },
|
|
264
285
|
status: { type: String },
|
|
265
286
|
accountId: { type: String },
|
|
@@ -299,6 +320,7 @@ function createEmailRuleSendSchema(collectionPrefix) {
|
|
|
299
320
|
schema.index({ ruleId: 1, userId: 1, sentAt: -1 });
|
|
300
321
|
schema.index({ userId: 1, sentAt: -1 });
|
|
301
322
|
schema.index({ ruleId: 1, sentAt: -1 });
|
|
323
|
+
schema.index({ status: 1, sentAt: -1 });
|
|
302
324
|
return schema;
|
|
303
325
|
}
|
|
304
326
|
function createEmailRuleRunLogSchema(collectionPrefix) {
|
|
@@ -341,8 +363,7 @@ function createEmailRuleRunLogSchema(collectionPrefix) {
|
|
|
341
363
|
}
|
|
342
364
|
}
|
|
343
365
|
);
|
|
344
|
-
schema.index({ runAt: -1 });
|
|
345
|
-
schema.index({ runAt: 1 }, { expireAfterSeconds: 90 * 86400 });
|
|
366
|
+
schema.index({ runAt: -1 }, { expireAfterSeconds: 90 * 86400 });
|
|
346
367
|
return schema;
|
|
347
368
|
}
|
|
348
369
|
function createEmailThrottleConfigSchema(collectionPrefix) {
|
|
@@ -469,15 +490,15 @@ var TemplateRenderService = class {
|
|
|
469
490
|
ensureHelpers();
|
|
470
491
|
}
|
|
471
492
|
renderSingle(subject, body, data, textBody) {
|
|
472
|
-
const subjectFn = Handlebars.compile(subject, { strict:
|
|
493
|
+
const subjectFn = Handlebars.compile(subject, { strict: false });
|
|
473
494
|
const resolvedSubject = subjectFn(data);
|
|
474
|
-
const bodyFn = Handlebars.compile(body, { strict:
|
|
495
|
+
const bodyFn = Handlebars.compile(body, { strict: false });
|
|
475
496
|
const resolvedBody = bodyFn(data);
|
|
476
497
|
const mjmlSource = wrapInMjml(resolvedBody);
|
|
477
498
|
const html = compileMjml(mjmlSource);
|
|
478
499
|
let text;
|
|
479
500
|
if (textBody) {
|
|
480
|
-
const textFn = Handlebars.compile(textBody, { strict:
|
|
501
|
+
const textFn = Handlebars.compile(textBody, { strict: false });
|
|
481
502
|
text = textFn(data);
|
|
482
503
|
} else {
|
|
483
504
|
text = htmlToPlainText(html);
|
|
@@ -487,20 +508,20 @@ var TemplateRenderService = class {
|
|
|
487
508
|
compileBatch(subject, body, textBody) {
|
|
488
509
|
const mjmlSource = wrapInMjml(body);
|
|
489
510
|
const htmlWithHandlebars = compileMjml(mjmlSource);
|
|
490
|
-
const subjectFn = Handlebars.compile(subject, { strict:
|
|
491
|
-
const bodyFn = Handlebars.compile(htmlWithHandlebars, { strict:
|
|
492
|
-
const textBodyFn = textBody ? Handlebars.compile(textBody, { strict:
|
|
511
|
+
const subjectFn = Handlebars.compile(subject, { strict: false });
|
|
512
|
+
const bodyFn = Handlebars.compile(htmlWithHandlebars, { strict: false });
|
|
513
|
+
const textBodyFn = textBody ? Handlebars.compile(textBody, { strict: false }) : void 0;
|
|
493
514
|
return { subjectFn, bodyFn, textBodyFn };
|
|
494
515
|
}
|
|
495
516
|
compileBatchVariants(subjects, bodies, textBody, preheaders) {
|
|
496
|
-
const subjectFns = subjects.map((s) => Handlebars.compile(s, { strict:
|
|
517
|
+
const subjectFns = subjects.map((s) => Handlebars.compile(s, { strict: false }));
|
|
497
518
|
const bodyFns = bodies.map((b) => {
|
|
498
519
|
const mjmlSource = wrapInMjml(b);
|
|
499
520
|
const htmlWithHandlebars = compileMjml(mjmlSource);
|
|
500
|
-
return Handlebars.compile(htmlWithHandlebars, { strict:
|
|
521
|
+
return Handlebars.compile(htmlWithHandlebars, { strict: false });
|
|
501
522
|
});
|
|
502
|
-
const textBodyFn = textBody ? Handlebars.compile(textBody, { strict:
|
|
503
|
-
const preheaderFns = preheaders && preheaders.length > 0 ? preheaders.map((p) => Handlebars.compile(p, { strict:
|
|
523
|
+
const textBodyFn = textBody ? Handlebars.compile(textBody, { strict: false }) : void 0;
|
|
524
|
+
const preheaderFns = preheaders && preheaders.length > 0 ? preheaders.map((p) => Handlebars.compile(p, { strict: false })) : void 0;
|
|
504
525
|
return { subjectFns, bodyFns, textBodyFn, preheaderFns };
|
|
505
526
|
}
|
|
506
527
|
renderFromCompiled(compiled, data) {
|
|
@@ -639,9 +660,10 @@ function slugify(name) {
|
|
|
639
660
|
return name.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
640
661
|
}
|
|
641
662
|
var TemplateService = class {
|
|
642
|
-
constructor(EmailTemplate, config) {
|
|
663
|
+
constructor(EmailTemplate, config, EmailRule) {
|
|
643
664
|
this.EmailTemplate = EmailTemplate;
|
|
644
665
|
this.config = config;
|
|
666
|
+
this.EmailRule = EmailRule;
|
|
645
667
|
}
|
|
646
668
|
renderService = new TemplateRenderService();
|
|
647
669
|
async list(filters) {
|
|
@@ -650,7 +672,14 @@ var TemplateService = class {
|
|
|
650
672
|
if (filters?.audience) query["audience"] = filters.audience;
|
|
651
673
|
if (filters?.platform) query["platform"] = filters.platform;
|
|
652
674
|
if (filters?.isActive !== void 0) query["isActive"] = filters.isActive;
|
|
653
|
-
|
|
675
|
+
const page = filters?.page ?? 1;
|
|
676
|
+
const limit = filters?.limit ?? 200;
|
|
677
|
+
const skip = (page - 1) * limit;
|
|
678
|
+
const [templates, total] = await Promise.all([
|
|
679
|
+
this.EmailTemplate.find(query).sort({ category: 1, name: 1 }).skip(skip).limit(limit),
|
|
680
|
+
this.EmailTemplate.countDocuments(query)
|
|
681
|
+
]);
|
|
682
|
+
return { templates, total };
|
|
654
683
|
}
|
|
655
684
|
async getById(id) {
|
|
656
685
|
return this.EmailTemplate.findById(id);
|
|
@@ -733,6 +762,13 @@ var TemplateService = class {
|
|
|
733
762
|
);
|
|
734
763
|
}
|
|
735
764
|
async delete(id) {
|
|
765
|
+
if (this.EmailRule) {
|
|
766
|
+
const activeRules = await this.EmailRule.find({ templateId: id, isActive: true });
|
|
767
|
+
if (activeRules.length > 0) {
|
|
768
|
+
const names = activeRules.map((r) => r.name).join(", ");
|
|
769
|
+
throw new Error(`Cannot delete template: ${activeRules.length} active rule(s) reference it (${names}). Deactivate them first.`);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
736
772
|
const result = await this.EmailTemplate.findByIdAndDelete(id);
|
|
737
773
|
return result !== null;
|
|
738
774
|
}
|
|
@@ -812,7 +848,8 @@ var TemplateService = class {
|
|
|
812
848
|
async previewWithRecipient(templateId, recipientData) {
|
|
813
849
|
const template = await this.EmailTemplate.findById(templateId);
|
|
814
850
|
if (!template) return null;
|
|
815
|
-
const
|
|
851
|
+
const variables = template.variables ?? [];
|
|
852
|
+
const data = this._buildSampleData(variables, { ...template.fields ?? {}, ...recipientData });
|
|
816
853
|
return this.renderService.renderPreview(
|
|
817
854
|
template.subjects[0],
|
|
818
855
|
template.bodies[0],
|
|
@@ -822,6 +859,125 @@ var TemplateService = class {
|
|
|
822
859
|
}
|
|
823
860
|
};
|
|
824
861
|
|
|
862
|
+
// src/controllers/collection.controller.ts
|
|
863
|
+
function flattenFields(fields, prefix = "", parentIsArray = false) {
|
|
864
|
+
const result = [];
|
|
865
|
+
for (const field of fields) {
|
|
866
|
+
const path = prefix ? `${prefix}.${field.name}` : field.name;
|
|
867
|
+
const isArray = field.type === "array";
|
|
868
|
+
if (field.type === "object" && field.fields?.length) {
|
|
869
|
+
result.push({
|
|
870
|
+
path,
|
|
871
|
+
type: "object",
|
|
872
|
+
label: field.label,
|
|
873
|
+
description: field.description
|
|
874
|
+
});
|
|
875
|
+
result.push(...flattenFields(field.fields, path, false));
|
|
876
|
+
} else if (isArray && field.fields?.length) {
|
|
877
|
+
result.push({
|
|
878
|
+
path: `${path}[]`,
|
|
879
|
+
type: "array",
|
|
880
|
+
label: field.label,
|
|
881
|
+
description: field.description,
|
|
882
|
+
isArray: true
|
|
883
|
+
});
|
|
884
|
+
result.push(...flattenFields(field.fields, `${path}[]`, true));
|
|
885
|
+
} else {
|
|
886
|
+
result.push({
|
|
887
|
+
path,
|
|
888
|
+
type: field.type,
|
|
889
|
+
label: field.label,
|
|
890
|
+
description: field.description,
|
|
891
|
+
enumValues: field.enumValues,
|
|
892
|
+
isArray: parentIsArray || isArray
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
return result;
|
|
897
|
+
}
|
|
898
|
+
function createCollectionController(collections) {
|
|
899
|
+
return {
|
|
900
|
+
list(_req, res) {
|
|
901
|
+
const summary = collections.map((c) => ({
|
|
902
|
+
name: c.name,
|
|
903
|
+
label: c.label,
|
|
904
|
+
description: c.description,
|
|
905
|
+
identifierField: c.identifierField,
|
|
906
|
+
fieldCount: c.fields.length,
|
|
907
|
+
joinCount: c.joins?.length ?? 0
|
|
908
|
+
}));
|
|
909
|
+
res.json({ collections: summary });
|
|
910
|
+
},
|
|
911
|
+
getFields(req, res) {
|
|
912
|
+
const { name } = req.params;
|
|
913
|
+
const collection = collections.find((c) => c.name === name);
|
|
914
|
+
if (!collection) {
|
|
915
|
+
res.status(404).json({ error: `Collection "${name}" not found` });
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
const fields = flattenFields(collection.fields);
|
|
919
|
+
if (collection.joins?.length) {
|
|
920
|
+
for (const join of collection.joins) {
|
|
921
|
+
const joinedCollection = collections.find((c) => c.name === join.from);
|
|
922
|
+
if (joinedCollection) {
|
|
923
|
+
const joinedFields = flattenFields(joinedCollection.fields, join.as);
|
|
924
|
+
fields.push(...joinedFields);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
res.json({
|
|
929
|
+
name: collection.name,
|
|
930
|
+
label: collection.label,
|
|
931
|
+
identifierField: collection.identifierField,
|
|
932
|
+
fields,
|
|
933
|
+
typeOperators: TYPE_OPERATORS
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// src/validation/condition.validator.ts
|
|
940
|
+
function validateConditions(conditions, collectionName, collections) {
|
|
941
|
+
if (!collectionName || collections.length === 0) return [];
|
|
942
|
+
const collection = collections.find((c) => c.name === collectionName);
|
|
943
|
+
if (!collection) return [];
|
|
944
|
+
const flatFields = flattenFields(collection.fields);
|
|
945
|
+
if (collection.joins?.length) {
|
|
946
|
+
for (const join of collection.joins) {
|
|
947
|
+
const joinedCollection = collections.find((c) => c.name === join.from);
|
|
948
|
+
if (joinedCollection) {
|
|
949
|
+
flatFields.push(...flattenFields(joinedCollection.fields, join.as));
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
const fieldMap = /* @__PURE__ */ new Map();
|
|
954
|
+
for (const f of flatFields) {
|
|
955
|
+
fieldMap.set(f.path, f);
|
|
956
|
+
}
|
|
957
|
+
const errors = [];
|
|
958
|
+
for (let i = 0; i < conditions.length; i++) {
|
|
959
|
+
const cond = conditions[i];
|
|
960
|
+
const fieldDef = fieldMap.get(cond.field);
|
|
961
|
+
if (!fieldDef) {
|
|
962
|
+
errors.push({
|
|
963
|
+
index: i,
|
|
964
|
+
field: cond.field,
|
|
965
|
+
message: `Field "${cond.field}" does not exist in collection "${collectionName}"`
|
|
966
|
+
});
|
|
967
|
+
continue;
|
|
968
|
+
}
|
|
969
|
+
const allowedOps = TYPE_OPERATORS[fieldDef.type];
|
|
970
|
+
if (allowedOps && !allowedOps.includes(cond.operator)) {
|
|
971
|
+
errors.push({
|
|
972
|
+
index: i,
|
|
973
|
+
field: cond.field,
|
|
974
|
+
message: `Operator "${cond.operator}" is not valid for field type "${fieldDef.type}"`
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
return errors;
|
|
979
|
+
}
|
|
980
|
+
|
|
825
981
|
// src/services/rule.service.ts
|
|
826
982
|
function isQueryTarget(target) {
|
|
827
983
|
return !target.mode || target.mode === "query";
|
|
@@ -872,8 +1028,15 @@ var RuleService = class {
|
|
|
872
1028
|
this.EmailRuleRunLog = EmailRuleRunLog;
|
|
873
1029
|
this.config = config;
|
|
874
1030
|
}
|
|
875
|
-
async list() {
|
|
876
|
-
|
|
1031
|
+
async list(opts) {
|
|
1032
|
+
const page = opts?.page ?? 1;
|
|
1033
|
+
const limit = opts?.limit ?? 200;
|
|
1034
|
+
const skip = (page - 1) * limit;
|
|
1035
|
+
const [rules, total] = await Promise.all([
|
|
1036
|
+
this.EmailRule.find().populate("templateId", "name slug").sort({ sortOrder: 1, createdAt: -1 }).skip(skip).limit(limit),
|
|
1037
|
+
this.EmailRule.countDocuments()
|
|
1038
|
+
]);
|
|
1039
|
+
return { rules, total };
|
|
877
1040
|
}
|
|
878
1041
|
async getById(id) {
|
|
879
1042
|
return this.EmailRule.findById(id);
|
|
@@ -900,6 +1063,14 @@ var RuleService = class {
|
|
|
900
1063
|
throw new RuleTemplateIncompatibleError("target.identifiers must be a non-empty array for list mode, validation failed");
|
|
901
1064
|
}
|
|
902
1065
|
}
|
|
1066
|
+
if (isQueryTarget(input.target) && input.target.collection && this.config.collections?.length) {
|
|
1067
|
+
const condErrors = validateConditions(input.target.conditions, input.target.collection, this.config.collections);
|
|
1068
|
+
if (condErrors.length > 0) {
|
|
1069
|
+
throw new RuleTemplateIncompatibleError(
|
|
1070
|
+
`Invalid conditions: ${condErrors.map((e) => e.message).join("; ")}`
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
903
1074
|
return this.EmailRule.createRule(input);
|
|
904
1075
|
}
|
|
905
1076
|
async update(id, input) {
|
|
@@ -929,6 +1100,17 @@ var RuleService = class {
|
|
|
929
1100
|
throw new RuleTemplateIncompatibleError(compatError);
|
|
930
1101
|
}
|
|
931
1102
|
}
|
|
1103
|
+
if (isQueryTarget(effectiveTarget)) {
|
|
1104
|
+
const qt = effectiveTarget;
|
|
1105
|
+
if (qt.collection && this.config.collections?.length) {
|
|
1106
|
+
const condErrors = validateConditions(qt.conditions || [], qt.collection, this.config.collections);
|
|
1107
|
+
if (condErrors.length > 0) {
|
|
1108
|
+
throw new RuleTemplateIncompatibleError(
|
|
1109
|
+
`Invalid conditions: ${condErrors.map((e) => e.message).join("; ")}`
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
932
1114
|
const setFields = {};
|
|
933
1115
|
for (const [key, value] of Object.entries(input)) {
|
|
934
1116
|
if (value !== void 0 && UPDATEABLE_FIELDS2.has(key)) {
|
|
@@ -983,7 +1165,10 @@ var RuleService = class {
|
|
|
983
1165
|
const sample2 = identifiers.slice(0, 10).map((id2) => ({ email: id2 }));
|
|
984
1166
|
return { matchedCount: matchedCount2, effectiveLimit, willProcess: willProcess2, ruleId: id, sample: sample2 };
|
|
985
1167
|
}
|
|
986
|
-
const
|
|
1168
|
+
const queryTarget = rule.target;
|
|
1169
|
+
const collectionName = queryTarget.collection;
|
|
1170
|
+
const collectionSchema = collectionName ? this.config.collections?.find((c) => c.name === collectionName) : void 0;
|
|
1171
|
+
const users = await this.config.adapters.queryUsers(rule.target, 5e4, collectionSchema ? { collectionSchema } : void 0);
|
|
987
1172
|
const matchedCount = users.length;
|
|
988
1173
|
const willProcess = Math.min(matchedCount, effectiveLimit);
|
|
989
1174
|
const sample = users.slice(0, 10).map((u) => ({
|
|
@@ -1005,8 +1190,25 @@ var RuleService = class {
|
|
|
1005
1190
|
rest.lastRunStats = void 0;
|
|
1006
1191
|
return this.EmailRule.create(rest);
|
|
1007
1192
|
}
|
|
1008
|
-
async getRunHistory(limit = 20) {
|
|
1009
|
-
|
|
1193
|
+
async getRunHistory(limit = 20, opts) {
|
|
1194
|
+
const filter = {};
|
|
1195
|
+
if (opts?.from || opts?.to) {
|
|
1196
|
+
filter.runAt = {};
|
|
1197
|
+
if (opts.from) filter.runAt["$gte"] = new Date(opts.from);
|
|
1198
|
+
if (opts.to) filter.runAt["$lte"] = /* @__PURE__ */ new Date(opts.to + "T23:59:59.999Z");
|
|
1199
|
+
}
|
|
1200
|
+
const page = opts?.page ?? 1;
|
|
1201
|
+
const skip = (page - 1) * limit;
|
|
1202
|
+
return this.EmailRuleRunLog.find(filter).sort({ runAt: -1 }).skip(skip).limit(limit);
|
|
1203
|
+
}
|
|
1204
|
+
async getRunHistoryCount(opts) {
|
|
1205
|
+
const filter = {};
|
|
1206
|
+
if (opts?.from || opts?.to) {
|
|
1207
|
+
filter.runAt = {};
|
|
1208
|
+
if (opts.from) filter.runAt["$gte"] = new Date(opts.from);
|
|
1209
|
+
if (opts.to) filter.runAt["$lte"] = /* @__PURE__ */ new Date(opts.to + "T23:59:59.999Z");
|
|
1210
|
+
}
|
|
1211
|
+
return this.EmailRuleRunLog.countDocuments(filter);
|
|
1010
1212
|
}
|
|
1011
1213
|
};
|
|
1012
1214
|
var MS_PER_DAY = 864e5;
|
|
@@ -1076,6 +1278,7 @@ var RuleRunnerService = class {
|
|
|
1076
1278
|
const lockAcquired = await this.lock.acquire();
|
|
1077
1279
|
if (!lockAcquired) {
|
|
1078
1280
|
this.logger.warn("Rule runner already executing, skipping");
|
|
1281
|
+
await this.updateRunProgress(runId, { status: "failed", currentRule: "Another run is already in progress" });
|
|
1079
1282
|
return { runId };
|
|
1080
1283
|
}
|
|
1081
1284
|
const runStartTime = Date.now();
|
|
@@ -1288,12 +1491,12 @@ var RuleRunnerService = class {
|
|
|
1288
1491
|
const dedupKey = identifier.id;
|
|
1289
1492
|
const lastSend = sendMap.get(dedupKey);
|
|
1290
1493
|
if (lastSend) {
|
|
1291
|
-
if (rule.sendOnce &&
|
|
1494
|
+
if (rule.sendOnce && rule.resendAfterDays == null) {
|
|
1292
1495
|
stats.skipped++;
|
|
1293
1496
|
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "skipped", accountId: "", templateId, runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: "send once" });
|
|
1294
1497
|
continue;
|
|
1295
1498
|
}
|
|
1296
|
-
if (rule.resendAfterDays) {
|
|
1499
|
+
if (rule.resendAfterDays != null) {
|
|
1297
1500
|
const daysSince = (Date.now() - new Date(lastSend.sentAt).getTime()) / MS_PER_DAY;
|
|
1298
1501
|
if (daysSince < rule.resendAfterDays) {
|
|
1299
1502
|
stats.skipped++;
|
|
@@ -1305,6 +1508,14 @@ var RuleRunnerService = class {
|
|
|
1305
1508
|
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "skipped", accountId: "", templateId, runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: "send once" });
|
|
1306
1509
|
continue;
|
|
1307
1510
|
}
|
|
1511
|
+
if (rule.cooldownDays) {
|
|
1512
|
+
const daysSince = (Date.now() - new Date(lastSend.sentAt).getTime()) / MS_PER_DAY;
|
|
1513
|
+
if (daysSince < rule.cooldownDays) {
|
|
1514
|
+
stats.skipped++;
|
|
1515
|
+
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "skipped", accountId: "", templateId, runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: "cooldown period" });
|
|
1516
|
+
continue;
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1308
1519
|
}
|
|
1309
1520
|
if (!this.checkThrottle(rule, dedupKey, email, throttleMap, throttleConfig, stats, templateId, runId)) continue;
|
|
1310
1521
|
const agentSelection = await this.config.adapters.selectAgent(identifier.id, { ruleId, templateId });
|
|
@@ -1390,7 +1601,7 @@ var RuleRunnerService = class {
|
|
|
1390
1601
|
lastSentDate: /* @__PURE__ */ new Date()
|
|
1391
1602
|
});
|
|
1392
1603
|
stats.sent++;
|
|
1393
|
-
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "sent", accountId: agentSelection.accountId, templateId, runId: runId || "", subjectIndex: si, bodyIndex: bi });
|
|
1604
|
+
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "sent", accountId: agentSelection.accountId, templateId, runId: runId || "", subjectIndex: si, bodyIndex: bi, preheaderIndex: pi });
|
|
1394
1605
|
totalProcessed++;
|
|
1395
1606
|
if (runId && totalProcessed % 10 === 0) {
|
|
1396
1607
|
await this.updateRunSendProgress(runId, stats);
|
|
@@ -1435,7 +1646,9 @@ var RuleRunnerService = class {
|
|
|
1435
1646
|
const limit = rule.maxPerRun || this.config.options?.defaultMaxPerRun || 500;
|
|
1436
1647
|
let users;
|
|
1437
1648
|
try {
|
|
1438
|
-
|
|
1649
|
+
const collectionName = rule.target?.collection;
|
|
1650
|
+
const collectionSchema = collectionName ? this.config.collections?.find((c) => c.name === collectionName) : void 0;
|
|
1651
|
+
users = await this.config.adapters.queryUsers(rule.target, limit, collectionSchema ? { collectionSchema } : void 0);
|
|
1439
1652
|
} catch (err) {
|
|
1440
1653
|
this.logger.error(`Rule "${rule.name}": query failed`, { error: err });
|
|
1441
1654
|
stats.errorCount = 1;
|
|
@@ -1499,12 +1712,12 @@ var RuleRunnerService = class {
|
|
|
1499
1712
|
}
|
|
1500
1713
|
const lastSend = sendMap.get(userId);
|
|
1501
1714
|
if (lastSend) {
|
|
1502
|
-
if (rule.sendOnce &&
|
|
1715
|
+
if (rule.sendOnce && rule.resendAfterDays == null) {
|
|
1503
1716
|
stats.skipped++;
|
|
1504
1717
|
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "skipped", accountId: "", templateId, runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: "send once" });
|
|
1505
1718
|
continue;
|
|
1506
1719
|
}
|
|
1507
|
-
if (rule.resendAfterDays) {
|
|
1720
|
+
if (rule.resendAfterDays != null) {
|
|
1508
1721
|
const daysSince = (Date.now() - new Date(lastSend.sentAt).getTime()) / MS_PER_DAY;
|
|
1509
1722
|
if (daysSince < rule.resendAfterDays) {
|
|
1510
1723
|
stats.skipped++;
|
|
@@ -1516,6 +1729,14 @@ var RuleRunnerService = class {
|
|
|
1516
1729
|
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "skipped", accountId: "", templateId, runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: "send once" });
|
|
1517
1730
|
continue;
|
|
1518
1731
|
}
|
|
1732
|
+
if (rule.cooldownDays) {
|
|
1733
|
+
const daysSince = (Date.now() - new Date(lastSend.sentAt).getTime()) / MS_PER_DAY;
|
|
1734
|
+
if (daysSince < rule.cooldownDays) {
|
|
1735
|
+
stats.skipped++;
|
|
1736
|
+
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "skipped", accountId: "", templateId, runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: "cooldown period" });
|
|
1737
|
+
continue;
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1519
1740
|
}
|
|
1520
1741
|
const identifier = identifierMap.get(email.toLowerCase().trim());
|
|
1521
1742
|
if (!identifier) {
|
|
@@ -1606,7 +1827,7 @@ var RuleRunnerService = class {
|
|
|
1606
1827
|
lastSentDate: /* @__PURE__ */ new Date()
|
|
1607
1828
|
});
|
|
1608
1829
|
stats.sent++;
|
|
1609
|
-
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "sent", accountId: agentSelection.accountId, templateId, runId: runId || "", subjectIndex: si, bodyIndex: bi });
|
|
1830
|
+
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "sent", accountId: agentSelection.accountId, templateId, runId: runId || "", subjectIndex: si, bodyIndex: bi, preheaderIndex: pi });
|
|
1610
1831
|
totalProcessed++;
|
|
1611
1832
|
if (runId && totalProcessed % 10 === 0) {
|
|
1612
1833
|
await this.updateRunSendProgress(runId, stats);
|
|
@@ -1760,7 +1981,7 @@ var RuleRunnerService = class {
|
|
|
1760
1981
|
this.updateRunProgress(runId, { status: "failed" }).catch(() => {
|
|
1761
1982
|
});
|
|
1762
1983
|
});
|
|
1763
|
-
return { runId };
|
|
1984
|
+
return { runId, started: true };
|
|
1764
1985
|
}
|
|
1765
1986
|
buildThrottleMap(recentSends) {
|
|
1766
1987
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -1795,14 +2016,16 @@ function createTemplateController(templateService, options) {
|
|
|
1795
2016
|
const validAudiences = options?.audiences || Object.values(TEMPLATE_AUDIENCE);
|
|
1796
2017
|
async function list(req, res) {
|
|
1797
2018
|
try {
|
|
1798
|
-
const { category, audience, platform, isActive } = req.query;
|
|
1799
|
-
const templates = await templateService.list({
|
|
2019
|
+
const { category, audience, platform, isActive, page, limit } = req.query;
|
|
2020
|
+
const { templates, total } = await templateService.list({
|
|
1800
2021
|
category,
|
|
1801
2022
|
audience,
|
|
1802
2023
|
platform,
|
|
1803
|
-
isActive: isActive !== void 0 ? isActive === "true" : void 0
|
|
2024
|
+
isActive: isActive !== void 0 ? isActive === "true" : void 0,
|
|
2025
|
+
page: Math.max(1, parseInt(String(page), 10) || 1),
|
|
2026
|
+
limit: Math.min(parseInt(String(limit), 10) || 200, 500)
|
|
1804
2027
|
});
|
|
1805
|
-
res.json({ success: true, data: { templates } });
|
|
2028
|
+
res.json({ success: true, data: { templates, total } });
|
|
1806
2029
|
} catch (error) {
|
|
1807
2030
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1808
2031
|
res.status(500).json({ success: false, error: message });
|
|
@@ -1973,10 +2196,12 @@ function createRuleController(ruleService, options) {
|
|
|
1973
2196
|
const platformValues = options?.platforms;
|
|
1974
2197
|
const validAudiences = options?.audiences || Object.values(TEMPLATE_AUDIENCE);
|
|
1975
2198
|
const validEmailTypes = Object.values(EMAIL_TYPE);
|
|
1976
|
-
async function list(
|
|
2199
|
+
async function list(req, res) {
|
|
1977
2200
|
try {
|
|
1978
|
-
const
|
|
1979
|
-
|
|
2201
|
+
const page = Math.max(1, parseInt(String(req.query.page), 10) || 1);
|
|
2202
|
+
const limit = Math.min(parseInt(String(req.query.limit), 10) || 200, 500);
|
|
2203
|
+
const { rules, total } = await ruleService.list({ page, limit });
|
|
2204
|
+
res.json({ success: true, data: { rules, total } });
|
|
1980
2205
|
} catch (error) {
|
|
1981
2206
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1982
2207
|
res.status(500).json({ success: false, error: message });
|
|
@@ -2107,8 +2332,13 @@ function createRuleController(ruleService, options) {
|
|
|
2107
2332
|
try {
|
|
2108
2333
|
const limitParam = req.query.limit;
|
|
2109
2334
|
const limit = parseInt(String(Array.isArray(limitParam) ? limitParam[0] : limitParam), 10) || 20;
|
|
2110
|
-
const
|
|
2111
|
-
|
|
2335
|
+
const pageParam = req.query.page;
|
|
2336
|
+
const page = Math.max(1, parseInt(String(Array.isArray(pageParam) ? pageParam[0] : pageParam), 10) || 1);
|
|
2337
|
+
const from = req.query.from ? String(req.query.from) : void 0;
|
|
2338
|
+
const to = req.query.to ? String(req.query.to) : void 0;
|
|
2339
|
+
const logs = await ruleService.getRunHistory(limit, { page, from, to });
|
|
2340
|
+
const total = await ruleService.getRunHistoryCount({ from, to });
|
|
2341
|
+
res.json({ success: true, data: { logs, total } });
|
|
2112
2342
|
} catch (error) {
|
|
2113
2343
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
2114
2344
|
res.status(500).json({ success: false, error: message });
|
|
@@ -2223,7 +2453,7 @@ function createSendLogController(EmailRuleSend) {
|
|
|
2223
2453
|
const filter = {};
|
|
2224
2454
|
if (ruleId) filter.ruleId = ruleId;
|
|
2225
2455
|
if (status) filter.status = status;
|
|
2226
|
-
if (email) filter.
|
|
2456
|
+
if (email) filter.userId = { $regex: email, $options: "i" };
|
|
2227
2457
|
if (from || to) {
|
|
2228
2458
|
filter.sentAt = {};
|
|
2229
2459
|
if (from) filter.sentAt.$gte = new Date(from);
|
|
@@ -2260,6 +2490,7 @@ function createRoutes(deps) {
|
|
|
2260
2490
|
const runnerCtrl = createRunnerController(deps.runnerService, deps.EmailRuleRunLog);
|
|
2261
2491
|
const settingsCtrl = createSettingsController(deps.EmailThrottleConfig);
|
|
2262
2492
|
const sendLogCtrl = createSendLogController(deps.EmailRuleSend);
|
|
2493
|
+
const collectionCtrl = createCollectionController(deps.collections || []);
|
|
2263
2494
|
const templateRouter = Router();
|
|
2264
2495
|
templateRouter.get("/", templateCtrl.list);
|
|
2265
2496
|
templateRouter.post("/", templateCtrl.create);
|
|
@@ -2290,10 +2521,14 @@ function createRoutes(deps) {
|
|
|
2290
2521
|
runnerRouter.get("/logs", ruleCtrl.runHistory);
|
|
2291
2522
|
const sendLogRouter = Router();
|
|
2292
2523
|
sendLogRouter.get("/", sendLogCtrl.list);
|
|
2524
|
+
const collectionRouter = Router();
|
|
2525
|
+
collectionRouter.get("/", collectionCtrl.list);
|
|
2526
|
+
collectionRouter.get("/:name/fields", collectionCtrl.getFields);
|
|
2293
2527
|
router.use("/templates", templateRouter);
|
|
2294
2528
|
router.use("/rules", ruleRouter);
|
|
2295
2529
|
router.use("/runner", runnerRouter);
|
|
2296
2530
|
router.use("/sends", sendLogRouter);
|
|
2531
|
+
router.use("/collections", collectionRouter);
|
|
2297
2532
|
router.get("/throttle", settingsCtrl.getThrottleConfig);
|
|
2298
2533
|
router.put("/throttle", settingsCtrl.updateThrottleConfig);
|
|
2299
2534
|
return router;
|
|
@@ -2309,6 +2544,19 @@ var configSchema = z.object({
|
|
|
2309
2544
|
findIdentifier: z.function(),
|
|
2310
2545
|
sendTestEmail: z.function().optional()
|
|
2311
2546
|
}),
|
|
2547
|
+
collections: z.array(z.object({
|
|
2548
|
+
name: z.string(),
|
|
2549
|
+
label: z.string().optional(),
|
|
2550
|
+
description: z.string().optional(),
|
|
2551
|
+
identifierField: z.string().optional(),
|
|
2552
|
+
fields: z.array(z.any()),
|
|
2553
|
+
joins: z.array(z.object({
|
|
2554
|
+
from: z.string(),
|
|
2555
|
+
localField: z.string(),
|
|
2556
|
+
foreignField: z.string(),
|
|
2557
|
+
as: z.string()
|
|
2558
|
+
})).optional()
|
|
2559
|
+
})).optional(),
|
|
2312
2560
|
platforms: z.array(z.string()).optional(),
|
|
2313
2561
|
audiences: z.array(z.string()).optional(),
|
|
2314
2562
|
categories: z.array(z.string()).optional(),
|
|
@@ -2385,7 +2633,10 @@ var SchedulerService = class {
|
|
|
2385
2633
|
}, { connection: connectionOpts, prefix: this.keyPrefix });
|
|
2386
2634
|
}
|
|
2387
2635
|
async stopWorker() {
|
|
2388
|
-
|
|
2636
|
+
if (this.worker) {
|
|
2637
|
+
await this.worker.close();
|
|
2638
|
+
this.worker = void 0;
|
|
2639
|
+
}
|
|
2389
2640
|
}
|
|
2390
2641
|
async getScheduledJobs() {
|
|
2391
2642
|
const jobs = await this.queue.getRepeatableJobs();
|
|
@@ -2422,7 +2673,7 @@ function createEmailRuleEngine(config) {
|
|
|
2422
2673
|
`${prefix}EmailThrottleConfig`,
|
|
2423
2674
|
createEmailThrottleConfigSchema(prefix)
|
|
2424
2675
|
);
|
|
2425
|
-
const templateService = new TemplateService(EmailTemplate, config);
|
|
2676
|
+
const templateService = new TemplateService(EmailTemplate, config, EmailRule);
|
|
2426
2677
|
const ruleService = new RuleService(EmailRule, EmailTemplate, EmailRuleRunLog, config);
|
|
2427
2678
|
const runnerService = new RuleRunnerService(
|
|
2428
2679
|
EmailRule,
|
|
@@ -2442,7 +2693,8 @@ function createEmailRuleEngine(config) {
|
|
|
2442
2693
|
platformValues: config.platforms,
|
|
2443
2694
|
categoryValues: config.categories,
|
|
2444
2695
|
audienceValues: config.audiences,
|
|
2445
|
-
logger: config.logger
|
|
2696
|
+
logger: config.logger,
|
|
2697
|
+
collections: config.collections || []
|
|
2446
2698
|
});
|
|
2447
2699
|
return {
|
|
2448
2700
|
routes,
|
|
@@ -2453,6 +2705,6 @@ function createEmailRuleEngine(config) {
|
|
|
2453
2705
|
};
|
|
2454
2706
|
}
|
|
2455
2707
|
|
|
2456
|
-
export { AlxEmailError, ConfigValidationError, DuplicateSlugError, EMAIL_SEND_STATUS, EMAIL_TYPE, LockAcquisitionError, RULE_OPERATOR, RUN_LOG_STATUS, RUN_TRIGGER, RuleNotFoundError, RuleRunnerService, RuleService, RuleTemplateIncompatibleError, SchedulerService, TARGET_MODE, TEMPLATE_AUDIENCE, TEMPLATE_CATEGORY, THROTTLE_WINDOW, TemplateNotFoundError, TemplateRenderService, TemplateService, TemplateSyntaxError, createEmailRuleEngine, createEmailRuleRunLogSchema, createEmailRuleSchema, createEmailRuleSendSchema, createEmailTemplateSchema, createEmailThrottleConfigSchema, validateConfig };
|
|
2708
|
+
export { AlxEmailError, ConfigValidationError, DuplicateSlugError, EMAIL_SEND_STATUS, EMAIL_TYPE, FIELD_TYPE, LockAcquisitionError, RULE_OPERATOR, RUN_LOG_STATUS, RUN_TRIGGER, RuleNotFoundError, RuleRunnerService, RuleService, RuleTemplateIncompatibleError, SchedulerService, TARGET_MODE, TEMPLATE_AUDIENCE, TEMPLATE_CATEGORY, THROTTLE_WINDOW, TYPE_OPERATORS, TemplateNotFoundError, TemplateRenderService, TemplateService, TemplateSyntaxError, createEmailRuleEngine, createEmailRuleRunLogSchema, createEmailRuleSchema, createEmailRuleSendSchema, createEmailTemplateSchema, createEmailThrottleConfigSchema, flattenFields, validateConditions, validateConfig };
|
|
2457
2709
|
//# sourceMappingURL=index.mjs.map
|
|
2458
2710
|
//# sourceMappingURL=index.mjs.map
|