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
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(),
|
|
@@ -897,7 +922,7 @@ var BullDriver = class _BullDriver {
|
|
|
897
922
|
bullQueues;
|
|
898
923
|
workers = null;
|
|
899
924
|
bull;
|
|
900
|
-
static async create(redisConfig) {
|
|
925
|
+
static async create(redisConfig, prefix) {
|
|
901
926
|
let bull;
|
|
902
927
|
try {
|
|
903
928
|
bull = await import('bullmq');
|
|
@@ -906,14 +931,22 @@ var BullDriver = class _BullDriver {
|
|
|
906
931
|
"mailery: queue driver 'bull' requires the 'bullmq' peer dependency. Run `npm install bullmq ioredis`."
|
|
907
932
|
);
|
|
908
933
|
}
|
|
934
|
+
if (prefix?.includes(":")) {
|
|
935
|
+
throw new Error(
|
|
936
|
+
`mailery: queue prefix "${prefix}" must not contain ':' \u2014 BullMQ uses it as the Redis key separator.`
|
|
937
|
+
);
|
|
938
|
+
}
|
|
909
939
|
const redis = isRedisLike(redisConfig) ? redisConfig : connect(redisConfig);
|
|
910
|
-
return new _BullDriver(bull, redis);
|
|
940
|
+
return new _BullDriver(bull, redis, prefix);
|
|
911
941
|
}
|
|
912
|
-
|
|
942
|
+
prefix;
|
|
943
|
+
constructor(bull, redis, prefix) {
|
|
913
944
|
this.bull = bull;
|
|
914
945
|
this.redis = redis;
|
|
946
|
+
this.prefix = prefix;
|
|
915
947
|
const opts = {
|
|
916
948
|
connection: redis,
|
|
949
|
+
prefix,
|
|
917
950
|
defaultJobOptions: {
|
|
918
951
|
removeOnComplete: { age: 24 * 3600, count: 1e3 },
|
|
919
952
|
removeOnFail: { age: 7 * 24 * 3600 }
|
|
@@ -945,7 +978,7 @@ var BullDriver = class _BullDriver {
|
|
|
945
978
|
}
|
|
946
979
|
async startWorkers(opts) {
|
|
947
980
|
if (this.workers) return;
|
|
948
|
-
const base = { connection: this.redis };
|
|
981
|
+
const base = { connection: this.redis, prefix: this.prefix };
|
|
949
982
|
const { Worker } = this.bull;
|
|
950
983
|
const tick = new Worker(
|
|
951
984
|
QUEUE_NAMES.tick,
|
|
@@ -1197,7 +1230,7 @@ var NoopDriver = class {
|
|
|
1197
1230
|
async function createQueueDriver(config, fallbackDb) {
|
|
1198
1231
|
switch (config.driver) {
|
|
1199
1232
|
case "bull":
|
|
1200
|
-
return BullDriver.create(config.redis);
|
|
1233
|
+
return BullDriver.create(config.redis, config.prefix);
|
|
1201
1234
|
case "agenda":
|
|
1202
1235
|
return AgendaDriver.create({
|
|
1203
1236
|
db: config.db ?? fallbackDb,
|
|
@@ -1658,6 +1691,12 @@ async function evaluatePredicate(predicate, ctx) {
|
|
|
1658
1691
|
if ("fieldExists" in p) {
|
|
1659
1692
|
return ctx.contact.fields[p.fieldExists] !== void 0;
|
|
1660
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
|
+
}
|
|
1661
1700
|
if ("subscriptionStatus" in p) {
|
|
1662
1701
|
const sub = await ctx.collections.subscriptions.findOne({ externalId: ctx.contact.externalId });
|
|
1663
1702
|
return sub?.status === p.subscriptionStatus;
|
|
@@ -2410,7 +2449,14 @@ async function handleFireEvent(run, step, ctx) {
|
|
|
2410
2449
|
await ctx.collections.events.insertOne({
|
|
2411
2450
|
externalId: run.externalId,
|
|
2412
2451
|
name: step.eventName,
|
|
2413
|
-
|
|
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 ?? {} },
|
|
2414
2460
|
dedupeKey,
|
|
2415
2461
|
occurredAt: /* @__PURE__ */ new Date(),
|
|
2416
2462
|
createdAt: /* @__PURE__ */ new Date()
|
|
@@ -3941,6 +3987,7 @@ var Mailer = class _Mailer {
|
|
|
3941
3987
|
* MAILER_MONGODB_URI — Mongo connection string (required)
|
|
3942
3988
|
* MAILER_MONGODB_DB — database name (optional; defaults to the URI default)
|
|
3943
3989
|
* MAILER_REDIS_URL — Redis connection URL (required)
|
|
3990
|
+
* MAILER_QUEUE_PREFIX — Redis key prefix to namespace this instance (optional)
|
|
3944
3991
|
* MAILER_PUBLIC_URL — base for tracking/unsub URLs (required)
|
|
3945
3992
|
* MAILER_UNSUBSCRIBE_SECRET — HMAC key (required)
|
|
3946
3993
|
* MAILER_SENDER_ADDRESS — postal address for CAN-SPAM (optional)
|
|
@@ -3989,7 +4036,11 @@ var Mailer = class _Mailer {
|
|
|
3989
4036
|
}
|
|
3990
4037
|
const defaultProvider = env.MAILER_DEFAULT_PROVIDER ?? Object.keys(providers)[0];
|
|
3991
4038
|
const driverEnv = env.MAILER_QUEUE_DRIVER ?? "bull";
|
|
3992
|
-
const queue = driverEnv === "agenda" ? { driver: "agenda" } : driverEnv === "noop" ? { driver: "noop" } : {
|
|
4039
|
+
const queue = driverEnv === "agenda" ? { driver: "agenda" } : driverEnv === "noop" ? { driver: "noop" } : {
|
|
4040
|
+
driver: "bull",
|
|
4041
|
+
redis: { url: required("MAILER_REDIS_URL") },
|
|
4042
|
+
prefix: env.MAILER_QUEUE_PREFIX
|
|
4043
|
+
};
|
|
3993
4044
|
return _Mailer.init({
|
|
3994
4045
|
db,
|
|
3995
4046
|
adapter,
|
|
@@ -4240,14 +4291,25 @@ var Mailer = class _Mailer {
|
|
|
4240
4291
|
* same handler that processes the business event ("user upgraded").
|
|
4241
4292
|
*/
|
|
4242
4293
|
async abortFlow(flowSlug, externalId, opts = {}) {
|
|
4243
|
-
const parsed = abortFlowInputSchema.parse({
|
|
4294
|
+
const parsed = abortFlowInputSchema.parse({
|
|
4295
|
+
flowSlug,
|
|
4296
|
+
externalId,
|
|
4297
|
+
reason: opts.reason,
|
|
4298
|
+
matchTriggerProperties: opts.matchTriggerProperties
|
|
4299
|
+
});
|
|
4244
4300
|
const flow = await this.collections.flows.findOne(
|
|
4245
4301
|
{ slug: parsed.flowSlug },
|
|
4246
4302
|
{ projection: { _id: 1 } }
|
|
4247
4303
|
);
|
|
4248
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
|
+
);
|
|
4249
4311
|
const result = await this.abortActiveRuns(
|
|
4250
|
-
{ externalId: parsed.externalId, flowId: flow._id },
|
|
4312
|
+
{ externalId: parsed.externalId, flowId: flow._id, ...triggerMatch },
|
|
4251
4313
|
parsed.reason ? `aborted_by_host:${parsed.reason}` : "aborted_by_host"
|
|
4252
4314
|
);
|
|
4253
4315
|
if (result.abortedRuns > 0 || result.cancelledSends > 0) {
|
|
@@ -4255,7 +4317,9 @@ var Mailer = class _Mailer {
|
|
|
4255
4317
|
actor: "host",
|
|
4256
4318
|
action: "flow.abort",
|
|
4257
4319
|
resource: { collection: "mailer_flow_runs", slug: parsed.flowSlug },
|
|
4258
|
-
|
|
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}` : ""}`
|
|
4259
4323
|
});
|
|
4260
4324
|
}
|
|
4261
4325
|
return result;
|
|
@@ -7421,6 +7485,8 @@ var PREDICATE_KINDS = [
|
|
|
7421
7485
|
{ value: "notHasTag", label: "does NOT have tag" },
|
|
7422
7486
|
{ value: "fieldEquals", label: "field equals" },
|
|
7423
7487
|
{ value: "fieldExists", label: "field exists" },
|
|
7488
|
+
{ value: "triggerPropertyEquals", label: "trigger property equals" },
|
|
7489
|
+
{ value: "triggerPropertyTruthy", label: "trigger property is set" },
|
|
7424
7490
|
{ value: "hasFiredEvent", label: "fired event" },
|
|
7425
7491
|
{ value: "notHasFiredEvent", label: "has NOT fired event" },
|
|
7426
7492
|
{ value: "subscriptionStatus", label: "subscription status" },
|
|
@@ -7448,6 +7514,10 @@ function defaultPredicate(kind) {
|
|
|
7448
7514
|
return { fieldEquals: { field: "tier", value: "Pro" } };
|
|
7449
7515
|
case "fieldExists":
|
|
7450
7516
|
return { fieldExists: "tier" };
|
|
7517
|
+
case "triggerPropertyEquals":
|
|
7518
|
+
return { triggerPropertyEquals: { key: "plan", value: "pro" } };
|
|
7519
|
+
case "triggerPropertyTruthy":
|
|
7520
|
+
return { triggerPropertyTruthy: "wasReferred" };
|
|
7451
7521
|
case "hasFiredEvent":
|
|
7452
7522
|
return { hasFiredEvent: "Activated app" };
|
|
7453
7523
|
case "notHasFiredEvent":
|
|
@@ -7538,7 +7608,7 @@ var DEDUPE_POLICIES = [
|
|
|
7538
7608
|
];
|
|
7539
7609
|
|
|
7540
7610
|
// src/server/index.ts
|
|
7541
|
-
var VERSION = "0.
|
|
7611
|
+
var VERSION = "0.10.0" ;
|
|
7542
7612
|
|
|
7543
7613
|
exports.DEDUPE_POLICIES = DEDUPE_POLICIES;
|
|
7544
7614
|
exports.FLOW_STEP_KINDS = FLOW_STEP_KINDS;
|