@christian-ek/sweego 0.1.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/README.md +357 -0
- package/dist/client/_generated/_ignore.d.ts +1 -0
- package/dist/client/_generated/_ignore.d.ts.map +1 -0
- package/dist/client/_generated/_ignore.js +3 -0
- package/dist/client/_generated/_ignore.js.map +1 -0
- package/dist/client/index.d.ts +282 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +265 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/webhook.d.ts +42 -0
- package/dist/client/webhook.d.ts.map +1 -0
- package/dist/client/webhook.js +89 -0
- package/dist/client/webhook.js.map +1 -0
- package/dist/component/_generated/api.d.ts +43 -0
- package/dist/component/_generated/api.d.ts.map +1 -0
- package/dist/component/_generated/api.js +31 -0
- package/dist/component/_generated/api.js.map +1 -0
- package/dist/component/_generated/component.d.ts +226 -0
- package/dist/component/_generated/component.d.ts.map +1 -0
- package/dist/component/_generated/component.js +11 -0
- package/dist/component/_generated/component.js.map +1 -0
- package/dist/component/_generated/dataModel.d.ts +46 -0
- package/dist/component/_generated/dataModel.d.ts.map +1 -0
- package/dist/component/_generated/dataModel.js +11 -0
- package/dist/component/_generated/dataModel.js.map +1 -0
- package/dist/component/_generated/server.d.ts +121 -0
- package/dist/component/_generated/server.d.ts.map +1 -0
- package/dist/component/_generated/server.js +78 -0
- package/dist/component/_generated/server.js.map +1 -0
- package/dist/component/convex.config.d.ts +3 -0
- package/dist/component/convex.config.d.ts.map +1 -0
- package/dist/component/convex.config.js +10 -0
- package/dist/component/convex.config.js.map +1 -0
- package/dist/component/lib.d.ts +319 -0
- package/dist/component/lib.d.ts.map +1 -0
- package/dist/component/lib.js +725 -0
- package/dist/component/lib.js.map +1 -0
- package/dist/component/schema.d.ts +259 -0
- package/dist/component/schema.d.ts.map +1 -0
- package/dist/component/schema.js +99 -0
- package/dist/component/schema.js.map +1 -0
- package/dist/component/shared.d.ts +280 -0
- package/dist/component/shared.d.ts.map +1 -0
- package/dist/component/shared.js +213 -0
- package/dist/component/shared.js.map +1 -0
- package/dist/component/sweego.d.ts +95 -0
- package/dist/component/sweego.d.ts.map +1 -0
- package/dist/component/sweego.js +210 -0
- package/dist/component/sweego.js.map +1 -0
- package/dist/component/utils.d.ts +16 -0
- package/dist/component/utils.d.ts.map +1 -0
- package/dist/component/utils.js +29 -0
- package/dist/component/utils.js.map +1 -0
- package/package.json +100 -0
- package/src/client/_generated/_ignore.ts +1 -0
- package/src/client/index.ts +490 -0
- package/src/client/webhook.test.ts +146 -0
- package/src/client/webhook.ts +130 -0
- package/src/component/_generated/api.ts +59 -0
- package/src/component/_generated/component.ts +244 -0
- package/src/component/_generated/dataModel.ts +60 -0
- package/src/component/_generated/server.ts +156 -0
- package/src/component/convex.config.ts +12 -0
- package/src/component/lib.test.ts +189 -0
- package/src/component/lib.ts +835 -0
- package/src/component/schema.ts +117 -0
- package/src/component/shared.test.ts +64 -0
- package/src/component/shared.ts +315 -0
- package/src/component/sweego.test.ts +141 -0
- package/src/component/sweego.ts +310 -0
- package/src/component/utils.ts +35 -0
- package/src/test.ts +20 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
import { convexTest } from "convex-test";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { api } from "./_generated/api.js";
|
|
5
|
+
import schema from "./schema.js";
|
|
6
|
+
|
|
7
|
+
const modules = import.meta.glob("./**/*.ts");
|
|
8
|
+
|
|
9
|
+
const setup = () => convexTest(schema, modules);
|
|
10
|
+
|
|
11
|
+
const options = {
|
|
12
|
+
apiKey: "test-key",
|
|
13
|
+
provider: "sweego",
|
|
14
|
+
initialBackoffMs: 1,
|
|
15
|
+
retryAttempts: 1,
|
|
16
|
+
testMode: false,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
describe("enqueueMessage validation", () => {
|
|
20
|
+
it("rejects a message with no body or template", async () => {
|
|
21
|
+
const t = setup();
|
|
22
|
+
await expect(
|
|
23
|
+
t.mutation(api.lib.enqueueMessage, {
|
|
24
|
+
options,
|
|
25
|
+
message: {
|
|
26
|
+
channel: "email",
|
|
27
|
+
bulk: false,
|
|
28
|
+
from: { email: "a@b.com" },
|
|
29
|
+
subject: "s",
|
|
30
|
+
emailRecipients: [{ email: "c@d.com" }],
|
|
31
|
+
},
|
|
32
|
+
}),
|
|
33
|
+
).rejects.toThrow(/alone is rejected by Sweego/);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("rejects an html-only email (Sweego needs text or templateId)", async () => {
|
|
37
|
+
const t = setup();
|
|
38
|
+
await expect(
|
|
39
|
+
t.mutation(api.lib.enqueueMessage, {
|
|
40
|
+
options,
|
|
41
|
+
message: {
|
|
42
|
+
channel: "email",
|
|
43
|
+
bulk: false,
|
|
44
|
+
from: { email: "a@b.com" },
|
|
45
|
+
subject: "s",
|
|
46
|
+
emailRecipients: [{ email: "c@d.com" }],
|
|
47
|
+
html: "<p>hi</p>",
|
|
48
|
+
},
|
|
49
|
+
}),
|
|
50
|
+
).rejects.toThrow(/alone is rejected by Sweego/);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("rejects html + templateId together", async () => {
|
|
54
|
+
const t = setup();
|
|
55
|
+
await expect(
|
|
56
|
+
t.mutation(api.lib.enqueueMessage, {
|
|
57
|
+
options,
|
|
58
|
+
message: {
|
|
59
|
+
channel: "email",
|
|
60
|
+
bulk: false,
|
|
61
|
+
from: { email: "a@b.com" },
|
|
62
|
+
subject: "s",
|
|
63
|
+
emailRecipients: [{ email: "c@d.com" }],
|
|
64
|
+
html: "<p>x</p>",
|
|
65
|
+
templateId: "t1",
|
|
66
|
+
},
|
|
67
|
+
}),
|
|
68
|
+
).rejects.toThrow(/either html or templateId/);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("rejects SMS without a campaignType", async () => {
|
|
72
|
+
const t = setup();
|
|
73
|
+
await expect(
|
|
74
|
+
t.mutation(api.lib.enqueueMessage, {
|
|
75
|
+
options,
|
|
76
|
+
message: {
|
|
77
|
+
channel: "sms",
|
|
78
|
+
bulk: false,
|
|
79
|
+
smsRecipients: [{ num: "+1", region: "US" }],
|
|
80
|
+
text: "hi",
|
|
81
|
+
},
|
|
82
|
+
}),
|
|
83
|
+
).rejects.toThrow(/campaignType/);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("handleEvent state machine", () => {
|
|
88
|
+
async function seedDelivery(t: ReturnType<typeof setup>, swgUid: string) {
|
|
89
|
+
return t.run(async (ctx) => {
|
|
90
|
+
const messageId = await ctx.db.insert("messages", {
|
|
91
|
+
channel: "email",
|
|
92
|
+
provider: "sweego",
|
|
93
|
+
status: "sent",
|
|
94
|
+
bulk: false,
|
|
95
|
+
finalizedAt: Date.now(),
|
|
96
|
+
});
|
|
97
|
+
await ctx.db.insert("deliveries", {
|
|
98
|
+
messageId,
|
|
99
|
+
swgUid,
|
|
100
|
+
recipientKey: "c@d.com",
|
|
101
|
+
channel: "email",
|
|
102
|
+
status: "sent",
|
|
103
|
+
delivered: false,
|
|
104
|
+
bounced: false,
|
|
105
|
+
softBounced: false,
|
|
106
|
+
complained: false,
|
|
107
|
+
unsubscribed: false,
|
|
108
|
+
opened: false,
|
|
109
|
+
clicked: false,
|
|
110
|
+
stopped: false,
|
|
111
|
+
finalizedAt: Number.MAX_SAFE_INTEGER,
|
|
112
|
+
});
|
|
113
|
+
return messageId;
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const readDelivery = (t: ReturnType<typeof setup>, swgUid: string) =>
|
|
118
|
+
t.run(async (ctx) =>
|
|
119
|
+
ctx.db
|
|
120
|
+
.query("deliveries")
|
|
121
|
+
.withIndex("by_swgUid", (q) => q.eq("swgUid", swgUid))
|
|
122
|
+
.unique(),
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
it("marks a delivery delivered and records the raw event", async () => {
|
|
126
|
+
const t = setup();
|
|
127
|
+
await seedDelivery(t, "uid1");
|
|
128
|
+
await t.mutation(api.lib.handleEvent, {
|
|
129
|
+
event: { event_type: "delivered", swg_uid: "uid1", channel: "email" },
|
|
130
|
+
});
|
|
131
|
+
const d = await readDelivery(t, "uid1");
|
|
132
|
+
expect(d?.status).toBe("delivered");
|
|
133
|
+
expect(d?.delivered).toBe(true);
|
|
134
|
+
expect(d?.lastEventType).toBe("delivered");
|
|
135
|
+
|
|
136
|
+
const events = await t.run(async (ctx) =>
|
|
137
|
+
ctx.db
|
|
138
|
+
.query("events")
|
|
139
|
+
.withIndex("by_swgUid", (q) => q.eq("swgUid", "uid1"))
|
|
140
|
+
.collect(),
|
|
141
|
+
);
|
|
142
|
+
expect(events.length).toBe(1);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("sets the opened flag and upgrades to terminal bounced", async () => {
|
|
146
|
+
const t = setup();
|
|
147
|
+
await seedDelivery(t, "uid2");
|
|
148
|
+
await t.mutation(api.lib.handleEvent, {
|
|
149
|
+
event: { event_type: "email_opened", swg_uid: "uid2" },
|
|
150
|
+
});
|
|
151
|
+
await t.mutation(api.lib.handleEvent, {
|
|
152
|
+
event: { event_type: "hard_bounce", swg_uid: "uid2" },
|
|
153
|
+
});
|
|
154
|
+
const d = await readDelivery(t, "uid2");
|
|
155
|
+
expect(d?.opened).toBe(true);
|
|
156
|
+
expect(d?.status).toBe("bounced");
|
|
157
|
+
expect(d?.bounced).toBe(true);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("ignores events for unknown swg_uids but still records them", async () => {
|
|
161
|
+
const t = setup();
|
|
162
|
+
await t.mutation(api.lib.handleEvent, {
|
|
163
|
+
event: { event_type: "delivered", swg_uid: "nope" },
|
|
164
|
+
});
|
|
165
|
+
const events = await t.run(async (ctx) =>
|
|
166
|
+
ctx.db
|
|
167
|
+
.query("events")
|
|
168
|
+
.withIndex("by_swgUid", (q) => q.eq("swgUid", "nope"))
|
|
169
|
+
.collect(),
|
|
170
|
+
);
|
|
171
|
+
expect(events.length).toBe(1);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("deduplicates redelivered webhooks by webhook-id", async () => {
|
|
175
|
+
const t = setup();
|
|
176
|
+
await seedDelivery(t, "uid3");
|
|
177
|
+
const event = { event_type: "email_opened", swg_uid: "uid3" };
|
|
178
|
+
await t.mutation(api.lib.handleEvent, { event, webhookId: "wh_1" });
|
|
179
|
+
// Same webhook-id again (a replay/redelivery) must be a no-op.
|
|
180
|
+
await t.mutation(api.lib.handleEvent, { event, webhookId: "wh_1" });
|
|
181
|
+
const events = await t.run(async (ctx) =>
|
|
182
|
+
ctx.db
|
|
183
|
+
.query("events")
|
|
184
|
+
.withIndex("by_swgUid", (q) => q.eq("swgUid", "uid3"))
|
|
185
|
+
.collect(),
|
|
186
|
+
);
|
|
187
|
+
expect(events.length).toBe(1);
|
|
188
|
+
});
|
|
189
|
+
});
|