@growth-labs/mailer 0.3.0 → 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.
Files changed (54) hide show
  1. package/dist/index.d.ts +1 -2
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +0 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/options.d.ts +24 -14
  6. package/dist/options.d.ts.map +1 -1
  7. package/dist/options.js +4 -0
  8. package/dist/options.js.map +1 -1
  9. package/dist/queue/consumer.d.ts +1 -6
  10. package/dist/queue/consumer.d.ts.map +1 -1
  11. package/dist/queue/consumer.js +46 -14
  12. package/dist/queue/consumer.js.map +1 -1
  13. package/dist/routes/track-click.js +1 -1
  14. package/dist/routes/track-click.js.map +1 -1
  15. package/dist/routes/track-open.js +1 -1
  16. package/dist/routes/track-open.js.map +1 -1
  17. package/dist/schema/index.d.ts +3 -0
  18. package/dist/schema/index.d.ts.map +1 -0
  19. package/dist/schema/index.js +3 -0
  20. package/dist/schema/index.js.map +1 -0
  21. package/dist/schema/sends.d.ts +312 -0
  22. package/dist/schema/sends.d.ts.map +1 -0
  23. package/dist/schema/sends.js +26 -0
  24. package/dist/schema/sends.js.map +1 -0
  25. package/dist/schema/subscribers.d.ts +253 -0
  26. package/dist/schema/subscribers.d.ts.map +1 -0
  27. package/dist/schema/subscribers.js +21 -0
  28. package/dist/schema/subscribers.js.map +1 -0
  29. package/dist/types.d.ts +30 -0
  30. package/dist/types.d.ts.map +1 -1
  31. package/dist/utils/bounce.d.ts.map +1 -1
  32. package/dist/utils/bounce.js +2 -1
  33. package/dist/utils/bounce.js.map +1 -1
  34. package/dist/utils/send.d.ts +7 -4
  35. package/dist/utils/send.d.ts.map +1 -1
  36. package/dist/utils/send.js +24 -9
  37. package/dist/utils/send.js.map +1 -1
  38. package/dist/utils/subscribers.js +1 -1
  39. package/dist/utils/subscribers.js.map +1 -1
  40. package/package.json +12 -4
  41. package/src/index.ts +2 -1
  42. package/src/options.ts +6 -0
  43. package/src/queue/consumer.ts +73 -20
  44. package/src/routes/track-click.ts +1 -1
  45. package/src/routes/track-open.ts +1 -1
  46. package/src/schema/index.ts +2 -0
  47. package/src/schema/sends.ts +30 -0
  48. package/src/schema/subscribers.ts +25 -0
  49. package/src/types.ts +37 -0
  50. package/src/utils/bounce.ts +2 -1
  51. package/src/utils/send.ts +43 -12
  52. package/src/utils/subscribers.ts +1 -1
  53. package/src/virtual.d.ts +4 -0
  54. 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
- export interface MailerEnv {
25
- DB: D1Database
26
- QUEUE: Queue
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 = drizzle(env.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 env.QUEUE.send(message)
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
- env: MailerEnv,
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 env.QUEUE.send(message)
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 = drizzle(env.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(env, db, options, {
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 = drizzle(env.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(env, db, options, {
290
+ const recipientCount = await fanOutToSubscribers(queue, db, options, {
260
291
  subject: params.subject,
261
292
  html,
262
293
  campaignId,
@@ -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
- )