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
package/dist/testing.js
CHANGED
|
@@ -491,22 +491,22 @@ function __awaiter(thisArg, _arguments, P, generator) {
|
|
|
491
491
|
return new (P || (P = Promise))(function(resolve, reject) {
|
|
492
492
|
function fulfilled(value) {
|
|
493
493
|
try {
|
|
494
|
-
|
|
494
|
+
step2(generator.next(value));
|
|
495
495
|
} catch (e) {
|
|
496
496
|
reject(e);
|
|
497
497
|
}
|
|
498
498
|
}
|
|
499
499
|
function rejected(value) {
|
|
500
500
|
try {
|
|
501
|
-
|
|
501
|
+
step2(generator["throw"](value));
|
|
502
502
|
} catch (e) {
|
|
503
503
|
reject(e);
|
|
504
504
|
}
|
|
505
505
|
}
|
|
506
|
-
function
|
|
506
|
+
function step2(result) {
|
|
507
507
|
result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
|
|
508
508
|
}
|
|
509
|
-
|
|
509
|
+
step2((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
510
510
|
});
|
|
511
511
|
}
|
|
512
512
|
function __generator(thisArg, body) {
|
|
@@ -519,10 +519,10 @@ function __generator(thisArg, body) {
|
|
|
519
519
|
}), g;
|
|
520
520
|
function verb(n) {
|
|
521
521
|
return function(v) {
|
|
522
|
-
return
|
|
522
|
+
return step2([n, v]);
|
|
523
523
|
};
|
|
524
524
|
}
|
|
525
|
-
function
|
|
525
|
+
function step2(op) {
|
|
526
526
|
if (f) throw new TypeError("Generator is already executing.");
|
|
527
527
|
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
528
528
|
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;
|
|
@@ -656,12 +656,12 @@ function __asyncGenerator(thisArg, _arguments, generator) {
|
|
|
656
656
|
}
|
|
657
657
|
function resume(n, v) {
|
|
658
658
|
try {
|
|
659
|
-
|
|
659
|
+
step2(g[n](v));
|
|
660
660
|
} catch (e) {
|
|
661
661
|
settle(q[0][3], e);
|
|
662
662
|
}
|
|
663
663
|
}
|
|
664
|
-
function
|
|
664
|
+
function step2(r) {
|
|
665
665
|
r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r);
|
|
666
666
|
}
|
|
667
667
|
function fulfill(value) {
|
|
@@ -7079,9 +7079,9 @@ var require_pack = __commonJS({
|
|
|
7079
7079
|
this.push(headers.encode(newHeader));
|
|
7080
7080
|
}
|
|
7081
7081
|
_doDrain() {
|
|
7082
|
-
const
|
|
7082
|
+
const drain2 = this._drain;
|
|
7083
7083
|
this._drain = noop;
|
|
7084
|
-
|
|
7084
|
+
drain2();
|
|
7085
7085
|
}
|
|
7086
7086
|
_predestroy() {
|
|
7087
7087
|
const err = getStreamError(this);
|
|
@@ -15472,10 +15472,10 @@ async function getBucketStatus(ctx, fromEmail, kind) {
|
|
|
15472
15472
|
}
|
|
15473
15473
|
|
|
15474
15474
|
// src/server/runner/send.ts
|
|
15475
|
-
async function handleSend(run,
|
|
15476
|
-
const template = await ctx.collections.templates.findOne({ slug:
|
|
15475
|
+
async function handleSend(run, step2, contact, flow, ctx) {
|
|
15476
|
+
const template = await ctx.collections.templates.findOne({ slug: step2.templateSlug });
|
|
15477
15477
|
if (!template) {
|
|
15478
|
-
await failFlowRun(run, `template not found: ${
|
|
15478
|
+
await failFlowRun(run, `template not found: ${step2.templateSlug}`, ctx);
|
|
15479
15479
|
return;
|
|
15480
15480
|
}
|
|
15481
15481
|
const dedupeKey = `flowrun:${run._id}:step${run.currentStepIndex}`;
|
|
@@ -15487,8 +15487,8 @@ async function handleSend(run, step, contact, flow, ctx) {
|
|
|
15487
15487
|
});
|
|
15488
15488
|
return;
|
|
15489
15489
|
}
|
|
15490
|
-
const providerName = pickProviderName(
|
|
15491
|
-
const renderCtx = buildRenderContext(contact, run,
|
|
15490
|
+
const providerName = pickProviderName(step2.providerOverride, template, ctx);
|
|
15491
|
+
const renderCtx = buildRenderContext(contact, run, step2.vars ?? {}, ctx);
|
|
15492
15492
|
let rendered;
|
|
15493
15493
|
try {
|
|
15494
15494
|
rendered = await renderTemplate(template, renderCtx, { helpers: ctx.handlebarsHelpers });
|
|
@@ -15525,7 +15525,7 @@ async function handleSend(run, step, contact, flow, ctx) {
|
|
|
15525
15525
|
bodyHash: sha256(rendered.html),
|
|
15526
15526
|
status: "queued",
|
|
15527
15527
|
renderedFrom: rendered.fromName ? { name: rendered.fromName, email: rendered.fromEmail } : void 0,
|
|
15528
|
-
vars:
|
|
15528
|
+
vars: step2.vars ?? {}
|
|
15529
15529
|
})
|
|
15530
15530
|
);
|
|
15531
15531
|
await ctx.queues.send.add(
|
|
@@ -15614,6 +15614,10 @@ async function dispatchSend(sendId, ctx) {
|
|
|
15614
15614
|
await markFailed(send._id, `provider_unknown: ${send.provider}`, ctx);
|
|
15615
15615
|
return;
|
|
15616
15616
|
}
|
|
15617
|
+
const headers = send.kind === "marketing" ? {
|
|
15618
|
+
"List-Unsubscribe": `<${renderCtx.unsubscribeUrl}>`,
|
|
15619
|
+
"List-Unsubscribe-Post": "List-Unsubscribe=One-Click"
|
|
15620
|
+
} : {};
|
|
15617
15621
|
try {
|
|
15618
15622
|
const result = await provider.send({
|
|
15619
15623
|
to: send.emailAtSend,
|
|
@@ -15623,10 +15627,7 @@ async function dispatchSend(sendId, ctx) {
|
|
|
15623
15627
|
subject: rendered.subject,
|
|
15624
15628
|
html: tracking.html,
|
|
15625
15629
|
text: rendered.plainText,
|
|
15626
|
-
headers
|
|
15627
|
-
"List-Unsubscribe": `<${renderCtx.unsubscribeUrl}>`,
|
|
15628
|
-
"List-Unsubscribe-Post": "List-Unsubscribe=One-Click"
|
|
15629
|
-
},
|
|
15630
|
+
headers,
|
|
15630
15631
|
messageMeta: { sendId: String(send._id) }
|
|
15631
15632
|
});
|
|
15632
15633
|
await ctx.collections.sends.updateOne(
|
|
@@ -15738,8 +15739,8 @@ async function processOneRunStep(runId, ctx) {
|
|
|
15738
15739
|
return;
|
|
15739
15740
|
}
|
|
15740
15741
|
const steps = await loadStepsForRun(run, flow, ctx);
|
|
15741
|
-
const
|
|
15742
|
-
if (!
|
|
15742
|
+
const step2 = locateStep(steps, run.currentStepIndex, run.currentBranchPath);
|
|
15743
|
+
if (!step2) {
|
|
15743
15744
|
await completeFlowRun(run, "completed", ctx);
|
|
15744
15745
|
return;
|
|
15745
15746
|
}
|
|
@@ -15753,34 +15754,34 @@ async function processOneRunStep(runId, ctx) {
|
|
|
15753
15754
|
await exitFlowRun(run, sub.status, ctx);
|
|
15754
15755
|
return;
|
|
15755
15756
|
}
|
|
15756
|
-
switch (
|
|
15757
|
+
switch (step2.type) {
|
|
15757
15758
|
case "wait":
|
|
15758
|
-
return handleWait(run,
|
|
15759
|
+
return handleWait(run, step2, ctx);
|
|
15759
15760
|
case "condition":
|
|
15760
|
-
return handleCondition(run,
|
|
15761
|
+
return handleCondition(run, step2, contact, ctx);
|
|
15761
15762
|
case "branch":
|
|
15762
|
-
return handleBranch(run,
|
|
15763
|
+
return handleBranch(run, step2, contact, ctx);
|
|
15763
15764
|
case "send": {
|
|
15764
|
-
if (
|
|
15765
|
-
const deliverAt = computeDeliveryTime(/* @__PURE__ */ new Date(),
|
|
15765
|
+
if (step2.delivery) {
|
|
15766
|
+
const deliverAt = computeDeliveryTime(/* @__PURE__ */ new Date(), step2.delivery, contact.timezone);
|
|
15766
15767
|
if (deliverAt.getTime() > Date.now() + 3e4) {
|
|
15767
15768
|
return deferSendForWindow(run, deliverAt, ctx);
|
|
15768
15769
|
}
|
|
15769
15770
|
}
|
|
15770
|
-
return handleSend(run,
|
|
15771
|
+
return handleSend(run, step2, contact, flow, ctx);
|
|
15771
15772
|
}
|
|
15772
15773
|
case "tag":
|
|
15773
|
-
return handleTag(run,
|
|
15774
|
+
return handleTag(run, step2, ctx);
|
|
15774
15775
|
case "fire_event":
|
|
15775
|
-
return handleFireEvent(run,
|
|
15776
|
+
return handleFireEvent(run, step2, ctx);
|
|
15776
15777
|
case "webhook":
|
|
15777
|
-
return handleWebhookStep(run,
|
|
15778
|
+
return handleWebhookStep(run, step2, ctx);
|
|
15778
15779
|
case "exit":
|
|
15779
|
-
return exitFlowRun(run,
|
|
15780
|
+
return exitFlowRun(run, step2.reason ?? "exit_step", ctx);
|
|
15780
15781
|
}
|
|
15781
15782
|
}
|
|
15782
|
-
async function handleWait(run,
|
|
15783
|
-
const ms = unitToMs(
|
|
15783
|
+
async function handleWait(run, step2, ctx) {
|
|
15784
|
+
const ms = unitToMs(step2.value, step2.unit);
|
|
15784
15785
|
const nextAt = new Date(Date.now() + ms);
|
|
15785
15786
|
const updated = await ctx.collections.flowRuns.findOneAndUpdate(
|
|
15786
15787
|
{ _id: run._id, currentStepIndex: run.currentStepIndex },
|
|
@@ -15805,22 +15806,22 @@ async function handleWait(run, step, ctx) {
|
|
|
15805
15806
|
{ delay: ms, jobId: `advance:${run._id}:${run.currentStepIndex + 1}` }
|
|
15806
15807
|
);
|
|
15807
15808
|
}
|
|
15808
|
-
async function handleCondition(run,
|
|
15809
|
+
async function handleCondition(run, step2, contact, ctx) {
|
|
15809
15810
|
if (!contact) return;
|
|
15810
|
-
const result = await evaluatePredicate(
|
|
15811
|
+
const result = await evaluatePredicate(step2.test, { contact, run, collections: ctx.collections });
|
|
15811
15812
|
if (result) {
|
|
15812
15813
|
await advanceStep(run, ctx, { action: "condition_evaluated", details: { result: true } });
|
|
15813
15814
|
return;
|
|
15814
15815
|
}
|
|
15815
|
-
if (
|
|
15816
|
+
if (step2.ifFalse === "continue") {
|
|
15816
15817
|
await advanceStep(run, ctx, { action: "condition_evaluated", details: { result: false, skipped: 1 } }, { stepInc: 2 });
|
|
15817
15818
|
return;
|
|
15818
15819
|
}
|
|
15819
15820
|
await exitFlowRun(run, "condition_false", ctx);
|
|
15820
15821
|
}
|
|
15821
|
-
async function handleBranch(run,
|
|
15822
|
+
async function handleBranch(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
|
const newPath = [...run.currentBranchPath, run.currentStepIndex, result ? "true" : "false", 0];
|
|
15825
15826
|
const updated = await ctx.collections.flowRuns.findOneAndUpdate(
|
|
15826
15827
|
{ _id: run._id, currentStepIndex: run.currentStepIndex },
|
|
@@ -15846,9 +15847,9 @@ async function handleBranch(run, step, contact, ctx) {
|
|
|
15846
15847
|
if (!updated) return;
|
|
15847
15848
|
await ctx.queues.advance.add("advance", { flowRunId: String(run._id) });
|
|
15848
15849
|
}
|
|
15849
|
-
async function handleTag(run,
|
|
15850
|
-
const adds =
|
|
15851
|
-
const removes =
|
|
15850
|
+
async function handleTag(run, step2, ctx) {
|
|
15851
|
+
const adds = step2.addTags ?? [];
|
|
15852
|
+
const removes = step2.removeTags ?? [];
|
|
15852
15853
|
if (ctx.adapter.addTags && adds.length > 0) {
|
|
15853
15854
|
await ctx.adapter.addTags(run.externalId, adds);
|
|
15854
15855
|
} else if (adds.length > 0) {
|
|
@@ -15869,13 +15870,13 @@ async function handleTag(run, step, ctx) {
|
|
|
15869
15870
|
}
|
|
15870
15871
|
await advanceStep(run, ctx, { action: "tagged", details: { addTags: adds, removeTags: removes } });
|
|
15871
15872
|
}
|
|
15872
|
-
async function handleFireEvent(run,
|
|
15873
|
-
const dedupeKey = `flowrun:${run._id}:step${run.currentStepIndex}:${
|
|
15873
|
+
async function handleFireEvent(run, step2, ctx) {
|
|
15874
|
+
const dedupeKey = `flowrun:${run._id}:step${run.currentStepIndex}:${step2.eventName}`;
|
|
15874
15875
|
try {
|
|
15875
15876
|
await ctx.collections.events.insertOne({
|
|
15876
15877
|
externalId: run.externalId,
|
|
15877
|
-
name:
|
|
15878
|
-
properties:
|
|
15878
|
+
name: step2.eventName,
|
|
15879
|
+
properties: step2.properties ?? {},
|
|
15879
15880
|
dedupeKey,
|
|
15880
15881
|
occurredAt: /* @__PURE__ */ new Date(),
|
|
15881
15882
|
createdAt: /* @__PURE__ */ new Date()
|
|
@@ -15883,27 +15884,27 @@ async function handleFireEvent(run, step, ctx) {
|
|
|
15883
15884
|
} catch (err) {
|
|
15884
15885
|
if (err?.code !== 11e3) throw err;
|
|
15885
15886
|
}
|
|
15886
|
-
await advanceStep(run, ctx, { action: "event_fired", details: { name:
|
|
15887
|
+
await advanceStep(run, ctx, { action: "event_fired", details: { name: step2.eventName } });
|
|
15887
15888
|
}
|
|
15888
|
-
async function handleWebhookStep(run,
|
|
15889
|
+
async function handleWebhookStep(run, step2, ctx) {
|
|
15889
15890
|
try {
|
|
15890
|
-
const res = await fetch(
|
|
15891
|
-
method:
|
|
15891
|
+
const res = await fetch(step2.url, {
|
|
15892
|
+
method: step2.method ?? "POST",
|
|
15892
15893
|
headers: { "Content-Type": "application/json" },
|
|
15893
|
-
body: JSON.stringify(
|
|
15894
|
+
body: JSON.stringify(step2.payload ?? { externalId: run.externalId, flowRunId: String(run._id) }),
|
|
15894
15895
|
signal: AbortSignal.timeout(1e4)
|
|
15895
15896
|
});
|
|
15896
15897
|
if (!res.ok) throw new Error(`webhook step returned ${res.status}`);
|
|
15897
|
-
await advanceStep(run, ctx, { action: "webhook_called", details: { url:
|
|
15898
|
+
await advanceStep(run, ctx, { action: "webhook_called", details: { url: step2.url, status: res.status } });
|
|
15898
15899
|
} catch (err) {
|
|
15899
15900
|
const attempts = (run.attemptsForCurrentStep ?? 0) + 1;
|
|
15900
15901
|
if (attempts >= ctx.config.webhookRetryAttempts) {
|
|
15901
|
-
if (
|
|
15902
|
+
if (step2.failureMode === "fail_run") {
|
|
15902
15903
|
await failFlowRun(run, `webhook failed: ${err?.message}`, ctx);
|
|
15903
15904
|
} else {
|
|
15904
15905
|
await advanceStep(run, ctx, {
|
|
15905
15906
|
action: "webhook_called",
|
|
15906
|
-
details: { url:
|
|
15907
|
+
details: { url: step2.url, error: err?.message, exhausted: true }
|
|
15907
15908
|
});
|
|
15908
15909
|
}
|
|
15909
15910
|
} else {
|
|
@@ -17839,6 +17840,207 @@ function matches(c, f) {
|
|
|
17839
17840
|
return true;
|
|
17840
17841
|
}
|
|
17841
17842
|
|
|
17843
|
+
// src/testing/recording-provider.ts
|
|
17844
|
+
var RecordingProvider = class {
|
|
17845
|
+
constructor(inner) {
|
|
17846
|
+
this.inner = inner;
|
|
17847
|
+
}
|
|
17848
|
+
inner;
|
|
17849
|
+
/** Every `SendArgs` handed to the provider, in call order. */
|
|
17850
|
+
sent = [];
|
|
17851
|
+
/** Same calls, with outcome attached. */
|
|
17852
|
+
records = [];
|
|
17853
|
+
get name() {
|
|
17854
|
+
return this.inner.name;
|
|
17855
|
+
}
|
|
17856
|
+
get sendRatePerSecond() {
|
|
17857
|
+
return this.inner.sendRatePerSecond;
|
|
17858
|
+
}
|
|
17859
|
+
async send(args) {
|
|
17860
|
+
this.sent.push(args);
|
|
17861
|
+
const record = { args, result: null, error: null, durationMs: 0, at: /* @__PURE__ */ new Date() };
|
|
17862
|
+
this.records.push(record);
|
|
17863
|
+
const startedAt = Date.now();
|
|
17864
|
+
try {
|
|
17865
|
+
const result = await this.inner.send(args);
|
|
17866
|
+
record.result = result;
|
|
17867
|
+
return result;
|
|
17868
|
+
} catch (err) {
|
|
17869
|
+
record.error = err instanceof Error ? err : new Error(String(err));
|
|
17870
|
+
throw err;
|
|
17871
|
+
} finally {
|
|
17872
|
+
record.durationMs = Date.now() - startedAt;
|
|
17873
|
+
}
|
|
17874
|
+
}
|
|
17875
|
+
async verifyWebhook(rawBody, headers) {
|
|
17876
|
+
return this.inner.verifyWebhook(rawBody, headers);
|
|
17877
|
+
}
|
|
17878
|
+
parseWebhookEvents(payload, headers) {
|
|
17879
|
+
return this.inner.parseWebhookEvents(payload, headers);
|
|
17880
|
+
}
|
|
17881
|
+
/** Last recorded send, or undefined. Sugar for the common single-send assertion. */
|
|
17882
|
+
get last() {
|
|
17883
|
+
return this.sent[this.sent.length - 1];
|
|
17884
|
+
}
|
|
17885
|
+
/** All sends addressed to `email` (case-insensitive). */
|
|
17886
|
+
toRecipient(email) {
|
|
17887
|
+
const lower = email.toLowerCase();
|
|
17888
|
+
return this.sent.filter((s) => s.to.toLowerCase() === lower);
|
|
17889
|
+
}
|
|
17890
|
+
reset() {
|
|
17891
|
+
this.sent.length = 0;
|
|
17892
|
+
this.records.length = 0;
|
|
17893
|
+
}
|
|
17894
|
+
};
|
|
17895
|
+
|
|
17896
|
+
// src/testing/builders.ts
|
|
17897
|
+
var BACKDATE_MS = 6e4;
|
|
17898
|
+
async function buildTemplate(spec) {
|
|
17899
|
+
const now = /* @__PURE__ */ new Date();
|
|
17900
|
+
const createdAt = spec.createdAt ?? new Date(now.getTime() - BACKDATE_MS);
|
|
17901
|
+
let mjml = "";
|
|
17902
|
+
let html;
|
|
17903
|
+
if (spec.html) {
|
|
17904
|
+
html = spec.html;
|
|
17905
|
+
} else {
|
|
17906
|
+
mjml = spec.mjml ?? wrapMjml(spec.text ?? "Hello {{contact.fields.firstName}}");
|
|
17907
|
+
const compiled = await compileTemplate(mjml);
|
|
17908
|
+
html = compiled.html;
|
|
17909
|
+
}
|
|
17910
|
+
const plainText = spec.plainText ?? derivePlaintext(html);
|
|
17911
|
+
return {
|
|
17912
|
+
slug: spec.slug,
|
|
17913
|
+
name: spec.name ?? spec.slug,
|
|
17914
|
+
description: spec.description ?? "",
|
|
17915
|
+
kind: spec.kind ?? "marketing",
|
|
17916
|
+
fromName: spec.fromName ?? "Test",
|
|
17917
|
+
fromEmail: spec.fromEmail ?? "hello@example.com",
|
|
17918
|
+
replyTo: spec.replyTo ?? null,
|
|
17919
|
+
providerOverride: spec.providerOverride ?? null,
|
|
17920
|
+
subject: spec.subject ?? "Test subject",
|
|
17921
|
+
preheader: spec.preheader ?? "",
|
|
17922
|
+
body: { mjml, editorJson: null, html, plainText, compiledAt: createdAt },
|
|
17923
|
+
variablesSchema: spec.variablesSchema ?? {},
|
|
17924
|
+
draft: null,
|
|
17925
|
+
tags: spec.tags ?? [],
|
|
17926
|
+
trackOpens: spec.trackOpens ?? false,
|
|
17927
|
+
trackClicks: spec.trackClicks ?? false,
|
|
17928
|
+
stats: {
|
|
17929
|
+
sent: 0,
|
|
17930
|
+
delivered: 0,
|
|
17931
|
+
opened: 0,
|
|
17932
|
+
clicked: 0,
|
|
17933
|
+
bounced: 0,
|
|
17934
|
+
complained: 0,
|
|
17935
|
+
unsubscribed: 0,
|
|
17936
|
+
lastSentAt: null
|
|
17937
|
+
},
|
|
17938
|
+
publishedAt: spec.published === false ? null : createdAt,
|
|
17939
|
+
publishedBy: spec.published === false ? null : "test",
|
|
17940
|
+
createdAt,
|
|
17941
|
+
updatedAt: createdAt
|
|
17942
|
+
};
|
|
17943
|
+
}
|
|
17944
|
+
function wrapMjml(body) {
|
|
17945
|
+
return `<mjml><mj-body><mj-section><mj-column><mj-text>${body}</mj-text></mj-column></mj-section></mj-body></mjml>`;
|
|
17946
|
+
}
|
|
17947
|
+
function buildFlow(spec) {
|
|
17948
|
+
const now = /* @__PURE__ */ new Date();
|
|
17949
|
+
const createdAt = spec.createdAt ?? new Date(now.getTime() - BACKDATE_MS);
|
|
17950
|
+
const trigger = spec.trigger ?? {
|
|
17951
|
+
type: "event",
|
|
17952
|
+
eventName: spec.eventName ?? `${spec.slug}:trigger`,
|
|
17953
|
+
once: spec.once ?? true
|
|
17954
|
+
};
|
|
17955
|
+
return {
|
|
17956
|
+
slug: spec.slug,
|
|
17957
|
+
name: spec.name ?? spec.slug,
|
|
17958
|
+
description: spec.description ?? "",
|
|
17959
|
+
trigger,
|
|
17960
|
+
enabled: spec.enabled ?? true,
|
|
17961
|
+
steps: spec.steps,
|
|
17962
|
+
version: spec.version ?? 1,
|
|
17963
|
+
draft: null,
|
|
17964
|
+
goal: spec.goal ?? "activation",
|
|
17965
|
+
audience: spec.audience ?? "test",
|
|
17966
|
+
expectedVolumePerWeek: null,
|
|
17967
|
+
stats: { activeRuns: 0, completedRuns: 0, sendsTotal: 0, sendsLast7Days: 0 },
|
|
17968
|
+
lastTriggerScanAt: null,
|
|
17969
|
+
publishedAt: createdAt,
|
|
17970
|
+
publishedBy: "test",
|
|
17971
|
+
createdAt,
|
|
17972
|
+
updatedAt: createdAt
|
|
17973
|
+
};
|
|
17974
|
+
}
|
|
17975
|
+
var step = {
|
|
17976
|
+
send(templateSlug, opts = {}) {
|
|
17977
|
+
return { type: "send", templateSlug, ...opts };
|
|
17978
|
+
},
|
|
17979
|
+
sendAt(templateSlug, delivery) {
|
|
17980
|
+
return { type: "send", templateSlug, delivery };
|
|
17981
|
+
},
|
|
17982
|
+
wait(value, unit = "minutes") {
|
|
17983
|
+
return { type: "wait", value, unit };
|
|
17984
|
+
},
|
|
17985
|
+
tag(addTags, removeTags) {
|
|
17986
|
+
return { type: "tag", addTags, removeTags };
|
|
17987
|
+
},
|
|
17988
|
+
exit(reason) {
|
|
17989
|
+
return { type: "exit", reason };
|
|
17990
|
+
},
|
|
17991
|
+
fireEvent(eventName, properties) {
|
|
17992
|
+
return { type: "fire_event", eventName, properties };
|
|
17993
|
+
}
|
|
17994
|
+
};
|
|
17995
|
+
|
|
17996
|
+
// src/testing/drive.ts
|
|
17997
|
+
async function drain(ctx, opts = {}) {
|
|
17998
|
+
const maxRounds = opts.maxRounds ?? 100;
|
|
17999
|
+
const shouldDispatch = opts.dispatch !== false;
|
|
18000
|
+
const attempted = /* @__PURE__ */ new Set();
|
|
18001
|
+
const errors = [];
|
|
18002
|
+
let dispatched = 0;
|
|
18003
|
+
let rounds = 0;
|
|
18004
|
+
while (rounds < maxRounds) {
|
|
18005
|
+
rounds++;
|
|
18006
|
+
await processNewlyFiredEventTriggers(ctx);
|
|
18007
|
+
await sweepStrandedFlowRuns(ctx);
|
|
18008
|
+
let didWork = false;
|
|
18009
|
+
if (shouldDispatch) {
|
|
18010
|
+
const queued = await ctx.collections.sends.find({ status: "queued" }, { projection: { _id: 1 } }).toArray();
|
|
18011
|
+
for (const row of queued) {
|
|
18012
|
+
const id = String(row._id);
|
|
18013
|
+
if (attempted.has(id)) continue;
|
|
18014
|
+
attempted.add(id);
|
|
18015
|
+
didWork = true;
|
|
18016
|
+
dispatched++;
|
|
18017
|
+
try {
|
|
18018
|
+
await dispatchSend(row._id, ctx);
|
|
18019
|
+
} catch (err) {
|
|
18020
|
+
errors.push(err instanceof Error ? err : new Error(String(err)));
|
|
18021
|
+
if (opts.throwOnSendError) throw err;
|
|
18022
|
+
}
|
|
18023
|
+
}
|
|
18024
|
+
}
|
|
18025
|
+
const due = await ctx.collections.flowRuns.countDocuments({
|
|
18026
|
+
status: "active",
|
|
18027
|
+
nextActionAt: { $lte: /* @__PURE__ */ new Date() }
|
|
18028
|
+
});
|
|
18029
|
+
if (!didWork && due === 0) {
|
|
18030
|
+
return { rounds, dispatched, errors, settled: true };
|
|
18031
|
+
}
|
|
18032
|
+
}
|
|
18033
|
+
return { rounds, dispatched, errors, settled: false };
|
|
18034
|
+
}
|
|
18035
|
+
async function dispatchQueued(ctx) {
|
|
18036
|
+
const queued = await ctx.collections.sends.find({ status: "queued" }, { projection: { _id: 1 } }).toArray();
|
|
18037
|
+
for (const row of queued) {
|
|
18038
|
+
await dispatchSend(row._id, ctx).catch(() => {
|
|
18039
|
+
});
|
|
18040
|
+
}
|
|
18041
|
+
return queued.length;
|
|
18042
|
+
}
|
|
18043
|
+
|
|
17842
18044
|
// src/testing/index.ts
|
|
17843
18045
|
async function createTestMailer(opts = {}) {
|
|
17844
18046
|
const { MongoMemoryServer } = await Promise.resolve().then(() => __toESM(require_mongodb_memory_server()));
|
|
@@ -17848,13 +18050,13 @@ async function createTestMailer(opts = {}) {
|
|
|
17848
18050
|
const db = client.db("mailery-test");
|
|
17849
18051
|
const memoryAdapter = opts.adapter ? null : new MemoryContactAdapter(opts.seedContacts ?? []);
|
|
17850
18052
|
const adapter = opts.adapter ?? memoryAdapter;
|
|
17851
|
-
const provider = opts.provider ??
|
|
18053
|
+
const provider = new RecordingProvider(await resolveProvider(opts.provider ?? "null"));
|
|
17852
18054
|
const mailer = await Mailer.init({
|
|
17853
18055
|
db,
|
|
17854
18056
|
adapter,
|
|
17855
|
-
queue: { driver: "noop" },
|
|
17856
|
-
providers: {
|
|
17857
|
-
defaultProvider:
|
|
18057
|
+
queue: opts.queue ?? { driver: "noop" },
|
|
18058
|
+
providers: { [provider.name]: provider },
|
|
18059
|
+
defaultProvider: provider.name,
|
|
17858
18060
|
publicUrl: "http://localhost:3000",
|
|
17859
18061
|
unsubscribeSecret: "test-unsub-secret-32-bytes-or-more-please",
|
|
17860
18062
|
senderAddress: "1 Test St, Brooklyn NY 11201",
|
|
@@ -17862,17 +18064,72 @@ async function createTestMailer(opts = {}) {
|
|
|
17862
18064
|
workerless: true,
|
|
17863
18065
|
...opts.config ?? {}
|
|
17864
18066
|
});
|
|
17865
|
-
|
|
17866
|
-
|
|
17867
|
-
|
|
17868
|
-
|
|
17869
|
-
|
|
17870
|
-
|
|
17871
|
-
|
|
18067
|
+
if (opts.startWorkers) await mailer.startWorkers();
|
|
18068
|
+
const ctx = mailer.getRunnerContext();
|
|
18069
|
+
const harness = {
|
|
18070
|
+
mailer,
|
|
18071
|
+
db,
|
|
18072
|
+
provider,
|
|
18073
|
+
adapter,
|
|
18074
|
+
memoryAdapter,
|
|
18075
|
+
ctx,
|
|
18076
|
+
async seedContact(contact, seedOpts = {}) {
|
|
18077
|
+
if (!memoryAdapter) {
|
|
18078
|
+
throw new Error("seedContact requires the built-in MemoryContactAdapter \u2014 you passed a custom `adapter`.");
|
|
18079
|
+
}
|
|
18080
|
+
memoryAdapter.upsert(contact);
|
|
18081
|
+
if (seedOpts.subscribe !== false) {
|
|
18082
|
+
await mailer.upsertSubscription({
|
|
18083
|
+
externalId: contact.externalId,
|
|
18084
|
+
source: seedOpts.source ?? "test"
|
|
18085
|
+
});
|
|
18086
|
+
}
|
|
18087
|
+
return contact;
|
|
18088
|
+
},
|
|
18089
|
+
async seedTemplate(spec) {
|
|
18090
|
+
const doc = await buildTemplate(spec);
|
|
18091
|
+
const res = await ctx.collections.templates.insertOne(doc);
|
|
18092
|
+
doc._id = res.insertedId;
|
|
18093
|
+
return doc;
|
|
18094
|
+
},
|
|
18095
|
+
async seedFlow(spec) {
|
|
18096
|
+
const doc = buildFlow(spec);
|
|
18097
|
+
const res = await ctx.collections.flows.insertOne(doc);
|
|
18098
|
+
doc._id = res.insertedId;
|
|
18099
|
+
return doc;
|
|
18100
|
+
},
|
|
18101
|
+
drain(drainOpts) {
|
|
18102
|
+
return drain(ctx, drainOpts);
|
|
18103
|
+
},
|
|
18104
|
+
stop: async () => {
|
|
18105
|
+
await mailer.stop().catch(() => {
|
|
18106
|
+
});
|
|
18107
|
+
await client.close().catch(() => {
|
|
18108
|
+
});
|
|
18109
|
+
await mongo.stop().catch(() => {
|
|
18110
|
+
});
|
|
18111
|
+
}
|
|
17872
18112
|
};
|
|
17873
|
-
return
|
|
18113
|
+
return harness;
|
|
18114
|
+
}
|
|
18115
|
+
async function resolveProvider(spec) {
|
|
18116
|
+
if (typeof spec !== "string") return spec;
|
|
18117
|
+
if (spec === "null") return new NullProvider();
|
|
18118
|
+
const apiKey = process.env.SENDGRID_API_KEY;
|
|
18119
|
+
if (!apiKey) {
|
|
18120
|
+
throw new Error(
|
|
18121
|
+
"createTestMailer({ provider: 'sendgrid' }) requires SENDGRID_API_KEY. Live tests should gate on it before constructing the harness."
|
|
18122
|
+
);
|
|
18123
|
+
}
|
|
18124
|
+
const { SendGridProvider: SendGridProvider2 } = await Promise.resolve().then(() => (init_sendgrid(), sendgrid_exports));
|
|
18125
|
+
return new SendGridProvider2({
|
|
18126
|
+
apiKey,
|
|
18127
|
+
// Sandbox unless explicitly told to deliver: SendGrid still authenticates
|
|
18128
|
+
// and validates the payload, it just never puts anything in an inbox.
|
|
18129
|
+
sandbox: process.env.MAILERY_LIVE_E2E !== "deliver"
|
|
18130
|
+
});
|
|
17874
18131
|
}
|
|
17875
18132
|
|
|
17876
|
-
export { MemoryContactAdapter, NullProvider, createTestMailer };
|
|
18133
|
+
export { MemoryContactAdapter, NullProvider, RecordingProvider, buildFlow, buildTemplate, createTestMailer, dispatchQueued, drain, step, wrapMjml };
|
|
17877
18134
|
//# sourceMappingURL=testing.js.map
|
|
17878
18135
|
//# sourceMappingURL=testing.js.map
|