mailery 0.8.1 → 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.
@@ -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-CwXmiVLe.cjs';
3
- export { s as NullProvider } from './null-CwXmiVLe.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-CwXmiVLe.js';
3
- export { s as NullProvider } from './null-CwXmiVLe.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(),
@@ -14623,7 +14648,7 @@ var BullDriver = class _BullDriver {
14623
14648
  bullQueues;
14624
14649
  workers = null;
14625
14650
  bull;
14626
- static async create(redisConfig) {
14651
+ static async create(redisConfig, prefix) {
14627
14652
  let bull;
14628
14653
  try {
14629
14654
  bull = await import('bullmq');
@@ -14632,14 +14657,22 @@ var BullDriver = class _BullDriver {
14632
14657
  "mailery: queue driver 'bull' requires the 'bullmq' peer dependency. Run `npm install bullmq ioredis`."
14633
14658
  );
14634
14659
  }
14660
+ if (prefix?.includes(":")) {
14661
+ throw new Error(
14662
+ `mailery: queue prefix "${prefix}" must not contain ':' \u2014 BullMQ uses it as the Redis key separator.`
14663
+ );
14664
+ }
14635
14665
  const redis = isRedisLike(redisConfig) ? redisConfig : connect(redisConfig);
14636
- return new _BullDriver(bull, redis);
14666
+ return new _BullDriver(bull, redis, prefix);
14637
14667
  }
14638
- constructor(bull, redis) {
14668
+ prefix;
14669
+ constructor(bull, redis, prefix) {
14639
14670
  this.bull = bull;
14640
14671
  this.redis = redis;
14672
+ this.prefix = prefix;
14641
14673
  const opts = {
14642
14674
  connection: redis,
14675
+ prefix,
14643
14676
  defaultJobOptions: {
14644
14677
  removeOnComplete: { age: 24 * 3600, count: 1e3 },
14645
14678
  removeOnFail: { age: 7 * 24 * 3600 }
@@ -14671,7 +14704,7 @@ var BullDriver = class _BullDriver {
14671
14704
  }
14672
14705
  async startWorkers(opts) {
14673
14706
  if (this.workers) return;
14674
- const base = { connection: this.redis };
14707
+ const base = { connection: this.redis, prefix: this.prefix };
14675
14708
  const { Worker } = this.bull;
14676
14709
  const tick = new Worker(
14677
14710
  QUEUE_NAMES.tick,
@@ -14923,7 +14956,7 @@ var NoopDriver = class {
14923
14956
  async function createQueueDriver(config, fallbackDb) {
14924
14957
  switch (config.driver) {
14925
14958
  case "bull":
14926
- return BullDriver.create(config.redis);
14959
+ return BullDriver.create(config.redis, config.prefix);
14927
14960
  case "agenda":
14928
14961
  return AgendaDriver.create({
14929
14962
  db: config.db ?? fallbackDb,
@@ -15346,6 +15379,12 @@ async function evaluatePredicate(predicate, ctx) {
15346
15379
  if ("fieldExists" in p) {
15347
15380
  return ctx.contact.fields[p.fieldExists] !== void 0;
15348
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
+ }
15349
15388
  if ("subscriptionStatus" in p) {
15350
15389
  const sub = await ctx.collections.subscriptions.findOne({ externalId: ctx.contact.externalId });
15351
15390
  return sub?.status === p.subscriptionStatus;
@@ -16092,7 +16131,14 @@ async function handleFireEvent(run, step2, ctx) {
16092
16131
  await ctx.collections.events.insertOne({
16093
16132
  externalId: run.externalId,
16094
16133
  name: step2.eventName,
16095
- properties: step2.properties ?? {},
16134
+ // Inherit the triggering event's properties so a handoff carries the
16135
+ // context that identifies what the run is ABOUT (which account, order,
16136
+ // subscription, ...). A step's `properties` are static — authored once in
16137
+ // the flow definition — so without this a fired event can only ever say
16138
+ // "this contact", losing the scope the originating event supplied, and
16139
+ // the receiving flow has nothing to resolve variables against. Explicit
16140
+ // step.properties win on conflict.
16141
+ properties: { ...run.triggerEvent?.properties ?? {}, ...step2.properties ?? {} },
16096
16142
  dedupeKey,
16097
16143
  occurredAt: /* @__PURE__ */ new Date(),
16098
16144
  createdAt: /* @__PURE__ */ new Date()
@@ -17351,6 +17397,7 @@ var Mailer = class _Mailer {
17351
17397
  * MAILER_MONGODB_URI — Mongo connection string (required)
17352
17398
  * MAILER_MONGODB_DB — database name (optional; defaults to the URI default)
17353
17399
  * MAILER_REDIS_URL — Redis connection URL (required)
17400
+ * MAILER_QUEUE_PREFIX — Redis key prefix to namespace this instance (optional)
17354
17401
  * MAILER_PUBLIC_URL — base for tracking/unsub URLs (required)
17355
17402
  * MAILER_UNSUBSCRIBE_SECRET — HMAC key (required)
17356
17403
  * MAILER_SENDER_ADDRESS — postal address for CAN-SPAM (optional)
@@ -17399,7 +17446,11 @@ var Mailer = class _Mailer {
17399
17446
  }
17400
17447
  const defaultProvider = env.MAILER_DEFAULT_PROVIDER ?? Object.keys(providers)[0];
17401
17448
  const driverEnv = env.MAILER_QUEUE_DRIVER ?? "bull";
17402
- const queue = driverEnv === "agenda" ? { driver: "agenda" } : driverEnv === "noop" ? { driver: "noop" } : { driver: "bull", redis: { url: required("MAILER_REDIS_URL") } };
17449
+ const queue = driverEnv === "agenda" ? { driver: "agenda" } : driverEnv === "noop" ? { driver: "noop" } : {
17450
+ driver: "bull",
17451
+ redis: { url: required("MAILER_REDIS_URL") },
17452
+ prefix: env.MAILER_QUEUE_PREFIX
17453
+ };
17403
17454
  return _Mailer.init({
17404
17455
  db,
17405
17456
  adapter,
@@ -17650,14 +17701,25 @@ var Mailer = class _Mailer {
17650
17701
  * same handler that processes the business event ("user upgraded").
17651
17702
  */
17652
17703
  async abortFlow(flowSlug, externalId, opts = {}) {
17653
- const parsed = abortFlowInputSchema.parse({ flowSlug, externalId, reason: opts.reason });
17704
+ const parsed = abortFlowInputSchema.parse({
17705
+ flowSlug,
17706
+ externalId,
17707
+ reason: opts.reason,
17708
+ matchTriggerProperties: opts.matchTriggerProperties
17709
+ });
17654
17710
  const flow = await this.collections.flows.findOne(
17655
17711
  { slug: parsed.flowSlug },
17656
17712
  { projection: { _id: 1 } }
17657
17713
  );
17658
17714
  if (!flow) throw new Error(`abortFlow: unknown flow slug "${parsed.flowSlug}"`);
17715
+ const triggerMatch = Object.fromEntries(
17716
+ Object.entries(parsed.matchTriggerProperties ?? {}).map(([k, v]) => [
17717
+ `triggerEvent.properties.${k}`,
17718
+ v
17719
+ ])
17720
+ );
17659
17721
  const result = await this.abortActiveRuns(
17660
- { externalId: parsed.externalId, flowId: flow._id },
17722
+ { externalId: parsed.externalId, flowId: flow._id, ...triggerMatch },
17661
17723
  parsed.reason ? `aborted_by_host:${parsed.reason}` : "aborted_by_host"
17662
17724
  );
17663
17725
  if (result.abortedRuns > 0 || result.cancelledSends > 0) {
@@ -17665,7 +17727,9 @@ var Mailer = class _Mailer {
17665
17727
  actor: "host",
17666
17728
  action: "flow.abort",
17667
17729
  resource: { collection: "mailer_flow_runs", slug: parsed.flowSlug },
17668
- diffSummary: `abortFlow slug=${parsed.flowSlug} externalId=${parsed.externalId} runs=${result.abortedRuns} sends=${result.cancelledSends}${parsed.reason ? ` reason=${parsed.reason}` : ""}`
17730
+ // Record the scope: without it a one-account abort and an abort-every-
17731
+ // run-for-this-contact are indistinguishable in the audit trail.
17732
+ 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}` : ""}`
17669
17733
  });
17670
17734
  }
17671
17735
  return result;