@astralibx/email-rule-engine 12.7.3 → 12.10.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 +802 -759
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +99 -12
- package/dist/index.d.ts +99 -12
- package/dist/index.mjs +799 -760
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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",
|
|
@@ -159,6 +179,17 @@ function createEmailTemplateSchema(platformValues, audienceValues, categoryValue
|
|
|
159
179
|
schema.index({ audience: 1, platform: 1, isActive: 1 });
|
|
160
180
|
return schema;
|
|
161
181
|
}
|
|
182
|
+
function createRunStatsSchema() {
|
|
183
|
+
return new mongoose.Schema({
|
|
184
|
+
matched: { type: Number, default: 0 },
|
|
185
|
+
sent: { type: Number, default: 0 },
|
|
186
|
+
skipped: { type: Number, default: 0 },
|
|
187
|
+
skippedByThrottle: { type: Number, default: 0 },
|
|
188
|
+
errorCount: { type: Number, default: 0 }
|
|
189
|
+
}, { _id: false });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/schemas/rule.schema.ts
|
|
162
193
|
function createEmailRuleSchema(platformValues, audienceValues, collectionPrefix) {
|
|
163
194
|
const RuleConditionSchema = new mongoose.Schema({
|
|
164
195
|
field: { type: String, required: true },
|
|
@@ -173,15 +204,10 @@ function createEmailRuleSchema(platformValues, audienceValues, collectionPrefix)
|
|
|
173
204
|
...platformValues ? { enum: platformValues } : {}
|
|
174
205
|
},
|
|
175
206
|
conditions: [RuleConditionSchema],
|
|
176
|
-
identifiers: [{ type: String }]
|
|
177
|
-
|
|
178
|
-
const RuleRunStatsSchema = new mongoose.Schema({
|
|
179
|
-
matched: { type: Number, default: 0 },
|
|
180
|
-
sent: { type: Number, default: 0 },
|
|
181
|
-
skipped: { type: Number, default: 0 },
|
|
182
|
-
skippedByThrottle: { type: Number, default: 0 },
|
|
183
|
-
errorCount: { type: Number, default: 0 }
|
|
207
|
+
identifiers: [{ type: String }],
|
|
208
|
+
collection: { type: String }
|
|
184
209
|
}, { _id: false });
|
|
210
|
+
const RuleRunStatsSchema = createRunStatsSchema();
|
|
185
211
|
const schema = new mongoose.Schema(
|
|
186
212
|
{
|
|
187
213
|
name: { type: String, required: true },
|
|
@@ -264,9 +290,9 @@ function createEmailRuleSendSchema(collectionPrefix) {
|
|
|
264
290
|
const schema = new mongoose.Schema(
|
|
265
291
|
{
|
|
266
292
|
ruleId: { type: mongoose.Schema.Types.ObjectId, ref: "EmailRule", required: true },
|
|
267
|
-
userId: { type:
|
|
268
|
-
emailIdentifierId: { type:
|
|
269
|
-
messageId: { type:
|
|
293
|
+
userId: { type: String, required: true },
|
|
294
|
+
emailIdentifierId: { type: String },
|
|
295
|
+
messageId: { type: String },
|
|
270
296
|
sentAt: { type: Date, required: true, default: () => /* @__PURE__ */ new Date() },
|
|
271
297
|
status: { type: String },
|
|
272
298
|
accountId: { type: String },
|
|
@@ -306,25 +332,17 @@ function createEmailRuleSendSchema(collectionPrefix) {
|
|
|
306
332
|
schema.index({ ruleId: 1, userId: 1, sentAt: -1 });
|
|
307
333
|
schema.index({ userId: 1, sentAt: -1 });
|
|
308
334
|
schema.index({ ruleId: 1, sentAt: -1 });
|
|
335
|
+
schema.index({ status: 1, sentAt: -1 });
|
|
309
336
|
return schema;
|
|
310
337
|
}
|
|
311
338
|
function createEmailRuleRunLogSchema(collectionPrefix) {
|
|
339
|
+
const baseStatsSchema = createRunStatsSchema();
|
|
312
340
|
const PerRuleStatsSchema = new mongoose.Schema({
|
|
313
341
|
ruleId: { type: mongoose.Schema.Types.ObjectId, ref: "EmailRule", required: true },
|
|
314
342
|
ruleName: { type: String, required: true },
|
|
315
|
-
|
|
316
|
-
sent: { type: Number, default: 0 },
|
|
317
|
-
skipped: { type: Number, default: 0 },
|
|
318
|
-
skippedByThrottle: { type: Number, default: 0 },
|
|
319
|
-
errorCount: { type: Number, default: 0 }
|
|
320
|
-
}, { _id: false });
|
|
321
|
-
const TotalStatsSchema = new mongoose.Schema({
|
|
322
|
-
matched: { type: Number, default: 0 },
|
|
323
|
-
sent: { type: Number, default: 0 },
|
|
324
|
-
skipped: { type: Number, default: 0 },
|
|
325
|
-
skippedByThrottle: { type: Number, default: 0 },
|
|
326
|
-
errorCount: { type: Number, default: 0 }
|
|
343
|
+
...baseStatsSchema.obj
|
|
327
344
|
}, { _id: false });
|
|
345
|
+
const TotalStatsSchema = createRunStatsSchema();
|
|
328
346
|
const schema = new mongoose.Schema(
|
|
329
347
|
{
|
|
330
348
|
runId: { type: String, index: true },
|
|
@@ -348,8 +366,7 @@ function createEmailRuleRunLogSchema(collectionPrefix) {
|
|
|
348
366
|
}
|
|
349
367
|
}
|
|
350
368
|
);
|
|
351
|
-
schema.index({ runAt: -1 });
|
|
352
|
-
schema.index({ runAt: 1 }, { expireAfterSeconds: 90 * 86400 });
|
|
369
|
+
schema.index({ runAt: -1 }, { expireAfterSeconds: 90 * 86400 });
|
|
353
370
|
return schema;
|
|
354
371
|
}
|
|
355
372
|
function createEmailThrottleConfigSchema(collectionPrefix) {
|
|
@@ -476,15 +493,15 @@ var TemplateRenderService = class {
|
|
|
476
493
|
ensureHelpers();
|
|
477
494
|
}
|
|
478
495
|
renderSingle(subject, body, data, textBody) {
|
|
479
|
-
const subjectFn = Handlebars__default.default.compile(subject, { strict:
|
|
496
|
+
const subjectFn = Handlebars__default.default.compile(subject, { strict: false });
|
|
480
497
|
const resolvedSubject = subjectFn(data);
|
|
481
|
-
const bodyFn = Handlebars__default.default.compile(body, { strict:
|
|
498
|
+
const bodyFn = Handlebars__default.default.compile(body, { strict: false });
|
|
482
499
|
const resolvedBody = bodyFn(data);
|
|
483
500
|
const mjmlSource = wrapInMjml(resolvedBody);
|
|
484
501
|
const html = compileMjml(mjmlSource);
|
|
485
502
|
let text;
|
|
486
503
|
if (textBody) {
|
|
487
|
-
const textFn = Handlebars__default.default.compile(textBody, { strict:
|
|
504
|
+
const textFn = Handlebars__default.default.compile(textBody, { strict: false });
|
|
488
505
|
text = textFn(data);
|
|
489
506
|
} else {
|
|
490
507
|
text = htmlToPlainText(html);
|
|
@@ -494,20 +511,20 @@ var TemplateRenderService = class {
|
|
|
494
511
|
compileBatch(subject, body, textBody) {
|
|
495
512
|
const mjmlSource = wrapInMjml(body);
|
|
496
513
|
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:
|
|
514
|
+
const subjectFn = Handlebars__default.default.compile(subject, { strict: false });
|
|
515
|
+
const bodyFn = Handlebars__default.default.compile(htmlWithHandlebars, { strict: false });
|
|
516
|
+
const textBodyFn = textBody ? Handlebars__default.default.compile(textBody, { strict: false }) : void 0;
|
|
500
517
|
return { subjectFn, bodyFn, textBodyFn };
|
|
501
518
|
}
|
|
502
519
|
compileBatchVariants(subjects, bodies, textBody, preheaders) {
|
|
503
|
-
const subjectFns = subjects.map((s) => Handlebars__default.default.compile(s, { strict:
|
|
520
|
+
const subjectFns = subjects.map((s) => Handlebars__default.default.compile(s, { strict: false }));
|
|
504
521
|
const bodyFns = bodies.map((b) => {
|
|
505
522
|
const mjmlSource = wrapInMjml(b);
|
|
506
523
|
const htmlWithHandlebars = compileMjml(mjmlSource);
|
|
507
|
-
return Handlebars__default.default.compile(htmlWithHandlebars, { strict:
|
|
524
|
+
return Handlebars__default.default.compile(htmlWithHandlebars, { strict: false });
|
|
508
525
|
});
|
|
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:
|
|
526
|
+
const textBodyFn = textBody ? Handlebars__default.default.compile(textBody, { strict: false }) : void 0;
|
|
527
|
+
const preheaderFns = preheaders && preheaders.length > 0 ? preheaders.map((p) => Handlebars__default.default.compile(p, { strict: false })) : void 0;
|
|
511
528
|
return { subjectFns, bodyFns, textBodyFn, preheaderFns };
|
|
512
529
|
}
|
|
513
530
|
renderFromCompiled(compiled, data) {
|
|
@@ -623,6 +640,38 @@ var DuplicateSlugError = class extends AlxEmailError {
|
|
|
623
640
|
}
|
|
624
641
|
};
|
|
625
642
|
|
|
643
|
+
// src/utils/query-helpers.ts
|
|
644
|
+
function isValidDateString(s) {
|
|
645
|
+
return s.trim() !== "" && !isNaN(new Date(s).getTime());
|
|
646
|
+
}
|
|
647
|
+
function buildDateRangeFilter(dateField, from, to) {
|
|
648
|
+
const validFrom = from && isValidDateString(from) ? from : void 0;
|
|
649
|
+
const validTo = to && isValidDateString(to) ? to : void 0;
|
|
650
|
+
if (!validFrom && !validTo) return {};
|
|
651
|
+
const filter = {};
|
|
652
|
+
filter[dateField] = {};
|
|
653
|
+
if (validFrom) filter[dateField].$gte = new Date(validFrom);
|
|
654
|
+
if (validTo) filter[dateField].$lte = /* @__PURE__ */ new Date(validTo + "T23:59:59.999Z");
|
|
655
|
+
return filter;
|
|
656
|
+
}
|
|
657
|
+
function calculatePagination(page, limit, maxLimit = 500) {
|
|
658
|
+
const rawPage = page != null && !isNaN(page) ? page : 1;
|
|
659
|
+
const rawLimit = limit != null && !isNaN(limit) ? limit : 200;
|
|
660
|
+
const p = Math.max(1, rawPage);
|
|
661
|
+
const l = Math.max(1, Math.min(rawLimit, maxLimit));
|
|
662
|
+
const skip = (p - 1) * l;
|
|
663
|
+
return { page: p, limit: l, skip };
|
|
664
|
+
}
|
|
665
|
+
function filterUpdateableFields(input, allowedFields) {
|
|
666
|
+
const result = {};
|
|
667
|
+
for (const [key, value] of Object.entries(input)) {
|
|
668
|
+
if (value !== void 0 && allowedFields.has(key)) {
|
|
669
|
+
result[key] = value;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return result;
|
|
673
|
+
}
|
|
674
|
+
|
|
626
675
|
// src/services/template.service.ts
|
|
627
676
|
function stripScriptTags(text) {
|
|
628
677
|
return text.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "");
|
|
@@ -646,9 +695,10 @@ function slugify(name) {
|
|
|
646
695
|
return name.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
647
696
|
}
|
|
648
697
|
var TemplateService = class {
|
|
649
|
-
constructor(EmailTemplate, config) {
|
|
698
|
+
constructor(EmailTemplate, config, EmailRule) {
|
|
650
699
|
this.EmailTemplate = EmailTemplate;
|
|
651
700
|
this.config = config;
|
|
701
|
+
this.EmailRule = EmailRule;
|
|
652
702
|
}
|
|
653
703
|
renderService = new TemplateRenderService();
|
|
654
704
|
async list(filters) {
|
|
@@ -657,7 +707,14 @@ var TemplateService = class {
|
|
|
657
707
|
if (filters?.audience) query["audience"] = filters.audience;
|
|
658
708
|
if (filters?.platform) query["platform"] = filters.platform;
|
|
659
709
|
if (filters?.isActive !== void 0) query["isActive"] = filters.isActive;
|
|
660
|
-
|
|
710
|
+
const page = filters?.page ?? 1;
|
|
711
|
+
const limit = filters?.limit ?? 200;
|
|
712
|
+
const skip = (page - 1) * limit;
|
|
713
|
+
const [templates, total] = await Promise.all([
|
|
714
|
+
this.EmailTemplate.find(query).sort({ category: 1, name: 1 }).skip(skip).limit(limit),
|
|
715
|
+
this.EmailTemplate.countDocuments(query)
|
|
716
|
+
]);
|
|
717
|
+
return { templates, total };
|
|
661
718
|
}
|
|
662
719
|
async getById(id) {
|
|
663
720
|
return this.EmailTemplate.findById(id);
|
|
@@ -723,12 +780,7 @@ var TemplateService = class {
|
|
|
723
780
|
const allContent = [...subjects, ...bodies, ...preheaders, textBody || ""].join(" ");
|
|
724
781
|
input.variables = this.renderService.extractVariables(allContent);
|
|
725
782
|
}
|
|
726
|
-
const setFields =
|
|
727
|
-
for (const [key, value] of Object.entries(input)) {
|
|
728
|
-
if (value !== void 0 && UPDATEABLE_FIELDS.has(key)) {
|
|
729
|
-
setFields[key] = value;
|
|
730
|
-
}
|
|
731
|
-
}
|
|
783
|
+
const setFields = filterUpdateableFields(input, UPDATEABLE_FIELDS);
|
|
732
784
|
const update = { $set: setFields };
|
|
733
785
|
if (input.textBody || input.subjects || input.bodies || input.preheaders) {
|
|
734
786
|
update["$inc"] = { version: 1 };
|
|
@@ -740,6 +792,13 @@ var TemplateService = class {
|
|
|
740
792
|
);
|
|
741
793
|
}
|
|
742
794
|
async delete(id) {
|
|
795
|
+
if (this.EmailRule) {
|
|
796
|
+
const activeRules = await this.EmailRule.find({ templateId: id, isActive: true });
|
|
797
|
+
if (activeRules.length > 0) {
|
|
798
|
+
const names = activeRules.map((r) => r.name).join(", ");
|
|
799
|
+
throw new Error(`Cannot delete template: ${activeRules.length} active rule(s) reference it (${names}). Deactivate them first.`);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
743
802
|
const result = await this.EmailTemplate.findByIdAndDelete(id);
|
|
744
803
|
return result !== null;
|
|
745
804
|
}
|
|
@@ -819,7 +878,8 @@ var TemplateService = class {
|
|
|
819
878
|
async previewWithRecipient(templateId, recipientData) {
|
|
820
879
|
const template = await this.EmailTemplate.findById(templateId);
|
|
821
880
|
if (!template) return null;
|
|
822
|
-
const
|
|
881
|
+
const variables = template.variables ?? [];
|
|
882
|
+
const data = this._buildSampleData(variables, { ...template.fields ?? {}, ...recipientData });
|
|
823
883
|
return this.renderService.renderPreview(
|
|
824
884
|
template.subjects[0],
|
|
825
885
|
template.bodies[0],
|
|
@@ -829,6 +889,125 @@ var TemplateService = class {
|
|
|
829
889
|
}
|
|
830
890
|
};
|
|
831
891
|
|
|
892
|
+
// src/controllers/collection.controller.ts
|
|
893
|
+
function flattenFields(fields, prefix = "", parentIsArray = false) {
|
|
894
|
+
const result = [];
|
|
895
|
+
for (const field of fields) {
|
|
896
|
+
const path = prefix ? `${prefix}.${field.name}` : field.name;
|
|
897
|
+
const isArray = field.type === "array";
|
|
898
|
+
if (field.type === "object" && field.fields?.length) {
|
|
899
|
+
result.push({
|
|
900
|
+
path,
|
|
901
|
+
type: "object",
|
|
902
|
+
label: field.label,
|
|
903
|
+
description: field.description
|
|
904
|
+
});
|
|
905
|
+
result.push(...flattenFields(field.fields, path, false));
|
|
906
|
+
} else if (isArray && field.fields?.length) {
|
|
907
|
+
result.push({
|
|
908
|
+
path: `${path}[]`,
|
|
909
|
+
type: "array",
|
|
910
|
+
label: field.label,
|
|
911
|
+
description: field.description,
|
|
912
|
+
isArray: true
|
|
913
|
+
});
|
|
914
|
+
result.push(...flattenFields(field.fields, `${path}[]`, true));
|
|
915
|
+
} else {
|
|
916
|
+
result.push({
|
|
917
|
+
path,
|
|
918
|
+
type: field.type,
|
|
919
|
+
label: field.label,
|
|
920
|
+
description: field.description,
|
|
921
|
+
enumValues: field.enumValues,
|
|
922
|
+
isArray: parentIsArray || isArray
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
return result;
|
|
927
|
+
}
|
|
928
|
+
function createCollectionController(collections) {
|
|
929
|
+
return {
|
|
930
|
+
list(_req, res) {
|
|
931
|
+
const summary = collections.map((c) => ({
|
|
932
|
+
name: c.name,
|
|
933
|
+
label: c.label,
|
|
934
|
+
description: c.description,
|
|
935
|
+
identifierField: c.identifierField,
|
|
936
|
+
fieldCount: c.fields.length,
|
|
937
|
+
joinCount: c.joins?.length ?? 0
|
|
938
|
+
}));
|
|
939
|
+
res.json({ collections: summary });
|
|
940
|
+
},
|
|
941
|
+
getFields(req, res) {
|
|
942
|
+
const { name } = req.params;
|
|
943
|
+
const collection = collections.find((c) => c.name === name);
|
|
944
|
+
if (!collection) {
|
|
945
|
+
res.status(404).json({ error: `Collection "${name}" not found` });
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
const fields = flattenFields(collection.fields);
|
|
949
|
+
if (collection.joins?.length) {
|
|
950
|
+
for (const join of collection.joins) {
|
|
951
|
+
const joinedCollection = collections.find((c) => c.name === join.from);
|
|
952
|
+
if (joinedCollection) {
|
|
953
|
+
const joinedFields = flattenFields(joinedCollection.fields, join.as);
|
|
954
|
+
fields.push(...joinedFields);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
res.json({
|
|
959
|
+
name: collection.name,
|
|
960
|
+
label: collection.label,
|
|
961
|
+
identifierField: collection.identifierField,
|
|
962
|
+
fields,
|
|
963
|
+
typeOperators: TYPE_OPERATORS
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// src/validation/condition.validator.ts
|
|
970
|
+
function validateConditions(conditions, collectionName, collections) {
|
|
971
|
+
if (!collectionName || collections.length === 0) return [];
|
|
972
|
+
const collection = collections.find((c) => c.name === collectionName);
|
|
973
|
+
if (!collection) return [];
|
|
974
|
+
const flatFields = flattenFields(collection.fields);
|
|
975
|
+
if (collection.joins?.length) {
|
|
976
|
+
for (const join of collection.joins) {
|
|
977
|
+
const joinedCollection = collections.find((c) => c.name === join.from);
|
|
978
|
+
if (joinedCollection) {
|
|
979
|
+
flatFields.push(...flattenFields(joinedCollection.fields, join.as));
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
const fieldMap = /* @__PURE__ */ new Map();
|
|
984
|
+
for (const f of flatFields) {
|
|
985
|
+
fieldMap.set(f.path, f);
|
|
986
|
+
}
|
|
987
|
+
const errors = [];
|
|
988
|
+
for (let i = 0; i < conditions.length; i++) {
|
|
989
|
+
const cond = conditions[i];
|
|
990
|
+
const fieldDef = fieldMap.get(cond.field);
|
|
991
|
+
if (!fieldDef) {
|
|
992
|
+
errors.push({
|
|
993
|
+
index: i,
|
|
994
|
+
field: cond.field,
|
|
995
|
+
message: `Field "${cond.field}" does not exist in collection "${collectionName}"`
|
|
996
|
+
});
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
const allowedOps = TYPE_OPERATORS[fieldDef.type];
|
|
1000
|
+
if (allowedOps && !allowedOps.includes(cond.operator)) {
|
|
1001
|
+
errors.push({
|
|
1002
|
+
index: i,
|
|
1003
|
+
field: cond.field,
|
|
1004
|
+
message: `Operator "${cond.operator}" is not valid for field type "${fieldDef.type}"`
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
return errors;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
832
1011
|
// src/services/rule.service.ts
|
|
833
1012
|
function isQueryTarget(target) {
|
|
834
1013
|
return !target.mode || target.mode === "query";
|
|
@@ -879,8 +1058,15 @@ var RuleService = class {
|
|
|
879
1058
|
this.EmailRuleRunLog = EmailRuleRunLog;
|
|
880
1059
|
this.config = config;
|
|
881
1060
|
}
|
|
882
|
-
async list() {
|
|
883
|
-
|
|
1061
|
+
async list(opts) {
|
|
1062
|
+
const page = opts?.page ?? 1;
|
|
1063
|
+
const limit = opts?.limit ?? 200;
|
|
1064
|
+
const skip = (page - 1) * limit;
|
|
1065
|
+
const [rules, total] = await Promise.all([
|
|
1066
|
+
this.EmailRule.find().populate("templateId", "name slug").sort({ sortOrder: 1, createdAt: -1 }).skip(skip).limit(limit),
|
|
1067
|
+
this.EmailRule.countDocuments()
|
|
1068
|
+
]);
|
|
1069
|
+
return { rules, total };
|
|
884
1070
|
}
|
|
885
1071
|
async getById(id) {
|
|
886
1072
|
return this.EmailRule.findById(id);
|
|
@@ -907,6 +1093,14 @@ var RuleService = class {
|
|
|
907
1093
|
throw new RuleTemplateIncompatibleError("target.identifiers must be a non-empty array for list mode, validation failed");
|
|
908
1094
|
}
|
|
909
1095
|
}
|
|
1096
|
+
if (isQueryTarget(input.target) && input.target.collection && this.config.collections?.length) {
|
|
1097
|
+
const condErrors = validateConditions(input.target.conditions, input.target.collection, this.config.collections);
|
|
1098
|
+
if (condErrors.length > 0) {
|
|
1099
|
+
throw new RuleTemplateIncompatibleError(
|
|
1100
|
+
`Invalid conditions: ${condErrors.map((e) => e.message).join("; ")}`
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
910
1104
|
return this.EmailRule.createRule(input);
|
|
911
1105
|
}
|
|
912
1106
|
async update(id, input) {
|
|
@@ -936,12 +1130,18 @@ var RuleService = class {
|
|
|
936
1130
|
throw new RuleTemplateIncompatibleError(compatError);
|
|
937
1131
|
}
|
|
938
1132
|
}
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
if (
|
|
942
|
-
|
|
1133
|
+
if (isQueryTarget(effectiveTarget)) {
|
|
1134
|
+
const qt = effectiveTarget;
|
|
1135
|
+
if (qt.collection && this.config.collections?.length) {
|
|
1136
|
+
const condErrors = validateConditions(qt.conditions || [], qt.collection, this.config.collections);
|
|
1137
|
+
if (condErrors.length > 0) {
|
|
1138
|
+
throw new RuleTemplateIncompatibleError(
|
|
1139
|
+
`Invalid conditions: ${condErrors.map((e) => e.message).join("; ")}`
|
|
1140
|
+
);
|
|
1141
|
+
}
|
|
943
1142
|
}
|
|
944
1143
|
}
|
|
1144
|
+
const setFields = filterUpdateableFields(input, UPDATEABLE_FIELDS2);
|
|
945
1145
|
return this.EmailRule.findByIdAndUpdate(
|
|
946
1146
|
id,
|
|
947
1147
|
{ $set: setFields },
|
|
@@ -990,7 +1190,10 @@ var RuleService = class {
|
|
|
990
1190
|
const sample2 = identifiers.slice(0, 10).map((id2) => ({ email: id2 }));
|
|
991
1191
|
return { matchedCount: matchedCount2, effectiveLimit, willProcess: willProcess2, ruleId: id, sample: sample2 };
|
|
992
1192
|
}
|
|
993
|
-
const
|
|
1193
|
+
const queryTarget = rule.target;
|
|
1194
|
+
const collectionName = queryTarget.collection;
|
|
1195
|
+
const collectionSchema = collectionName ? this.config.collections?.find((c) => c.name === collectionName) : void 0;
|
|
1196
|
+
const users = await this.config.adapters.queryUsers(rule.target, 5e4, collectionSchema ? { collectionSchema } : void 0);
|
|
994
1197
|
const matchedCount = users.length;
|
|
995
1198
|
const willProcess = Math.min(matchedCount, effectiveLimit);
|
|
996
1199
|
const sample = users.slice(0, 10).map((u) => ({
|
|
@@ -1012,8 +1215,14 @@ var RuleService = class {
|
|
|
1012
1215
|
rest.lastRunStats = void 0;
|
|
1013
1216
|
return this.EmailRule.create(rest);
|
|
1014
1217
|
}
|
|
1015
|
-
async getRunHistory(limit = 20) {
|
|
1016
|
-
|
|
1218
|
+
async getRunHistory(limit = 20, opts) {
|
|
1219
|
+
const filter = buildDateRangeFilter("runAt", opts?.from, opts?.to);
|
|
1220
|
+
const pagination = calculatePagination(opts?.page, limit);
|
|
1221
|
+
return this.EmailRuleRunLog.find(filter).sort({ runAt: -1 }).skip(pagination.skip).limit(pagination.limit);
|
|
1222
|
+
}
|
|
1223
|
+
async getRunHistoryCount(opts) {
|
|
1224
|
+
const filter = buildDateRangeFilter("runAt", opts?.from, opts?.to);
|
|
1225
|
+
return this.EmailRuleRunLog.countDocuments(filter);
|
|
1017
1226
|
}
|
|
1018
1227
|
};
|
|
1019
1228
|
var MS_PER_DAY = 864e5;
|
|
@@ -1083,6 +1292,7 @@ var RuleRunnerService = class {
|
|
|
1083
1292
|
const lockAcquired = await this.lock.acquire();
|
|
1084
1293
|
if (!lockAcquired) {
|
|
1085
1294
|
this.logger.warn("Rule runner already executing, skipping");
|
|
1295
|
+
await this.updateRunProgress(runId, { status: "failed", currentRule: "Another run is already in progress" });
|
|
1086
1296
|
return { runId };
|
|
1087
1297
|
}
|
|
1088
1298
|
const runStartTime = Date.now();
|
|
@@ -1233,193 +1443,252 @@ var RuleRunnerService = class {
|
|
|
1233
1443
|
}
|
|
1234
1444
|
return this.executeQueryMode(rule, template, throttleMap, throttleConfig, stats, runId);
|
|
1235
1445
|
}
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1446
|
+
emitSendEvent(rule, email, status, templateId, runId, opts) {
|
|
1447
|
+
this.config.hooks?.onSend?.({
|
|
1448
|
+
ruleId: rule._id.toString(),
|
|
1449
|
+
ruleName: rule.name,
|
|
1450
|
+
email,
|
|
1451
|
+
status,
|
|
1452
|
+
accountId: opts?.accountId ?? "",
|
|
1453
|
+
templateId,
|
|
1454
|
+
runId: runId || "",
|
|
1455
|
+
subjectIndex: opts?.subjectIndex ?? -1,
|
|
1456
|
+
bodyIndex: opts?.bodyIndex ?? -1,
|
|
1457
|
+
preheaderIndex: opts?.preheaderIndex,
|
|
1458
|
+
failureReason: opts?.failureReason
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1461
|
+
async processSingleUser(params) {
|
|
1462
|
+
const { rule, email, userKey, identifier, user, sendMap, throttleMap, throttleConfig, template, compiledVariants, templateId, ruleId, runId, stats } = params;
|
|
1463
|
+
const lastSend = sendMap.get(userKey);
|
|
1464
|
+
if (lastSend) {
|
|
1465
|
+
if (rule.sendOnce && rule.resendAfterDays == null) {
|
|
1466
|
+
stats.skipped++;
|
|
1467
|
+
this.emitSendEvent(rule, email, "skipped", templateId, runId || "", { failureReason: "send once" });
|
|
1468
|
+
return "skipped";
|
|
1469
|
+
}
|
|
1470
|
+
if (rule.resendAfterDays != null) {
|
|
1471
|
+
const daysSince = (Date.now() - new Date(lastSend.sentAt).getTime()) / MS_PER_DAY;
|
|
1472
|
+
if (daysSince < rule.resendAfterDays) {
|
|
1473
|
+
stats.skipped++;
|
|
1474
|
+
this.emitSendEvent(rule, email, "skipped", templateId, runId || "", { failureReason: "resend too soon" });
|
|
1475
|
+
return "skipped";
|
|
1476
|
+
}
|
|
1477
|
+
} else {
|
|
1478
|
+
stats.skipped++;
|
|
1479
|
+
this.emitSendEvent(rule, email, "skipped", templateId, runId || "", { failureReason: "send once" });
|
|
1480
|
+
return "skipped";
|
|
1481
|
+
}
|
|
1482
|
+
if (rule.cooldownDays) {
|
|
1483
|
+
const daysSince = (Date.now() - new Date(lastSend.sentAt).getTime()) / MS_PER_DAY;
|
|
1484
|
+
if (daysSince < rule.cooldownDays) {
|
|
1485
|
+
stats.skipped++;
|
|
1486
|
+
this.emitSendEvent(rule, email, "skipped", templateId, runId || "", { failureReason: "cooldown period" });
|
|
1487
|
+
return "skipped";
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1242
1490
|
}
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1491
|
+
if (!this.checkThrottle(rule, userKey, email, throttleMap, throttleConfig, stats, templateId, runId)) return "skipped";
|
|
1492
|
+
const agentSelection = await this.config.adapters.selectAgent(identifier.id, { ruleId, templateId });
|
|
1493
|
+
if (!agentSelection) {
|
|
1494
|
+
stats.skipped++;
|
|
1495
|
+
this.emitSendEvent(rule, email, "skipped", templateId, runId || "", { failureReason: "no account available" });
|
|
1496
|
+
return "skipped";
|
|
1497
|
+
}
|
|
1498
|
+
const resolvedData = this.config.adapters.resolveData(user);
|
|
1499
|
+
const templateData = { ...template.fields || {}, ...resolvedData };
|
|
1500
|
+
const si = Math.floor(Math.random() * compiledVariants.subjectFns.length);
|
|
1501
|
+
const bi = Math.floor(Math.random() * compiledVariants.bodyFns.length);
|
|
1502
|
+
const renderedSubject = compiledVariants.subjectFns[si](templateData);
|
|
1503
|
+
const renderedHtml = compiledVariants.bodyFns[bi](templateData);
|
|
1504
|
+
const renderedText = compiledVariants.textBodyFn ? compiledVariants.textBodyFn(templateData) : this.templateRenderer.htmlToText(renderedHtml);
|
|
1505
|
+
let finalHtml = renderedHtml;
|
|
1506
|
+
let finalText = renderedText;
|
|
1507
|
+
let finalSubject = renderedSubject;
|
|
1508
|
+
let pi;
|
|
1509
|
+
if (compiledVariants.preheaderFns && compiledVariants.preheaderFns.length > 0) {
|
|
1510
|
+
pi = Math.floor(Math.random() * compiledVariants.preheaderFns.length);
|
|
1511
|
+
const renderedPreheader = compiledVariants.preheaderFns[pi](templateData);
|
|
1512
|
+
if (renderedPreheader) {
|
|
1513
|
+
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>`;
|
|
1514
|
+
finalHtml = finalHtml.replace(/(<body[^>]*>)/i, `$1${preheaderHtml}`);
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
if (this.config.hooks?.beforeSend) {
|
|
1518
|
+
try {
|
|
1519
|
+
const modified = await this.config.hooks.beforeSend({
|
|
1520
|
+
htmlBody: finalHtml,
|
|
1521
|
+
textBody: finalText,
|
|
1522
|
+
subject: finalSubject,
|
|
1523
|
+
account: {
|
|
1524
|
+
id: agentSelection.accountId,
|
|
1525
|
+
email: agentSelection.email,
|
|
1526
|
+
metadata: agentSelection.metadata
|
|
1527
|
+
},
|
|
1528
|
+
user: {
|
|
1529
|
+
id: String(userKey),
|
|
1530
|
+
email,
|
|
1531
|
+
name: String(user.name || user.firstName || "")
|
|
1532
|
+
},
|
|
1533
|
+
context: {
|
|
1534
|
+
ruleId,
|
|
1535
|
+
templateId,
|
|
1536
|
+
runId: runId || ""
|
|
1537
|
+
}
|
|
1538
|
+
});
|
|
1539
|
+
finalHtml = modified.htmlBody;
|
|
1540
|
+
finalText = modified.textBody;
|
|
1541
|
+
finalSubject = modified.subject;
|
|
1542
|
+
} catch (hookErr) {
|
|
1543
|
+
this.logger.error(`beforeSend hook failed for email ${email}: ${hookErr.message}`);
|
|
1544
|
+
stats.errorCount++;
|
|
1545
|
+
this.emitSendEvent(rule, email, "error", templateId, runId || "", { accountId: agentSelection.accountId, subjectIndex: si, bodyIndex: bi, failureReason: hookErr.message });
|
|
1546
|
+
return "error";
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
await this.config.adapters.sendEmail({
|
|
1550
|
+
identifierId: identifier.id,
|
|
1551
|
+
contactId: identifier.contactId,
|
|
1552
|
+
accountId: agentSelection.accountId,
|
|
1553
|
+
subject: finalSubject,
|
|
1554
|
+
htmlBody: finalHtml,
|
|
1555
|
+
textBody: finalText,
|
|
1556
|
+
ruleId,
|
|
1557
|
+
autoApprove: rule.autoApprove ?? true,
|
|
1558
|
+
attachments: template.attachments || []
|
|
1559
|
+
});
|
|
1560
|
+
await this.EmailRuleSend.logSend(
|
|
1561
|
+
ruleId,
|
|
1562
|
+
userKey,
|
|
1563
|
+
identifier.id,
|
|
1564
|
+
void 0,
|
|
1565
|
+
{ status: "sent", accountId: agentSelection.accountId, subject: finalSubject, subjectIndex: si, bodyIndex: bi, preheaderIndex: pi }
|
|
1566
|
+
);
|
|
1567
|
+
const current = throttleMap.get(userKey) || { today: 0, thisWeek: 0};
|
|
1568
|
+
throttleMap.set(userKey, {
|
|
1569
|
+
today: current.today + 1,
|
|
1570
|
+
thisWeek: current.thisWeek + 1,
|
|
1571
|
+
lastSentDate: /* @__PURE__ */ new Date()
|
|
1572
|
+
});
|
|
1573
|
+
stats.sent++;
|
|
1574
|
+
this.emitSendEvent(rule, email, "sent", templateId, runId || "", { accountId: agentSelection.accountId, subjectIndex: si, bodyIndex: bi, preheaderIndex: pi });
|
|
1575
|
+
return "sent";
|
|
1576
|
+
}
|
|
1577
|
+
async resolveIdentifiers(emails) {
|
|
1249
1578
|
const identifierResults = await processInChunks(
|
|
1250
|
-
|
|
1579
|
+
emails,
|
|
1251
1580
|
async (email) => {
|
|
1252
1581
|
const result = await this.config.adapters.findIdentifier(email);
|
|
1253
1582
|
return result ? { email, ...result } : null;
|
|
1254
1583
|
},
|
|
1255
1584
|
IDENTIFIER_CHUNK_SIZE
|
|
1256
1585
|
);
|
|
1257
|
-
const
|
|
1586
|
+
const map = /* @__PURE__ */ new Map();
|
|
1258
1587
|
for (const result of identifierResults) {
|
|
1259
1588
|
if (result) {
|
|
1260
|
-
|
|
1589
|
+
map.set(result.email, { id: result.id, contactId: result.contactId });
|
|
1261
1590
|
}
|
|
1262
1591
|
}
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
const
|
|
1267
|
-
for (const send of
|
|
1592
|
+
return map;
|
|
1593
|
+
}
|
|
1594
|
+
buildSendMap(sends) {
|
|
1595
|
+
const map = /* @__PURE__ */ new Map();
|
|
1596
|
+
for (const send of sends) {
|
|
1268
1597
|
const uid = send.userId.toString();
|
|
1269
|
-
if (!
|
|
1270
|
-
|
|
1598
|
+
if (!map.has(uid)) {
|
|
1599
|
+
map.set(uid, send);
|
|
1271
1600
|
}
|
|
1272
1601
|
}
|
|
1602
|
+
return map;
|
|
1603
|
+
}
|
|
1604
|
+
compileTemplateVariants(template) {
|
|
1273
1605
|
const preheaders = template.preheaders || [];
|
|
1274
|
-
|
|
1606
|
+
return this.templateRenderer.compileBatchVariants(
|
|
1275
1607
|
template.subjects,
|
|
1276
1608
|
template.bodies,
|
|
1277
1609
|
template.textBody,
|
|
1278
1610
|
preheaders
|
|
1279
1611
|
);
|
|
1612
|
+
}
|
|
1613
|
+
async checkCancelled(runId, index) {
|
|
1614
|
+
if (!runId || index % 10 !== 0) return false;
|
|
1615
|
+
const cancelKey = `${this.keyPrefix}run:${runId}:cancel`;
|
|
1616
|
+
return !!await this.redis.exists(cancelKey);
|
|
1617
|
+
}
|
|
1618
|
+
async applySendDelay(isLast) {
|
|
1619
|
+
if (isLast) return;
|
|
1620
|
+
const delayMs = this.config.options?.delayBetweenSendsMs || 0;
|
|
1621
|
+
const jitterMs = this.config.options?.jitterMs || 0;
|
|
1622
|
+
if (delayMs > 0 || jitterMs > 0) {
|
|
1623
|
+
const totalDelay = delayMs + Math.floor(Math.random() * (jitterMs + 1));
|
|
1624
|
+
if (totalDelay > 0) await new Promise((resolve) => setTimeout(resolve, totalDelay));
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
async finalizeRuleStats(rule, stats, ruleId, templateId, runId) {
|
|
1628
|
+
await this.EmailRule.findByIdAndUpdate(rule._id, {
|
|
1629
|
+
$set: { lastRunAt: /* @__PURE__ */ new Date(), lastRunStats: stats },
|
|
1630
|
+
$inc: { totalSent: stats.sent, totalSkipped: stats.skipped }
|
|
1631
|
+
});
|
|
1632
|
+
this.config.hooks?.onRuleComplete?.({ ruleId, ruleName: rule.name, stats, templateId, runId: runId || "" });
|
|
1633
|
+
}
|
|
1634
|
+
async executeListMode(rule, template, throttleMap, throttleConfig, stats, runId) {
|
|
1635
|
+
const rawIdentifiers = rule.target.identifiers || [];
|
|
1636
|
+
const uniqueEmails = [...new Set(rawIdentifiers.map((e) => e.toLowerCase().trim()).filter(Boolean))];
|
|
1637
|
+
const limit = rule.maxPerRun || this.config.options?.defaultMaxPerRun || 500;
|
|
1638
|
+
if (uniqueEmails.length > limit) {
|
|
1639
|
+
this.logger.warn(`Rule "${rule.name}" matched ${uniqueEmails.length} users but maxPerRun is ${limit} \u2014 only ${limit} will be processed`, { ruleId: rule._id.toString(), matchedCount: uniqueEmails.length, maxPerRun: limit });
|
|
1640
|
+
}
|
|
1641
|
+
const emailsToProcess = uniqueEmails.slice(0, limit);
|
|
1642
|
+
stats.matched = emailsToProcess.length;
|
|
1643
|
+
const ruleId = rule._id.toString();
|
|
1644
|
+
const templateId = rule.templateId.toString();
|
|
1645
|
+
this.config.hooks?.onRuleStart?.({ ruleId, ruleName: rule.name, matchedCount: emailsToProcess.length, templateId, runId: runId || "" });
|
|
1646
|
+
if (emailsToProcess.length === 0) return stats;
|
|
1647
|
+
const identifierMap = await this.resolveIdentifiers(emailsToProcess);
|
|
1648
|
+
const validEmails = emailsToProcess.filter((e) => identifierMap.has(e));
|
|
1649
|
+
const identifierIds = validEmails.map((e) => identifierMap.get(e).id);
|
|
1650
|
+
const allRuleSends = await this.EmailRuleSend.find({ ruleId: rule._id, userId: { $in: identifierIds } }).sort({ sentAt: -1 }).lean();
|
|
1651
|
+
const sendMap = this.buildSendMap(allRuleSends);
|
|
1652
|
+
const compiledVariants = this.compileTemplateVariants(template);
|
|
1280
1653
|
let totalProcessed = 0;
|
|
1281
1654
|
for (let i = 0; i < emailsToProcess.length; i++) {
|
|
1282
1655
|
const email = emailsToProcess[i];
|
|
1283
|
-
if (runId
|
|
1284
|
-
const cancelKey = `${this.keyPrefix}run:${runId}:cancel`;
|
|
1285
|
-
const cancelled = await this.redis.exists(cancelKey);
|
|
1286
|
-
if (cancelled) break;
|
|
1287
|
-
}
|
|
1656
|
+
if (await this.checkCancelled(runId, i)) break;
|
|
1288
1657
|
try {
|
|
1289
1658
|
const identifier = identifierMap.get(email);
|
|
1290
1659
|
if (!identifier) {
|
|
1291
1660
|
stats.skipped++;
|
|
1292
|
-
this.
|
|
1293
|
-
continue;
|
|
1294
|
-
}
|
|
1295
|
-
const dedupKey = identifier.id;
|
|
1296
|
-
const lastSend = sendMap.get(dedupKey);
|
|
1297
|
-
if (lastSend) {
|
|
1298
|
-
if (rule.sendOnce && !rule.resendAfterDays) {
|
|
1299
|
-
stats.skipped++;
|
|
1300
|
-
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "skipped", accountId: "", templateId, runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: "send once" });
|
|
1301
|
-
continue;
|
|
1302
|
-
}
|
|
1303
|
-
if (rule.resendAfterDays) {
|
|
1304
|
-
const daysSince = (Date.now() - new Date(lastSend.sentAt).getTime()) / MS_PER_DAY;
|
|
1305
|
-
if (daysSince < rule.resendAfterDays) {
|
|
1306
|
-
stats.skipped++;
|
|
1307
|
-
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "skipped", accountId: "", templateId, runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: "resend too soon" });
|
|
1308
|
-
continue;
|
|
1309
|
-
}
|
|
1310
|
-
} else {
|
|
1311
|
-
stats.skipped++;
|
|
1312
|
-
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "skipped", accountId: "", templateId, runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: "send once" });
|
|
1313
|
-
continue;
|
|
1314
|
-
}
|
|
1315
|
-
}
|
|
1316
|
-
if (!this.checkThrottle(rule, dedupKey, email, throttleMap, throttleConfig, stats, templateId, runId)) continue;
|
|
1317
|
-
const agentSelection = await this.config.adapters.selectAgent(identifier.id, { ruleId, templateId });
|
|
1318
|
-
if (!agentSelection) {
|
|
1319
|
-
stats.skipped++;
|
|
1320
|
-
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "skipped", accountId: "", templateId, runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: "no account available" });
|
|
1661
|
+
this.emitSendEvent(rule, email, "invalid", templateId, runId || "", { failureReason: "invalid email" });
|
|
1321
1662
|
continue;
|
|
1322
1663
|
}
|
|
1323
|
-
const
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
if (compiledVariants.preheaderFns && compiledVariants.preheaderFns.length > 0) {
|
|
1336
|
-
pi = Math.floor(Math.random() * compiledVariants.preheaderFns.length);
|
|
1337
|
-
const renderedPreheader = compiledVariants.preheaderFns[pi](templateData);
|
|
1338
|
-
if (renderedPreheader) {
|
|
1339
|
-
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>`;
|
|
1340
|
-
finalHtml = finalHtml.replace(/(<body[^>]*>)/i, `$1${preheaderHtml}`);
|
|
1341
|
-
}
|
|
1342
|
-
}
|
|
1343
|
-
if (this.config.hooks?.beforeSend) {
|
|
1344
|
-
try {
|
|
1345
|
-
const modified = await this.config.hooks.beforeSend({
|
|
1346
|
-
htmlBody: finalHtml,
|
|
1347
|
-
textBody: finalText,
|
|
1348
|
-
subject: finalSubject,
|
|
1349
|
-
account: {
|
|
1350
|
-
id: agentSelection.accountId,
|
|
1351
|
-
email: agentSelection.email,
|
|
1352
|
-
metadata: agentSelection.metadata
|
|
1353
|
-
},
|
|
1354
|
-
user: {
|
|
1355
|
-
id: dedupKey,
|
|
1356
|
-
email,
|
|
1357
|
-
name: ""
|
|
1358
|
-
},
|
|
1359
|
-
context: {
|
|
1360
|
-
ruleId,
|
|
1361
|
-
templateId,
|
|
1362
|
-
runId: runId || ""
|
|
1363
|
-
}
|
|
1364
|
-
});
|
|
1365
|
-
finalHtml = modified.htmlBody;
|
|
1366
|
-
finalText = modified.textBody;
|
|
1367
|
-
finalSubject = modified.subject;
|
|
1368
|
-
} catch (hookErr) {
|
|
1369
|
-
this.logger.error(`beforeSend hook failed for email ${email}: ${hookErr.message}`);
|
|
1370
|
-
stats.errorCount++;
|
|
1371
|
-
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "error", accountId: agentSelection.accountId, templateId, runId: runId || "", subjectIndex: si, bodyIndex: bi, failureReason: hookErr.message });
|
|
1372
|
-
continue;
|
|
1373
|
-
}
|
|
1374
|
-
}
|
|
1375
|
-
await this.config.adapters.sendEmail({
|
|
1376
|
-
identifierId: identifier.id,
|
|
1377
|
-
contactId: identifier.contactId,
|
|
1378
|
-
accountId: agentSelection.accountId,
|
|
1379
|
-
subject: finalSubject,
|
|
1380
|
-
htmlBody: finalHtml,
|
|
1381
|
-
textBody: finalText,
|
|
1664
|
+
const result = await this.processSingleUser({
|
|
1665
|
+
rule,
|
|
1666
|
+
email,
|
|
1667
|
+
userKey: identifier.id,
|
|
1668
|
+
identifier,
|
|
1669
|
+
user: { _id: identifier.id, email },
|
|
1670
|
+
sendMap,
|
|
1671
|
+
throttleMap,
|
|
1672
|
+
throttleConfig,
|
|
1673
|
+
template,
|
|
1674
|
+
compiledVariants,
|
|
1675
|
+
templateId,
|
|
1382
1676
|
ruleId,
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
});
|
|
1386
|
-
await this.EmailRuleSend.logSend(
|
|
1387
|
-
ruleId,
|
|
1388
|
-
dedupKey,
|
|
1389
|
-
identifier.id,
|
|
1390
|
-
void 0,
|
|
1391
|
-
{ status: "sent", accountId: agentSelection.accountId, subject: finalSubject, subjectIndex: si, bodyIndex: bi, preheaderIndex: pi }
|
|
1392
|
-
);
|
|
1393
|
-
const current = throttleMap.get(dedupKey) || { today: 0, thisWeek: 0, lastSentDate: null };
|
|
1394
|
-
throttleMap.set(dedupKey, {
|
|
1395
|
-
today: current.today + 1,
|
|
1396
|
-
thisWeek: current.thisWeek + 1,
|
|
1397
|
-
lastSentDate: /* @__PURE__ */ new Date()
|
|
1677
|
+
runId,
|
|
1678
|
+
stats
|
|
1398
1679
|
});
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
await this.updateRunSendProgress(runId, stats);
|
|
1404
|
-
}
|
|
1405
|
-
if (i < emailsToProcess.length - 1) {
|
|
1406
|
-
const delayMs = this.config.options?.delayBetweenSendsMs || 0;
|
|
1407
|
-
const jitterMs = this.config.options?.jitterMs || 0;
|
|
1408
|
-
if (delayMs > 0 || jitterMs > 0) {
|
|
1409
|
-
const totalDelay = delayMs + Math.floor(Math.random() * (jitterMs + 1));
|
|
1410
|
-
if (totalDelay > 0) await new Promise((resolve) => setTimeout(resolve, totalDelay));
|
|
1411
|
-
}
|
|
1680
|
+
if (result === "sent") {
|
|
1681
|
+
totalProcessed++;
|
|
1682
|
+
if (runId && totalProcessed % 10 === 0) await this.updateRunSendProgress(runId, stats);
|
|
1683
|
+
await this.applySendDelay(i >= emailsToProcess.length - 1);
|
|
1412
1684
|
}
|
|
1413
1685
|
} catch (err) {
|
|
1414
1686
|
stats.errorCount++;
|
|
1415
|
-
this.
|
|
1687
|
+
this.emitSendEvent(rule, email, "error", templateId, runId || "", { failureReason: err.message || "unknown error" });
|
|
1416
1688
|
this.logger.error(`Rule "${rule.name}" failed for identifier ${email}`, { error: err });
|
|
1417
1689
|
}
|
|
1418
1690
|
}
|
|
1419
|
-
await this.
|
|
1420
|
-
$set: { lastRunAt: /* @__PURE__ */ new Date(), lastRunStats: stats },
|
|
1421
|
-
$inc: { totalSent: stats.sent, totalSkipped: stats.skipped }
|
|
1422
|
-
});
|
|
1691
|
+
await this.finalizeRuleStats(rule, stats, ruleId, templateId, runId);
|
|
1423
1692
|
if (rule.sendOnce) {
|
|
1424
1693
|
const allIdentifiers = rule.target.identifiers || [];
|
|
1425
1694
|
const totalIdentifiers = new Set(allIdentifiers.map((e) => e.toLowerCase().trim()).filter(Boolean)).size;
|
|
@@ -1435,14 +1704,15 @@ var RuleRunnerService = class {
|
|
|
1435
1704
|
this.logger.info(`Rule '${rule.name}' auto-disabled \u2014 all identifiers processed`);
|
|
1436
1705
|
}
|
|
1437
1706
|
}
|
|
1438
|
-
this.config.hooks?.onRuleComplete?.({ ruleId, ruleName: rule.name, stats, templateId, runId: runId || "" });
|
|
1439
1707
|
return stats;
|
|
1440
1708
|
}
|
|
1441
1709
|
async executeQueryMode(rule, template, throttleMap, throttleConfig, stats, runId) {
|
|
1442
1710
|
const limit = rule.maxPerRun || this.config.options?.defaultMaxPerRun || 500;
|
|
1443
1711
|
let users;
|
|
1444
1712
|
try {
|
|
1445
|
-
|
|
1713
|
+
const collectionName = rule.target?.collection;
|
|
1714
|
+
const collectionSchema = collectionName ? this.config.collections?.find((c) => c.name === collectionName) : void 0;
|
|
1715
|
+
users = await this.config.adapters.queryUsers(rule.target, limit, collectionSchema ? { collectionSchema } : void 0);
|
|
1446
1716
|
} catch (err) {
|
|
1447
1717
|
this.logger.error(`Rule "${rule.name}": query failed`, { error: err });
|
|
1448
1718
|
stats.errorCount = 1;
|
|
@@ -1457,186 +1727,58 @@ var RuleRunnerService = class {
|
|
|
1457
1727
|
const userIds = users.map((u) => u._id?.toString()).filter(Boolean);
|
|
1458
1728
|
const emails = users.map((u) => u.email).filter(Boolean);
|
|
1459
1729
|
const allRuleSends = await this.EmailRuleSend.find({ ruleId: rule._id, userId: { $in: userIds } }).sort({ sentAt: -1 }).lean();
|
|
1460
|
-
const sendMap =
|
|
1461
|
-
for (const send of allRuleSends) {
|
|
1462
|
-
const uid = send.userId.toString();
|
|
1463
|
-
if (!sendMap.has(uid)) {
|
|
1464
|
-
sendMap.set(uid, send);
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1730
|
+
const sendMap = this.buildSendMap(allRuleSends);
|
|
1467
1731
|
const uniqueEmails = [...new Set(emails.map((e) => e.toLowerCase().trim()))];
|
|
1468
|
-
const
|
|
1469
|
-
|
|
1470
|
-
async (email) => {
|
|
1471
|
-
const result = await this.config.adapters.findIdentifier(email);
|
|
1472
|
-
return result ? { email, ...result } : null;
|
|
1473
|
-
},
|
|
1474
|
-
IDENTIFIER_CHUNK_SIZE
|
|
1475
|
-
);
|
|
1476
|
-
const identifierMap = /* @__PURE__ */ new Map();
|
|
1477
|
-
for (const result of identifierResults) {
|
|
1478
|
-
if (result) {
|
|
1479
|
-
identifierMap.set(result.email, { id: result.id, contactId: result.contactId });
|
|
1480
|
-
}
|
|
1481
|
-
}
|
|
1482
|
-
const preheadersQ = template.preheaders || [];
|
|
1483
|
-
const compiledVariants = this.templateRenderer.compileBatchVariants(
|
|
1484
|
-
template.subjects,
|
|
1485
|
-
template.bodies,
|
|
1486
|
-
template.textBody,
|
|
1487
|
-
preheadersQ
|
|
1488
|
-
);
|
|
1732
|
+
const identifierMap = await this.resolveIdentifiers(uniqueEmails);
|
|
1733
|
+
const compiledVariants = this.compileTemplateVariants(template);
|
|
1489
1734
|
const ruleId = rule._id.toString();
|
|
1490
1735
|
const templateId = rule.templateId.toString();
|
|
1491
1736
|
let totalProcessed = 0;
|
|
1492
1737
|
for (let i = 0; i < users.length; i++) {
|
|
1493
1738
|
const user = users[i];
|
|
1494
|
-
if (runId
|
|
1495
|
-
const cancelKey = `${this.keyPrefix}run:${runId}:cancel`;
|
|
1496
|
-
const cancelled = await this.redis.exists(cancelKey);
|
|
1497
|
-
if (cancelled) break;
|
|
1498
|
-
}
|
|
1739
|
+
if (await this.checkCancelled(runId, i)) break;
|
|
1499
1740
|
try {
|
|
1500
1741
|
const userId = user._id?.toString();
|
|
1501
1742
|
const email = user.email;
|
|
1502
1743
|
if (!userId || !email) {
|
|
1503
1744
|
stats.skipped++;
|
|
1504
|
-
this.
|
|
1745
|
+
this.emitSendEvent(rule, email || "unknown", "invalid", templateId, runId || "", { failureReason: "invalid email" });
|
|
1505
1746
|
continue;
|
|
1506
1747
|
}
|
|
1507
|
-
const lastSend = sendMap.get(userId);
|
|
1508
|
-
if (lastSend) {
|
|
1509
|
-
if (rule.sendOnce && !rule.resendAfterDays) {
|
|
1510
|
-
stats.skipped++;
|
|
1511
|
-
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "skipped", accountId: "", templateId, runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: "send once" });
|
|
1512
|
-
continue;
|
|
1513
|
-
}
|
|
1514
|
-
if (rule.resendAfterDays) {
|
|
1515
|
-
const daysSince = (Date.now() - new Date(lastSend.sentAt).getTime()) / MS_PER_DAY;
|
|
1516
|
-
if (daysSince < rule.resendAfterDays) {
|
|
1517
|
-
stats.skipped++;
|
|
1518
|
-
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "skipped", accountId: "", templateId, runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: "resend too soon" });
|
|
1519
|
-
continue;
|
|
1520
|
-
}
|
|
1521
|
-
} else {
|
|
1522
|
-
stats.skipped++;
|
|
1523
|
-
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "skipped", accountId: "", templateId, runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: "send once" });
|
|
1524
|
-
continue;
|
|
1525
|
-
}
|
|
1526
|
-
}
|
|
1527
1748
|
const identifier = identifierMap.get(email.toLowerCase().trim());
|
|
1528
1749
|
if (!identifier) {
|
|
1529
1750
|
stats.skipped++;
|
|
1530
|
-
this.
|
|
1751
|
+
this.emitSendEvent(rule, email, "invalid", templateId, runId || "", { failureReason: "invalid email" });
|
|
1531
1752
|
continue;
|
|
1532
1753
|
}
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
const renderedHtml = compiledVariants.bodyFns[bi](templateData);
|
|
1546
|
-
const renderedText = compiledVariants.textBodyFn ? compiledVariants.textBodyFn(templateData) : this.templateRenderer.htmlToText(renderedHtml);
|
|
1547
|
-
let finalHtml = renderedHtml;
|
|
1548
|
-
let finalText = renderedText;
|
|
1549
|
-
let finalSubject = renderedSubject;
|
|
1550
|
-
let pi;
|
|
1551
|
-
if (compiledVariants.preheaderFns && compiledVariants.preheaderFns.length > 0) {
|
|
1552
|
-
pi = Math.floor(Math.random() * compiledVariants.preheaderFns.length);
|
|
1553
|
-
const renderedPreheader = compiledVariants.preheaderFns[pi](templateData);
|
|
1554
|
-
if (renderedPreheader) {
|
|
1555
|
-
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>`;
|
|
1556
|
-
finalHtml = finalHtml.replace(/(<body[^>]*>)/i, `$1${preheaderHtml}`);
|
|
1557
|
-
}
|
|
1558
|
-
}
|
|
1559
|
-
if (this.config.hooks?.beforeSend) {
|
|
1560
|
-
try {
|
|
1561
|
-
const modified = await this.config.hooks.beforeSend({
|
|
1562
|
-
htmlBody: finalHtml,
|
|
1563
|
-
textBody: finalText,
|
|
1564
|
-
subject: finalSubject,
|
|
1565
|
-
account: {
|
|
1566
|
-
id: agentSelection.accountId,
|
|
1567
|
-
email: agentSelection.email,
|
|
1568
|
-
metadata: agentSelection.metadata
|
|
1569
|
-
},
|
|
1570
|
-
user: {
|
|
1571
|
-
id: String(userId),
|
|
1572
|
-
email,
|
|
1573
|
-
name: String(user.name || user.firstName || "")
|
|
1574
|
-
},
|
|
1575
|
-
context: {
|
|
1576
|
-
ruleId,
|
|
1577
|
-
templateId,
|
|
1578
|
-
runId: runId || ""
|
|
1579
|
-
}
|
|
1580
|
-
});
|
|
1581
|
-
finalHtml = modified.htmlBody;
|
|
1582
|
-
finalText = modified.textBody;
|
|
1583
|
-
finalSubject = modified.subject;
|
|
1584
|
-
} catch (hookErr) {
|
|
1585
|
-
this.logger.error(`beforeSend hook failed for email ${email}: ${hookErr.message}`);
|
|
1586
|
-
stats.errorCount++;
|
|
1587
|
-
this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "error", accountId: agentSelection.accountId, templateId, runId: runId || "", subjectIndex: si, bodyIndex: bi, failureReason: hookErr.message });
|
|
1588
|
-
continue;
|
|
1589
|
-
}
|
|
1590
|
-
}
|
|
1591
|
-
await this.config.adapters.sendEmail({
|
|
1592
|
-
identifierId: identifier.id,
|
|
1593
|
-
contactId: identifier.contactId,
|
|
1594
|
-
accountId: agentSelection.accountId,
|
|
1595
|
-
subject: finalSubject,
|
|
1596
|
-
htmlBody: finalHtml,
|
|
1597
|
-
textBody: finalText,
|
|
1754
|
+
const result = await this.processSingleUser({
|
|
1755
|
+
rule,
|
|
1756
|
+
email,
|
|
1757
|
+
userKey: userId,
|
|
1758
|
+
identifier,
|
|
1759
|
+
user,
|
|
1760
|
+
sendMap,
|
|
1761
|
+
throttleMap,
|
|
1762
|
+
throttleConfig,
|
|
1763
|
+
template,
|
|
1764
|
+
compiledVariants,
|
|
1765
|
+
templateId,
|
|
1598
1766
|
ruleId,
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
});
|
|
1602
|
-
await this.EmailRuleSend.logSend(
|
|
1603
|
-
ruleId,
|
|
1604
|
-
userId,
|
|
1605
|
-
identifier.id,
|
|
1606
|
-
void 0,
|
|
1607
|
-
{ status: "sent", accountId: agentSelection.accountId, subject: finalSubject, subjectIndex: si, bodyIndex: bi, preheaderIndex: pi }
|
|
1608
|
-
);
|
|
1609
|
-
const current = throttleMap.get(userId) || { today: 0, thisWeek: 0, lastSentDate: null };
|
|
1610
|
-
throttleMap.set(userId, {
|
|
1611
|
-
today: current.today + 1,
|
|
1612
|
-
thisWeek: current.thisWeek + 1,
|
|
1613
|
-
lastSentDate: /* @__PURE__ */ new Date()
|
|
1767
|
+
runId,
|
|
1768
|
+
stats
|
|
1614
1769
|
});
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
await this.updateRunSendProgress(runId, stats);
|
|
1620
|
-
}
|
|
1621
|
-
if (i < users.length - 1) {
|
|
1622
|
-
const delayMs = this.config.options?.delayBetweenSendsMs || 0;
|
|
1623
|
-
const jitterMs = this.config.options?.jitterMs || 0;
|
|
1624
|
-
if (delayMs > 0 || jitterMs > 0) {
|
|
1625
|
-
const totalDelay = delayMs + Math.floor(Math.random() * (jitterMs + 1));
|
|
1626
|
-
if (totalDelay > 0) await new Promise((resolve) => setTimeout(resolve, totalDelay));
|
|
1627
|
-
}
|
|
1770
|
+
if (result === "sent") {
|
|
1771
|
+
totalProcessed++;
|
|
1772
|
+
if (runId && totalProcessed % 10 === 0) await this.updateRunSendProgress(runId, stats);
|
|
1773
|
+
await this.applySendDelay(i >= users.length - 1);
|
|
1628
1774
|
}
|
|
1629
1775
|
} catch (err) {
|
|
1630
1776
|
stats.errorCount++;
|
|
1631
|
-
this.
|
|
1777
|
+
this.emitSendEvent(rule, user.email || "unknown", "error", templateId, runId || "", { failureReason: err.message || "unknown error" });
|
|
1632
1778
|
this.logger.error(`Rule "${rule.name}" failed for user ${user._id?.toString()}`, { error: err });
|
|
1633
1779
|
}
|
|
1634
1780
|
}
|
|
1635
|
-
await this.
|
|
1636
|
-
$set: { lastRunAt: /* @__PURE__ */ new Date(), lastRunStats: stats },
|
|
1637
|
-
$inc: { totalSent: stats.sent, totalSkipped: stats.skipped }
|
|
1638
|
-
});
|
|
1639
|
-
this.config.hooks?.onRuleComplete?.({ ruleId, ruleName: rule.name, stats, templateId, runId: runId || "" });
|
|
1781
|
+
await this.finalizeRuleStats(rule, stats, ruleId, templateId, runId);
|
|
1640
1782
|
return stats;
|
|
1641
1783
|
}
|
|
1642
1784
|
checkThrottle(rule, userId, email, throttleMap, config, stats, templateId, runId) {
|
|
@@ -1648,19 +1790,19 @@ var RuleRunnerService = class {
|
|
|
1648
1790
|
const userThrottle = throttleMap.get(userId) || { today: 0, thisWeek: 0, lastSentDate: null };
|
|
1649
1791
|
if (userThrottle.today >= dailyLimit) {
|
|
1650
1792
|
stats.skippedByThrottle++;
|
|
1651
|
-
this.
|
|
1793
|
+
this.emitSendEvent(rule, email, "throttled", templateId || "", runId || "", { failureReason: "daily throttle limit" });
|
|
1652
1794
|
return false;
|
|
1653
1795
|
}
|
|
1654
1796
|
if (userThrottle.thisWeek >= weeklyLimit) {
|
|
1655
1797
|
stats.skippedByThrottle++;
|
|
1656
|
-
this.
|
|
1798
|
+
this.emitSendEvent(rule, email, "throttled", templateId || "", runId || "", { failureReason: "weekly throttle limit" });
|
|
1657
1799
|
return false;
|
|
1658
1800
|
}
|
|
1659
1801
|
if (userThrottle.lastSentDate) {
|
|
1660
1802
|
const daysSinceLastSend = (Date.now() - userThrottle.lastSentDate.getTime()) / MS_PER_DAY;
|
|
1661
1803
|
if (daysSinceLastSend < minGap) {
|
|
1662
1804
|
stats.skippedByThrottle++;
|
|
1663
|
-
this.
|
|
1805
|
+
this.emitSendEvent(rule, email, "throttled", templateId || "", runId || "", { failureReason: "min gap days" });
|
|
1664
1806
|
return false;
|
|
1665
1807
|
}
|
|
1666
1808
|
}
|
|
@@ -1767,7 +1909,7 @@ var RuleRunnerService = class {
|
|
|
1767
1909
|
this.updateRunProgress(runId, { status: "failed" }).catch(() => {
|
|
1768
1910
|
});
|
|
1769
1911
|
});
|
|
1770
|
-
return { runId };
|
|
1912
|
+
return { runId, started: true };
|
|
1771
1913
|
}
|
|
1772
1914
|
buildThrottleMap(recentSends) {
|
|
1773
1915
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -1788,467 +1930,342 @@ var RuleRunnerService = class {
|
|
|
1788
1930
|
return map;
|
|
1789
1931
|
}
|
|
1790
1932
|
};
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
}
|
|
1933
|
+
|
|
1934
|
+
// src/utils/controller.ts
|
|
1794
1935
|
function getErrorStatus(message) {
|
|
1795
|
-
if (message.includes("already exists") || message.includes("validation failed")) return 400;
|
|
1796
1936
|
if (message.includes("not found")) return 404;
|
|
1937
|
+
if (message.includes("already exists") || message.includes("validation failed") || message.includes("mismatch") || message.includes("Cannot activate") || message.includes("Cannot delete")) return 400;
|
|
1797
1938
|
return 500;
|
|
1798
1939
|
}
|
|
1940
|
+
function isValidValue(allowed, value) {
|
|
1941
|
+
return typeof value === "string" && allowed.includes(value);
|
|
1942
|
+
}
|
|
1943
|
+
function asyncHandler(handler) {
|
|
1944
|
+
return (req, res) => {
|
|
1945
|
+
handler(req, res).catch((error) => {
|
|
1946
|
+
if (res.headersSent) return;
|
|
1947
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1948
|
+
const status = getErrorStatus(message);
|
|
1949
|
+
res.status(status).json({ success: false, error: message });
|
|
1950
|
+
});
|
|
1951
|
+
};
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
// src/controllers/template.controller.ts
|
|
1799
1955
|
function createTemplateController(templateService, options) {
|
|
1800
1956
|
const platformValues = options?.platforms;
|
|
1801
1957
|
const validCategories = options?.categories || Object.values(TEMPLATE_CATEGORY);
|
|
1802
1958
|
const validAudiences = options?.audiences || Object.values(TEMPLATE_AUDIENCE);
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1959
|
+
const list = asyncHandler(async (req, res) => {
|
|
1960
|
+
const { category, audience, platform, isActive, page, limit } = req.query;
|
|
1961
|
+
const { templates, total } = await templateService.list({
|
|
1962
|
+
category,
|
|
1963
|
+
audience,
|
|
1964
|
+
platform,
|
|
1965
|
+
isActive: isActive !== void 0 ? isActive === "true" : void 0,
|
|
1966
|
+
...calculatePagination(parseInt(String(page), 10) || void 0, parseInt(String(limit), 10) || void 0)
|
|
1967
|
+
});
|
|
1968
|
+
res.json({ success: true, data: { templates, total } });
|
|
1969
|
+
});
|
|
1970
|
+
const getById = asyncHandler(async (req, res) => {
|
|
1971
|
+
const template = await templateService.getById(core.getParam(req, "id"));
|
|
1972
|
+
if (!template) {
|
|
1973
|
+
return res.status(404).json({ success: false, error: "Template not found" });
|
|
1816
1974
|
}
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
}
|
|
1824
|
-
res.json({ success: true, data: { template } });
|
|
1825
|
-
} catch (error) {
|
|
1826
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1827
|
-
res.status(500).json({ success: false, error: message });
|
|
1975
|
+
res.json({ success: true, data: { template } });
|
|
1976
|
+
});
|
|
1977
|
+
const create = asyncHandler(async (req, res) => {
|
|
1978
|
+
const { name, subjects, bodies, category, audience, platform, preheaders } = req.body;
|
|
1979
|
+
if (!name || !subjects || subjects.length === 0 || !bodies || bodies.length === 0 || !category || !audience || !platform) {
|
|
1980
|
+
return res.status(400).json({ success: false, error: "name, subjects, bodies, category, audience, and platform are required" });
|
|
1828
1981
|
}
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
try {
|
|
1832
|
-
const { name, subjects, bodies, category, audience, platform, preheaders } = req.body;
|
|
1833
|
-
if (!name || !subjects || subjects.length === 0 || !bodies || bodies.length === 0 || !category || !audience || !platform) {
|
|
1834
|
-
return res.status(400).json({ success: false, error: "name, subjects, bodies, category, audience, and platform are required" });
|
|
1835
|
-
}
|
|
1836
|
-
if (!isValidValue(validCategories, category)) {
|
|
1837
|
-
return res.status(400).json({ success: false, error: `Invalid category. Must be one of: ${validCategories.join(", ")}` });
|
|
1838
|
-
}
|
|
1839
|
-
if (!isValidValue(validAudiences, audience)) {
|
|
1840
|
-
return res.status(400).json({ success: false, error: `Invalid audience. Must be one of: ${validAudiences.join(", ")}` });
|
|
1841
|
-
}
|
|
1842
|
-
if (platformValues && !platformValues.includes(platform)) {
|
|
1843
|
-
return res.status(400).json({ success: false, error: `Invalid platform. Must be one of: ${platformValues.join(", ")}` });
|
|
1844
|
-
}
|
|
1845
|
-
const template = await templateService.create(req.body);
|
|
1846
|
-
res.status(201).json({ success: true, data: { template } });
|
|
1847
|
-
} catch (error) {
|
|
1848
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1849
|
-
res.status(getErrorStatus(message)).json({ success: false, error: message });
|
|
1982
|
+
if (!isValidValue(validCategories, category)) {
|
|
1983
|
+
return res.status(400).json({ success: false, error: `Invalid category. Must be one of: ${validCategories.join(", ")}` });
|
|
1850
1984
|
}
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
try {
|
|
1854
|
-
const template = await templateService.update(core.getParam(req, "id"), req.body);
|
|
1855
|
-
if (!template) {
|
|
1856
|
-
return res.status(404).json({ success: false, error: "Template not found" });
|
|
1857
|
-
}
|
|
1858
|
-
res.json({ success: true, data: { template } });
|
|
1859
|
-
} catch (error) {
|
|
1860
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1861
|
-
res.status(getErrorStatus(message)).json({ success: false, error: message });
|
|
1985
|
+
if (!isValidValue(validAudiences, audience)) {
|
|
1986
|
+
return res.status(400).json({ success: false, error: `Invalid audience. Must be one of: ${validAudiences.join(", ")}` });
|
|
1862
1987
|
}
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
try {
|
|
1866
|
-
const deleted = await templateService.delete(core.getParam(req, "id"));
|
|
1867
|
-
if (!deleted) {
|
|
1868
|
-
return res.status(404).json({ success: false, error: "Template not found" });
|
|
1869
|
-
}
|
|
1870
|
-
res.json({ success: true });
|
|
1871
|
-
} catch (error) {
|
|
1872
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1873
|
-
res.status(500).json({ success: false, error: message });
|
|
1988
|
+
if (platformValues && !platformValues.includes(platform)) {
|
|
1989
|
+
return res.status(400).json({ success: false, error: `Invalid platform. Must be one of: ${platformValues.join(", ")}` });
|
|
1874
1990
|
}
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
}
|
|
1882
|
-
res.json({ success: true, data: { template } });
|
|
1883
|
-
} catch (error) {
|
|
1884
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1885
|
-
res.status(500).json({ success: false, error: message });
|
|
1991
|
+
const template = await templateService.create(req.body);
|
|
1992
|
+
res.status(201).json({ success: true, data: { template } });
|
|
1993
|
+
});
|
|
1994
|
+
const update = asyncHandler(async (req, res) => {
|
|
1995
|
+
const template = await templateService.update(core.getParam(req, "id"), req.body);
|
|
1996
|
+
if (!template) {
|
|
1997
|
+
return res.status(404).json({ success: false, error: "Template not found" });
|
|
1886
1998
|
}
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
return res.status(404).json({ success: false, error: "Template not found" });
|
|
1894
|
-
}
|
|
1895
|
-
res.json({ success: true, data: result });
|
|
1896
|
-
} catch (error) {
|
|
1897
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1898
|
-
res.status(500).json({ success: false, error: message });
|
|
1999
|
+
res.json({ success: true, data: { template } });
|
|
2000
|
+
});
|
|
2001
|
+
const remove = asyncHandler(async (req, res) => {
|
|
2002
|
+
const deleted = await templateService.delete(core.getParam(req, "id"));
|
|
2003
|
+
if (!deleted) {
|
|
2004
|
+
return res.status(404).json({ success: false, error: "Template not found" });
|
|
1899
2005
|
}
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
}
|
|
1907
|
-
const result = await templateService.previewRaw(subject, body, sampleData || {}, variables, textBody);
|
|
1908
|
-
res.json({ success: true, data: result });
|
|
1909
|
-
} catch (error) {
|
|
1910
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1911
|
-
res.status(500).json({ success: false, error: message });
|
|
2006
|
+
res.json({ success: true });
|
|
2007
|
+
});
|
|
2008
|
+
const toggleActive = asyncHandler(async (req, res) => {
|
|
2009
|
+
const template = await templateService.toggleActive(core.getParam(req, "id"));
|
|
2010
|
+
if (!template) {
|
|
2011
|
+
return res.status(404).json({ success: false, error: "Template not found" });
|
|
1912
2012
|
}
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
}
|
|
1920
|
-
const result = await templateService.validate(templateBody);
|
|
1921
|
-
res.json({ success: true, data: result });
|
|
1922
|
-
} catch (error) {
|
|
1923
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1924
|
-
res.status(500).json({ success: false, error: message });
|
|
2013
|
+
res.json({ success: true, data: { template } });
|
|
2014
|
+
});
|
|
2015
|
+
const preview = asyncHandler(async (req, res) => {
|
|
2016
|
+
const { sampleData } = req.body;
|
|
2017
|
+
const result = await templateService.preview(core.getParam(req, "id"), sampleData || {});
|
|
2018
|
+
if (!result) {
|
|
2019
|
+
return res.status(404).json({ success: false, error: "Template not found" });
|
|
1925
2020
|
}
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
res.json({ success:
|
|
1932
|
-
} catch (error) {
|
|
1933
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1934
|
-
res.status(error instanceof Error && error.message === "Template not found" ? 404 : 500).json({ success: false, error: message });
|
|
2021
|
+
res.json({ success: true, data: result });
|
|
2022
|
+
});
|
|
2023
|
+
const previewRaw = asyncHandler(async (req, res) => {
|
|
2024
|
+
const { subject, body, textBody, sampleData, variables } = req.body;
|
|
2025
|
+
if (!subject || !body) {
|
|
2026
|
+
return res.status(400).json({ success: false, error: "subject and body are required" });
|
|
1935
2027
|
}
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
}
|
|
1943
|
-
const result = await templateService.sendTestEmail(core.getParam(req, "id"), testEmail, sampleData || {});
|
|
1944
|
-
if (!result.success) {
|
|
1945
|
-
return res.status(400).json({ success: false, error: result.error });
|
|
1946
|
-
}
|
|
1947
|
-
res.json({ success: true });
|
|
1948
|
-
} catch (error) {
|
|
1949
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1950
|
-
res.status(500).json({ success: false, error: message });
|
|
2028
|
+
const result = await templateService.previewRaw(subject, body, sampleData || {}, variables, textBody);
|
|
2029
|
+
res.json({ success: true, data: result });
|
|
2030
|
+
});
|
|
2031
|
+
const validate = asyncHandler(async (req, res) => {
|
|
2032
|
+
const { body: templateBody } = req.body;
|
|
2033
|
+
if (!templateBody) {
|
|
2034
|
+
return res.status(400).json({ success: false, error: "body is required" });
|
|
1951
2035
|
}
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
res.json({ success:
|
|
1964
|
-
} catch (error) {
|
|
1965
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1966
|
-
res.status(500).json({ success: false, error: message });
|
|
2036
|
+
const result = await templateService.validate(templateBody);
|
|
2037
|
+
res.json({ success: true, data: result });
|
|
2038
|
+
});
|
|
2039
|
+
const clone = asyncHandler(async (req, res) => {
|
|
2040
|
+
const { name } = req.body;
|
|
2041
|
+
const result = await templateService.clone(core.getParam(req, "id"), name);
|
|
2042
|
+
res.json({ success: true, data: result });
|
|
2043
|
+
});
|
|
2044
|
+
const sendTestEmail = asyncHandler(async (req, res) => {
|
|
2045
|
+
const { testEmail, sampleData } = req.body;
|
|
2046
|
+
if (!testEmail) {
|
|
2047
|
+
return res.status(400).json({ success: false, error: "testEmail is required" });
|
|
1967
2048
|
}
|
|
1968
|
-
|
|
2049
|
+
const result = await templateService.sendTestEmail(core.getParam(req, "id"), testEmail, sampleData || {});
|
|
2050
|
+
if (!result.success) {
|
|
2051
|
+
return res.status(400).json({ success: false, error: result.error });
|
|
2052
|
+
}
|
|
2053
|
+
res.json({ success: true });
|
|
2054
|
+
});
|
|
2055
|
+
const previewWithRecipient = asyncHandler(async (req, res) => {
|
|
2056
|
+
const { recipientData } = req.body;
|
|
2057
|
+
if (!recipientData || typeof recipientData !== "object") {
|
|
2058
|
+
return res.status(400).json({ success: false, error: "recipientData object is required" });
|
|
2059
|
+
}
|
|
2060
|
+
const result = await templateService.previewWithRecipient(core.getParam(req, "id"), recipientData);
|
|
2061
|
+
if (!result) {
|
|
2062
|
+
return res.status(404).json({ success: false, error: "Template not found" });
|
|
2063
|
+
}
|
|
2064
|
+
res.json({ success: true, data: result });
|
|
2065
|
+
});
|
|
1969
2066
|
return { list, getById, create, update, remove, toggleActive, preview, previewRaw, validate, sendTestEmail, clone, previewWithRecipient };
|
|
1970
2067
|
}
|
|
1971
|
-
function isValidValue2(allowed, value) {
|
|
1972
|
-
return typeof value === "string" && allowed.includes(value);
|
|
1973
|
-
}
|
|
1974
|
-
function getErrorStatus2(message) {
|
|
1975
|
-
if (message.includes("not found")) return 404;
|
|
1976
|
-
if (message.includes("mismatch") || message.includes("validation failed") || message.includes("Cannot activate")) return 400;
|
|
1977
|
-
return 500;
|
|
1978
|
-
}
|
|
1979
2068
|
function createRuleController(ruleService, options) {
|
|
1980
2069
|
const platformValues = options?.platforms;
|
|
1981
2070
|
const validAudiences = options?.audiences || Object.values(TEMPLATE_AUDIENCE);
|
|
1982
2071
|
const validEmailTypes = Object.values(EMAIL_TYPE);
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
2072
|
+
const list = asyncHandler(async (req, res) => {
|
|
2073
|
+
const { page, limit } = calculatePagination(
|
|
2074
|
+
parseInt(String(req.query.page), 10) || void 0,
|
|
2075
|
+
parseInt(String(req.query.limit), 10) || void 0
|
|
2076
|
+
);
|
|
2077
|
+
const { rules, total } = await ruleService.list({ page, limit });
|
|
2078
|
+
res.json({ success: true, data: { rules, total } });
|
|
2079
|
+
});
|
|
2080
|
+
const getById = asyncHandler(async (req, res) => {
|
|
2081
|
+
const rule = await ruleService.getById(core.getParam(req, "id"));
|
|
2082
|
+
if (!rule) {
|
|
2083
|
+
return res.status(404).json({ success: false, error: "Rule not found" });
|
|
1990
2084
|
}
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
}
|
|
1998
|
-
res.json({ success: true, data: { rule } });
|
|
1999
|
-
} catch (error) {
|
|
2000
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
2001
|
-
res.status(500).json({ success: false, error: message });
|
|
2085
|
+
res.json({ success: true, data: { rule } });
|
|
2086
|
+
});
|
|
2087
|
+
const create = asyncHandler(async (req, res) => {
|
|
2088
|
+
const { name, target, templateId } = req.body;
|
|
2089
|
+
if (!name || !target || !templateId) {
|
|
2090
|
+
return res.status(400).json({ success: false, error: "name, target, and templateId are required" });
|
|
2002
2091
|
}
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2092
|
+
const mode = target.mode || "query";
|
|
2093
|
+
if (mode === "list") {
|
|
2094
|
+
if (!Array.isArray(target.identifiers) || target.identifiers.length === 0) {
|
|
2095
|
+
return res.status(400).json({ success: false, error: "target.identifiers must be a non-empty array for list mode" });
|
|
2096
|
+
}
|
|
2097
|
+
} else {
|
|
2098
|
+
if (!target.role || !isValidValue(validAudiences, target.role)) {
|
|
2099
|
+
return res.status(400).json({ success: false, error: `Invalid target.role. Must be one of: ${validAudiences.join(", ")}` });
|
|
2100
|
+
}
|
|
2101
|
+
if (platformValues && !platformValues.includes(target.platform)) {
|
|
2102
|
+
return res.status(400).json({ success: false, error: `Invalid target.platform. Must be one of: ${platformValues.join(", ")}` });
|
|
2103
|
+
}
|
|
2104
|
+
if (!Array.isArray(target.conditions)) {
|
|
2105
|
+
return res.status(400).json({ success: false, error: "target.conditions must be an array" });
|
|
2009
2106
|
}
|
|
2107
|
+
}
|
|
2108
|
+
if (req.body.emailType && !isValidValue(validEmailTypes, req.body.emailType)) {
|
|
2109
|
+
return res.status(400).json({ success: false, error: `Invalid emailType. Must be one of: ${validEmailTypes.join(", ")}` });
|
|
2110
|
+
}
|
|
2111
|
+
const rule = await ruleService.create(req.body);
|
|
2112
|
+
res.status(201).json({ success: true, data: { rule } });
|
|
2113
|
+
});
|
|
2114
|
+
const update = asyncHandler(async (req, res) => {
|
|
2115
|
+
const { target, emailType } = req.body;
|
|
2116
|
+
if (target) {
|
|
2010
2117
|
const mode = target.mode || "query";
|
|
2011
2118
|
if (mode === "list") {
|
|
2012
|
-
if (!Array.isArray(target.identifiers) || target.identifiers.length === 0) {
|
|
2119
|
+
if (target.identifiers && (!Array.isArray(target.identifiers) || target.identifiers.length === 0)) {
|
|
2013
2120
|
return res.status(400).json({ success: false, error: "target.identifiers must be a non-empty array for list mode" });
|
|
2014
2121
|
}
|
|
2015
2122
|
} else {
|
|
2016
|
-
if (
|
|
2123
|
+
if (target.role && !isValidValue(validAudiences, target.role)) {
|
|
2017
2124
|
return res.status(400).json({ success: false, error: `Invalid target.role. Must be one of: ${validAudiences.join(", ")}` });
|
|
2018
2125
|
}
|
|
2019
|
-
if (platformValues && !platformValues.includes(target.platform)) {
|
|
2126
|
+
if (target.platform && platformValues && !platformValues.includes(target.platform)) {
|
|
2020
2127
|
return res.status(400).json({ success: false, error: `Invalid target.platform. Must be one of: ${platformValues.join(", ")}` });
|
|
2021
2128
|
}
|
|
2022
|
-
if (!Array.isArray(target.conditions)) {
|
|
2129
|
+
if (target.conditions && !Array.isArray(target.conditions)) {
|
|
2023
2130
|
return res.status(400).json({ success: false, error: "target.conditions must be an array" });
|
|
2024
2131
|
}
|
|
2025
2132
|
}
|
|
2026
|
-
if (req.body.emailType && !isValidValue2(validEmailTypes, req.body.emailType)) {
|
|
2027
|
-
return res.status(400).json({ success: false, error: `Invalid emailType. Must be one of: ${validEmailTypes.join(", ")}` });
|
|
2028
|
-
}
|
|
2029
|
-
const rule = await ruleService.create(req.body);
|
|
2030
|
-
res.status(201).json({ success: true, data: { rule } });
|
|
2031
|
-
} catch (error) {
|
|
2032
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
2033
|
-
res.status(getErrorStatus2(message)).json({ success: false, error: message });
|
|
2034
|
-
}
|
|
2035
|
-
}
|
|
2036
|
-
async function update(req, res) {
|
|
2037
|
-
try {
|
|
2038
|
-
const { target, emailType } = req.body;
|
|
2039
|
-
if (target) {
|
|
2040
|
-
const mode = target.mode || "query";
|
|
2041
|
-
if (mode === "list") {
|
|
2042
|
-
if (target.identifiers && (!Array.isArray(target.identifiers) || target.identifiers.length === 0)) {
|
|
2043
|
-
return res.status(400).json({ success: false, error: "target.identifiers must be a non-empty array for list mode" });
|
|
2044
|
-
}
|
|
2045
|
-
} else {
|
|
2046
|
-
if (target.role && !isValidValue2(validAudiences, target.role)) {
|
|
2047
|
-
return res.status(400).json({ success: false, error: `Invalid target.role. Must be one of: ${validAudiences.join(", ")}` });
|
|
2048
|
-
}
|
|
2049
|
-
if (target.platform && platformValues && !platformValues.includes(target.platform)) {
|
|
2050
|
-
return res.status(400).json({ success: false, error: `Invalid target.platform. Must be one of: ${platformValues.join(", ")}` });
|
|
2051
|
-
}
|
|
2052
|
-
if (target.conditions && !Array.isArray(target.conditions)) {
|
|
2053
|
-
return res.status(400).json({ success: false, error: "target.conditions must be an array" });
|
|
2054
|
-
}
|
|
2055
|
-
}
|
|
2056
|
-
}
|
|
2057
|
-
if (emailType && !isValidValue2(validEmailTypes, emailType)) {
|
|
2058
|
-
return res.status(400).json({ success: false, error: `Invalid emailType. Must be one of: ${validEmailTypes.join(", ")}` });
|
|
2059
|
-
}
|
|
2060
|
-
const rule = await ruleService.update(core.getParam(req, "id"), req.body);
|
|
2061
|
-
if (!rule) {
|
|
2062
|
-
return res.status(404).json({ success: false, error: "Rule not found" });
|
|
2063
|
-
}
|
|
2064
|
-
res.json({ success: true, data: { rule } });
|
|
2065
|
-
} catch (error) {
|
|
2066
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
2067
|
-
res.status(getErrorStatus2(message)).json({ success: false, error: message });
|
|
2068
|
-
}
|
|
2069
|
-
}
|
|
2070
|
-
async function remove(req, res) {
|
|
2071
|
-
try {
|
|
2072
|
-
const result = await ruleService.delete(core.getParam(req, "id"));
|
|
2073
|
-
if (!result.deleted && !result.disabled) {
|
|
2074
|
-
return res.status(404).json({ success: false, error: "Rule not found" });
|
|
2075
|
-
}
|
|
2076
|
-
res.json({ success: true, data: result });
|
|
2077
|
-
} catch (error) {
|
|
2078
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
2079
|
-
res.status(500).json({ success: false, error: message });
|
|
2080
2133
|
}
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
try {
|
|
2084
|
-
const rule = await ruleService.toggleActive(core.getParam(req, "id"));
|
|
2085
|
-
if (!rule) {
|
|
2086
|
-
return res.status(404).json({ success: false, error: "Rule not found" });
|
|
2087
|
-
}
|
|
2088
|
-
res.json({ success: true, data: { rule } });
|
|
2089
|
-
} catch (error) {
|
|
2090
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
2091
|
-
res.status(getErrorStatus2(message)).json({ success: false, error: message });
|
|
2134
|
+
if (emailType && !isValidValue(validEmailTypes, emailType)) {
|
|
2135
|
+
return res.status(400).json({ success: false, error: `Invalid emailType. Must be one of: ${validEmailTypes.join(", ")}` });
|
|
2092
2136
|
}
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
const result = await ruleService.dryRun(core.getParam(req, "id"));
|
|
2097
|
-
res.json({ success: true, data: result });
|
|
2098
|
-
} catch (error) {
|
|
2099
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
2100
|
-
res.status(getErrorStatus2(message)).json({ success: false, error: message });
|
|
2137
|
+
const rule = await ruleService.update(core.getParam(req, "id"), req.body);
|
|
2138
|
+
if (!rule) {
|
|
2139
|
+
return res.status(404).json({ success: false, error: "Rule not found" });
|
|
2101
2140
|
}
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
res.json({ success:
|
|
2108
|
-
} catch (error) {
|
|
2109
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
2110
|
-
res.status(error instanceof Error && error.message === "Rule not found" ? 404 : 500).json({ success: false, error: message });
|
|
2141
|
+
res.json({ success: true, data: { rule } });
|
|
2142
|
+
});
|
|
2143
|
+
const remove = asyncHandler(async (req, res) => {
|
|
2144
|
+
const result = await ruleService.delete(core.getParam(req, "id"));
|
|
2145
|
+
if (!result.deleted && !result.disabled) {
|
|
2146
|
+
return res.status(404).json({ success: false, error: "Rule not found" });
|
|
2111
2147
|
}
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
res.json({ success: true, data: { logs } });
|
|
2119
|
-
} catch (error) {
|
|
2120
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
2121
|
-
res.status(500).json({ success: false, error: message });
|
|
2148
|
+
res.json({ success: true, data: result });
|
|
2149
|
+
});
|
|
2150
|
+
const toggleActive = asyncHandler(async (req, res) => {
|
|
2151
|
+
const rule = await ruleService.toggleActive(core.getParam(req, "id"));
|
|
2152
|
+
if (!rule) {
|
|
2153
|
+
return res.status(404).json({ success: false, error: "Rule not found" });
|
|
2122
2154
|
}
|
|
2123
|
-
|
|
2155
|
+
res.json({ success: true, data: { rule } });
|
|
2156
|
+
});
|
|
2157
|
+
const dryRun = asyncHandler(async (req, res) => {
|
|
2158
|
+
const result = await ruleService.dryRun(core.getParam(req, "id"));
|
|
2159
|
+
res.json({ success: true, data: result });
|
|
2160
|
+
});
|
|
2161
|
+
const clone = asyncHandler(async (req, res) => {
|
|
2162
|
+
const { name } = req.body;
|
|
2163
|
+
const result = await ruleService.clone(core.getParam(req, "id"), name);
|
|
2164
|
+
res.json({ success: true, data: result });
|
|
2165
|
+
});
|
|
2166
|
+
const runHistory = asyncHandler(async (req, res) => {
|
|
2167
|
+
const { page, limit } = calculatePagination(
|
|
2168
|
+
parseInt(String(req.query.page), 10) || void 0,
|
|
2169
|
+
parseInt(String(req.query.limit), 10) || 20
|
|
2170
|
+
);
|
|
2171
|
+
const from = req.query.from ? String(req.query.from) : void 0;
|
|
2172
|
+
const to = req.query.to ? String(req.query.to) : void 0;
|
|
2173
|
+
const logs = await ruleService.getRunHistory(limit, { page, from, to });
|
|
2174
|
+
const total = await ruleService.getRunHistoryCount({ from, to });
|
|
2175
|
+
res.json({ success: true, data: { logs, total } });
|
|
2176
|
+
});
|
|
2124
2177
|
return { list, getById, create, update, remove, toggleActive, dryRun, runHistory, clone };
|
|
2125
2178
|
}
|
|
2126
2179
|
function createRunnerController(runnerService, EmailRuleRunLog, logger) {
|
|
2127
|
-
|
|
2180
|
+
const triggerManualRun = asyncHandler(async (_req, res) => {
|
|
2128
2181
|
const { runId } = runnerService.trigger(RUN_TRIGGER.Manual);
|
|
2129
2182
|
res.json({ success: true, data: { message: "Rule run triggered", runId } });
|
|
2130
|
-
}
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
async function getStatusByRunId(req, res) {
|
|
2141
|
-
try {
|
|
2142
|
-
const status = await runnerService.getStatus(core.getParam(req, "runId"));
|
|
2143
|
-
if (!status) {
|
|
2144
|
-
res.status(404).json({ success: false, error: "Run not found" });
|
|
2145
|
-
return;
|
|
2146
|
-
}
|
|
2147
|
-
res.json({ success: true, data: status });
|
|
2148
|
-
} catch (error) {
|
|
2149
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
2150
|
-
res.status(500).json({ success: false, error: message });
|
|
2183
|
+
});
|
|
2184
|
+
const getLatestRun = asyncHandler(async (_req, res) => {
|
|
2185
|
+
const latestRun = await EmailRuleRunLog.findOne().sort({ runAt: -1 });
|
|
2186
|
+
res.json({ success: true, data: { latestRun } });
|
|
2187
|
+
});
|
|
2188
|
+
const getStatusByRunId = asyncHandler(async (req, res) => {
|
|
2189
|
+
const status = await runnerService.getStatus(core.getParam(req, "runId"));
|
|
2190
|
+
if (!status) {
|
|
2191
|
+
res.status(404).json({ success: false, error: "Run not found" });
|
|
2192
|
+
return;
|
|
2151
2193
|
}
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
}
|
|
2160
|
-
res.json({ success: true, data: { message: "Cancel requested" } });
|
|
2161
|
-
} catch (error) {
|
|
2162
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
2163
|
-
res.status(500).json({ success: false, error: message });
|
|
2194
|
+
res.json({ success: true, data: status });
|
|
2195
|
+
});
|
|
2196
|
+
const cancelRun = asyncHandler(async (req, res) => {
|
|
2197
|
+
const result = await runnerService.cancel(core.getParam(req, "runId"));
|
|
2198
|
+
if (!result.ok) {
|
|
2199
|
+
res.status(404).json({ success: false, error: "Run not found" });
|
|
2200
|
+
return;
|
|
2164
2201
|
}
|
|
2165
|
-
|
|
2202
|
+
res.json({ success: true, data: { message: "Cancel requested" } });
|
|
2203
|
+
});
|
|
2166
2204
|
return { triggerManualRun, getLatestRun, getStatusByRunId, cancelRun };
|
|
2167
2205
|
}
|
|
2168
2206
|
|
|
2169
2207
|
// src/controllers/settings.controller.ts
|
|
2170
2208
|
function createSettingsController(EmailThrottleConfig) {
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
try {
|
|
2182
|
-
const { maxPerUserPerDay, maxPerUserPerWeek, minGapDays } = req.body;
|
|
2183
|
-
const updates = {};
|
|
2184
|
-
if (maxPerUserPerDay !== void 0) {
|
|
2185
|
-
if (!Number.isInteger(maxPerUserPerDay) || maxPerUserPerDay < 1) {
|
|
2186
|
-
return res.status(400).json({ success: false, error: "maxPerUserPerDay must be a positive integer" });
|
|
2187
|
-
}
|
|
2188
|
-
updates.maxPerUserPerDay = maxPerUserPerDay;
|
|
2189
|
-
}
|
|
2190
|
-
if (maxPerUserPerWeek !== void 0) {
|
|
2191
|
-
if (!Number.isInteger(maxPerUserPerWeek) || maxPerUserPerWeek < 1) {
|
|
2192
|
-
return res.status(400).json({ success: false, error: "maxPerUserPerWeek must be a positive integer" });
|
|
2193
|
-
}
|
|
2194
|
-
updates.maxPerUserPerWeek = maxPerUserPerWeek;
|
|
2195
|
-
}
|
|
2196
|
-
if (minGapDays !== void 0) {
|
|
2197
|
-
if (!Number.isInteger(minGapDays) || minGapDays < 0) {
|
|
2198
|
-
return res.status(400).json({ success: false, error: "minGapDays must be a non-negative integer" });
|
|
2199
|
-
}
|
|
2200
|
-
updates.minGapDays = minGapDays;
|
|
2209
|
+
const getThrottleConfig = asyncHandler(async (_req, res) => {
|
|
2210
|
+
const config = await EmailThrottleConfig.getConfig();
|
|
2211
|
+
res.json({ success: true, data: { config } });
|
|
2212
|
+
});
|
|
2213
|
+
const updateThrottleConfig = asyncHandler(async (req, res) => {
|
|
2214
|
+
const { maxPerUserPerDay, maxPerUserPerWeek, minGapDays } = req.body;
|
|
2215
|
+
const updates = {};
|
|
2216
|
+
if (maxPerUserPerDay !== void 0) {
|
|
2217
|
+
if (!Number.isInteger(maxPerUserPerDay) || maxPerUserPerDay < 1) {
|
|
2218
|
+
return res.status(400).json({ success: false, error: "maxPerUserPerDay must be a positive integer" });
|
|
2201
2219
|
}
|
|
2202
|
-
|
|
2203
|
-
|
|
2220
|
+
updates.maxPerUserPerDay = maxPerUserPerDay;
|
|
2221
|
+
}
|
|
2222
|
+
if (maxPerUserPerWeek !== void 0) {
|
|
2223
|
+
if (!Number.isInteger(maxPerUserPerWeek) || maxPerUserPerWeek < 1) {
|
|
2224
|
+
return res.status(400).json({ success: false, error: "maxPerUserPerWeek must be a positive integer" });
|
|
2204
2225
|
}
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
if (
|
|
2209
|
-
return res.status(400).json({ success: false, error: "
|
|
2226
|
+
updates.maxPerUserPerWeek = maxPerUserPerWeek;
|
|
2227
|
+
}
|
|
2228
|
+
if (minGapDays !== void 0) {
|
|
2229
|
+
if (!Number.isInteger(minGapDays) || minGapDays < 0) {
|
|
2230
|
+
return res.status(400).json({ success: false, error: "minGapDays must be a non-negative integer" });
|
|
2210
2231
|
}
|
|
2211
|
-
|
|
2212
|
-
config._id,
|
|
2213
|
-
{ $set: updates },
|
|
2214
|
-
{ new: true }
|
|
2215
|
-
);
|
|
2216
|
-
res.json({ success: true, data: { config: updated } });
|
|
2217
|
-
} catch (error) {
|
|
2218
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
2219
|
-
res.status(500).json({ success: false, error: message });
|
|
2232
|
+
updates.minGapDays = minGapDays;
|
|
2220
2233
|
}
|
|
2221
|
-
|
|
2234
|
+
if (Object.keys(updates).length === 0) {
|
|
2235
|
+
return res.status(400).json({ success: false, error: "No valid fields to update" });
|
|
2236
|
+
}
|
|
2237
|
+
const config = await EmailThrottleConfig.getConfig();
|
|
2238
|
+
const finalDaily = updates.maxPerUserPerDay ?? config.maxPerUserPerDay;
|
|
2239
|
+
const finalWeekly = updates.maxPerUserPerWeek ?? config.maxPerUserPerWeek;
|
|
2240
|
+
if (finalWeekly < finalDaily) {
|
|
2241
|
+
return res.status(400).json({ success: false, error: "maxPerUserPerWeek must be >= maxPerUserPerDay" });
|
|
2242
|
+
}
|
|
2243
|
+
const updated = await EmailThrottleConfig.findByIdAndUpdate(
|
|
2244
|
+
config._id,
|
|
2245
|
+
{ $set: updates },
|
|
2246
|
+
{ new: true }
|
|
2247
|
+
);
|
|
2248
|
+
res.json({ success: true, data: { config: updated } });
|
|
2249
|
+
});
|
|
2222
2250
|
return { getThrottleConfig, updateThrottleConfig };
|
|
2223
2251
|
}
|
|
2224
2252
|
|
|
2225
2253
|
// src/controllers/send-log.controller.ts
|
|
2226
2254
|
function createSendLogController(EmailRuleSend) {
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
const skip = (pageNum - 1) * limitNum;
|
|
2242
|
-
const [sends, total] = await Promise.all([
|
|
2243
|
-
EmailRuleSend.find(filter).sort({ sentAt: -1 }).skip(skip).limit(limitNum).lean(),
|
|
2244
|
-
EmailRuleSend.countDocuments(filter)
|
|
2245
|
-
]);
|
|
2246
|
-
res.json({ success: true, data: { sends, total } });
|
|
2247
|
-
} catch (error) {
|
|
2248
|
-
const message = error instanceof Error ? error.message : "Failed to query send logs";
|
|
2249
|
-
res.status(500).json({ success: false, error: message });
|
|
2250
|
-
}
|
|
2251
|
-
}
|
|
2255
|
+
const list = asyncHandler(async (req, res) => {
|
|
2256
|
+
const { ruleId, status, email, from, to, page, limit } = req.query;
|
|
2257
|
+
const filter = {};
|
|
2258
|
+
if (ruleId) filter.ruleId = ruleId;
|
|
2259
|
+
if (status) filter.status = status;
|
|
2260
|
+
if (email) filter.userId = { $regex: email, $options: "i" };
|
|
2261
|
+
Object.assign(filter, buildDateRangeFilter("sentAt", from, to));
|
|
2262
|
+
const pagination = calculatePagination(Number(page) || void 0, Number(limit) || 50, 200);
|
|
2263
|
+
const [sends, total] = await Promise.all([
|
|
2264
|
+
EmailRuleSend.find(filter).sort({ sentAt: -1 }).skip(pagination.skip).limit(pagination.limit).lean(),
|
|
2265
|
+
EmailRuleSend.countDocuments(filter)
|
|
2266
|
+
]);
|
|
2267
|
+
res.json({ success: true, data: { sends, total } });
|
|
2268
|
+
});
|
|
2252
2269
|
return { list };
|
|
2253
2270
|
}
|
|
2254
2271
|
|
|
@@ -2267,6 +2284,7 @@ function createRoutes(deps) {
|
|
|
2267
2284
|
const runnerCtrl = createRunnerController(deps.runnerService, deps.EmailRuleRunLog);
|
|
2268
2285
|
const settingsCtrl = createSettingsController(deps.EmailThrottleConfig);
|
|
2269
2286
|
const sendLogCtrl = createSendLogController(deps.EmailRuleSend);
|
|
2287
|
+
const collectionCtrl = createCollectionController(deps.collections || []);
|
|
2270
2288
|
const templateRouter = express.Router();
|
|
2271
2289
|
templateRouter.get("/", templateCtrl.list);
|
|
2272
2290
|
templateRouter.post("/", templateCtrl.create);
|
|
@@ -2297,10 +2315,14 @@ function createRoutes(deps) {
|
|
|
2297
2315
|
runnerRouter.get("/logs", ruleCtrl.runHistory);
|
|
2298
2316
|
const sendLogRouter = express.Router();
|
|
2299
2317
|
sendLogRouter.get("/", sendLogCtrl.list);
|
|
2318
|
+
const collectionRouter = express.Router();
|
|
2319
|
+
collectionRouter.get("/", collectionCtrl.list);
|
|
2320
|
+
collectionRouter.get("/:name/fields", collectionCtrl.getFields);
|
|
2300
2321
|
router.use("/templates", templateRouter);
|
|
2301
2322
|
router.use("/rules", ruleRouter);
|
|
2302
2323
|
router.use("/runner", runnerRouter);
|
|
2303
2324
|
router.use("/sends", sendLogRouter);
|
|
2325
|
+
router.use("/collections", collectionRouter);
|
|
2304
2326
|
router.get("/throttle", settingsCtrl.getThrottleConfig);
|
|
2305
2327
|
router.put("/throttle", settingsCtrl.updateThrottleConfig);
|
|
2306
2328
|
return router;
|
|
@@ -2316,6 +2338,19 @@ var configSchema = zod.z.object({
|
|
|
2316
2338
|
findIdentifier: zod.z.function(),
|
|
2317
2339
|
sendTestEmail: zod.z.function().optional()
|
|
2318
2340
|
}),
|
|
2341
|
+
collections: zod.z.array(zod.z.object({
|
|
2342
|
+
name: zod.z.string(),
|
|
2343
|
+
label: zod.z.string().optional(),
|
|
2344
|
+
description: zod.z.string().optional(),
|
|
2345
|
+
identifierField: zod.z.string().optional(),
|
|
2346
|
+
fields: zod.z.array(zod.z.any()),
|
|
2347
|
+
joins: zod.z.array(zod.z.object({
|
|
2348
|
+
from: zod.z.string(),
|
|
2349
|
+
localField: zod.z.string(),
|
|
2350
|
+
foreignField: zod.z.string(),
|
|
2351
|
+
as: zod.z.string()
|
|
2352
|
+
})).optional()
|
|
2353
|
+
})).optional(),
|
|
2319
2354
|
platforms: zod.z.array(zod.z.string()).optional(),
|
|
2320
2355
|
audiences: zod.z.array(zod.z.string()).optional(),
|
|
2321
2356
|
categories: zod.z.array(zod.z.string()).optional(),
|
|
@@ -2392,7 +2427,10 @@ var SchedulerService = class {
|
|
|
2392
2427
|
}, { connection: connectionOpts, prefix: this.keyPrefix });
|
|
2393
2428
|
}
|
|
2394
2429
|
async stopWorker() {
|
|
2395
|
-
|
|
2430
|
+
if (this.worker) {
|
|
2431
|
+
await this.worker.close();
|
|
2432
|
+
this.worker = void 0;
|
|
2433
|
+
}
|
|
2396
2434
|
}
|
|
2397
2435
|
async getScheduledJobs() {
|
|
2398
2436
|
const jobs = await this.queue.getRepeatableJobs();
|
|
@@ -2429,7 +2467,7 @@ function createEmailRuleEngine(config) {
|
|
|
2429
2467
|
`${prefix}EmailThrottleConfig`,
|
|
2430
2468
|
createEmailThrottleConfigSchema(prefix)
|
|
2431
2469
|
);
|
|
2432
|
-
const templateService = new TemplateService(EmailTemplate, config);
|
|
2470
|
+
const templateService = new TemplateService(EmailTemplate, config, EmailRule);
|
|
2433
2471
|
const ruleService = new RuleService(EmailRule, EmailTemplate, EmailRuleRunLog, config);
|
|
2434
2472
|
const runnerService = new RuleRunnerService(
|
|
2435
2473
|
EmailRule,
|
|
@@ -2449,7 +2487,8 @@ function createEmailRuleEngine(config) {
|
|
|
2449
2487
|
platformValues: config.platforms,
|
|
2450
2488
|
categoryValues: config.categories,
|
|
2451
2489
|
audienceValues: config.audiences,
|
|
2452
|
-
logger: config.logger
|
|
2490
|
+
logger: config.logger,
|
|
2491
|
+
collections: config.collections || []
|
|
2453
2492
|
});
|
|
2454
2493
|
return {
|
|
2455
2494
|
routes,
|
|
@@ -2469,6 +2508,7 @@ exports.ConfigValidationError = ConfigValidationError;
|
|
|
2469
2508
|
exports.DuplicateSlugError = DuplicateSlugError;
|
|
2470
2509
|
exports.EMAIL_SEND_STATUS = EMAIL_SEND_STATUS;
|
|
2471
2510
|
exports.EMAIL_TYPE = EMAIL_TYPE;
|
|
2511
|
+
exports.FIELD_TYPE = FIELD_TYPE;
|
|
2472
2512
|
exports.LockAcquisitionError = LockAcquisitionError;
|
|
2473
2513
|
exports.RULE_OPERATOR = RULE_OPERATOR;
|
|
2474
2514
|
exports.RUN_LOG_STATUS = RUN_LOG_STATUS;
|
|
@@ -2482,6 +2522,7 @@ exports.TARGET_MODE = TARGET_MODE;
|
|
|
2482
2522
|
exports.TEMPLATE_AUDIENCE = TEMPLATE_AUDIENCE;
|
|
2483
2523
|
exports.TEMPLATE_CATEGORY = TEMPLATE_CATEGORY;
|
|
2484
2524
|
exports.THROTTLE_WINDOW = THROTTLE_WINDOW;
|
|
2525
|
+
exports.TYPE_OPERATORS = TYPE_OPERATORS;
|
|
2485
2526
|
exports.TemplateNotFoundError = TemplateNotFoundError;
|
|
2486
2527
|
exports.TemplateRenderService = TemplateRenderService;
|
|
2487
2528
|
exports.TemplateService = TemplateService;
|
|
@@ -2492,6 +2533,8 @@ exports.createEmailRuleSchema = createEmailRuleSchema;
|
|
|
2492
2533
|
exports.createEmailRuleSendSchema = createEmailRuleSendSchema;
|
|
2493
2534
|
exports.createEmailTemplateSchema = createEmailTemplateSchema;
|
|
2494
2535
|
exports.createEmailThrottleConfigSchema = createEmailThrottleConfigSchema;
|
|
2536
|
+
exports.flattenFields = flattenFields;
|
|
2537
|
+
exports.validateConditions = validateConditions;
|
|
2495
2538
|
exports.validateConfig = validateConfig;
|
|
2496
2539
|
//# sourceMappingURL=index.cjs.map
|
|
2497
2540
|
//# sourceMappingURL=index.cjs.map
|