@astralibx/email-rule-engine 12.7.3 → 12.9.0

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