mailery 0.9.0 → 0.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
@@ -414,7 +414,25 @@ var tagInputSchema = zod.z.object({
414
414
  var abortFlowInputSchema = zod.z.object({
415
415
  flowSlug: slugSchema,
416
416
  externalId: externalIdSchema,
417
- reason: zod.z.string().min(1).max(200).optional()
417
+ reason: zod.z.string().min(1).max(200).optional(),
418
+ /**
419
+ * Restrict the abort to runs whose trigger event carried these properties —
420
+ * e.g. `{ accountId }` to cancel one account's series while the same
421
+ * contact's other accounts keep running. Omit to abort every active run for
422
+ * the contact on this flow.
423
+ *
424
+ * Keys and values are both constrained because these go straight into a
425
+ * Mongo query. Values are primitives only: an object value like
426
+ * `{ $ne: null }` would reach the query as an OPERATOR and match every
427
+ * scoped run, turning a one-account abort into abort-everything. Hosts
428
+ * typically pass an id from a request body, so treat it as untrusted. The
429
+ * key regex likewise blocks `$`-prefixed keys and dots (a dot would silently
430
+ * extend the path and change match semantics).
431
+ */
432
+ matchTriggerProperties: zod.z.record(
433
+ zod.z.string().regex(/^[A-Za-z0-9_]+$/),
434
+ zod.z.union([zod.z.string(), zod.z.number(), zod.z.boolean(), zod.z.null()])
435
+ ).optional()
418
436
  });
419
437
  var abortAllFlowsInputSchema = abortFlowInputSchema.omit({ flowSlug: true });
420
438
  var sendOneOffInputSchema = zod.z.object({
@@ -483,6 +501,13 @@ var predicateSchema = zod.z.lazy(
483
501
  zod.z.object({ notHasTag: zod.z.string() }),
484
502
  zod.z.object({ fieldEquals: zod.z.object({ field: zod.z.string(), value: zod.z.unknown() }) }),
485
503
  zod.z.object({ fieldExists: zod.z.string() }),
504
+ zod.z.object({
505
+ triggerPropertyEquals: zod.z.object({
506
+ key: zod.z.string().min(1),
507
+ value: zod.z.union([zod.z.string(), zod.z.number(), zod.z.boolean(), zod.z.null()])
508
+ })
509
+ }),
510
+ zod.z.object({ triggerPropertyTruthy: zod.z.string().min(1) }),
486
511
  zod.z.object({
487
512
  hasFiredEvent: zod.z.string(),
488
513
  sinceFlowStart: zod.z.boolean().optional(),
@@ -1666,6 +1691,12 @@ async function evaluatePredicate(predicate, ctx) {
1666
1691
  if ("fieldExists" in p) {
1667
1692
  return ctx.contact.fields[p.fieldExists] !== void 0;
1668
1693
  }
1694
+ if ("triggerPropertyEquals" in p) {
1695
+ return (ctx.run.triggerEvent?.properties ?? {})[p.triggerPropertyEquals.key] === p.triggerPropertyEquals.value;
1696
+ }
1697
+ if ("triggerPropertyTruthy" in p) {
1698
+ return Boolean((ctx.run.triggerEvent?.properties ?? {})[p.triggerPropertyTruthy]);
1699
+ }
1669
1700
  if ("subscriptionStatus" in p) {
1670
1701
  const sub = await ctx.collections.subscriptions.findOne({ externalId: ctx.contact.externalId });
1671
1702
  return sub?.status === p.subscriptionStatus;
@@ -2418,7 +2449,14 @@ async function handleFireEvent(run, step, ctx) {
2418
2449
  await ctx.collections.events.insertOne({
2419
2450
  externalId: run.externalId,
2420
2451
  name: step.eventName,
2421
- properties: step.properties ?? {},
2452
+ // Inherit the triggering event's properties so a handoff carries the
2453
+ // context that identifies what the run is ABOUT (which account, order,
2454
+ // subscription, ...). A step's `properties` are static — authored once in
2455
+ // the flow definition — so without this a fired event can only ever say
2456
+ // "this contact", losing the scope the originating event supplied, and
2457
+ // the receiving flow has nothing to resolve variables against. Explicit
2458
+ // step.properties win on conflict.
2459
+ properties: { ...run.triggerEvent?.properties ?? {}, ...step.properties ?? {} },
2422
2460
  dedupeKey,
2423
2461
  occurredAt: /* @__PURE__ */ new Date(),
2424
2462
  createdAt: /* @__PURE__ */ new Date()
@@ -4253,14 +4291,25 @@ var Mailer = class _Mailer {
4253
4291
  * same handler that processes the business event ("user upgraded").
4254
4292
  */
4255
4293
  async abortFlow(flowSlug, externalId, opts = {}) {
4256
- const parsed = abortFlowInputSchema.parse({ flowSlug, externalId, reason: opts.reason });
4294
+ const parsed = abortFlowInputSchema.parse({
4295
+ flowSlug,
4296
+ externalId,
4297
+ reason: opts.reason,
4298
+ matchTriggerProperties: opts.matchTriggerProperties
4299
+ });
4257
4300
  const flow = await this.collections.flows.findOne(
4258
4301
  { slug: parsed.flowSlug },
4259
4302
  { projection: { _id: 1 } }
4260
4303
  );
4261
4304
  if (!flow) throw new Error(`abortFlow: unknown flow slug "${parsed.flowSlug}"`);
4305
+ const triggerMatch = Object.fromEntries(
4306
+ Object.entries(parsed.matchTriggerProperties ?? {}).map(([k, v]) => [
4307
+ `triggerEvent.properties.${k}`,
4308
+ v
4309
+ ])
4310
+ );
4262
4311
  const result = await this.abortActiveRuns(
4263
- { externalId: parsed.externalId, flowId: flow._id },
4312
+ { externalId: parsed.externalId, flowId: flow._id, ...triggerMatch },
4264
4313
  parsed.reason ? `aborted_by_host:${parsed.reason}` : "aborted_by_host"
4265
4314
  );
4266
4315
  if (result.abortedRuns > 0 || result.cancelledSends > 0) {
@@ -4268,7 +4317,9 @@ var Mailer = class _Mailer {
4268
4317
  actor: "host",
4269
4318
  action: "flow.abort",
4270
4319
  resource: { collection: "mailer_flow_runs", slug: parsed.flowSlug },
4271
- diffSummary: `abortFlow slug=${parsed.flowSlug} externalId=${parsed.externalId} runs=${result.abortedRuns} sends=${result.cancelledSends}${parsed.reason ? ` reason=${parsed.reason}` : ""}`
4320
+ // Record the scope: without it a one-account abort and an abort-every-
4321
+ // run-for-this-contact are indistinguishable in the audit trail.
4322
+ diffSummary: `abortFlow slug=${parsed.flowSlug} externalId=${parsed.externalId} scope=${parsed.matchTriggerProperties ? JSON.stringify(parsed.matchTriggerProperties) : "all"} runs=${result.abortedRuns} sends=${result.cancelledSends}${parsed.reason ? ` reason=${parsed.reason}` : ""}`
4272
4323
  });
4273
4324
  }
4274
4325
  return result;
@@ -7434,6 +7485,8 @@ var PREDICATE_KINDS = [
7434
7485
  { value: "notHasTag", label: "does NOT have tag" },
7435
7486
  { value: "fieldEquals", label: "field equals" },
7436
7487
  { value: "fieldExists", label: "field exists" },
7488
+ { value: "triggerPropertyEquals", label: "trigger property equals" },
7489
+ { value: "triggerPropertyTruthy", label: "trigger property is set" },
7437
7490
  { value: "hasFiredEvent", label: "fired event" },
7438
7491
  { value: "notHasFiredEvent", label: "has NOT fired event" },
7439
7492
  { value: "subscriptionStatus", label: "subscription status" },
@@ -7461,6 +7514,10 @@ function defaultPredicate(kind) {
7461
7514
  return { fieldEquals: { field: "tier", value: "Pro" } };
7462
7515
  case "fieldExists":
7463
7516
  return { fieldExists: "tier" };
7517
+ case "triggerPropertyEquals":
7518
+ return { triggerPropertyEquals: { key: "plan", value: "pro" } };
7519
+ case "triggerPropertyTruthy":
7520
+ return { triggerPropertyTruthy: "wasReferred" };
7464
7521
  case "hasFiredEvent":
7465
7522
  return { hasFiredEvent: "Activated app" };
7466
7523
  case "notHasFiredEvent":
@@ -7551,7 +7608,7 @@ var DEDUPE_POLICIES = [
7551
7608
  ];
7552
7609
 
7553
7610
  // src/server/index.ts
7554
- var VERSION = "0.9.0" ;
7611
+ var VERSION = "0.10.0" ;
7555
7612
 
7556
7613
  exports.DEDUPE_POLICIES = DEDUPE_POLICIES;
7557
7614
  exports.FLOW_STEP_KINDS = FLOW_STEP_KINDS;