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.
- package/dist/admin/spa/index-BTww-4AA.js +41 -0
- package/dist/admin/spa/index-BTww-4AA.js.map +1 -0
- package/dist/admin/spa/index.html +1 -1
- package/dist/admin/spa/{template-editor-DwMKy5sw.js → template-editor-CrOKLpSw.js} +2 -2
- package/dist/admin/spa/{template-editor-DwMKy5sw.js.map → template-editor-CrOKLpSw.js.map} +1 -1
- package/dist/index.cjs +82 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -4
- package/dist/index.d.ts +8 -4
- package/dist/index.js +82 -12
- package/dist/index.js.map +1 -1
- package/dist/{null-CwXmiVLe.d.cts → null-CU_EApC6.d.cts} +24 -0
- package/dist/{null-CwXmiVLe.d.ts → null-CU_EApC6.d.ts} +24 -0
- package/dist/testing.cjs +75 -11
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +2 -2
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +75 -11
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
- package/dist/admin/spa/index-B3ndbezm.js +0 -41
- package/dist/admin/spa/index-B3ndbezm.js.map +0 -1
|
@@ -113,6 +113,21 @@ type Predicate = {
|
|
|
113
113
|
};
|
|
114
114
|
} | {
|
|
115
115
|
fieldExists: string;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Tests a property of the event that STARTED this run, so the answer is
|
|
119
|
+
* per-run rather than per-contact. Use when the gate depends on what the run
|
|
120
|
+
* is about (which account, plan, order) instead of a durable trait of the
|
|
121
|
+
* person: a contact-level tag is shared by every concurrent run and the last
|
|
122
|
+
* writer wins, which silently changes branching in runs already in flight.
|
|
123
|
+
*/
|
|
124
|
+
| {
|
|
125
|
+
triggerPropertyEquals: {
|
|
126
|
+
key: string;
|
|
127
|
+
value: string | number | boolean | null;
|
|
128
|
+
};
|
|
129
|
+
} | {
|
|
130
|
+
triggerPropertyTruthy: string;
|
|
116
131
|
} | {
|
|
117
132
|
hasFiredEvent: string;
|
|
118
133
|
sinceFlowStart?: boolean;
|
|
@@ -378,6 +393,13 @@ interface Queues {
|
|
|
378
393
|
type QueueDriverConfig = {
|
|
379
394
|
driver: 'bull';
|
|
380
395
|
redis: RedisOptions | IORedis;
|
|
396
|
+
/**
|
|
397
|
+
* Redis key prefix for all queue keys (BullMQ default: 'bull').
|
|
398
|
+
* Namespaces multiple mailery instances on one Redis cluster — e.g.
|
|
399
|
+
* 'mailery-dev' / 'mailery-prod' to isolate environments. Must not
|
|
400
|
+
* contain ':' (BullMQ uses it as the key separator).
|
|
401
|
+
*/
|
|
402
|
+
prefix?: string;
|
|
381
403
|
} | {
|
|
382
404
|
driver: 'agenda';
|
|
383
405
|
db?: Db;
|
|
@@ -1382,6 +1404,7 @@ declare class Mailer {
|
|
|
1382
1404
|
* MAILER_MONGODB_URI — Mongo connection string (required)
|
|
1383
1405
|
* MAILER_MONGODB_DB — database name (optional; defaults to the URI default)
|
|
1384
1406
|
* MAILER_REDIS_URL — Redis connection URL (required)
|
|
1407
|
+
* MAILER_QUEUE_PREFIX — Redis key prefix to namespace this instance (optional)
|
|
1385
1408
|
* MAILER_PUBLIC_URL — base for tracking/unsub URLs (required)
|
|
1386
1409
|
* MAILER_UNSUBSCRIBE_SECRET — HMAC key (required)
|
|
1387
1410
|
* MAILER_SENDER_ADDRESS — postal address for CAN-SPAM (optional)
|
|
@@ -1419,6 +1442,7 @@ declare class Mailer {
|
|
|
1419
1442
|
*/
|
|
1420
1443
|
abortFlow(flowSlug: string, externalId: string, opts?: {
|
|
1421
1444
|
reason?: string;
|
|
1445
|
+
matchTriggerProperties?: Record<string, unknown>;
|
|
1422
1446
|
}): Promise<{
|
|
1423
1447
|
abortedRuns: number;
|
|
1424
1448
|
cancelledSends: number;
|
|
@@ -113,6 +113,21 @@ type Predicate = {
|
|
|
113
113
|
};
|
|
114
114
|
} | {
|
|
115
115
|
fieldExists: string;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Tests a property of the event that STARTED this run, so the answer is
|
|
119
|
+
* per-run rather than per-contact. Use when the gate depends on what the run
|
|
120
|
+
* is about (which account, plan, order) instead of a durable trait of the
|
|
121
|
+
* person: a contact-level tag is shared by every concurrent run and the last
|
|
122
|
+
* writer wins, which silently changes branching in runs already in flight.
|
|
123
|
+
*/
|
|
124
|
+
| {
|
|
125
|
+
triggerPropertyEquals: {
|
|
126
|
+
key: string;
|
|
127
|
+
value: string | number | boolean | null;
|
|
128
|
+
};
|
|
129
|
+
} | {
|
|
130
|
+
triggerPropertyTruthy: string;
|
|
116
131
|
} | {
|
|
117
132
|
hasFiredEvent: string;
|
|
118
133
|
sinceFlowStart?: boolean;
|
|
@@ -378,6 +393,13 @@ interface Queues {
|
|
|
378
393
|
type QueueDriverConfig = {
|
|
379
394
|
driver: 'bull';
|
|
380
395
|
redis: RedisOptions | IORedis;
|
|
396
|
+
/**
|
|
397
|
+
* Redis key prefix for all queue keys (BullMQ default: 'bull').
|
|
398
|
+
* Namespaces multiple mailery instances on one Redis cluster — e.g.
|
|
399
|
+
* 'mailery-dev' / 'mailery-prod' to isolate environments. Must not
|
|
400
|
+
* contain ':' (BullMQ uses it as the key separator).
|
|
401
|
+
*/
|
|
402
|
+
prefix?: string;
|
|
381
403
|
} | {
|
|
382
404
|
driver: 'agenda';
|
|
383
405
|
db?: Db;
|
|
@@ -1382,6 +1404,7 @@ declare class Mailer {
|
|
|
1382
1404
|
* MAILER_MONGODB_URI — Mongo connection string (required)
|
|
1383
1405
|
* MAILER_MONGODB_DB — database name (optional; defaults to the URI default)
|
|
1384
1406
|
* MAILER_REDIS_URL — Redis connection URL (required)
|
|
1407
|
+
* MAILER_QUEUE_PREFIX — Redis key prefix to namespace this instance (optional)
|
|
1385
1408
|
* MAILER_PUBLIC_URL — base for tracking/unsub URLs (required)
|
|
1386
1409
|
* MAILER_UNSUBSCRIBE_SECRET — HMAC key (required)
|
|
1387
1410
|
* MAILER_SENDER_ADDRESS — postal address for CAN-SPAM (optional)
|
|
@@ -1419,6 +1442,7 @@ declare class Mailer {
|
|
|
1419
1442
|
*/
|
|
1420
1443
|
abortFlow(flowSlug: string, externalId: string, opts?: {
|
|
1421
1444
|
reason?: string;
|
|
1445
|
+
matchTriggerProperties?: Record<string, unknown>;
|
|
1422
1446
|
}): Promise<{
|
|
1423
1447
|
abortedRuns: number;
|
|
1424
1448
|
cancelledSends: number;
|
package/dist/testing.cjs
CHANGED
|
@@ -14208,7 +14208,25 @@ var tagInputSchema = zod.z.object({
|
|
|
14208
14208
|
var abortFlowInputSchema = zod.z.object({
|
|
14209
14209
|
flowSlug: slugSchema,
|
|
14210
14210
|
externalId: externalIdSchema,
|
|
14211
|
-
reason: zod.z.string().min(1).max(200).optional()
|
|
14211
|
+
reason: zod.z.string().min(1).max(200).optional(),
|
|
14212
|
+
/**
|
|
14213
|
+
* Restrict the abort to runs whose trigger event carried these properties —
|
|
14214
|
+
* e.g. `{ accountId }` to cancel one account's series while the same
|
|
14215
|
+
* contact's other accounts keep running. Omit to abort every active run for
|
|
14216
|
+
* the contact on this flow.
|
|
14217
|
+
*
|
|
14218
|
+
* Keys and values are both constrained because these go straight into a
|
|
14219
|
+
* Mongo query. Values are primitives only: an object value like
|
|
14220
|
+
* `{ $ne: null }` would reach the query as an OPERATOR and match every
|
|
14221
|
+
* scoped run, turning a one-account abort into abort-everything. Hosts
|
|
14222
|
+
* typically pass an id from a request body, so treat it as untrusted. The
|
|
14223
|
+
* key regex likewise blocks `$`-prefixed keys and dots (a dot would silently
|
|
14224
|
+
* extend the path and change match semantics).
|
|
14225
|
+
*/
|
|
14226
|
+
matchTriggerProperties: zod.z.record(
|
|
14227
|
+
zod.z.string().regex(/^[A-Za-z0-9_]+$/),
|
|
14228
|
+
zod.z.union([zod.z.string(), zod.z.number(), zod.z.boolean(), zod.z.null()])
|
|
14229
|
+
).optional()
|
|
14212
14230
|
});
|
|
14213
14231
|
var abortAllFlowsInputSchema = abortFlowInputSchema.omit({ flowSlug: true });
|
|
14214
14232
|
var sendOneOffInputSchema = zod.z.object({
|
|
@@ -14277,6 +14295,13 @@ var predicateSchema = zod.z.lazy(
|
|
|
14277
14295
|
zod.z.object({ notHasTag: zod.z.string() }),
|
|
14278
14296
|
zod.z.object({ fieldEquals: zod.z.object({ field: zod.z.string(), value: zod.z.unknown() }) }),
|
|
14279
14297
|
zod.z.object({ fieldExists: zod.z.string() }),
|
|
14298
|
+
zod.z.object({
|
|
14299
|
+
triggerPropertyEquals: zod.z.object({
|
|
14300
|
+
key: zod.z.string().min(1),
|
|
14301
|
+
value: zod.z.union([zod.z.string(), zod.z.number(), zod.z.boolean(), zod.z.null()])
|
|
14302
|
+
})
|
|
14303
|
+
}),
|
|
14304
|
+
zod.z.object({ triggerPropertyTruthy: zod.z.string().min(1) }),
|
|
14280
14305
|
zod.z.object({
|
|
14281
14306
|
hasFiredEvent: zod.z.string(),
|
|
14282
14307
|
sinceFlowStart: zod.z.boolean().optional(),
|
|
@@ -14636,7 +14661,7 @@ var BullDriver = class _BullDriver {
|
|
|
14636
14661
|
bullQueues;
|
|
14637
14662
|
workers = null;
|
|
14638
14663
|
bull;
|
|
14639
|
-
static async create(redisConfig) {
|
|
14664
|
+
static async create(redisConfig, prefix) {
|
|
14640
14665
|
let bull;
|
|
14641
14666
|
try {
|
|
14642
14667
|
bull = await import('bullmq');
|
|
@@ -14645,14 +14670,22 @@ var BullDriver = class _BullDriver {
|
|
|
14645
14670
|
"mailery: queue driver 'bull' requires the 'bullmq' peer dependency. Run `npm install bullmq ioredis`."
|
|
14646
14671
|
);
|
|
14647
14672
|
}
|
|
14673
|
+
if (prefix?.includes(":")) {
|
|
14674
|
+
throw new Error(
|
|
14675
|
+
`mailery: queue prefix "${prefix}" must not contain ':' \u2014 BullMQ uses it as the Redis key separator.`
|
|
14676
|
+
);
|
|
14677
|
+
}
|
|
14648
14678
|
const redis = isRedisLike(redisConfig) ? redisConfig : connect(redisConfig);
|
|
14649
|
-
return new _BullDriver(bull, redis);
|
|
14679
|
+
return new _BullDriver(bull, redis, prefix);
|
|
14650
14680
|
}
|
|
14651
|
-
|
|
14681
|
+
prefix;
|
|
14682
|
+
constructor(bull, redis, prefix) {
|
|
14652
14683
|
this.bull = bull;
|
|
14653
14684
|
this.redis = redis;
|
|
14685
|
+
this.prefix = prefix;
|
|
14654
14686
|
const opts = {
|
|
14655
14687
|
connection: redis,
|
|
14688
|
+
prefix,
|
|
14656
14689
|
defaultJobOptions: {
|
|
14657
14690
|
removeOnComplete: { age: 24 * 3600, count: 1e3 },
|
|
14658
14691
|
removeOnFail: { age: 7 * 24 * 3600 }
|
|
@@ -14684,7 +14717,7 @@ var BullDriver = class _BullDriver {
|
|
|
14684
14717
|
}
|
|
14685
14718
|
async startWorkers(opts) {
|
|
14686
14719
|
if (this.workers) return;
|
|
14687
|
-
const base = { connection: this.redis };
|
|
14720
|
+
const base = { connection: this.redis, prefix: this.prefix };
|
|
14688
14721
|
const { Worker } = this.bull;
|
|
14689
14722
|
const tick = new Worker(
|
|
14690
14723
|
QUEUE_NAMES.tick,
|
|
@@ -14936,7 +14969,7 @@ var NoopDriver = class {
|
|
|
14936
14969
|
async function createQueueDriver(config, fallbackDb) {
|
|
14937
14970
|
switch (config.driver) {
|
|
14938
14971
|
case "bull":
|
|
14939
|
-
return BullDriver.create(config.redis);
|
|
14972
|
+
return BullDriver.create(config.redis, config.prefix);
|
|
14940
14973
|
case "agenda":
|
|
14941
14974
|
return AgendaDriver.create({
|
|
14942
14975
|
db: config.db ?? fallbackDb,
|
|
@@ -15359,6 +15392,12 @@ async function evaluatePredicate(predicate, ctx) {
|
|
|
15359
15392
|
if ("fieldExists" in p) {
|
|
15360
15393
|
return ctx.contact.fields[p.fieldExists] !== void 0;
|
|
15361
15394
|
}
|
|
15395
|
+
if ("triggerPropertyEquals" in p) {
|
|
15396
|
+
return (ctx.run.triggerEvent?.properties ?? {})[p.triggerPropertyEquals.key] === p.triggerPropertyEquals.value;
|
|
15397
|
+
}
|
|
15398
|
+
if ("triggerPropertyTruthy" in p) {
|
|
15399
|
+
return Boolean((ctx.run.triggerEvent?.properties ?? {})[p.triggerPropertyTruthy]);
|
|
15400
|
+
}
|
|
15362
15401
|
if ("subscriptionStatus" in p) {
|
|
15363
15402
|
const sub = await ctx.collections.subscriptions.findOne({ externalId: ctx.contact.externalId });
|
|
15364
15403
|
return sub?.status === p.subscriptionStatus;
|
|
@@ -16105,7 +16144,14 @@ async function handleFireEvent(run, step2, ctx) {
|
|
|
16105
16144
|
await ctx.collections.events.insertOne({
|
|
16106
16145
|
externalId: run.externalId,
|
|
16107
16146
|
name: step2.eventName,
|
|
16108
|
-
|
|
16147
|
+
// Inherit the triggering event's properties so a handoff carries the
|
|
16148
|
+
// context that identifies what the run is ABOUT (which account, order,
|
|
16149
|
+
// subscription, ...). A step's `properties` are static — authored once in
|
|
16150
|
+
// the flow definition — so without this a fired event can only ever say
|
|
16151
|
+
// "this contact", losing the scope the originating event supplied, and
|
|
16152
|
+
// the receiving flow has nothing to resolve variables against. Explicit
|
|
16153
|
+
// step.properties win on conflict.
|
|
16154
|
+
properties: { ...run.triggerEvent?.properties ?? {}, ...step2.properties ?? {} },
|
|
16109
16155
|
dedupeKey,
|
|
16110
16156
|
occurredAt: /* @__PURE__ */ new Date(),
|
|
16111
16157
|
createdAt: /* @__PURE__ */ new Date()
|
|
@@ -17364,6 +17410,7 @@ var Mailer = class _Mailer {
|
|
|
17364
17410
|
* MAILER_MONGODB_URI — Mongo connection string (required)
|
|
17365
17411
|
* MAILER_MONGODB_DB — database name (optional; defaults to the URI default)
|
|
17366
17412
|
* MAILER_REDIS_URL — Redis connection URL (required)
|
|
17413
|
+
* MAILER_QUEUE_PREFIX — Redis key prefix to namespace this instance (optional)
|
|
17367
17414
|
* MAILER_PUBLIC_URL — base for tracking/unsub URLs (required)
|
|
17368
17415
|
* MAILER_UNSUBSCRIBE_SECRET — HMAC key (required)
|
|
17369
17416
|
* MAILER_SENDER_ADDRESS — postal address for CAN-SPAM (optional)
|
|
@@ -17412,7 +17459,11 @@ var Mailer = class _Mailer {
|
|
|
17412
17459
|
}
|
|
17413
17460
|
const defaultProvider = env.MAILER_DEFAULT_PROVIDER ?? Object.keys(providers)[0];
|
|
17414
17461
|
const driverEnv = env.MAILER_QUEUE_DRIVER ?? "bull";
|
|
17415
|
-
const queue = driverEnv === "agenda" ? { driver: "agenda" } : driverEnv === "noop" ? { driver: "noop" } : {
|
|
17462
|
+
const queue = driverEnv === "agenda" ? { driver: "agenda" } : driverEnv === "noop" ? { driver: "noop" } : {
|
|
17463
|
+
driver: "bull",
|
|
17464
|
+
redis: { url: required("MAILER_REDIS_URL") },
|
|
17465
|
+
prefix: env.MAILER_QUEUE_PREFIX
|
|
17466
|
+
};
|
|
17416
17467
|
return _Mailer.init({
|
|
17417
17468
|
db,
|
|
17418
17469
|
adapter,
|
|
@@ -17663,14 +17714,25 @@ var Mailer = class _Mailer {
|
|
|
17663
17714
|
* same handler that processes the business event ("user upgraded").
|
|
17664
17715
|
*/
|
|
17665
17716
|
async abortFlow(flowSlug, externalId, opts = {}) {
|
|
17666
|
-
const parsed = abortFlowInputSchema.parse({
|
|
17717
|
+
const parsed = abortFlowInputSchema.parse({
|
|
17718
|
+
flowSlug,
|
|
17719
|
+
externalId,
|
|
17720
|
+
reason: opts.reason,
|
|
17721
|
+
matchTriggerProperties: opts.matchTriggerProperties
|
|
17722
|
+
});
|
|
17667
17723
|
const flow = await this.collections.flows.findOne(
|
|
17668
17724
|
{ slug: parsed.flowSlug },
|
|
17669
17725
|
{ projection: { _id: 1 } }
|
|
17670
17726
|
);
|
|
17671
17727
|
if (!flow) throw new Error(`abortFlow: unknown flow slug "${parsed.flowSlug}"`);
|
|
17728
|
+
const triggerMatch = Object.fromEntries(
|
|
17729
|
+
Object.entries(parsed.matchTriggerProperties ?? {}).map(([k, v]) => [
|
|
17730
|
+
`triggerEvent.properties.${k}`,
|
|
17731
|
+
v
|
|
17732
|
+
])
|
|
17733
|
+
);
|
|
17672
17734
|
const result = await this.abortActiveRuns(
|
|
17673
|
-
{ externalId: parsed.externalId, flowId: flow._id },
|
|
17735
|
+
{ externalId: parsed.externalId, flowId: flow._id, ...triggerMatch },
|
|
17674
17736
|
parsed.reason ? `aborted_by_host:${parsed.reason}` : "aborted_by_host"
|
|
17675
17737
|
);
|
|
17676
17738
|
if (result.abortedRuns > 0 || result.cancelledSends > 0) {
|
|
@@ -17678,7 +17740,9 @@ var Mailer = class _Mailer {
|
|
|
17678
17740
|
actor: "host",
|
|
17679
17741
|
action: "flow.abort",
|
|
17680
17742
|
resource: { collection: "mailer_flow_runs", slug: parsed.flowSlug },
|
|
17681
|
-
|
|
17743
|
+
// Record the scope: without it a one-account abort and an abort-every-
|
|
17744
|
+
// run-for-this-contact are indistinguishable in the audit trail.
|
|
17745
|
+
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
17746
|
});
|
|
17683
17747
|
}
|
|
17684
17748
|
return result;
|