@growth-labs/mailer 0.2.2 → 0.4.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/_internal/schema-probe.d.ts +30 -0
- package/dist/_internal/schema-probe.d.ts.map +1 -0
- package/dist/_internal/schema-probe.js +68 -0
- package/dist/_internal/schema-probe.js.map +1 -0
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/options.d.ts +24 -14
- package/dist/options.d.ts.map +1 -1
- package/dist/options.js +4 -0
- package/dist/options.js.map +1 -1
- package/dist/queue/consumer.d.ts +1 -6
- package/dist/queue/consumer.d.ts.map +1 -1
- package/dist/queue/consumer.js +57 -13
- package/dist/queue/consumer.js.map +1 -1
- package/dist/routes/confirm.d.ts.map +1 -1
- package/dist/routes/confirm.js +5 -0
- package/dist/routes/confirm.js.map +1 -1
- package/dist/routes/subscribe.d.ts.map +1 -1
- package/dist/routes/subscribe.js +6 -0
- package/dist/routes/subscribe.js.map +1 -1
- package/dist/routes/track-click.d.ts.map +1 -1
- package/dist/routes/track-click.js +17 -10
- package/dist/routes/track-click.js.map +1 -1
- package/dist/routes/track-open.d.ts.map +1 -1
- package/dist/routes/track-open.js +16 -10
- package/dist/routes/track-open.js.map +1 -1
- package/dist/routes/unsubscribe.d.ts.map +1 -1
- package/dist/routes/unsubscribe.js +10 -0
- package/dist/routes/unsubscribe.js.map +1 -1
- package/dist/schema/index.d.ts +3 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +3 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/sends.d.ts +312 -0
- package/dist/schema/sends.d.ts.map +1 -0
- package/dist/schema/sends.js +26 -0
- package/dist/schema/sends.js.map +1 -0
- package/dist/schema/subscribers.d.ts +253 -0
- package/dist/schema/subscribers.d.ts.map +1 -0
- package/dist/schema/subscribers.js +21 -0
- package/dist/schema/subscribers.js.map +1 -0
- package/dist/types.d.ts +30 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/bounce.d.ts.map +1 -1
- package/dist/utils/bounce.js +2 -1
- package/dist/utils/bounce.js.map +1 -1
- package/dist/utils/send.d.ts +7 -4
- package/dist/utils/send.d.ts.map +1 -1
- package/dist/utils/send.js +24 -9
- package/dist/utils/send.js.map +1 -1
- package/dist/utils/subscribers.js +1 -1
- package/dist/utils/subscribers.js.map +1 -1
- package/migrations/0001_create_gl_mailer_tables.sql +48 -0
- package/package.json +13 -4
- package/src/_internal/schema-probe.ts +89 -0
- package/src/index.ts +2 -1
- package/src/options.ts +6 -0
- package/src/queue/consumer.ts +85 -19
- package/src/routes/confirm.ts +6 -0
- package/src/routes/subscribe.ts +7 -0
- package/src/routes/track-click.ts +22 -15
- package/src/routes/track-open.ts +21 -15
- package/src/routes/unsubscribe.ts +9 -0
- package/src/schema/index.ts +2 -0
- package/src/schema/sends.ts +30 -0
- package/src/schema/subscribers.ts +25 -0
- package/src/types.ts +37 -0
- package/src/utils/bounce.ts +2 -1
- package/src/utils/send.ts +43 -12
- package/src/utils/subscribers.ts +1 -1
- package/src/virtual.d.ts +4 -0
- package/src/schema.ts +0 -56
package/src/utils/send.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { eq } from 'drizzle-orm'
|
|
|
2
2
|
import { drizzle } from 'drizzle-orm/d1'
|
|
3
3
|
import { ulid } from 'ulidx'
|
|
4
4
|
import type { ResolvedMailerOptions } from '../options.js'
|
|
5
|
-
import { emailSends } from '../schema.js'
|
|
5
|
+
import { emailSends } from '../schema/sends.js'
|
|
6
6
|
import type {
|
|
7
7
|
DigestItem,
|
|
8
8
|
EmailQueueMessage,
|
|
@@ -21,9 +21,38 @@ type DrizzleDB = ReturnType<typeof drizzle>
|
|
|
21
21
|
|
|
22
22
|
// ─── MailerEnv ───
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Producer-side env. The actual binding *names* come from
|
|
26
|
+
* `options.d1Binding` / `options.queueBinding` (default `SITE_DB` /
|
|
27
|
+
* `EMAIL_QUEUE`); MailerEnv is a generic record so consumers can pass
|
|
28
|
+
* their full Worker env without renaming bindings.
|
|
29
|
+
*/
|
|
30
|
+
export type MailerEnv = Record<string, unknown>
|
|
31
|
+
|
|
32
|
+
interface ResolvedProducerBindings {
|
|
33
|
+
db: DrizzleDB
|
|
34
|
+
queue: Queue
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function resolveProducerBindings(
|
|
38
|
+
env: MailerEnv,
|
|
39
|
+
options: ResolvedMailerOptions,
|
|
40
|
+
): ResolvedProducerBindings {
|
|
41
|
+
const d1 = env[options.d1Binding] as D1Database | undefined
|
|
42
|
+
if (!d1) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`[mailer] env.${options.d1Binding} is undefined; ` +
|
|
45
|
+
'pass d1Binding option to the mailer integration or bind that name in wrangler.toml.',
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
const queue = env[options.queueBinding] as Queue | undefined
|
|
49
|
+
if (!queue) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`[mailer] env.${options.queueBinding} is undefined; ` +
|
|
52
|
+
'pass queueBinding option to the mailer integration or bind that name in wrangler.toml.',
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
return { db: drizzle(d1), queue }
|
|
27
56
|
}
|
|
28
57
|
|
|
29
58
|
// ─── sendTransactional ───
|
|
@@ -40,7 +69,7 @@ export async function sendTransactional(
|
|
|
40
69
|
data?: Record<string, unknown>
|
|
41
70
|
},
|
|
42
71
|
): Promise<{ trackingId: string }> {
|
|
43
|
-
const db =
|
|
72
|
+
const { db, queue } = resolveProducerBindings(env, options)
|
|
44
73
|
const trackingId = ulid()
|
|
45
74
|
const subscriberId = params.subscriberId ?? ulid()
|
|
46
75
|
|
|
@@ -76,6 +105,7 @@ export async function sendTransactional(
|
|
|
76
105
|
|
|
77
106
|
// Build and enqueue the message
|
|
78
107
|
const message: EmailQueueMessage = {
|
|
108
|
+
siteId: options.siteId,
|
|
79
109
|
type: 'transactional',
|
|
80
110
|
recipients: [
|
|
81
111
|
{
|
|
@@ -92,7 +122,7 @@ export async function sendTransactional(
|
|
|
92
122
|
replyTo: options.replyTo,
|
|
93
123
|
}
|
|
94
124
|
|
|
95
|
-
await
|
|
125
|
+
await queue.send(message)
|
|
96
126
|
|
|
97
127
|
return { trackingId }
|
|
98
128
|
}
|
|
@@ -100,7 +130,7 @@ export async function sendTransactional(
|
|
|
100
130
|
// ─── Fan-out helper (shared by campaign + digest) ───
|
|
101
131
|
|
|
102
132
|
async function fanOutToSubscribers(
|
|
103
|
-
|
|
133
|
+
queue: Queue,
|
|
104
134
|
db: DrizzleDB,
|
|
105
135
|
options: ResolvedMailerOptions,
|
|
106
136
|
params: {
|
|
@@ -154,6 +184,7 @@ async function fanOutToSubscribers(
|
|
|
154
184
|
}
|
|
155
185
|
|
|
156
186
|
const message: EmailQueueMessage = {
|
|
187
|
+
siteId: options.siteId,
|
|
157
188
|
type: params.type,
|
|
158
189
|
recipients,
|
|
159
190
|
subject: params.subject,
|
|
@@ -162,7 +193,7 @@ async function fanOutToSubscribers(
|
|
|
162
193
|
replyTo: options.replyTo,
|
|
163
194
|
campaignId: params.campaignId,
|
|
164
195
|
}
|
|
165
|
-
await
|
|
196
|
+
await queue.send(message)
|
|
166
197
|
|
|
167
198
|
recipientCount += batch.length
|
|
168
199
|
cursor = batch[batch.length - 1].id
|
|
@@ -186,7 +217,7 @@ export async function sendCampaign(
|
|
|
186
217
|
},
|
|
187
218
|
): Promise<{ campaignId: string; recipientCount: number }> {
|
|
188
219
|
const campaignId = params.campaignId ?? ulid()
|
|
189
|
-
const db =
|
|
220
|
+
const { db, queue } = resolveProducerBindings(env, options)
|
|
190
221
|
|
|
191
222
|
// Render HTML
|
|
192
223
|
let html: string
|
|
@@ -211,7 +242,7 @@ export async function sendCampaign(
|
|
|
211
242
|
html = injectTrackingPixel(html, trackOpenUrl)
|
|
212
243
|
html = rewriteLinksForTracking(html, trackClickUrl)
|
|
213
244
|
|
|
214
|
-
const recipientCount = await fanOutToSubscribers(
|
|
245
|
+
const recipientCount = await fanOutToSubscribers(queue, db, options, {
|
|
215
246
|
subject: params.subject,
|
|
216
247
|
html,
|
|
217
248
|
campaignId,
|
|
@@ -236,7 +267,7 @@ export async function sendDigest(
|
|
|
236
267
|
},
|
|
237
268
|
): Promise<{ campaignId: string; recipientCount: number }> {
|
|
238
269
|
const campaignId = params.campaignId ?? ulid()
|
|
239
|
-
const db =
|
|
270
|
+
const { db, queue } = resolveProducerBindings(env, options)
|
|
240
271
|
|
|
241
272
|
// Render digest items then full template
|
|
242
273
|
const itemsHtml = renderDigestItems(params.items)
|
|
@@ -256,7 +287,7 @@ export async function sendDigest(
|
|
|
256
287
|
html = injectTrackingPixel(html, trackOpenUrl)
|
|
257
288
|
html = rewriteLinksForTracking(html, trackClickUrl)
|
|
258
289
|
|
|
259
|
-
const recipientCount = await fanOutToSubscribers(
|
|
290
|
+
const recipientCount = await fanOutToSubscribers(queue, db, options, {
|
|
260
291
|
subject: params.subject,
|
|
261
292
|
html,
|
|
262
293
|
campaignId,
|
package/src/utils/subscribers.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { and, eq, gt, inArray, like, lt, or, sql } from 'drizzle-orm'
|
|
2
2
|
import type { drizzle } from 'drizzle-orm/d1'
|
|
3
3
|
import { ulid } from 'ulidx'
|
|
4
|
-
import { subscribers } from '../schema.js'
|
|
4
|
+
import { subscribers } from '../schema/subscribers.js'
|
|
5
5
|
import type { Subscriber, SubscriberAttribution, SubscriberFilter } from '../types.js'
|
|
6
6
|
|
|
7
7
|
type DrizzleDB = ReturnType<typeof drizzle>
|
package/src/virtual.d.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
declare module 'virtual:growth-labs/mailer/config' {
|
|
2
|
+
import type { SiteConfigLookup } from './types.js'
|
|
2
3
|
export const config: {
|
|
4
|
+
siteId: string
|
|
3
5
|
senderName: string
|
|
4
6
|
fromAddress: string
|
|
5
7
|
replyTo?: string
|
|
6
8
|
d1Binding: string
|
|
7
9
|
queueBinding: string
|
|
10
|
+
senderBinding: string
|
|
11
|
+
siteConfigLookup?: SiteConfigLookup
|
|
8
12
|
turnstileSiteKey: string
|
|
9
13
|
turnstileSecretKey: string
|
|
10
14
|
doubleOptIn: boolean
|
package/src/schema.ts
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
|
|
2
|
-
|
|
3
|
-
export const subscribers = sqliteTable(
|
|
4
|
-
'gl_subscribers',
|
|
5
|
-
{
|
|
6
|
-
id: text('id').primaryKey(),
|
|
7
|
-
email: text('email').notNull().unique(),
|
|
8
|
-
name: text('name'),
|
|
9
|
-
status: text('status').notNull().default('pending'),
|
|
10
|
-
preferences: text('preferences').notNull().default('[]'),
|
|
11
|
-
source: text('source').notNull(),
|
|
12
|
-
attribution: text('attribution'),
|
|
13
|
-
softBounceCount: integer('soft_bounce_count').notNull().default(0),
|
|
14
|
-
subscribedAt: text('subscribed_at').notNull(),
|
|
15
|
-
confirmedAt: text('confirmed_at'),
|
|
16
|
-
unsubscribedAt: text('unsubscribed_at'),
|
|
17
|
-
createdAt: text('created_at').notNull(),
|
|
18
|
-
updatedAt: text('updated_at').notNull(),
|
|
19
|
-
},
|
|
20
|
-
(table) => [
|
|
21
|
-
index('idx_subscribers_status').on(table.status),
|
|
22
|
-
index('idx_subscribers_email').on(table.email),
|
|
23
|
-
index('idx_subscribers_subscribed_at').on(table.subscribedAt),
|
|
24
|
-
],
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
export const emailSends = sqliteTable(
|
|
28
|
-
'gl_email_sends',
|
|
29
|
-
{
|
|
30
|
-
id: text('id').primaryKey(),
|
|
31
|
-
subscriberId: text('subscriber_id')
|
|
32
|
-
.notNull()
|
|
33
|
-
.references(() => subscribers.id),
|
|
34
|
-
campaignId: text('campaign_id'),
|
|
35
|
-
email: text('email').notNull(),
|
|
36
|
-
subject: text('subject').notNull(),
|
|
37
|
-
type: text('type').notNull(),
|
|
38
|
-
status: text('status').notNull().default('queued'),
|
|
39
|
-
sentAt: text('sent_at'),
|
|
40
|
-
deliveredAt: text('delivered_at'),
|
|
41
|
-
openedAt: text('opened_at'),
|
|
42
|
-
clickedAt: text('clicked_at'),
|
|
43
|
-
bouncedAt: text('bounced_at'),
|
|
44
|
-
bounceType: text('bounce_type'),
|
|
45
|
-
complainedAt: text('complained_at'),
|
|
46
|
-
trackingId: text('tracking_id').notNull().unique(),
|
|
47
|
-
createdAt: text('created_at').notNull(),
|
|
48
|
-
},
|
|
49
|
-
(table) => [
|
|
50
|
-
index('idx_sends_subscriber').on(table.subscriberId),
|
|
51
|
-
index('idx_sends_campaign').on(table.campaignId),
|
|
52
|
-
index('idx_sends_tracking').on(table.trackingId),
|
|
53
|
-
index('idx_sends_status').on(table.status),
|
|
54
|
-
index('idx_sends_type_created').on(table.type, table.createdAt),
|
|
55
|
-
],
|
|
56
|
-
)
|