@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/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
- }, { _id: false });
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: mongoose.Schema.Types.ObjectId, required: true },
268
- emailIdentifierId: { type: mongoose.Schema.Types.ObjectId },
269
- messageId: { type: mongoose.Schema.Types.ObjectId },
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
- matched: { type: Number, default: 0 },
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: true });
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: true });
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: true });
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: true });
498
- const bodyFn = Handlebars__default.default.compile(htmlWithHandlebars, { strict: true });
499
- const textBodyFn = textBody ? Handlebars__default.default.compile(textBody, { strict: true }) : void 0;
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: true }));
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: true });
524
+ return Handlebars__default.default.compile(htmlWithHandlebars, { strict: false });
508
525
  });
509
- const textBodyFn = textBody ? Handlebars__default.default.compile(textBody, { strict: true }) : void 0;
510
- const preheaderFns = preheaders && preheaders.length > 0 ? preheaders.map((p) => Handlebars__default.default.compile(p, { strict: true })) : void 0;
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
- return this.EmailTemplate.find(query).sort({ category: 1, name: 1 });
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 data = { ...template.fields ?? {}, ...recipientData };
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
- return this.EmailRule.find().populate("templateId", "name slug").sort({ sortOrder: 1, createdAt: -1 });
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
- const setFields = {};
940
- for (const [key, value] of Object.entries(input)) {
941
- if (value !== void 0 && UPDATEABLE_FIELDS2.has(key)) {
942
- setFields[key] = value;
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 users = await this.config.adapters.queryUsers(rule.target, 5e4);
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
- return this.EmailRuleRunLog.getRecent(limit);
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
- async executeListMode(rule, template, throttleMap, throttleConfig, stats, runId) {
1237
- const rawIdentifiers = rule.target.identifiers || [];
1238
- const uniqueEmails = [...new Set(rawIdentifiers.map((e) => e.toLowerCase().trim()).filter(Boolean))];
1239
- const limit = rule.maxPerRun || this.config.options?.defaultMaxPerRun || 500;
1240
- if (uniqueEmails.length > limit) {
1241
- 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 });
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
- const emailsToProcess = uniqueEmails.slice(0, limit);
1244
- stats.matched = emailsToProcess.length;
1245
- const ruleId = rule._id.toString();
1246
- const templateId = rule.templateId.toString();
1247
- this.config.hooks?.onRuleStart?.({ ruleId, ruleName: rule.name, matchedCount: emailsToProcess.length, templateId, runId: runId || "" });
1248
- if (emailsToProcess.length === 0) return stats;
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
- emailsToProcess,
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 identifierMap = /* @__PURE__ */ new Map();
1586
+ const map = /* @__PURE__ */ new Map();
1258
1587
  for (const result of identifierResults) {
1259
1588
  if (result) {
1260
- identifierMap.set(result.email, { id: result.id, contactId: result.contactId });
1589
+ map.set(result.email, { id: result.id, contactId: result.contactId });
1261
1590
  }
1262
1591
  }
1263
- const validEmails = emailsToProcess.filter((e) => identifierMap.has(e));
1264
- const identifierIds = validEmails.map((e) => identifierMap.get(e).id);
1265
- const allRuleSends = await this.EmailRuleSend.find({ ruleId: rule._id, userId: { $in: identifierIds } }).sort({ sentAt: -1 }).lean();
1266
- const sendMap = /* @__PURE__ */ new Map();
1267
- for (const send of allRuleSends) {
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 (!sendMap.has(uid)) {
1270
- sendMap.set(uid, send);
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
- const compiledVariants = this.templateRenderer.compileBatchVariants(
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 && i % 10 === 0) {
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.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "invalid", accountId: "", templateId, runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: "invalid email" });
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 user = { _id: identifier.id, email };
1324
- const resolvedData = this.config.adapters.resolveData(user);
1325
- const templateData = { ...template.fields || {}, ...resolvedData };
1326
- const si = Math.floor(Math.random() * compiledVariants.subjectFns.length);
1327
- const bi = Math.floor(Math.random() * compiledVariants.bodyFns.length);
1328
- const renderedSubject = compiledVariants.subjectFns[si](templateData);
1329
- const renderedHtml = compiledVariants.bodyFns[bi](templateData);
1330
- const renderedText = compiledVariants.textBodyFn ? compiledVariants.textBodyFn(templateData) : this.templateRenderer.htmlToText(renderedHtml);
1331
- let finalHtml = renderedHtml;
1332
- let finalText = renderedText;
1333
- let finalSubject = renderedSubject;
1334
- let pi;
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
- autoApprove: rule.autoApprove ?? true,
1384
- attachments: template.attachments || []
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
- stats.sent++;
1400
- this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "sent", accountId: agentSelection.accountId, templateId, runId: runId || "", subjectIndex: si, bodyIndex: bi });
1401
- totalProcessed++;
1402
- if (runId && totalProcessed % 10 === 0) {
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.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "error", accountId: "", templateId, runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: err.message || "unknown error" });
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.EmailRule.findByIdAndUpdate(rule._id, {
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
- users = await this.config.adapters.queryUsers(rule.target, limit);
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 = /* @__PURE__ */ new Map();
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 identifierResults = await processInChunks(
1469
- uniqueEmails,
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 && i % 10 === 0) {
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.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email: email || "unknown", status: "invalid", accountId: "", templateId, runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: "invalid email" });
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.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "invalid", accountId: "", templateId, runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: "invalid email" });
1751
+ this.emitSendEvent(rule, email, "invalid", templateId, runId || "", { failureReason: "invalid email" });
1531
1752
  continue;
1532
1753
  }
1533
- if (!this.checkThrottle(rule, userId, email, throttleMap, throttleConfig, stats, templateId, runId)) continue;
1534
- const agentSelection = await this.config.adapters.selectAgent(identifier.id, { ruleId, templateId });
1535
- if (!agentSelection) {
1536
- stats.skipped++;
1537
- this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "skipped", accountId: "", templateId, runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: "no account available" });
1538
- continue;
1539
- }
1540
- const resolvedData = this.config.adapters.resolveData(user);
1541
- const templateData = { ...template.fields || {}, ...resolvedData };
1542
- const si = Math.floor(Math.random() * compiledVariants.subjectFns.length);
1543
- const bi = Math.floor(Math.random() * compiledVariants.bodyFns.length);
1544
- const renderedSubject = compiledVariants.subjectFns[si](templateData);
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
- autoApprove: rule.autoApprove ?? true,
1600
- attachments: template.attachments || []
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
- stats.sent++;
1616
- this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "sent", accountId: agentSelection.accountId, templateId, runId: runId || "", subjectIndex: si, bodyIndex: bi });
1617
- totalProcessed++;
1618
- if (runId && totalProcessed % 10 === 0) {
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.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email: user.email || "unknown", status: "error", accountId: "", templateId, runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: err.message || "unknown error" });
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.EmailRule.findByIdAndUpdate(rule._id, {
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.config.hooks?.onSend?.({ ruleId: rule._id.toString(), ruleName: rule.name, email, status: "throttled", accountId: "", templateId: templateId || "", runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: "daily throttle limit" });
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.config.hooks?.onSend?.({ ruleId: rule._id.toString(), ruleName: rule.name, email, status: "throttled", accountId: "", templateId: templateId || "", runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: "weekly throttle limit" });
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.config.hooks?.onSend?.({ ruleId: rule._id.toString(), ruleName: rule.name, email, status: "throttled", accountId: "", templateId: templateId || "", runId: runId || "", subjectIndex: -1, bodyIndex: -1, failureReason: "min gap days" });
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
- function isValidValue(allowed, value) {
1792
- return typeof value === "string" && allowed.includes(value);
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
- async function list(req, res) {
1804
- try {
1805
- const { category, audience, platform, isActive } = req.query;
1806
- const templates = await templateService.list({
1807
- category,
1808
- audience,
1809
- platform,
1810
- isActive: isActive !== void 0 ? isActive === "true" : void 0
1811
- });
1812
- res.json({ success: true, data: { templates } });
1813
- } catch (error) {
1814
- const message = error instanceof Error ? error.message : "Unknown error";
1815
- res.status(500).json({ success: false, error: message });
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
- async function getById(req, res) {
1819
- try {
1820
- const template = await templateService.getById(core.getParam(req, "id"));
1821
- if (!template) {
1822
- return res.status(404).json({ success: false, error: "Template not found" });
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
- async function create(req, res) {
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
- async function update(req, res) {
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
- async function remove(req, res) {
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
- async function toggleActive(req, res) {
1877
- try {
1878
- const template = await templateService.toggleActive(core.getParam(req, "id"));
1879
- if (!template) {
1880
- return res.status(404).json({ success: false, error: "Template not found" });
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
- async function preview(req, res) {
1889
- try {
1890
- const { sampleData } = req.body;
1891
- const result = await templateService.preview(core.getParam(req, "id"), sampleData || {});
1892
- if (!result) {
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
- async function previewRaw(req, res) {
1902
- try {
1903
- const { subject, body, textBody, sampleData, variables } = req.body;
1904
- if (!subject || !body) {
1905
- return res.status(400).json({ success: false, error: "subject and body are required" });
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
- async function validate(req, res) {
1915
- try {
1916
- const { body: templateBody } = req.body;
1917
- if (!templateBody) {
1918
- return res.status(400).json({ success: false, error: "body is required" });
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
- async function clone(req, res) {
1928
- try {
1929
- const { name } = req.body;
1930
- const result = await templateService.clone(core.getParam(req, "id"), name);
1931
- res.json({ success: true, data: result });
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
- async function sendTestEmail(req, res) {
1938
- try {
1939
- const { testEmail, sampleData } = req.body;
1940
- if (!testEmail) {
1941
- return res.status(400).json({ success: false, error: "testEmail is required" });
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
- async function previewWithRecipient(req, res) {
1954
- try {
1955
- const { recipientData } = req.body;
1956
- if (!recipientData || typeof recipientData !== "object") {
1957
- return res.status(400).json({ success: false, error: "recipientData object is required" });
1958
- }
1959
- const result = await templateService.previewWithRecipient(core.getParam(req, "id"), recipientData);
1960
- if (!result) {
1961
- return res.status(404).json({ success: false, error: "Template not found" });
1962
- }
1963
- res.json({ success: true, data: result });
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
- async function list(_req, res) {
1984
- try {
1985
- const rules = await ruleService.list();
1986
- res.json({ success: true, data: { rules } });
1987
- } catch (error) {
1988
- const message = error instanceof Error ? error.message : "Unknown error";
1989
- res.status(500).json({ success: false, error: message });
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
- async function getById(req, res) {
1993
- try {
1994
- const rule = await ruleService.getById(core.getParam(req, "id"));
1995
- if (!rule) {
1996
- return res.status(404).json({ success: false, error: "Rule not found" });
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
- async function create(req, res) {
2005
- try {
2006
- const { name, target, templateId } = req.body;
2007
- if (!name || !target || !templateId) {
2008
- return res.status(400).json({ success: false, error: "name, target, and templateId are required" });
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 (!target.role || !isValidValue2(validAudiences, target.role)) {
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
- async function toggleActive(req, res) {
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
- async function dryRun(req, res) {
2095
- try {
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
- async function clone(req, res) {
2104
- try {
2105
- const { name } = req.body;
2106
- const result = await ruleService.clone(core.getParam(req, "id"), name);
2107
- res.json({ success: true, data: result });
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
- async function runHistory(req, res) {
2114
- try {
2115
- const limitParam = req.query.limit;
2116
- const limit = parseInt(String(Array.isArray(limitParam) ? limitParam[0] : limitParam), 10) || 20;
2117
- const logs = await ruleService.getRunHistory(limit);
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
- async function triggerManualRun(_req, res) {
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
- async function getLatestRun(_req, res) {
2132
- try {
2133
- const latestRun = await EmailRuleRunLog.findOne().sort({ runAt: -1 });
2134
- res.json({ success: true, data: { latestRun } });
2135
- } catch (error) {
2136
- const message = error instanceof Error ? error.message : "Unknown error";
2137
- res.status(500).json({ success: false, error: message });
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
- async function cancelRun(req, res) {
2154
- try {
2155
- const result = await runnerService.cancel(core.getParam(req, "runId"));
2156
- if (!result.ok) {
2157
- res.status(404).json({ success: false, error: "Run not found" });
2158
- return;
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
- async function getThrottleConfig(_req, res) {
2172
- try {
2173
- const config = await EmailThrottleConfig.getConfig();
2174
- res.json({ success: true, data: { config } });
2175
- } catch (error) {
2176
- const message = error instanceof Error ? error.message : "Unknown error";
2177
- res.status(500).json({ success: false, error: message });
2178
- }
2179
- }
2180
- async function updateThrottleConfig(req, res) {
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
- if (Object.keys(updates).length === 0) {
2203
- return res.status(400).json({ success: false, error: "No valid fields to update" });
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
- const config = await EmailThrottleConfig.getConfig();
2206
- const finalDaily = updates.maxPerUserPerDay ?? config.maxPerUserPerDay;
2207
- const finalWeekly = updates.maxPerUserPerWeek ?? config.maxPerUserPerWeek;
2208
- if (finalWeekly < finalDaily) {
2209
- return res.status(400).json({ success: false, error: "maxPerUserPerWeek must be >= maxPerUserPerDay" });
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
- const updated = await EmailThrottleConfig.findByIdAndUpdate(
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
- async function list(req, res) {
2228
- try {
2229
- const { ruleId, status, email, from, to, page, limit } = req.query;
2230
- const filter = {};
2231
- if (ruleId) filter.ruleId = ruleId;
2232
- if (status) filter.status = status;
2233
- if (email) filter.email = { $regex: email, $options: "i" };
2234
- if (from || to) {
2235
- filter.sentAt = {};
2236
- if (from) filter.sentAt.$gte = new Date(from);
2237
- if (to) filter.sentAt.$lte = new Date(to);
2238
- }
2239
- const pageNum = Number(page) || 1;
2240
- const limitNum = Math.min(Number(limit) || 50, 200);
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
- await this.worker?.close();
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