mailery 0.7.0 → 0.8.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-CjQTOX9H.js → index-B3ndbezm.js} +3 -3
- package/dist/admin/spa/index-B3ndbezm.js.map +1 -0
- package/dist/admin/spa/index.html +1 -1
- package/dist/admin/spa/{template-editor-yH0Oz4Ix.js → template-editor-DwMKy5sw.js} +3 -3
- package/dist/admin/spa/{template-editor-yH0Oz4Ix.js.map → template-editor-DwMKy5sw.js.map} +1 -1
- package/dist/index.cjs +25 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +25 -11
- package/dist/index.js.map +1 -1
- package/dist/{null-B0rPgE5_.d.cts → null-BCxlHRJ0.d.cts} +9 -1
- package/dist/{null-B0rPgE5_.d.ts → null-BCxlHRJ0.d.ts} +9 -1
- package/dist/testing.cjs +331 -67
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +233 -13
- package/dist/testing.d.ts +233 -13
- package/dist/testing.js +325 -68
- package/dist/testing.js.map +1 -1
- package/package.json +4 -1
- package/dist/admin/spa/index-CjQTOX9H.js.map +0 -1
|
@@ -527,6 +527,14 @@ interface MailTesterConfig {
|
|
|
527
527
|
apiKey: string;
|
|
528
528
|
/** Minimum score (0-10) below which publish is blocked. Default 8.0. */
|
|
529
529
|
minScore?: number;
|
|
530
|
+
/**
|
|
531
|
+
* When true, publishing content that has no cached score is blocked too —
|
|
532
|
+
* the operator must run a deliverability check for that exact content first.
|
|
533
|
+
* Default false, which only blocks content already known to score below
|
|
534
|
+
* `minScore`; in that mode any edit (even whitespace) changes the content
|
|
535
|
+
* key, misses the cache, and publishes freely.
|
|
536
|
+
*/
|
|
537
|
+
requireScore?: boolean;
|
|
530
538
|
/**
|
|
531
539
|
* How long a successful score remains cached for the same content. Default 24.
|
|
532
540
|
* Re-publishing the same body within the window does not burn a credit.
|
|
@@ -1469,4 +1477,4 @@ declare class NullProvider implements MailProvider {
|
|
|
1469
1477
|
reset(): void;
|
|
1470
1478
|
}
|
|
1471
1479
|
|
|
1472
|
-
export { getCollections as $, type AdapterFilter as A, type BroadcastDoc as B, type ContactAdapter as C, type DeliveryWindow as D, type EventDoc as E, type FlowStep as F, type SenderDomainValidation as G, type HealthDoc as H, type SubscriptionDoc as I, type SubscriptionStatus as J, type SuppressionDoc as K, type LeadDoc as L, type MailProvider as M, type NormalizedEvent as N, type OutboxDoc as O, type Predicate as P, type SuppressionReason as Q, type RunnerContext as R, type SendArgs as S, type TemplateDoc as T, type TemplateKind as U, type TemplateVersionDoc as V, type VarsAdapter as W, type VarsResolveInfo as X, type WebhookEventDoc as Y, defineVars as Z, ensureIndexes as _, type Contact as a, validateSenderDomain as a0, varsJsonSchema as a1, type SendResult as b, type MailTesterFeedback as c, Mailer as d, type SuppressionScope as e, type SegmentFilter as f, type AuditLogDoc as g, type BroadcastStatus as h, type CircuitBreakerThresholds as i, type Collections as j, type ContactTagDoc as k, type FlowDoc as l, type FlowGoal as m, type FlowRunDoc as n, type FlowRunStatus as o, type FlowVersionDoc as p, type HealthStatus as q, type MailerConfig as r, NullProvider as s, RESERVED_VAR_KEYS as t, type RedisOptions as u, type SegmentDefinition as v, type SendDoc as w, type SendStatus as x, type SenderDomainConfig as y, type SenderDomainRegistry as z };
|
|
1480
|
+
export { getCollections as $, type AdapterFilter as A, type BroadcastDoc as B, type ContactAdapter as C, type DeliveryWindow as D, type EventDoc as E, type FlowStep as F, type SenderDomainValidation as G, type HealthDoc as H, type SubscriptionDoc as I, type SubscriptionStatus as J, type SuppressionDoc as K, type LeadDoc as L, type MailProvider as M, type NormalizedEvent as N, type OutboxDoc as O, type Predicate as P, type SuppressionReason as Q, type RunnerContext as R, type SendArgs as S, type TemplateDoc as T, type TemplateKind as U, type TemplateVersionDoc as V, type VarsAdapter as W, type VarsResolveInfo as X, type WebhookEventDoc as Y, defineVars as Z, ensureIndexes as _, type Contact as a, validateSenderDomain as a0, varsJsonSchema as a1, type FlowTrigger as a2, type QueueDriverConfig as a3, type SendResult as b, type MailTesterFeedback as c, Mailer as d, type SuppressionScope as e, type SegmentFilter as f, type AuditLogDoc as g, type BroadcastStatus as h, type CircuitBreakerThresholds as i, type Collections as j, type ContactTagDoc as k, type FlowDoc as l, type FlowGoal as m, type FlowRunDoc as n, type FlowRunStatus as o, type FlowVersionDoc as p, type HealthStatus as q, type MailerConfig as r, NullProvider as s, RESERVED_VAR_KEYS as t, type RedisOptions as u, type SegmentDefinition as v, type SendDoc as w, type SendStatus as x, type SenderDomainConfig as y, type SenderDomainRegistry as z };
|
|
@@ -527,6 +527,14 @@ interface MailTesterConfig {
|
|
|
527
527
|
apiKey: string;
|
|
528
528
|
/** Minimum score (0-10) below which publish is blocked. Default 8.0. */
|
|
529
529
|
minScore?: number;
|
|
530
|
+
/**
|
|
531
|
+
* When true, publishing content that has no cached score is blocked too —
|
|
532
|
+
* the operator must run a deliverability check for that exact content first.
|
|
533
|
+
* Default false, which only blocks content already known to score below
|
|
534
|
+
* `minScore`; in that mode any edit (even whitespace) changes the content
|
|
535
|
+
* key, misses the cache, and publishes freely.
|
|
536
|
+
*/
|
|
537
|
+
requireScore?: boolean;
|
|
530
538
|
/**
|
|
531
539
|
* How long a successful score remains cached for the same content. Default 24.
|
|
532
540
|
* Re-publishing the same body within the window does not burn a credit.
|
|
@@ -1469,4 +1477,4 @@ declare class NullProvider implements MailProvider {
|
|
|
1469
1477
|
reset(): void;
|
|
1470
1478
|
}
|
|
1471
1479
|
|
|
1472
|
-
export { getCollections as $, type AdapterFilter as A, type BroadcastDoc as B, type ContactAdapter as C, type DeliveryWindow as D, type EventDoc as E, type FlowStep as F, type SenderDomainValidation as G, type HealthDoc as H, type SubscriptionDoc as I, type SubscriptionStatus as J, type SuppressionDoc as K, type LeadDoc as L, type MailProvider as M, type NormalizedEvent as N, type OutboxDoc as O, type Predicate as P, type SuppressionReason as Q, type RunnerContext as R, type SendArgs as S, type TemplateDoc as T, type TemplateKind as U, type TemplateVersionDoc as V, type VarsAdapter as W, type VarsResolveInfo as X, type WebhookEventDoc as Y, defineVars as Z, ensureIndexes as _, type Contact as a, validateSenderDomain as a0, varsJsonSchema as a1, type SendResult as b, type MailTesterFeedback as c, Mailer as d, type SuppressionScope as e, type SegmentFilter as f, type AuditLogDoc as g, type BroadcastStatus as h, type CircuitBreakerThresholds as i, type Collections as j, type ContactTagDoc as k, type FlowDoc as l, type FlowGoal as m, type FlowRunDoc as n, type FlowRunStatus as o, type FlowVersionDoc as p, type HealthStatus as q, type MailerConfig as r, NullProvider as s, RESERVED_VAR_KEYS as t, type RedisOptions as u, type SegmentDefinition as v, type SendDoc as w, type SendStatus as x, type SenderDomainConfig as y, type SenderDomainRegistry as z };
|
|
1480
|
+
export { getCollections as $, type AdapterFilter as A, type BroadcastDoc as B, type ContactAdapter as C, type DeliveryWindow as D, type EventDoc as E, type FlowStep as F, type SenderDomainValidation as G, type HealthDoc as H, type SubscriptionDoc as I, type SubscriptionStatus as J, type SuppressionDoc as K, type LeadDoc as L, type MailProvider as M, type NormalizedEvent as N, type OutboxDoc as O, type Predicate as P, type SuppressionReason as Q, type RunnerContext as R, type SendArgs as S, type TemplateDoc as T, type TemplateKind as U, type TemplateVersionDoc as V, type VarsAdapter as W, type VarsResolveInfo as X, type WebhookEventDoc as Y, defineVars as Z, ensureIndexes as _, type Contact as a, validateSenderDomain as a0, varsJsonSchema as a1, type FlowTrigger as a2, type QueueDriverConfig as a3, type SendResult as b, type MailTesterFeedback as c, Mailer as d, type SuppressionScope as e, type SegmentFilter as f, type AuditLogDoc as g, type BroadcastStatus as h, type CircuitBreakerThresholds as i, type Collections as j, type ContactTagDoc as k, type FlowDoc as l, type FlowGoal as m, type FlowRunDoc as n, type FlowRunStatus as o, type FlowVersionDoc as p, type HealthStatus as q, type MailerConfig as r, NullProvider as s, RESERVED_VAR_KEYS as t, type RedisOptions as u, type SegmentDefinition as v, type SendDoc as w, type SendStatus as x, type SenderDomainConfig as y, type SenderDomainRegistry as z };
|
package/dist/testing.cjs
CHANGED
|
@@ -504,22 +504,22 @@ function __awaiter(thisArg, _arguments, P, generator) {
|
|
|
504
504
|
return new (P || (P = Promise))(function(resolve, reject) {
|
|
505
505
|
function fulfilled(value) {
|
|
506
506
|
try {
|
|
507
|
-
|
|
507
|
+
step2(generator.next(value));
|
|
508
508
|
} catch (e) {
|
|
509
509
|
reject(e);
|
|
510
510
|
}
|
|
511
511
|
}
|
|
512
512
|
function rejected(value) {
|
|
513
513
|
try {
|
|
514
|
-
|
|
514
|
+
step2(generator["throw"](value));
|
|
515
515
|
} catch (e) {
|
|
516
516
|
reject(e);
|
|
517
517
|
}
|
|
518
518
|
}
|
|
519
|
-
function
|
|
519
|
+
function step2(result) {
|
|
520
520
|
result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
|
|
521
521
|
}
|
|
522
|
-
|
|
522
|
+
step2((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
523
523
|
});
|
|
524
524
|
}
|
|
525
525
|
function __generator(thisArg, body) {
|
|
@@ -532,10 +532,10 @@ function __generator(thisArg, body) {
|
|
|
532
532
|
}), g;
|
|
533
533
|
function verb(n) {
|
|
534
534
|
return function(v) {
|
|
535
|
-
return
|
|
535
|
+
return step2([n, v]);
|
|
536
536
|
};
|
|
537
537
|
}
|
|
538
|
-
function
|
|
538
|
+
function step2(op) {
|
|
539
539
|
if (f) throw new TypeError("Generator is already executing.");
|
|
540
540
|
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
541
541
|
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
@@ -669,12 +669,12 @@ function __asyncGenerator(thisArg, _arguments, generator) {
|
|
|
669
669
|
}
|
|
670
670
|
function resume(n, v) {
|
|
671
671
|
try {
|
|
672
|
-
|
|
672
|
+
step2(g[n](v));
|
|
673
673
|
} catch (e) {
|
|
674
674
|
settle(q[0][3], e);
|
|
675
675
|
}
|
|
676
676
|
}
|
|
677
|
-
function
|
|
677
|
+
function step2(r) {
|
|
678
678
|
r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r);
|
|
679
679
|
}
|
|
680
680
|
function fulfill(value) {
|
|
@@ -7092,9 +7092,9 @@ var require_pack = __commonJS({
|
|
|
7092
7092
|
this.push(headers.encode(newHeader));
|
|
7093
7093
|
}
|
|
7094
7094
|
_doDrain() {
|
|
7095
|
-
const
|
|
7095
|
+
const drain2 = this._drain;
|
|
7096
7096
|
this._drain = noop;
|
|
7097
|
-
|
|
7097
|
+
drain2();
|
|
7098
7098
|
}
|
|
7099
7099
|
_predestroy() {
|
|
7100
7100
|
const err = getStreamError(this);
|
|
@@ -15485,10 +15485,10 @@ async function getBucketStatus(ctx, fromEmail, kind) {
|
|
|
15485
15485
|
}
|
|
15486
15486
|
|
|
15487
15487
|
// src/server/runner/send.ts
|
|
15488
|
-
async function handleSend(run,
|
|
15489
|
-
const template = await ctx.collections.templates.findOne({ slug:
|
|
15488
|
+
async function handleSend(run, step2, contact, flow, ctx) {
|
|
15489
|
+
const template = await ctx.collections.templates.findOne({ slug: step2.templateSlug });
|
|
15490
15490
|
if (!template) {
|
|
15491
|
-
await failFlowRun(run, `template not found: ${
|
|
15491
|
+
await failFlowRun(run, `template not found: ${step2.templateSlug}`, ctx);
|
|
15492
15492
|
return;
|
|
15493
15493
|
}
|
|
15494
15494
|
const dedupeKey = `flowrun:${run._id}:step${run.currentStepIndex}`;
|
|
@@ -15500,8 +15500,8 @@ async function handleSend(run, step, contact, flow, ctx) {
|
|
|
15500
15500
|
});
|
|
15501
15501
|
return;
|
|
15502
15502
|
}
|
|
15503
|
-
const providerName = pickProviderName(
|
|
15504
|
-
const renderCtx = buildRenderContext(contact, run,
|
|
15503
|
+
const providerName = pickProviderName(step2.providerOverride, template, ctx);
|
|
15504
|
+
const renderCtx = buildRenderContext(contact, run, step2.vars ?? {}, ctx);
|
|
15505
15505
|
let rendered;
|
|
15506
15506
|
try {
|
|
15507
15507
|
rendered = await renderTemplate(template, renderCtx, { helpers: ctx.handlebarsHelpers });
|
|
@@ -15538,7 +15538,7 @@ async function handleSend(run, step, contact, flow, ctx) {
|
|
|
15538
15538
|
bodyHash: sha256(rendered.html),
|
|
15539
15539
|
status: "queued",
|
|
15540
15540
|
renderedFrom: rendered.fromName ? { name: rendered.fromName, email: rendered.fromEmail } : void 0,
|
|
15541
|
-
vars:
|
|
15541
|
+
vars: step2.vars ?? {}
|
|
15542
15542
|
})
|
|
15543
15543
|
);
|
|
15544
15544
|
await ctx.queues.send.add(
|
|
@@ -15627,6 +15627,10 @@ async function dispatchSend(sendId, ctx) {
|
|
|
15627
15627
|
await markFailed(send._id, `provider_unknown: ${send.provider}`, ctx);
|
|
15628
15628
|
return;
|
|
15629
15629
|
}
|
|
15630
|
+
const headers = send.kind === "marketing" ? {
|
|
15631
|
+
"List-Unsubscribe": `<${renderCtx.unsubscribeUrl}>`,
|
|
15632
|
+
"List-Unsubscribe-Post": "List-Unsubscribe=One-Click"
|
|
15633
|
+
} : {};
|
|
15630
15634
|
try {
|
|
15631
15635
|
const result = await provider.send({
|
|
15632
15636
|
to: send.emailAtSend,
|
|
@@ -15636,10 +15640,7 @@ async function dispatchSend(sendId, ctx) {
|
|
|
15636
15640
|
subject: rendered.subject,
|
|
15637
15641
|
html: tracking.html,
|
|
15638
15642
|
text: rendered.plainText,
|
|
15639
|
-
headers
|
|
15640
|
-
"List-Unsubscribe": `<${renderCtx.unsubscribeUrl}>`,
|
|
15641
|
-
"List-Unsubscribe-Post": "List-Unsubscribe=One-Click"
|
|
15642
|
-
},
|
|
15643
|
+
headers,
|
|
15643
15644
|
messageMeta: { sendId: String(send._id) }
|
|
15644
15645
|
});
|
|
15645
15646
|
await ctx.collections.sends.updateOne(
|
|
@@ -15751,8 +15752,8 @@ async function processOneRunStep(runId, ctx) {
|
|
|
15751
15752
|
return;
|
|
15752
15753
|
}
|
|
15753
15754
|
const steps = await loadStepsForRun(run, flow, ctx);
|
|
15754
|
-
const
|
|
15755
|
-
if (!
|
|
15755
|
+
const step2 = locateStep(steps, run.currentStepIndex, run.currentBranchPath);
|
|
15756
|
+
if (!step2) {
|
|
15756
15757
|
await completeFlowRun(run, "completed", ctx);
|
|
15757
15758
|
return;
|
|
15758
15759
|
}
|
|
@@ -15766,34 +15767,34 @@ async function processOneRunStep(runId, ctx) {
|
|
|
15766
15767
|
await exitFlowRun(run, sub.status, ctx);
|
|
15767
15768
|
return;
|
|
15768
15769
|
}
|
|
15769
|
-
switch (
|
|
15770
|
+
switch (step2.type) {
|
|
15770
15771
|
case "wait":
|
|
15771
|
-
return handleWait(run,
|
|
15772
|
+
return handleWait(run, step2, ctx);
|
|
15772
15773
|
case "condition":
|
|
15773
|
-
return handleCondition(run,
|
|
15774
|
+
return handleCondition(run, step2, contact, ctx);
|
|
15774
15775
|
case "branch":
|
|
15775
|
-
return handleBranch(run,
|
|
15776
|
+
return handleBranch(run, step2, contact, ctx);
|
|
15776
15777
|
case "send": {
|
|
15777
|
-
if (
|
|
15778
|
-
const deliverAt = computeDeliveryTime(/* @__PURE__ */ new Date(),
|
|
15778
|
+
if (step2.delivery) {
|
|
15779
|
+
const deliverAt = computeDeliveryTime(/* @__PURE__ */ new Date(), step2.delivery, contact.timezone);
|
|
15779
15780
|
if (deliverAt.getTime() > Date.now() + 3e4) {
|
|
15780
15781
|
return deferSendForWindow(run, deliverAt, ctx);
|
|
15781
15782
|
}
|
|
15782
15783
|
}
|
|
15783
|
-
return handleSend(run,
|
|
15784
|
+
return handleSend(run, step2, contact, flow, ctx);
|
|
15784
15785
|
}
|
|
15785
15786
|
case "tag":
|
|
15786
|
-
return handleTag(run,
|
|
15787
|
+
return handleTag(run, step2, ctx);
|
|
15787
15788
|
case "fire_event":
|
|
15788
|
-
return handleFireEvent(run,
|
|
15789
|
+
return handleFireEvent(run, step2, ctx);
|
|
15789
15790
|
case "webhook":
|
|
15790
|
-
return handleWebhookStep(run,
|
|
15791
|
+
return handleWebhookStep(run, step2, ctx);
|
|
15791
15792
|
case "exit":
|
|
15792
|
-
return exitFlowRun(run,
|
|
15793
|
+
return exitFlowRun(run, step2.reason ?? "exit_step", ctx);
|
|
15793
15794
|
}
|
|
15794
15795
|
}
|
|
15795
|
-
async function handleWait(run,
|
|
15796
|
-
const ms = unitToMs(
|
|
15796
|
+
async function handleWait(run, step2, ctx) {
|
|
15797
|
+
const ms = unitToMs(step2.value, step2.unit);
|
|
15797
15798
|
const nextAt = new Date(Date.now() + ms);
|
|
15798
15799
|
const updated = await ctx.collections.flowRuns.findOneAndUpdate(
|
|
15799
15800
|
{ _id: run._id, currentStepIndex: run.currentStepIndex },
|
|
@@ -15818,22 +15819,22 @@ async function handleWait(run, step, ctx) {
|
|
|
15818
15819
|
{ delay: ms, jobId: `advance:${run._id}:${run.currentStepIndex + 1}` }
|
|
15819
15820
|
);
|
|
15820
15821
|
}
|
|
15821
|
-
async function handleCondition(run,
|
|
15822
|
+
async function handleCondition(run, step2, contact, ctx) {
|
|
15822
15823
|
if (!contact) return;
|
|
15823
|
-
const result = await evaluatePredicate(
|
|
15824
|
+
const result = await evaluatePredicate(step2.test, { contact, run, collections: ctx.collections });
|
|
15824
15825
|
if (result) {
|
|
15825
15826
|
await advanceStep(run, ctx, { action: "condition_evaluated", details: { result: true } });
|
|
15826
15827
|
return;
|
|
15827
15828
|
}
|
|
15828
|
-
if (
|
|
15829
|
+
if (step2.ifFalse === "continue") {
|
|
15829
15830
|
await advanceStep(run, ctx, { action: "condition_evaluated", details: { result: false, skipped: 1 } }, { stepInc: 2 });
|
|
15830
15831
|
return;
|
|
15831
15832
|
}
|
|
15832
15833
|
await exitFlowRun(run, "condition_false", ctx);
|
|
15833
15834
|
}
|
|
15834
|
-
async function handleBranch(run,
|
|
15835
|
+
async function handleBranch(run, step2, contact, ctx) {
|
|
15835
15836
|
if (!contact) return;
|
|
15836
|
-
const result = await evaluatePredicate(
|
|
15837
|
+
const result = await evaluatePredicate(step2.test, { contact, run, collections: ctx.collections });
|
|
15837
15838
|
const newPath = [...run.currentBranchPath, run.currentStepIndex, result ? "true" : "false", 0];
|
|
15838
15839
|
const updated = await ctx.collections.flowRuns.findOneAndUpdate(
|
|
15839
15840
|
{ _id: run._id, currentStepIndex: run.currentStepIndex },
|
|
@@ -15859,9 +15860,9 @@ async function handleBranch(run, step, contact, ctx) {
|
|
|
15859
15860
|
if (!updated) return;
|
|
15860
15861
|
await ctx.queues.advance.add("advance", { flowRunId: String(run._id) });
|
|
15861
15862
|
}
|
|
15862
|
-
async function handleTag(run,
|
|
15863
|
-
const adds =
|
|
15864
|
-
const removes =
|
|
15863
|
+
async function handleTag(run, step2, ctx) {
|
|
15864
|
+
const adds = step2.addTags ?? [];
|
|
15865
|
+
const removes = step2.removeTags ?? [];
|
|
15865
15866
|
if (ctx.adapter.addTags && adds.length > 0) {
|
|
15866
15867
|
await ctx.adapter.addTags(run.externalId, adds);
|
|
15867
15868
|
} else if (adds.length > 0) {
|
|
@@ -15882,13 +15883,13 @@ async function handleTag(run, step, ctx) {
|
|
|
15882
15883
|
}
|
|
15883
15884
|
await advanceStep(run, ctx, { action: "tagged", details: { addTags: adds, removeTags: removes } });
|
|
15884
15885
|
}
|
|
15885
|
-
async function handleFireEvent(run,
|
|
15886
|
-
const dedupeKey = `flowrun:${run._id}:step${run.currentStepIndex}:${
|
|
15886
|
+
async function handleFireEvent(run, step2, ctx) {
|
|
15887
|
+
const dedupeKey = `flowrun:${run._id}:step${run.currentStepIndex}:${step2.eventName}`;
|
|
15887
15888
|
try {
|
|
15888
15889
|
await ctx.collections.events.insertOne({
|
|
15889
15890
|
externalId: run.externalId,
|
|
15890
|
-
name:
|
|
15891
|
-
properties:
|
|
15891
|
+
name: step2.eventName,
|
|
15892
|
+
properties: step2.properties ?? {},
|
|
15892
15893
|
dedupeKey,
|
|
15893
15894
|
occurredAt: /* @__PURE__ */ new Date(),
|
|
15894
15895
|
createdAt: /* @__PURE__ */ new Date()
|
|
@@ -15896,27 +15897,27 @@ async function handleFireEvent(run, step, ctx) {
|
|
|
15896
15897
|
} catch (err) {
|
|
15897
15898
|
if (err?.code !== 11e3) throw err;
|
|
15898
15899
|
}
|
|
15899
|
-
await advanceStep(run, ctx, { action: "event_fired", details: { name:
|
|
15900
|
+
await advanceStep(run, ctx, { action: "event_fired", details: { name: step2.eventName } });
|
|
15900
15901
|
}
|
|
15901
|
-
async function handleWebhookStep(run,
|
|
15902
|
+
async function handleWebhookStep(run, step2, ctx) {
|
|
15902
15903
|
try {
|
|
15903
|
-
const res = await fetch(
|
|
15904
|
-
method:
|
|
15904
|
+
const res = await fetch(step2.url, {
|
|
15905
|
+
method: step2.method ?? "POST",
|
|
15905
15906
|
headers: { "Content-Type": "application/json" },
|
|
15906
|
-
body: JSON.stringify(
|
|
15907
|
+
body: JSON.stringify(step2.payload ?? { externalId: run.externalId, flowRunId: String(run._id) }),
|
|
15907
15908
|
signal: AbortSignal.timeout(1e4)
|
|
15908
15909
|
});
|
|
15909
15910
|
if (!res.ok) throw new Error(`webhook step returned ${res.status}`);
|
|
15910
|
-
await advanceStep(run, ctx, { action: "webhook_called", details: { url:
|
|
15911
|
+
await advanceStep(run, ctx, { action: "webhook_called", details: { url: step2.url, status: res.status } });
|
|
15911
15912
|
} catch (err) {
|
|
15912
15913
|
const attempts = (run.attemptsForCurrentStep ?? 0) + 1;
|
|
15913
15914
|
if (attempts >= ctx.config.webhookRetryAttempts) {
|
|
15914
|
-
if (
|
|
15915
|
+
if (step2.failureMode === "fail_run") {
|
|
15915
15916
|
await failFlowRun(run, `webhook failed: ${err?.message}`, ctx);
|
|
15916
15917
|
} else {
|
|
15917
15918
|
await advanceStep(run, ctx, {
|
|
15918
15919
|
action: "webhook_called",
|
|
15919
|
-
details: { url:
|
|
15920
|
+
details: { url: step2.url, error: err?.message, exhausted: true }
|
|
15920
15921
|
});
|
|
15921
15922
|
}
|
|
15922
15923
|
} else {
|
|
@@ -17852,6 +17853,207 @@ function matches(c, f) {
|
|
|
17852
17853
|
return true;
|
|
17853
17854
|
}
|
|
17854
17855
|
|
|
17856
|
+
// src/testing/recording-provider.ts
|
|
17857
|
+
var RecordingProvider = class {
|
|
17858
|
+
constructor(inner) {
|
|
17859
|
+
this.inner = inner;
|
|
17860
|
+
}
|
|
17861
|
+
inner;
|
|
17862
|
+
/** Every `SendArgs` handed to the provider, in call order. */
|
|
17863
|
+
sent = [];
|
|
17864
|
+
/** Same calls, with outcome attached. */
|
|
17865
|
+
records = [];
|
|
17866
|
+
get name() {
|
|
17867
|
+
return this.inner.name;
|
|
17868
|
+
}
|
|
17869
|
+
get sendRatePerSecond() {
|
|
17870
|
+
return this.inner.sendRatePerSecond;
|
|
17871
|
+
}
|
|
17872
|
+
async send(args) {
|
|
17873
|
+
this.sent.push(args);
|
|
17874
|
+
const record = { args, result: null, error: null, durationMs: 0, at: /* @__PURE__ */ new Date() };
|
|
17875
|
+
this.records.push(record);
|
|
17876
|
+
const startedAt = Date.now();
|
|
17877
|
+
try {
|
|
17878
|
+
const result = await this.inner.send(args);
|
|
17879
|
+
record.result = result;
|
|
17880
|
+
return result;
|
|
17881
|
+
} catch (err) {
|
|
17882
|
+
record.error = err instanceof Error ? err : new Error(String(err));
|
|
17883
|
+
throw err;
|
|
17884
|
+
} finally {
|
|
17885
|
+
record.durationMs = Date.now() - startedAt;
|
|
17886
|
+
}
|
|
17887
|
+
}
|
|
17888
|
+
async verifyWebhook(rawBody, headers) {
|
|
17889
|
+
return this.inner.verifyWebhook(rawBody, headers);
|
|
17890
|
+
}
|
|
17891
|
+
parseWebhookEvents(payload, headers) {
|
|
17892
|
+
return this.inner.parseWebhookEvents(payload, headers);
|
|
17893
|
+
}
|
|
17894
|
+
/** Last recorded send, or undefined. Sugar for the common single-send assertion. */
|
|
17895
|
+
get last() {
|
|
17896
|
+
return this.sent[this.sent.length - 1];
|
|
17897
|
+
}
|
|
17898
|
+
/** All sends addressed to `email` (case-insensitive). */
|
|
17899
|
+
toRecipient(email) {
|
|
17900
|
+
const lower = email.toLowerCase();
|
|
17901
|
+
return this.sent.filter((s) => s.to.toLowerCase() === lower);
|
|
17902
|
+
}
|
|
17903
|
+
reset() {
|
|
17904
|
+
this.sent.length = 0;
|
|
17905
|
+
this.records.length = 0;
|
|
17906
|
+
}
|
|
17907
|
+
};
|
|
17908
|
+
|
|
17909
|
+
// src/testing/builders.ts
|
|
17910
|
+
var BACKDATE_MS = 6e4;
|
|
17911
|
+
async function buildTemplate(spec) {
|
|
17912
|
+
const now = /* @__PURE__ */ new Date();
|
|
17913
|
+
const createdAt = spec.createdAt ?? new Date(now.getTime() - BACKDATE_MS);
|
|
17914
|
+
let mjml = "";
|
|
17915
|
+
let html;
|
|
17916
|
+
if (spec.html) {
|
|
17917
|
+
html = spec.html;
|
|
17918
|
+
} else {
|
|
17919
|
+
mjml = spec.mjml ?? wrapMjml(spec.text ?? "Hello {{contact.fields.firstName}}");
|
|
17920
|
+
const compiled = await compileTemplate(mjml);
|
|
17921
|
+
html = compiled.html;
|
|
17922
|
+
}
|
|
17923
|
+
const plainText = spec.plainText ?? derivePlaintext(html);
|
|
17924
|
+
return {
|
|
17925
|
+
slug: spec.slug,
|
|
17926
|
+
name: spec.name ?? spec.slug,
|
|
17927
|
+
description: spec.description ?? "",
|
|
17928
|
+
kind: spec.kind ?? "marketing",
|
|
17929
|
+
fromName: spec.fromName ?? "Test",
|
|
17930
|
+
fromEmail: spec.fromEmail ?? "hello@example.com",
|
|
17931
|
+
replyTo: spec.replyTo ?? null,
|
|
17932
|
+
providerOverride: spec.providerOverride ?? null,
|
|
17933
|
+
subject: spec.subject ?? "Test subject",
|
|
17934
|
+
preheader: spec.preheader ?? "",
|
|
17935
|
+
body: { mjml, editorJson: null, html, plainText, compiledAt: createdAt },
|
|
17936
|
+
variablesSchema: spec.variablesSchema ?? {},
|
|
17937
|
+
draft: null,
|
|
17938
|
+
tags: spec.tags ?? [],
|
|
17939
|
+
trackOpens: spec.trackOpens ?? false,
|
|
17940
|
+
trackClicks: spec.trackClicks ?? false,
|
|
17941
|
+
stats: {
|
|
17942
|
+
sent: 0,
|
|
17943
|
+
delivered: 0,
|
|
17944
|
+
opened: 0,
|
|
17945
|
+
clicked: 0,
|
|
17946
|
+
bounced: 0,
|
|
17947
|
+
complained: 0,
|
|
17948
|
+
unsubscribed: 0,
|
|
17949
|
+
lastSentAt: null
|
|
17950
|
+
},
|
|
17951
|
+
publishedAt: spec.published === false ? null : createdAt,
|
|
17952
|
+
publishedBy: spec.published === false ? null : "test",
|
|
17953
|
+
createdAt,
|
|
17954
|
+
updatedAt: createdAt
|
|
17955
|
+
};
|
|
17956
|
+
}
|
|
17957
|
+
function wrapMjml(body) {
|
|
17958
|
+
return `<mjml><mj-body><mj-section><mj-column><mj-text>${body}</mj-text></mj-column></mj-section></mj-body></mjml>`;
|
|
17959
|
+
}
|
|
17960
|
+
function buildFlow(spec) {
|
|
17961
|
+
const now = /* @__PURE__ */ new Date();
|
|
17962
|
+
const createdAt = spec.createdAt ?? new Date(now.getTime() - BACKDATE_MS);
|
|
17963
|
+
const trigger = spec.trigger ?? {
|
|
17964
|
+
type: "event",
|
|
17965
|
+
eventName: spec.eventName ?? `${spec.slug}:trigger`,
|
|
17966
|
+
once: spec.once ?? true
|
|
17967
|
+
};
|
|
17968
|
+
return {
|
|
17969
|
+
slug: spec.slug,
|
|
17970
|
+
name: spec.name ?? spec.slug,
|
|
17971
|
+
description: spec.description ?? "",
|
|
17972
|
+
trigger,
|
|
17973
|
+
enabled: spec.enabled ?? true,
|
|
17974
|
+
steps: spec.steps,
|
|
17975
|
+
version: spec.version ?? 1,
|
|
17976
|
+
draft: null,
|
|
17977
|
+
goal: spec.goal ?? "activation",
|
|
17978
|
+
audience: spec.audience ?? "test",
|
|
17979
|
+
expectedVolumePerWeek: null,
|
|
17980
|
+
stats: { activeRuns: 0, completedRuns: 0, sendsTotal: 0, sendsLast7Days: 0 },
|
|
17981
|
+
lastTriggerScanAt: null,
|
|
17982
|
+
publishedAt: createdAt,
|
|
17983
|
+
publishedBy: "test",
|
|
17984
|
+
createdAt,
|
|
17985
|
+
updatedAt: createdAt
|
|
17986
|
+
};
|
|
17987
|
+
}
|
|
17988
|
+
var step = {
|
|
17989
|
+
send(templateSlug, opts = {}) {
|
|
17990
|
+
return { type: "send", templateSlug, ...opts };
|
|
17991
|
+
},
|
|
17992
|
+
sendAt(templateSlug, delivery) {
|
|
17993
|
+
return { type: "send", templateSlug, delivery };
|
|
17994
|
+
},
|
|
17995
|
+
wait(value, unit = "minutes") {
|
|
17996
|
+
return { type: "wait", value, unit };
|
|
17997
|
+
},
|
|
17998
|
+
tag(addTags, removeTags) {
|
|
17999
|
+
return { type: "tag", addTags, removeTags };
|
|
18000
|
+
},
|
|
18001
|
+
exit(reason) {
|
|
18002
|
+
return { type: "exit", reason };
|
|
18003
|
+
},
|
|
18004
|
+
fireEvent(eventName, properties) {
|
|
18005
|
+
return { type: "fire_event", eventName, properties };
|
|
18006
|
+
}
|
|
18007
|
+
};
|
|
18008
|
+
|
|
18009
|
+
// src/testing/drive.ts
|
|
18010
|
+
async function drain(ctx, opts = {}) {
|
|
18011
|
+
const maxRounds = opts.maxRounds ?? 100;
|
|
18012
|
+
const shouldDispatch = opts.dispatch !== false;
|
|
18013
|
+
const attempted = /* @__PURE__ */ new Set();
|
|
18014
|
+
const errors = [];
|
|
18015
|
+
let dispatched = 0;
|
|
18016
|
+
let rounds = 0;
|
|
18017
|
+
while (rounds < maxRounds) {
|
|
18018
|
+
rounds++;
|
|
18019
|
+
await processNewlyFiredEventTriggers(ctx);
|
|
18020
|
+
await sweepStrandedFlowRuns(ctx);
|
|
18021
|
+
let didWork = false;
|
|
18022
|
+
if (shouldDispatch) {
|
|
18023
|
+
const queued = await ctx.collections.sends.find({ status: "queued" }, { projection: { _id: 1 } }).toArray();
|
|
18024
|
+
for (const row of queued) {
|
|
18025
|
+
const id = String(row._id);
|
|
18026
|
+
if (attempted.has(id)) continue;
|
|
18027
|
+
attempted.add(id);
|
|
18028
|
+
didWork = true;
|
|
18029
|
+
dispatched++;
|
|
18030
|
+
try {
|
|
18031
|
+
await dispatchSend(row._id, ctx);
|
|
18032
|
+
} catch (err) {
|
|
18033
|
+
errors.push(err instanceof Error ? err : new Error(String(err)));
|
|
18034
|
+
if (opts.throwOnSendError) throw err;
|
|
18035
|
+
}
|
|
18036
|
+
}
|
|
18037
|
+
}
|
|
18038
|
+
const due = await ctx.collections.flowRuns.countDocuments({
|
|
18039
|
+
status: "active",
|
|
18040
|
+
nextActionAt: { $lte: /* @__PURE__ */ new Date() }
|
|
18041
|
+
});
|
|
18042
|
+
if (!didWork && due === 0) {
|
|
18043
|
+
return { rounds, dispatched, errors, settled: true };
|
|
18044
|
+
}
|
|
18045
|
+
}
|
|
18046
|
+
return { rounds, dispatched, errors, settled: false };
|
|
18047
|
+
}
|
|
18048
|
+
async function dispatchQueued(ctx) {
|
|
18049
|
+
const queued = await ctx.collections.sends.find({ status: "queued" }, { projection: { _id: 1 } }).toArray();
|
|
18050
|
+
for (const row of queued) {
|
|
18051
|
+
await dispatchSend(row._id, ctx).catch(() => {
|
|
18052
|
+
});
|
|
18053
|
+
}
|
|
18054
|
+
return queued.length;
|
|
18055
|
+
}
|
|
18056
|
+
|
|
17855
18057
|
// src/testing/index.ts
|
|
17856
18058
|
async function createTestMailer(opts = {}) {
|
|
17857
18059
|
const { MongoMemoryServer } = await Promise.resolve().then(() => __toESM(require_mongodb_memory_server()));
|
|
@@ -17861,13 +18063,13 @@ async function createTestMailer(opts = {}) {
|
|
|
17861
18063
|
const db = client.db("mailery-test");
|
|
17862
18064
|
const memoryAdapter = opts.adapter ? null : new MemoryContactAdapter(opts.seedContacts ?? []);
|
|
17863
18065
|
const adapter = opts.adapter ?? memoryAdapter;
|
|
17864
|
-
const provider = opts.provider ??
|
|
18066
|
+
const provider = new RecordingProvider(await resolveProvider(opts.provider ?? "null"));
|
|
17865
18067
|
const mailer = await Mailer.init({
|
|
17866
18068
|
db,
|
|
17867
18069
|
adapter,
|
|
17868
|
-
queue: { driver: "noop" },
|
|
17869
|
-
providers: {
|
|
17870
|
-
defaultProvider:
|
|
18070
|
+
queue: opts.queue ?? { driver: "noop" },
|
|
18071
|
+
providers: { [provider.name]: provider },
|
|
18072
|
+
defaultProvider: provider.name,
|
|
17871
18073
|
publicUrl: "http://localhost:3000",
|
|
17872
18074
|
unsubscribeSecret: "test-unsub-secret-32-bytes-or-more-please",
|
|
17873
18075
|
senderAddress: "1 Test St, Brooklyn NY 11201",
|
|
@@ -17875,19 +18077,81 @@ async function createTestMailer(opts = {}) {
|
|
|
17875
18077
|
workerless: true,
|
|
17876
18078
|
...opts.config ?? {}
|
|
17877
18079
|
});
|
|
17878
|
-
|
|
17879
|
-
|
|
17880
|
-
|
|
17881
|
-
|
|
17882
|
-
|
|
17883
|
-
|
|
17884
|
-
|
|
18080
|
+
if (opts.startWorkers) await mailer.startWorkers();
|
|
18081
|
+
const ctx = mailer.getRunnerContext();
|
|
18082
|
+
const harness = {
|
|
18083
|
+
mailer,
|
|
18084
|
+
db,
|
|
18085
|
+
provider,
|
|
18086
|
+
adapter,
|
|
18087
|
+
memoryAdapter,
|
|
18088
|
+
ctx,
|
|
18089
|
+
async seedContact(contact, seedOpts = {}) {
|
|
18090
|
+
if (!memoryAdapter) {
|
|
18091
|
+
throw new Error("seedContact requires the built-in MemoryContactAdapter \u2014 you passed a custom `adapter`.");
|
|
18092
|
+
}
|
|
18093
|
+
memoryAdapter.upsert(contact);
|
|
18094
|
+
if (seedOpts.subscribe !== false) {
|
|
18095
|
+
await mailer.upsertSubscription({
|
|
18096
|
+
externalId: contact.externalId,
|
|
18097
|
+
source: seedOpts.source ?? "test"
|
|
18098
|
+
});
|
|
18099
|
+
}
|
|
18100
|
+
return contact;
|
|
18101
|
+
},
|
|
18102
|
+
async seedTemplate(spec) {
|
|
18103
|
+
const doc = await buildTemplate(spec);
|
|
18104
|
+
const res = await ctx.collections.templates.insertOne(doc);
|
|
18105
|
+
doc._id = res.insertedId;
|
|
18106
|
+
return doc;
|
|
18107
|
+
},
|
|
18108
|
+
async seedFlow(spec) {
|
|
18109
|
+
const doc = buildFlow(spec);
|
|
18110
|
+
const res = await ctx.collections.flows.insertOne(doc);
|
|
18111
|
+
doc._id = res.insertedId;
|
|
18112
|
+
return doc;
|
|
18113
|
+
},
|
|
18114
|
+
drain(drainOpts) {
|
|
18115
|
+
return drain(ctx, drainOpts);
|
|
18116
|
+
},
|
|
18117
|
+
stop: async () => {
|
|
18118
|
+
await mailer.stop().catch(() => {
|
|
18119
|
+
});
|
|
18120
|
+
await client.close().catch(() => {
|
|
18121
|
+
});
|
|
18122
|
+
await mongo.stop().catch(() => {
|
|
18123
|
+
});
|
|
18124
|
+
}
|
|
17885
18125
|
};
|
|
17886
|
-
return
|
|
18126
|
+
return harness;
|
|
18127
|
+
}
|
|
18128
|
+
async function resolveProvider(spec) {
|
|
18129
|
+
if (typeof spec !== "string") return spec;
|
|
18130
|
+
if (spec === "null") return new NullProvider();
|
|
18131
|
+
const apiKey = process.env.SENDGRID_API_KEY;
|
|
18132
|
+
if (!apiKey) {
|
|
18133
|
+
throw new Error(
|
|
18134
|
+
"createTestMailer({ provider: 'sendgrid' }) requires SENDGRID_API_KEY. Live tests should gate on it before constructing the harness."
|
|
18135
|
+
);
|
|
18136
|
+
}
|
|
18137
|
+
const { SendGridProvider: SendGridProvider2 } = await Promise.resolve().then(() => (init_sendgrid(), sendgrid_exports));
|
|
18138
|
+
return new SendGridProvider2({
|
|
18139
|
+
apiKey,
|
|
18140
|
+
// Sandbox unless explicitly told to deliver: SendGrid still authenticates
|
|
18141
|
+
// and validates the payload, it just never puts anything in an inbox.
|
|
18142
|
+
sandbox: process.env.MAILERY_LIVE_E2E !== "deliver"
|
|
18143
|
+
});
|
|
17887
18144
|
}
|
|
17888
18145
|
|
|
17889
18146
|
exports.MemoryContactAdapter = MemoryContactAdapter;
|
|
17890
18147
|
exports.NullProvider = NullProvider;
|
|
18148
|
+
exports.RecordingProvider = RecordingProvider;
|
|
18149
|
+
exports.buildFlow = buildFlow;
|
|
18150
|
+
exports.buildTemplate = buildTemplate;
|
|
17891
18151
|
exports.createTestMailer = createTestMailer;
|
|
18152
|
+
exports.dispatchQueued = dispatchQueued;
|
|
18153
|
+
exports.drain = drain;
|
|
18154
|
+
exports.step = step;
|
|
18155
|
+
exports.wrapMjml = wrapMjml;
|
|
17892
18156
|
//# sourceMappingURL=testing.cjs.map
|
|
17893
18157
|
//# sourceMappingURL=testing.cjs.map
|