@astralibx/email-rule-engine 12.10.0 → 12.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,6 +15,7 @@ npm install @astralibx/email-rule-engine
15
15
 
16
16
  | Peer Dependency | Version |
17
17
  |-----------------|---------|
18
+ | `@astralibx/core` | `^1.2.0` |
18
19
  | `express` | `^4.18.0 \|\| ^5.0.0` |
19
20
  | `mongoose` | `^7.0.0 \|\| ^8.0.0` |
20
21
  | `ioredis` | `^5.0.0` |
package/dist/index.cjs CHANGED
@@ -205,7 +205,7 @@ function createEmailRuleSchema(platformValues, audienceValues, collectionPrefix)
205
205
  },
206
206
  conditions: [RuleConditionSchema],
207
207
  identifiers: [{ type: String }],
208
- collection: { type: String }
208
+ collectionName: { type: String }
209
209
  }, { _id: false });
210
210
  const RuleRunStatsSchema = createRunStatsSchema();
211
211
  const schema = new mongoose.Schema(
@@ -375,7 +375,16 @@ function createEmailThrottleConfigSchema(collectionPrefix) {
375
375
  maxPerUserPerDay: { type: Number, default: 1 },
376
376
  maxPerUserPerWeek: { type: Number, default: 2 },
377
377
  minGapDays: { type: Number, default: 3 },
378
- throttleWindow: { type: String, enum: Object.values(THROTTLE_WINDOW), default: THROTTLE_WINDOW.Rolling }
378
+ throttleWindow: { type: String, enum: Object.values(THROTTLE_WINDOW), default: THROTTLE_WINDOW.Rolling },
379
+ sendWindow: {
380
+ type: {
381
+ startHour: { type: Number, min: 0, max: 23 },
382
+ endHour: { type: Number, min: 0, max: 23 },
383
+ timezone: { type: String }
384
+ },
385
+ _id: false,
386
+ default: void 0
387
+ }
379
388
  },
380
389
  {
381
390
  timestamps: true,
@@ -1093,8 +1102,8 @@ var RuleService = class {
1093
1102
  throw new RuleTemplateIncompatibleError("target.identifiers must be a non-empty array for list mode, validation failed");
1094
1103
  }
1095
1104
  }
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);
1105
+ if (isQueryTarget(input.target) && input.target.collectionName && this.config.collections?.length) {
1106
+ const condErrors = validateConditions(input.target.conditions, input.target.collectionName, this.config.collections);
1098
1107
  if (condErrors.length > 0) {
1099
1108
  throw new RuleTemplateIncompatibleError(
1100
1109
  `Invalid conditions: ${condErrors.map((e) => e.message).join("; ")}`
@@ -1132,8 +1141,8 @@ var RuleService = class {
1132
1141
  }
1133
1142
  if (isQueryTarget(effectiveTarget)) {
1134
1143
  const qt = effectiveTarget;
1135
- if (qt.collection && this.config.collections?.length) {
1136
- const condErrors = validateConditions(qt.conditions || [], qt.collection, this.config.collections);
1144
+ if (qt.collectionName && this.config.collections?.length) {
1145
+ const condErrors = validateConditions(qt.conditions || [], qt.collectionName, this.config.collections);
1137
1146
  if (condErrors.length > 0) {
1138
1147
  throw new RuleTemplateIncompatibleError(
1139
1148
  `Invalid conditions: ${condErrors.map((e) => e.message).join("; ")}`
@@ -1191,7 +1200,7 @@ var RuleService = class {
1191
1200
  return { matchedCount: matchedCount2, effectiveLimit, willProcess: willProcess2, ruleId: id, sample: sample2 };
1192
1201
  }
1193
1202
  const queryTarget = rule.target;
1194
- const collectionName = queryTarget.collection;
1203
+ const collectionName = queryTarget.collectionName;
1195
1204
  const collectionSchema = collectionName ? this.config.collections?.find((c) => c.name === collectionName) : void 0;
1196
1205
  const users = await this.config.adapters.queryUsers(rule.target, 5e4, collectionSchema ? { collectionSchema } : void 0);
1197
1206
  const matchedCount = users.length;
@@ -1279,16 +1288,6 @@ var RuleRunnerService = class {
1279
1288
  async runAllRules(triggeredBy = RUN_TRIGGER.Cron, runId) {
1280
1289
  if (!runId) runId = crypto__default.default.randomUUID();
1281
1290
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
1282
- if (this.config.options?.sendWindow) {
1283
- const { startHour, endHour, timezone } = this.config.options.sendWindow;
1284
- const now = /* @__PURE__ */ new Date();
1285
- const formatter = new Intl.DateTimeFormat("en-US", { hour: "numeric", hour12: false, timeZone: timezone });
1286
- const currentHour = parseInt(formatter.format(now), 10);
1287
- if (currentHour < startHour || currentHour >= endHour) {
1288
- this.logger.info("Outside send window, skipping run", { currentHour, startHour, endHour, timezone });
1289
- return { runId };
1290
- }
1291
- }
1292
1291
  const lockAcquired = await this.lock.acquire();
1293
1292
  if (!lockAcquired) {
1294
1293
  this.logger.warn("Rule runner already executing, skipping");
@@ -1307,9 +1306,20 @@ var RuleRunnerService = class {
1307
1306
  let runStatus = "completed";
1308
1307
  try {
1309
1308
  const throttleConfig = await this.EmailThrottleConfig.getConfig();
1309
+ const sendWindow = throttleConfig.sendWindow ?? this.config.options?.sendWindow;
1310
+ if (sendWindow) {
1311
+ const { startHour, endHour, timezone } = sendWindow;
1312
+ const now2 = /* @__PURE__ */ new Date();
1313
+ const formatter = new Intl.DateTimeFormat("en-US", { hour: "numeric", hour12: false, timeZone: timezone });
1314
+ const currentHour = parseInt(formatter.format(now2), 10);
1315
+ if (currentHour < startHour || currentHour >= endHour) {
1316
+ this.logger.info("Outside send window, skipping run", { currentHour, startHour, endHour, timezone });
1317
+ return { runId };
1318
+ }
1319
+ }
1310
1320
  const allActiveRules = await this.EmailRule.findActive();
1311
1321
  const now = /* @__PURE__ */ new Date();
1312
- const tz = this.config.options?.sendWindow?.timezone;
1322
+ const tz = sendWindow?.timezone ?? this.config.options?.sendWindow?.timezone;
1313
1323
  const activeRules = allActiveRules.filter((rule) => {
1314
1324
  if (rule.validFrom) {
1315
1325
  const localNow = getLocalDate(now, tz);
@@ -1710,7 +1720,7 @@ var RuleRunnerService = class {
1710
1720
  const limit = rule.maxPerRun || this.config.options?.defaultMaxPerRun || 500;
1711
1721
  let users;
1712
1722
  try {
1713
- const collectionName = rule.target?.collection;
1723
+ const collectionName = rule.target?.collectionName;
1714
1724
  const collectionSchema = collectionName ? this.config.collections?.find((c) => c.name === collectionName) : void 0;
1715
1725
  users = await this.config.adapters.queryUsers(rule.target, limit, collectionSchema ? { collectionSchema } : void 0);
1716
1726
  } catch (err) {
@@ -2211,7 +2221,7 @@ function createSettingsController(EmailThrottleConfig) {
2211
2221
  res.json({ success: true, data: { config } });
2212
2222
  });
2213
2223
  const updateThrottleConfig = asyncHandler(async (req, res) => {
2214
- const { maxPerUserPerDay, maxPerUserPerWeek, minGapDays } = req.body;
2224
+ const { maxPerUserPerDay, maxPerUserPerWeek, minGapDays, sendWindow } = req.body;
2215
2225
  const updates = {};
2216
2226
  if (maxPerUserPerDay !== void 0) {
2217
2227
  if (!Number.isInteger(maxPerUserPerDay) || maxPerUserPerDay < 1) {
@@ -2231,6 +2241,23 @@ function createSettingsController(EmailThrottleConfig) {
2231
2241
  }
2232
2242
  updates.minGapDays = minGapDays;
2233
2243
  }
2244
+ if (sendWindow !== void 0) {
2245
+ if (sendWindow === null) {
2246
+ updates.sendWindow = void 0;
2247
+ } else {
2248
+ const { startHour, endHour, timezone } = sendWindow;
2249
+ if (!Number.isInteger(startHour) || startHour < 0 || startHour > 23) {
2250
+ return res.status(400).json({ success: false, error: "sendWindow.startHour must be an integer 0-23" });
2251
+ }
2252
+ if (!Number.isInteger(endHour) || endHour < 0 || endHour > 23) {
2253
+ return res.status(400).json({ success: false, error: "sendWindow.endHour must be an integer 0-23" });
2254
+ }
2255
+ if (typeof timezone !== "string" || !timezone.trim()) {
2256
+ return res.status(400).json({ success: false, error: "sendWindow.timezone must be a non-empty string" });
2257
+ }
2258
+ updates.sendWindow = { startHour, endHour, timezone: timezone.trim() };
2259
+ }
2260
+ }
2234
2261
  if (Object.keys(updates).length === 0) {
2235
2262
  return res.status(400).json({ success: false, error: "No valid fields to update" });
2236
2263
  }
@@ -2240,9 +2267,21 @@ function createSettingsController(EmailThrottleConfig) {
2240
2267
  if (finalWeekly < finalDaily) {
2241
2268
  return res.status(400).json({ success: false, error: "maxPerUserPerWeek must be >= maxPerUserPerDay" });
2242
2269
  }
2270
+ const setFields = {};
2271
+ const unsetFields = {};
2272
+ for (const [key, value] of Object.entries(updates)) {
2273
+ if (value === void 0) {
2274
+ unsetFields[key] = "";
2275
+ } else {
2276
+ setFields[key] = value;
2277
+ }
2278
+ }
2279
+ const updateOp = {};
2280
+ if (Object.keys(setFields).length > 0) updateOp["$set"] = setFields;
2281
+ if (Object.keys(unsetFields).length > 0) updateOp["$unset"] = unsetFields;
2243
2282
  const updated = await EmailThrottleConfig.findByIdAndUpdate(
2244
2283
  config._id,
2245
- { $set: updates },
2284
+ updateOp,
2246
2285
  { new: true }
2247
2286
  );
2248
2287
  res.json({ success: true, data: { config: updated } });