@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/README.md
CHANGED
|
@@ -96,6 +96,7 @@ The `createEmailRuleEngine(config)` factory accepts an `EmailRuleEngineConfig` o
|
|
|
96
96
|
| `db` | Yes | Mongoose connection and optional collection prefix |
|
|
97
97
|
| `redis` | Yes | ioredis connection for distributed locking |
|
|
98
98
|
| `adapters` | Yes | 5 required + 1 optional function bridging your app to the engine |
|
|
99
|
+
| `collections` | No | MongoDB collection schemas for field dropdowns, type-aware operators, and validation |
|
|
99
100
|
| `platforms` | No | Valid platform values for schema validation |
|
|
100
101
|
| `audiences` | No | Valid audience/role values for schema validation |
|
|
101
102
|
| `categories` | No | Valid template categories for schema validation |
|
|
@@ -114,6 +115,8 @@ See [docs/configuration.md](https://github.com/Hariprakash1997/astralib/blob/mai
|
|
|
114
115
|
5. [Execution Flow](https://github.com/Hariprakash1997/astralib/blob/main/packages/email/rule-engine/docs/execution-flow.md) — Understand how the runner processes rules
|
|
115
116
|
6. [Throttling](https://github.com/Hariprakash1997/astralib/blob/main/packages/email/rule-engine/docs/throttling.md) — Configure per-user send limits
|
|
116
117
|
|
|
118
|
+
7. [Collections](https://github.com/Hariprakash1997/astralib/blob/main/packages/email/rule-engine/docs/collections.md) — Register collection schemas for field dropdowns and type-aware validation
|
|
119
|
+
|
|
117
120
|
Reference: [API Routes](https://github.com/Hariprakash1997/astralib/blob/main/packages/email/rule-engine/docs/api-routes.md) | [Programmatic API](https://github.com/Hariprakash1997/astralib/blob/main/packages/email/rule-engine/docs/programmatic-api.md) | [Types](https://github.com/Hariprakash1997/astralib/blob/main/packages/email/rule-engine/docs/types.md) | [Constants](https://github.com/Hariprakash1997/astralib/blob/main/packages/email/rule-engine/docs/constants.md) | [Error Handling](https://github.com/Hariprakash1997/astralib/blob/main/packages/email/rule-engine/docs/error-handling.md)
|
|
118
121
|
|
|
119
122
|
### Redis Key Prefix (Required for Multi-Project Deployments)
|
package/dist/index.cjs
CHANGED
|
@@ -18,6 +18,26 @@ var crypto__default = /*#__PURE__*/_interopDefault(crypto);
|
|
|
18
18
|
|
|
19
19
|
// src/schemas/template.schema.ts
|
|
20
20
|
|
|
21
|
+
// src/constants/field-types.ts
|
|
22
|
+
var FIELD_TYPE = {
|
|
23
|
+
String: "string",
|
|
24
|
+
Number: "number",
|
|
25
|
+
Boolean: "boolean",
|
|
26
|
+
Date: "date",
|
|
27
|
+
ObjectId: "objectId",
|
|
28
|
+
Array: "array",
|
|
29
|
+
Object: "object"
|
|
30
|
+
};
|
|
31
|
+
var TYPE_OPERATORS = {
|
|
32
|
+
string: ["eq", "neq", "contains", "in", "not_in", "exists", "not_exists"],
|
|
33
|
+
number: ["eq", "neq", "gt", "gte", "lt", "lte", "in", "not_in", "exists", "not_exists"],
|
|
34
|
+
boolean: ["eq", "neq", "exists", "not_exists"],
|
|
35
|
+
date: ["eq", "neq", "gt", "gte", "lt", "lte", "exists", "not_exists"],
|
|
36
|
+
objectId: ["eq", "neq", "in", "not_in", "exists", "not_exists"],
|
|
37
|
+
array: ["contains", "in", "not_in", "exists", "not_exists"],
|
|
38
|
+
object: ["exists", "not_exists"]
|
|
39
|
+
};
|
|
40
|
+
|
|
21
41
|
// src/constants/index.ts
|
|
22
42
|
var TEMPLATE_CATEGORY = {
|
|
23
43
|
Onboarding: "onboarding",
|
|
@@ -173,7 +193,8 @@ function createEmailRuleSchema(platformValues, audienceValues, collectionPrefix)
|
|
|
173
193
|
...platformValues ? { enum: platformValues } : {}
|
|
174
194
|
},
|
|
175
195
|
conditions: [RuleConditionSchema],
|
|
176
|
-
identifiers: [{ type: String }]
|
|
196
|
+
identifiers: [{ type: String }],
|
|
197
|
+
collection: { type: String }
|
|
177
198
|
}, { _id: false });
|
|
178
199
|
const RuleRunStatsSchema = new mongoose.Schema({
|
|
179
200
|
matched: { type: Number, default: 0 },
|
|
@@ -264,9 +285,9 @@ function createEmailRuleSendSchema(collectionPrefix) {
|
|
|
264
285
|
const schema = new mongoose.Schema(
|
|
265
286
|
{
|
|
266
287
|
ruleId: { type: mongoose.Schema.Types.ObjectId, ref: "EmailRule", required: true },
|
|
267
|
-
userId: { type:
|
|
268
|
-
emailIdentifierId: { type:
|
|
269
|
-
messageId: { type:
|
|
288
|
+
userId: { type: String, required: true },
|
|
289
|
+
emailIdentifierId: { type: String },
|
|
290
|
+
messageId: { type: String },
|
|
270
291
|
sentAt: { type: Date, required: true, default: () => /* @__PURE__ */ new Date() },
|
|
271
292
|
status: { type: String },
|
|
272
293
|
accountId: { type: String },
|
|
@@ -306,6 +327,7 @@ function createEmailRuleSendSchema(collectionPrefix) {
|
|
|
306
327
|
schema.index({ ruleId: 1, userId: 1, sentAt: -1 });
|
|
307
328
|
schema.index({ userId: 1, sentAt: -1 });
|
|
308
329
|
schema.index({ ruleId: 1, sentAt: -1 });
|
|
330
|
+
schema.index({ status: 1, sentAt: -1 });
|
|
309
331
|
return schema;
|
|
310
332
|
}
|
|
311
333
|
function createEmailRuleRunLogSchema(collectionPrefix) {
|
|
@@ -348,8 +370,7 @@ function createEmailRuleRunLogSchema(collectionPrefix) {
|
|
|
348
370
|
}
|
|
349
371
|
}
|
|
350
372
|
);
|
|
351
|
-
schema.index({ runAt: -1 });
|
|
352
|
-
schema.index({ runAt: 1 }, { expireAfterSeconds: 90 * 86400 });
|
|
373
|
+
schema.index({ runAt: -1 }, { expireAfterSeconds: 90 * 86400 });
|
|
353
374
|
return schema;
|
|
354
375
|
}
|
|
355
376
|
function createEmailThrottleConfigSchema(collectionPrefix) {
|
|
@@ -476,15 +497,15 @@ var TemplateRenderService = class {
|
|
|
476
497
|
ensureHelpers();
|
|
477
498
|
}
|
|
478
499
|
renderSingle(subject, body, data, textBody) {
|
|
479
|
-
const subjectFn = Handlebars__default.default.compile(subject, { strict:
|
|
500
|
+
const subjectFn = Handlebars__default.default.compile(subject, { strict: false });
|
|
480
501
|
const resolvedSubject = subjectFn(data);
|
|
481
|
-
const bodyFn = Handlebars__default.default.compile(body, { strict:
|
|
502
|
+
const bodyFn = Handlebars__default.default.compile(body, { strict: false });
|
|
482
503
|
const resolvedBody = bodyFn(data);
|
|
483
504
|
const mjmlSource = wrapInMjml(resolvedBody);
|
|
484
505
|
const html = compileMjml(mjmlSource);
|
|
485
506
|
let text;
|
|
486
507
|
if (textBody) {
|
|
487
|
-
const textFn = Handlebars__default.default.compile(textBody, { strict:
|
|
508
|
+
const textFn = Handlebars__default.default.compile(textBody, { strict: false });
|
|
488
509
|
text = textFn(data);
|
|
489
510
|
} else {
|
|
490
511
|
text = htmlToPlainText(html);
|
|
@@ -494,20 +515,20 @@ var TemplateRenderService = class {
|
|
|
494
515
|
compileBatch(subject, body, textBody) {
|
|
495
516
|
const mjmlSource = wrapInMjml(body);
|
|
496
517
|
const htmlWithHandlebars = compileMjml(mjmlSource);
|
|
497
|
-
const subjectFn = Handlebars__default.default.compile(subject, { strict:
|
|
498
|
-
const bodyFn = Handlebars__default.default.compile(htmlWithHandlebars, { strict:
|
|
499
|
-
const textBodyFn = textBody ? Handlebars__default.default.compile(textBody, { strict:
|
|
518
|
+
const subjectFn = Handlebars__default.default.compile(subject, { strict: false });
|
|
519
|
+
const bodyFn = Handlebars__default.default.compile(htmlWithHandlebars, { strict: false });
|
|
520
|
+
const textBodyFn = textBody ? Handlebars__default.default.compile(textBody, { strict: false }) : void 0;
|
|
500
521
|
return { subjectFn, bodyFn, textBodyFn };
|
|
501
522
|
}
|
|
502
523
|
compileBatchVariants(subjects, bodies, textBody, preheaders) {
|
|
503
|
-
const subjectFns = subjects.map((s) => Handlebars__default.default.compile(s, { strict:
|
|
524
|
+
const subjectFns = subjects.map((s) => Handlebars__default.default.compile(s, { strict: false }));
|
|
504
525
|
const bodyFns = bodies.map((b) => {
|
|
505
526
|
const mjmlSource = wrapInMjml(b);
|
|
506
527
|
const htmlWithHandlebars = compileMjml(mjmlSource);
|
|
507
|
-
return Handlebars__default.default.compile(htmlWithHandlebars, { strict:
|
|
528
|
+
return Handlebars__default.default.compile(htmlWithHandlebars, { strict: false });
|
|
508
529
|
});
|
|
509
|
-
const textBodyFn = textBody ? Handlebars__default.default.compile(textBody, { strict:
|
|
510
|
-
const preheaderFns = preheaders && preheaders.length > 0 ? preheaders.map((p) => Handlebars__default.default.compile(p, { strict:
|
|
530
|
+
const textBodyFn = textBody ? Handlebars__default.default.compile(textBody, { strict: false }) : void 0;
|
|
531
|
+
const preheaderFns = preheaders && preheaders.length > 0 ? preheaders.map((p) => Handlebars__default.default.compile(p, { strict: false })) : void 0;
|
|
511
532
|
return { subjectFns, bodyFns, textBodyFn, preheaderFns };
|
|
512
533
|
}
|
|
513
534
|
renderFromCompiled(compiled, data) {
|
|
@@ -646,9 +667,10 @@ function slugify(name) {
|
|
|
646
667
|
return name.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
647
668
|
}
|
|
648
669
|
var TemplateService = class {
|
|
649
|
-
constructor(EmailTemplate, config) {
|
|
670
|
+
constructor(EmailTemplate, config, EmailRule) {
|
|
650
671
|
this.EmailTemplate = EmailTemplate;
|
|
651
672
|
this.config = config;
|
|
673
|
+
this.EmailRule = EmailRule;
|
|
652
674
|
}
|
|
653
675
|
renderService = new TemplateRenderService();
|
|
654
676
|
async list(filters) {
|
|
@@ -657,7 +679,14 @@ var TemplateService = class {
|
|
|
657
679
|
if (filters?.audience) query["audience"] = filters.audience;
|
|
658
680
|
if (filters?.platform) query["platform"] = filters.platform;
|
|
659
681
|
if (filters?.isActive !== void 0) query["isActive"] = filters.isActive;
|
|
660
|
-
|
|
682
|
+
const page = filters?.page ?? 1;
|
|
683
|
+
const limit = filters?.limit ?? 200;
|
|
684
|
+
const skip = (page - 1) * limit;
|
|
685
|
+
const [templates, total] = await Promise.all([
|
|
686
|
+
this.EmailTemplate.find(query).sort({ category: 1, name: 1 }).skip(skip).limit(limit),
|
|
687
|
+
this.EmailTemplate.countDocuments(query)
|
|
688
|
+
]);
|
|
689
|
+
return { templates, total };
|
|
661
690
|
}
|
|
662
691
|
async getById(id) {
|
|
663
692
|
return this.EmailTemplate.findById(id);
|
|
@@ -740,6 +769,13 @@ var TemplateService = class {
|
|
|
740
769
|
);
|
|
741
770
|
}
|
|
742
771
|
async delete(id) {
|
|
772
|
+
if (this.EmailRule) {
|
|
773
|
+
const activeRules = await this.EmailRule.find({ templateId: id, isActive: true });
|
|
774
|
+
if (activeRules.length > 0) {
|
|
775
|
+
const names = activeRules.map((r) => r.name).join(", ");
|
|
776
|
+
throw new Error(`Cannot delete template: ${activeRules.length} active rule(s) reference it (${names}). Deactivate them first.`);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
743
779
|
const result = await this.EmailTemplate.findByIdAndDelete(id);
|
|
744
780
|
return result !== null;
|
|
745
781
|
}
|
|
@@ -819,7 +855,8 @@ var TemplateService = class {
|
|
|
819
855
|
async previewWithRecipient(templateId, recipientData) {
|
|
820
856
|
const template = await this.EmailTemplate.findById(templateId);
|
|
821
857
|
if (!template) return null;
|
|
822
|
-
const
|
|
858
|
+
const variables = template.variables ?? [];
|
|
859
|
+
const data = this._buildSampleData(variables, { ...template.fields ?? {}, ...recipientData });
|
|
823
860
|
return this.renderService.renderPreview(
|
|
824
861
|
template.subjects[0],
|
|
825
862
|
template.bodies[0],
|
|
@@ -829,6 +866,125 @@ var TemplateService = class {
|
|
|
829
866
|
}
|
|
830
867
|
};
|
|
831
868
|
|
|
869
|
+
// src/controllers/collection.controller.ts
|
|
870
|
+
function flattenFields(fields, prefix = "", parentIsArray = false) {
|
|
871
|
+
const result = [];
|
|
872
|
+
for (const field of fields) {
|
|
873
|
+
const path = prefix ? `${prefix}.${field.name}` : field.name;
|
|
874
|
+
const isArray = field.type === "array";
|
|
875
|
+
if (field.type === "object" && field.fields?.length) {
|
|
876
|
+
result.push({
|
|
877
|
+
path,
|
|
878
|
+
type: "object",
|
|
879
|
+
label: field.label,
|
|
880
|
+
description: field.description
|
|
881
|
+
});
|
|
882
|
+
result.push(...flattenFields(field.fields, path, false));
|
|
883
|
+
} else if (isArray && field.fields?.length) {
|
|
884
|
+
result.push({
|
|
885
|
+
path: `${path}[]`,
|
|
886
|
+
type: "array",
|
|
887
|
+
label: field.label,
|
|
888
|
+
description: field.description,
|
|
889
|
+
isArray: true
|
|
890
|
+
});
|
|
891
|
+
result.push(...flattenFields(field.fields, `${path}[]`, true));
|
|
892
|
+
} else {
|
|
893
|
+
result.push({
|
|
894
|
+
path,
|
|
895
|
+
type: field.type,
|
|
896
|
+
label: field.label,
|
|
897
|
+
description: field.description,
|
|
898
|
+
enumValues: field.enumValues,
|
|
899
|
+
isArray: parentIsArray || isArray
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
return result;
|
|
904
|
+
}
|
|
905
|
+
function createCollectionController(collections) {
|
|
906
|
+
return {
|
|
907
|
+
list(_req, res) {
|
|
908
|
+
const summary = collections.map((c) => ({
|
|
909
|
+
name: c.name,
|
|
910
|
+
label: c.label,
|
|
911
|
+
description: c.description,
|
|
912
|
+
identifierField: c.identifierField,
|
|
913
|
+
fieldCount: c.fields.length,
|
|
914
|
+
joinCount: c.joins?.length ?? 0
|
|
915
|
+
}));
|
|
916
|
+
res.json({ collections: summary });
|
|
917
|
+
},
|
|
918
|
+
getFields(req, res) {
|
|
919
|
+
const { name } = req.params;
|
|
920
|
+
const collection = collections.find((c) => c.name === name);
|
|
921
|
+
if (!collection) {
|
|
922
|
+
res.status(404).json({ error: `Collection "${name}" not found` });
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
const fields = flattenFields(collection.fields);
|
|
926
|
+
if (collection.joins?.length) {
|
|
927
|
+
for (const join of collection.joins) {
|
|
928
|
+
const joinedCollection = collections.find((c) => c.name === join.from);
|
|
929
|
+
if (joinedCollection) {
|
|
930
|
+
const joinedFields = flattenFields(joinedCollection.fields, join.as);
|
|
931
|
+
fields.push(...joinedFields);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
res.json({
|
|
936
|
+
name: collection.name,
|
|
937
|
+
label: collection.label,
|
|
938
|
+
identifierField: collection.identifierField,
|
|
939
|
+
fields,
|
|
940
|
+
typeOperators: TYPE_OPERATORS
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// src/validation/condition.validator.ts
|
|
947
|
+
function validateConditions(conditions, collectionName, collections) {
|
|
948
|
+
if (!collectionName || collections.length === 0) return [];
|
|
949
|
+
const collection = collections.find((c) => c.name === collectionName);
|
|
950
|
+
if (!collection) return [];
|
|
951
|
+
const flatFields = flattenFields(collection.fields);
|
|
952
|
+
if (collection.joins?.length) {
|
|
953
|
+
for (const join of collection.joins) {
|
|
954
|
+
const joinedCollection = collections.find((c) => c.name === join.from);
|
|
955
|
+
if (joinedCollection) {
|
|
956
|
+
flatFields.push(...flattenFields(joinedCollection.fields, join.as));
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
const fieldMap = /* @__PURE__ */ new Map();
|
|
961
|
+
for (const f of flatFields) {
|
|
962
|
+
fieldMap.set(f.path, f);
|
|
963
|
+
}
|
|
964
|
+
const errors = [];
|
|
965
|
+
for (let i = 0; i < conditions.length; i++) {
|
|
966
|
+
const cond = conditions[i];
|
|
967
|
+
const fieldDef = fieldMap.get(cond.field);
|
|
968
|
+
if (!fieldDef) {
|
|
969
|
+
errors.push({
|
|
970
|
+
index: i,
|
|
971
|
+
field: cond.field,
|
|
972
|
+
message: `Field "${cond.field}" does not exist in collection "${collectionName}"`
|
|
973
|
+
});
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
const allowedOps = TYPE_OPERATORS[fieldDef.type];
|
|
977
|
+
if (allowedOps && !allowedOps.includes(cond.operator)) {
|
|
978
|
+
errors.push({
|
|
979
|
+
index: i,
|
|
980
|
+
field: cond.field,
|
|
981
|
+
message: `Operator "${cond.operator}" is not valid for field type "${fieldDef.type}"`
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
return errors;
|
|
986
|
+
}
|
|
987
|
+
|
|
832
988
|
// src/services/rule.service.ts
|
|
833
989
|
function isQueryTarget(target) {
|
|
834
990
|
return !target.mode || target.mode === "query";
|
|
@@ -879,8 +1035,15 @@ var RuleService = class {
|
|
|
879
1035
|
this.EmailRuleRunLog = EmailRuleRunLog;
|
|
880
1036
|
this.config = config;
|
|
881
1037
|
}
|
|
882
|
-
async list() {
|
|
883
|
-
|
|
1038
|
+
async list(opts) {
|
|
1039
|
+
const page = opts?.page ?? 1;
|
|
1040
|
+
const limit = opts?.limit ?? 200;
|
|
1041
|
+
const skip = (page - 1) * limit;
|
|
1042
|
+
const [rules, total] = await Promise.all([
|
|
1043
|
+
this.EmailRule.find().populate("templateId", "name slug").sort({ sortOrder: 1, createdAt: -1 }).skip(skip).limit(limit),
|
|
1044
|
+
this.EmailRule.countDocuments()
|
|
1045
|
+
]);
|
|
1046
|
+
return { rules, total };
|
|
884
1047
|
}
|
|
885
1048
|
async getById(id) {
|
|
886
1049
|
return this.EmailRule.findById(id);
|
|
@@ -907,6 +1070,14 @@ var RuleService = class {
|
|
|
907
1070
|
throw new RuleTemplateIncompatibleError("target.identifiers must be a non-empty array for list mode, validation failed");
|
|
908
1071
|
}
|
|
909
1072
|
}
|
|
1073
|
+
if (isQueryTarget(input.target) && input.target.collection && this.config.collections?.length) {
|
|
1074
|
+
const condErrors = validateConditions(input.target.conditions, input.target.collection, this.config.collections);
|
|
1075
|
+
if (condErrors.length > 0) {
|
|
1076
|
+
throw new RuleTemplateIncompatibleError(
|
|
1077
|
+
`Invalid conditions: ${condErrors.map((e) => e.message).join("; ")}`
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
910
1081
|
return this.EmailRule.createRule(input);
|
|
911
1082
|
}
|
|
912
1083
|
async update(id, input) {
|
|
@@ -936,6 +1107,17 @@ var RuleService = class {
|
|
|
936
1107
|
throw new RuleTemplateIncompatibleError(compatError);
|
|
937
1108
|
}
|
|
938
1109
|
}
|
|
1110
|
+
if (isQueryTarget(effectiveTarget)) {
|
|
1111
|
+
const qt = effectiveTarget;
|
|
1112
|
+
if (qt.collection && this.config.collections?.length) {
|
|
1113
|
+
const condErrors = validateConditions(qt.conditions || [], qt.collection, this.config.collections);
|
|
1114
|
+
if (condErrors.length > 0) {
|
|
1115
|
+
throw new RuleTemplateIncompatibleError(
|
|
1116
|
+
`Invalid conditions: ${condErrors.map((e) => e.message).join("; ")}`
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
939
1121
|
const setFields = {};
|
|
940
1122
|
for (const [key, value] of Object.entries(input)) {
|
|
941
1123
|
if (value !== void 0 && UPDATEABLE_FIELDS2.has(key)) {
|
|
@@ -990,7 +1172,10 @@ var RuleService = class {
|
|
|
990
1172
|
const sample2 = identifiers.slice(0, 10).map((id2) => ({ email: id2 }));
|
|
991
1173
|
return { matchedCount: matchedCount2, effectiveLimit, willProcess: willProcess2, ruleId: id, sample: sample2 };
|
|
992
1174
|
}
|
|
993
|
-
const
|
|
1175
|
+
const queryTarget = rule.target;
|
|
1176
|
+
const collectionName = queryTarget.collection;
|
|
1177
|
+
const collectionSchema = collectionName ? this.config.collections?.find((c) => c.name === collectionName) : void 0;
|
|
1178
|
+
const users = await this.config.adapters.queryUsers(rule.target, 5e4, collectionSchema ? { collectionSchema } : void 0);
|
|
994
1179
|
const matchedCount = users.length;
|
|
995
1180
|
const willProcess = Math.min(matchedCount, effectiveLimit);
|
|
996
1181
|
const sample = users.slice(0, 10).map((u) => ({
|
|
@@ -1012,8 +1197,25 @@ var RuleService = class {
|
|
|
1012
1197
|
rest.lastRunStats = void 0;
|
|
1013
1198
|
return this.EmailRule.create(rest);
|
|
1014
1199
|
}
|
|
1015
|
-
async getRunHistory(limit = 20) {
|
|
1016
|
-
|
|
1200
|
+
async getRunHistory(limit = 20, opts) {
|
|
1201
|
+
const filter = {};
|
|
1202
|
+
if (opts?.from || opts?.to) {
|
|
1203
|
+
filter.runAt = {};
|
|
1204
|
+
if (opts.from) filter.runAt["$gte"] = new Date(opts.from);
|
|
1205
|
+
if (opts.to) filter.runAt["$lte"] = /* @__PURE__ */ new Date(opts.to + "T23:59:59.999Z");
|
|
1206
|
+
}
|
|
1207
|
+
const page = opts?.page ?? 1;
|
|
1208
|
+
const skip = (page - 1) * limit;
|
|
1209
|
+
return this.EmailRuleRunLog.find(filter).sort({ runAt: -1 }).skip(skip).limit(limit);
|
|
1210
|
+
}
|
|
1211
|
+
async getRunHistoryCount(opts) {
|
|
1212
|
+
const filter = {};
|
|
1213
|
+
if (opts?.from || opts?.to) {
|
|
1214
|
+
filter.runAt = {};
|
|
1215
|
+
if (opts.from) filter.runAt["$gte"] = new Date(opts.from);
|
|
1216
|
+
if (opts.to) filter.runAt["$lte"] = /* @__PURE__ */ new Date(opts.to + "T23:59:59.999Z");
|
|
1217
|
+
}
|
|
1218
|
+
return this.EmailRuleRunLog.countDocuments(filter);
|
|
1017
1219
|
}
|
|
1018
1220
|
};
|
|
1019
1221
|
var MS_PER_DAY = 864e5;
|
|
@@ -1083,6 +1285,7 @@ var RuleRunnerService = class {
|
|
|
1083
1285
|
const lockAcquired = await this.lock.acquire();
|
|
1084
1286
|
if (!lockAcquired) {
|
|
1085
1287
|
this.logger.warn("Rule runner already executing, skipping");
|
|
1288
|
+
await this.updateRunProgress(runId, { status: "failed", currentRule: "Another run is already in progress" });
|
|
1086
1289
|
return { runId };
|
|
1087
1290
|
}
|
|
1088
1291
|
const runStartTime = Date.now();
|
|
@@ -1295,12 +1498,12 @@ var RuleRunnerService = class {
|
|
|
1295
1498
|
const dedupKey = identifier.id;
|
|
1296
1499
|
const lastSend = sendMap.get(dedupKey);
|
|
1297
1500
|
if (lastSend) {
|
|
1298
|
-
if (rule.sendOnce &&
|
|
1501
|
+
if (rule.sendOnce && rule.resendAfterDays == null) {
|
|
1299
1502
|
stats.skipped++;
|
|
1300
1503
|
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "skipped", accountId: "", templateId, runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: "send once" });
|
|
1301
1504
|
continue;
|
|
1302
1505
|
}
|
|
1303
|
-
if (rule.resendAfterDays) {
|
|
1506
|
+
if (rule.resendAfterDays != null) {
|
|
1304
1507
|
const daysSince = (Date.now() - new Date(lastSend.sentAt).getTime()) / MS_PER_DAY;
|
|
1305
1508
|
if (daysSince < rule.resendAfterDays) {
|
|
1306
1509
|
stats.skipped++;
|
|
@@ -1312,6 +1515,14 @@ var RuleRunnerService = class {
|
|
|
1312
1515
|
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "skipped", accountId: "", templateId, runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: "send once" });
|
|
1313
1516
|
continue;
|
|
1314
1517
|
}
|
|
1518
|
+
if (rule.cooldownDays) {
|
|
1519
|
+
const daysSince = (Date.now() - new Date(lastSend.sentAt).getTime()) / MS_PER_DAY;
|
|
1520
|
+
if (daysSince < rule.cooldownDays) {
|
|
1521
|
+
stats.skipped++;
|
|
1522
|
+
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "skipped", accountId: "", templateId, runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: "cooldown period" });
|
|
1523
|
+
continue;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1315
1526
|
}
|
|
1316
1527
|
if (!this.checkThrottle(rule, dedupKey, email, throttleMap, throttleConfig, stats, templateId, runId)) continue;
|
|
1317
1528
|
const agentSelection = await this.config.adapters.selectAgent(identifier.id, { ruleId, templateId });
|
|
@@ -1397,7 +1608,7 @@ var RuleRunnerService = class {
|
|
|
1397
1608
|
lastSentDate: /* @__PURE__ */ new Date()
|
|
1398
1609
|
});
|
|
1399
1610
|
stats.sent++;
|
|
1400
|
-
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "sent", accountId: agentSelection.accountId, templateId, runId: runId || "", subjectIndex: si, bodyIndex: bi });
|
|
1611
|
+
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "sent", accountId: agentSelection.accountId, templateId, runId: runId || "", subjectIndex: si, bodyIndex: bi, preheaderIndex: pi });
|
|
1401
1612
|
totalProcessed++;
|
|
1402
1613
|
if (runId && totalProcessed % 10 === 0) {
|
|
1403
1614
|
await this.updateRunSendProgress(runId, stats);
|
|
@@ -1442,7 +1653,9 @@ var RuleRunnerService = class {
|
|
|
1442
1653
|
const limit = rule.maxPerRun || this.config.options?.defaultMaxPerRun || 500;
|
|
1443
1654
|
let users;
|
|
1444
1655
|
try {
|
|
1445
|
-
|
|
1656
|
+
const collectionName = rule.target?.collection;
|
|
1657
|
+
const collectionSchema = collectionName ? this.config.collections?.find((c) => c.name === collectionName) : void 0;
|
|
1658
|
+
users = await this.config.adapters.queryUsers(rule.target, limit, collectionSchema ? { collectionSchema } : void 0);
|
|
1446
1659
|
} catch (err) {
|
|
1447
1660
|
this.logger.error(`Rule "${rule.name}": query failed`, { error: err });
|
|
1448
1661
|
stats.errorCount = 1;
|
|
@@ -1506,12 +1719,12 @@ var RuleRunnerService = class {
|
|
|
1506
1719
|
}
|
|
1507
1720
|
const lastSend = sendMap.get(userId);
|
|
1508
1721
|
if (lastSend) {
|
|
1509
|
-
if (rule.sendOnce &&
|
|
1722
|
+
if (rule.sendOnce && rule.resendAfterDays == null) {
|
|
1510
1723
|
stats.skipped++;
|
|
1511
1724
|
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "skipped", accountId: "", templateId, runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: "send once" });
|
|
1512
1725
|
continue;
|
|
1513
1726
|
}
|
|
1514
|
-
if (rule.resendAfterDays) {
|
|
1727
|
+
if (rule.resendAfterDays != null) {
|
|
1515
1728
|
const daysSince = (Date.now() - new Date(lastSend.sentAt).getTime()) / MS_PER_DAY;
|
|
1516
1729
|
if (daysSince < rule.resendAfterDays) {
|
|
1517
1730
|
stats.skipped++;
|
|
@@ -1523,6 +1736,14 @@ var RuleRunnerService = class {
|
|
|
1523
1736
|
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "skipped", accountId: "", templateId, runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: "send once" });
|
|
1524
1737
|
continue;
|
|
1525
1738
|
}
|
|
1739
|
+
if (rule.cooldownDays) {
|
|
1740
|
+
const daysSince = (Date.now() - new Date(lastSend.sentAt).getTime()) / MS_PER_DAY;
|
|
1741
|
+
if (daysSince < rule.cooldownDays) {
|
|
1742
|
+
stats.skipped++;
|
|
1743
|
+
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "skipped", accountId: "", templateId, runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: "cooldown period" });
|
|
1744
|
+
continue;
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1526
1747
|
}
|
|
1527
1748
|
const identifier = identifierMap.get(email.toLowerCase().trim());
|
|
1528
1749
|
if (!identifier) {
|
|
@@ -1613,7 +1834,7 @@ var RuleRunnerService = class {
|
|
|
1613
1834
|
lastSentDate: /* @__PURE__ */ new Date()
|
|
1614
1835
|
});
|
|
1615
1836
|
stats.sent++;
|
|
1616
|
-
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "sent", accountId: agentSelection.accountId, templateId, runId: runId || "", subjectIndex: si, bodyIndex: bi });
|
|
1837
|
+
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "sent", accountId: agentSelection.accountId, templateId, runId: runId || "", subjectIndex: si, bodyIndex: bi, preheaderIndex: pi });
|
|
1617
1838
|
totalProcessed++;
|
|
1618
1839
|
if (runId && totalProcessed % 10 === 0) {
|
|
1619
1840
|
await this.updateRunSendProgress(runId, stats);
|
|
@@ -1767,7 +1988,7 @@ var RuleRunnerService = class {
|
|
|
1767
1988
|
this.updateRunProgress(runId, { status: "failed" }).catch(() => {
|
|
1768
1989
|
});
|
|
1769
1990
|
});
|
|
1770
|
-
return { runId };
|
|
1991
|
+
return { runId, started: true };
|
|
1771
1992
|
}
|
|
1772
1993
|
buildThrottleMap(recentSends) {
|
|
1773
1994
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -1802,14 +2023,16 @@ function createTemplateController(templateService, options) {
|
|
|
1802
2023
|
const validAudiences = options?.audiences || Object.values(TEMPLATE_AUDIENCE);
|
|
1803
2024
|
async function list(req, res) {
|
|
1804
2025
|
try {
|
|
1805
|
-
const { category, audience, platform, isActive } = req.query;
|
|
1806
|
-
const templates = await templateService.list({
|
|
2026
|
+
const { category, audience, platform, isActive, page, limit } = req.query;
|
|
2027
|
+
const { templates, total } = await templateService.list({
|
|
1807
2028
|
category,
|
|
1808
2029
|
audience,
|
|
1809
2030
|
platform,
|
|
1810
|
-
isActive: isActive !== void 0 ? isActive === "true" : void 0
|
|
2031
|
+
isActive: isActive !== void 0 ? isActive === "true" : void 0,
|
|
2032
|
+
page: Math.max(1, parseInt(String(page), 10) || 1),
|
|
2033
|
+
limit: Math.min(parseInt(String(limit), 10) || 200, 500)
|
|
1811
2034
|
});
|
|
1812
|
-
res.json({ success: true, data: { templates } });
|
|
2035
|
+
res.json({ success: true, data: { templates, total } });
|
|
1813
2036
|
} catch (error) {
|
|
1814
2037
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1815
2038
|
res.status(500).json({ success: false, error: message });
|
|
@@ -1980,10 +2203,12 @@ function createRuleController(ruleService, options) {
|
|
|
1980
2203
|
const platformValues = options?.platforms;
|
|
1981
2204
|
const validAudiences = options?.audiences || Object.values(TEMPLATE_AUDIENCE);
|
|
1982
2205
|
const validEmailTypes = Object.values(EMAIL_TYPE);
|
|
1983
|
-
async function list(
|
|
2206
|
+
async function list(req, res) {
|
|
1984
2207
|
try {
|
|
1985
|
-
const
|
|
1986
|
-
|
|
2208
|
+
const page = Math.max(1, parseInt(String(req.query.page), 10) || 1);
|
|
2209
|
+
const limit = Math.min(parseInt(String(req.query.limit), 10) || 200, 500);
|
|
2210
|
+
const { rules, total } = await ruleService.list({ page, limit });
|
|
2211
|
+
res.json({ success: true, data: { rules, total } });
|
|
1987
2212
|
} catch (error) {
|
|
1988
2213
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1989
2214
|
res.status(500).json({ success: false, error: message });
|
|
@@ -2114,8 +2339,13 @@ function createRuleController(ruleService, options) {
|
|
|
2114
2339
|
try {
|
|
2115
2340
|
const limitParam = req.query.limit;
|
|
2116
2341
|
const limit = parseInt(String(Array.isArray(limitParam) ? limitParam[0] : limitParam), 10) || 20;
|
|
2117
|
-
const
|
|
2118
|
-
|
|
2342
|
+
const pageParam = req.query.page;
|
|
2343
|
+
const page = Math.max(1, parseInt(String(Array.isArray(pageParam) ? pageParam[0] : pageParam), 10) || 1);
|
|
2344
|
+
const from = req.query.from ? String(req.query.from) : void 0;
|
|
2345
|
+
const to = req.query.to ? String(req.query.to) : void 0;
|
|
2346
|
+
const logs = await ruleService.getRunHistory(limit, { page, from, to });
|
|
2347
|
+
const total = await ruleService.getRunHistoryCount({ from, to });
|
|
2348
|
+
res.json({ success: true, data: { logs, total } });
|
|
2119
2349
|
} catch (error) {
|
|
2120
2350
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
2121
2351
|
res.status(500).json({ success: false, error: message });
|
|
@@ -2230,7 +2460,7 @@ function createSendLogController(EmailRuleSend) {
|
|
|
2230
2460
|
const filter = {};
|
|
2231
2461
|
if (ruleId) filter.ruleId = ruleId;
|
|
2232
2462
|
if (status) filter.status = status;
|
|
2233
|
-
if (email) filter.
|
|
2463
|
+
if (email) filter.userId = { $regex: email, $options: "i" };
|
|
2234
2464
|
if (from || to) {
|
|
2235
2465
|
filter.sentAt = {};
|
|
2236
2466
|
if (from) filter.sentAt.$gte = new Date(from);
|
|
@@ -2267,6 +2497,7 @@ function createRoutes(deps) {
|
|
|
2267
2497
|
const runnerCtrl = createRunnerController(deps.runnerService, deps.EmailRuleRunLog);
|
|
2268
2498
|
const settingsCtrl = createSettingsController(deps.EmailThrottleConfig);
|
|
2269
2499
|
const sendLogCtrl = createSendLogController(deps.EmailRuleSend);
|
|
2500
|
+
const collectionCtrl = createCollectionController(deps.collections || []);
|
|
2270
2501
|
const templateRouter = express.Router();
|
|
2271
2502
|
templateRouter.get("/", templateCtrl.list);
|
|
2272
2503
|
templateRouter.post("/", templateCtrl.create);
|
|
@@ -2297,10 +2528,14 @@ function createRoutes(deps) {
|
|
|
2297
2528
|
runnerRouter.get("/logs", ruleCtrl.runHistory);
|
|
2298
2529
|
const sendLogRouter = express.Router();
|
|
2299
2530
|
sendLogRouter.get("/", sendLogCtrl.list);
|
|
2531
|
+
const collectionRouter = express.Router();
|
|
2532
|
+
collectionRouter.get("/", collectionCtrl.list);
|
|
2533
|
+
collectionRouter.get("/:name/fields", collectionCtrl.getFields);
|
|
2300
2534
|
router.use("/templates", templateRouter);
|
|
2301
2535
|
router.use("/rules", ruleRouter);
|
|
2302
2536
|
router.use("/runner", runnerRouter);
|
|
2303
2537
|
router.use("/sends", sendLogRouter);
|
|
2538
|
+
router.use("/collections", collectionRouter);
|
|
2304
2539
|
router.get("/throttle", settingsCtrl.getThrottleConfig);
|
|
2305
2540
|
router.put("/throttle", settingsCtrl.updateThrottleConfig);
|
|
2306
2541
|
return router;
|
|
@@ -2316,6 +2551,19 @@ var configSchema = zod.z.object({
|
|
|
2316
2551
|
findIdentifier: zod.z.function(),
|
|
2317
2552
|
sendTestEmail: zod.z.function().optional()
|
|
2318
2553
|
}),
|
|
2554
|
+
collections: zod.z.array(zod.z.object({
|
|
2555
|
+
name: zod.z.string(),
|
|
2556
|
+
label: zod.z.string().optional(),
|
|
2557
|
+
description: zod.z.string().optional(),
|
|
2558
|
+
identifierField: zod.z.string().optional(),
|
|
2559
|
+
fields: zod.z.array(zod.z.any()),
|
|
2560
|
+
joins: zod.z.array(zod.z.object({
|
|
2561
|
+
from: zod.z.string(),
|
|
2562
|
+
localField: zod.z.string(),
|
|
2563
|
+
foreignField: zod.z.string(),
|
|
2564
|
+
as: zod.z.string()
|
|
2565
|
+
})).optional()
|
|
2566
|
+
})).optional(),
|
|
2319
2567
|
platforms: zod.z.array(zod.z.string()).optional(),
|
|
2320
2568
|
audiences: zod.z.array(zod.z.string()).optional(),
|
|
2321
2569
|
categories: zod.z.array(zod.z.string()).optional(),
|
|
@@ -2392,7 +2640,10 @@ var SchedulerService = class {
|
|
|
2392
2640
|
}, { connection: connectionOpts, prefix: this.keyPrefix });
|
|
2393
2641
|
}
|
|
2394
2642
|
async stopWorker() {
|
|
2395
|
-
|
|
2643
|
+
if (this.worker) {
|
|
2644
|
+
await this.worker.close();
|
|
2645
|
+
this.worker = void 0;
|
|
2646
|
+
}
|
|
2396
2647
|
}
|
|
2397
2648
|
async getScheduledJobs() {
|
|
2398
2649
|
const jobs = await this.queue.getRepeatableJobs();
|
|
@@ -2429,7 +2680,7 @@ function createEmailRuleEngine(config) {
|
|
|
2429
2680
|
`${prefix}EmailThrottleConfig`,
|
|
2430
2681
|
createEmailThrottleConfigSchema(prefix)
|
|
2431
2682
|
);
|
|
2432
|
-
const templateService = new TemplateService(EmailTemplate, config);
|
|
2683
|
+
const templateService = new TemplateService(EmailTemplate, config, EmailRule);
|
|
2433
2684
|
const ruleService = new RuleService(EmailRule, EmailTemplate, EmailRuleRunLog, config);
|
|
2434
2685
|
const runnerService = new RuleRunnerService(
|
|
2435
2686
|
EmailRule,
|
|
@@ -2449,7 +2700,8 @@ function createEmailRuleEngine(config) {
|
|
|
2449
2700
|
platformValues: config.platforms,
|
|
2450
2701
|
categoryValues: config.categories,
|
|
2451
2702
|
audienceValues: config.audiences,
|
|
2452
|
-
logger: config.logger
|
|
2703
|
+
logger: config.logger,
|
|
2704
|
+
collections: config.collections || []
|
|
2453
2705
|
});
|
|
2454
2706
|
return {
|
|
2455
2707
|
routes,
|
|
@@ -2469,6 +2721,7 @@ exports.ConfigValidationError = ConfigValidationError;
|
|
|
2469
2721
|
exports.DuplicateSlugError = DuplicateSlugError;
|
|
2470
2722
|
exports.EMAIL_SEND_STATUS = EMAIL_SEND_STATUS;
|
|
2471
2723
|
exports.EMAIL_TYPE = EMAIL_TYPE;
|
|
2724
|
+
exports.FIELD_TYPE = FIELD_TYPE;
|
|
2472
2725
|
exports.LockAcquisitionError = LockAcquisitionError;
|
|
2473
2726
|
exports.RULE_OPERATOR = RULE_OPERATOR;
|
|
2474
2727
|
exports.RUN_LOG_STATUS = RUN_LOG_STATUS;
|
|
@@ -2482,6 +2735,7 @@ exports.TARGET_MODE = TARGET_MODE;
|
|
|
2482
2735
|
exports.TEMPLATE_AUDIENCE = TEMPLATE_AUDIENCE;
|
|
2483
2736
|
exports.TEMPLATE_CATEGORY = TEMPLATE_CATEGORY;
|
|
2484
2737
|
exports.THROTTLE_WINDOW = THROTTLE_WINDOW;
|
|
2738
|
+
exports.TYPE_OPERATORS = TYPE_OPERATORS;
|
|
2485
2739
|
exports.TemplateNotFoundError = TemplateNotFoundError;
|
|
2486
2740
|
exports.TemplateRenderService = TemplateRenderService;
|
|
2487
2741
|
exports.TemplateService = TemplateService;
|
|
@@ -2492,6 +2746,8 @@ exports.createEmailRuleSchema = createEmailRuleSchema;
|
|
|
2492
2746
|
exports.createEmailRuleSendSchema = createEmailRuleSendSchema;
|
|
2493
2747
|
exports.createEmailTemplateSchema = createEmailTemplateSchema;
|
|
2494
2748
|
exports.createEmailThrottleConfigSchema = createEmailThrottleConfigSchema;
|
|
2749
|
+
exports.flattenFields = flattenFields;
|
|
2750
|
+
exports.validateConditions = validateConditions;
|
|
2495
2751
|
exports.validateConfig = validateConfig;
|
|
2496
2752
|
//# sourceMappingURL=index.cjs.map
|
|
2497
2753
|
//# sourceMappingURL=index.cjs.map
|