mailery 0.9.0 → 0.10.1

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.
@@ -1,6 +1,6 @@
1
1
  import { Db } from 'mongodb';
2
- import { C as ContactAdapter, a as Contact, A as AdapterFilter, M as MailProvider, S as SendArgs, b as SendResult, N as NormalizedEvent, F as FlowStep, a2 as FlowTrigger, m as FlowGoal, U as TemplateKind, T as TemplateDoc, l as FlowDoc, D as DeliveryWindow, R as RunnerContext, d as Mailer, a3 as QueueDriverConfig } from './null-BRSHAxvg.cjs';
3
- export { s as NullProvider } from './null-BRSHAxvg.cjs';
2
+ import { C as ContactAdapter, a as Contact, A as AdapterFilter, M as MailProvider, S as SendArgs, b as SendResult, N as NormalizedEvent, F as FlowStep, a2 as FlowTrigger, m as FlowGoal, U as TemplateKind, T as TemplateDoc, l as FlowDoc, D as DeliveryWindow, R as RunnerContext, d as Mailer, a3 as QueueDriverConfig } from './null-CU_EApC6.cjs';
3
+ export { s as NullProvider } from './null-CU_EApC6.cjs';
4
4
  import 'zod';
5
5
  import 'handlebars';
6
6
  import 'ioredis';
package/dist/testing.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Db } from 'mongodb';
2
- import { C as ContactAdapter, a as Contact, A as AdapterFilter, M as MailProvider, S as SendArgs, b as SendResult, N as NormalizedEvent, F as FlowStep, a2 as FlowTrigger, m as FlowGoal, U as TemplateKind, T as TemplateDoc, l as FlowDoc, D as DeliveryWindow, R as RunnerContext, d as Mailer, a3 as QueueDriverConfig } from './null-BRSHAxvg.js';
3
- export { s as NullProvider } from './null-BRSHAxvg.js';
2
+ import { C as ContactAdapter, a as Contact, A as AdapterFilter, M as MailProvider, S as SendArgs, b as SendResult, N as NormalizedEvent, F as FlowStep, a2 as FlowTrigger, m as FlowGoal, U as TemplateKind, T as TemplateDoc, l as FlowDoc, D as DeliveryWindow, R as RunnerContext, d as Mailer, a3 as QueueDriverConfig } from './null-CU_EApC6.js';
3
+ export { s as NullProvider } from './null-CU_EApC6.js';
4
4
  import 'zod';
5
5
  import 'handlebars';
6
6
  import 'ioredis';
package/dist/testing.js CHANGED
@@ -14195,7 +14195,25 @@ var tagInputSchema = z.object({
14195
14195
  var abortFlowInputSchema = z.object({
14196
14196
  flowSlug: slugSchema,
14197
14197
  externalId: externalIdSchema,
14198
- reason: z.string().min(1).max(200).optional()
14198
+ reason: z.string().min(1).max(200).optional(),
14199
+ /**
14200
+ * Restrict the abort to runs whose trigger event carried these properties —
14201
+ * e.g. `{ accountId }` to cancel one account's series while the same
14202
+ * contact's other accounts keep running. Omit to abort every active run for
14203
+ * the contact on this flow.
14204
+ *
14205
+ * Keys and values are both constrained because these go straight into a
14206
+ * Mongo query. Values are primitives only: an object value like
14207
+ * `{ $ne: null }` would reach the query as an OPERATOR and match every
14208
+ * scoped run, turning a one-account abort into abort-everything. Hosts
14209
+ * typically pass an id from a request body, so treat it as untrusted. The
14210
+ * key regex likewise blocks `$`-prefixed keys and dots (a dot would silently
14211
+ * extend the path and change match semantics).
14212
+ */
14213
+ matchTriggerProperties: z.record(
14214
+ z.string().regex(/^[A-Za-z0-9_]+$/),
14215
+ z.union([z.string(), z.number(), z.boolean(), z.null()])
14216
+ ).optional()
14199
14217
  });
14200
14218
  var abortAllFlowsInputSchema = abortFlowInputSchema.omit({ flowSlug: true });
14201
14219
  var sendOneOffInputSchema = z.object({
@@ -14264,6 +14282,13 @@ var predicateSchema = z.lazy(
14264
14282
  z.object({ notHasTag: z.string() }),
14265
14283
  z.object({ fieldEquals: z.object({ field: z.string(), value: z.unknown() }) }),
14266
14284
  z.object({ fieldExists: z.string() }),
14285
+ z.object({
14286
+ triggerPropertyEquals: z.object({
14287
+ key: z.string().min(1),
14288
+ value: z.union([z.string(), z.number(), z.boolean(), z.null()])
14289
+ })
14290
+ }),
14291
+ z.object({ triggerPropertyTruthy: z.string().min(1) }),
14267
14292
  z.object({
14268
14293
  hasFiredEvent: z.string(),
14269
14294
  sinceFlowStart: z.boolean().optional(),
@@ -15354,6 +15379,12 @@ async function evaluatePredicate(predicate, ctx) {
15354
15379
  if ("fieldExists" in p) {
15355
15380
  return ctx.contact.fields[p.fieldExists] !== void 0;
15356
15381
  }
15382
+ if ("triggerPropertyEquals" in p) {
15383
+ return (ctx.run.triggerEvent?.properties ?? {})[p.triggerPropertyEquals.key] === p.triggerPropertyEquals.value;
15384
+ }
15385
+ if ("triggerPropertyTruthy" in p) {
15386
+ return Boolean((ctx.run.triggerEvent?.properties ?? {})[p.triggerPropertyTruthy]);
15387
+ }
15357
15388
  if ("subscriptionStatus" in p) {
15358
15389
  const sub = await ctx.collections.subscriptions.findOne({ externalId: ctx.contact.externalId });
15359
15390
  return sub?.status === p.subscriptionStatus;
@@ -15954,9 +15985,11 @@ function sha256(s) {
15954
15985
  }
15955
15986
 
15956
15987
  // src/server/runner/step.ts
15988
+ var DUE_SKEW_MS = 1e3;
15957
15989
  async function processOneRunStep(runId, ctx) {
15958
15990
  const run = await ctx.collections.flowRuns.findOne({ _id: runId });
15959
15991
  if (!run || run.status !== "active") return;
15992
+ if (run.nextActionAt && run.nextActionAt.getTime() > Date.now() + DUE_SKEW_MS) return;
15960
15993
  const flow = await ctx.collections.flows.findOne({ _id: run.flowId });
15961
15994
  if (!flow) {
15962
15995
  await failFlowRun(run, "flow_missing", ctx);
@@ -16100,7 +16133,14 @@ async function handleFireEvent(run, step2, ctx) {
16100
16133
  await ctx.collections.events.insertOne({
16101
16134
  externalId: run.externalId,
16102
16135
  name: step2.eventName,
16103
- properties: step2.properties ?? {},
16136
+ // Inherit the triggering event's properties so a handoff carries the
16137
+ // context that identifies what the run is ABOUT (which account, order,
16138
+ // subscription, ...). A step's `properties` are static — authored once in
16139
+ // the flow definition — so without this a fired event can only ever say
16140
+ // "this contact", losing the scope the originating event supplied, and
16141
+ // the receiving flow has nothing to resolve variables against. Explicit
16142
+ // step.properties win on conflict.
16143
+ properties: { ...run.triggerEvent?.properties ?? {}, ...step2.properties ?? {} },
16104
16144
  dedupeKey,
16105
16145
  occurredAt: /* @__PURE__ */ new Date(),
16106
16146
  createdAt: /* @__PURE__ */ new Date()
@@ -17663,14 +17703,25 @@ var Mailer = class _Mailer {
17663
17703
  * same handler that processes the business event ("user upgraded").
17664
17704
  */
17665
17705
  async abortFlow(flowSlug, externalId, opts = {}) {
17666
- const parsed = abortFlowInputSchema.parse({ flowSlug, externalId, reason: opts.reason });
17706
+ const parsed = abortFlowInputSchema.parse({
17707
+ flowSlug,
17708
+ externalId,
17709
+ reason: opts.reason,
17710
+ matchTriggerProperties: opts.matchTriggerProperties
17711
+ });
17667
17712
  const flow = await this.collections.flows.findOne(
17668
17713
  { slug: parsed.flowSlug },
17669
17714
  { projection: { _id: 1 } }
17670
17715
  );
17671
17716
  if (!flow) throw new Error(`abortFlow: unknown flow slug "${parsed.flowSlug}"`);
17717
+ const triggerMatch = Object.fromEntries(
17718
+ Object.entries(parsed.matchTriggerProperties ?? {}).map(([k, v]) => [
17719
+ `triggerEvent.properties.${k}`,
17720
+ v
17721
+ ])
17722
+ );
17672
17723
  const result = await this.abortActiveRuns(
17673
- { externalId: parsed.externalId, flowId: flow._id },
17724
+ { externalId: parsed.externalId, flowId: flow._id, ...triggerMatch },
17674
17725
  parsed.reason ? `aborted_by_host:${parsed.reason}` : "aborted_by_host"
17675
17726
  );
17676
17727
  if (result.abortedRuns > 0 || result.cancelledSends > 0) {
@@ -17678,7 +17729,9 @@ var Mailer = class _Mailer {
17678
17729
  actor: "host",
17679
17730
  action: "flow.abort",
17680
17731
  resource: { collection: "mailer_flow_runs", slug: parsed.flowSlug },
17681
- diffSummary: `abortFlow slug=${parsed.flowSlug} externalId=${parsed.externalId} runs=${result.abortedRuns} sends=${result.cancelledSends}${parsed.reason ? ` reason=${parsed.reason}` : ""}`
17732
+ // Record the scope: without it a one-account abort and an abort-every-
17733
+ // run-for-this-contact are indistinguishable in the audit trail.
17734
+ 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}` : ""}`
17682
17735
  });
17683
17736
  }
17684
17737
  return result;