@growth-labs/mailer 0.2.2 → 0.3.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/queue/consumer.d.ts.map +1 -1
- package/dist/queue/consumer.js +12 -0
- 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 +16 -9
- 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 +15 -9
- 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/migrations/0001_create_gl_mailer_tables.sql +48 -0
- package/package.json +2 -1
- package/src/_internal/schema-probe.ts +89 -0
- package/src/queue/consumer.ts +13 -0
- package/src/routes/confirm.ts +6 -0
- package/src/routes/subscribe.ts +7 -0
- package/src/routes/track-click.ts +21 -14
- package/src/routes/track-open.ts +20 -14
- package/src/routes/unsubscribe.ts +9 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export declare const GL_MAILER_SCHEMA_MISSING = "GL_MAILER_SCHEMA_MISSING";
|
|
2
|
+
export type ProbeResult = {
|
|
3
|
+
ok: boolean;
|
|
4
|
+
};
|
|
5
|
+
interface D1PreparedStatement {
|
|
6
|
+
first<T = unknown>(): Promise<T | null>;
|
|
7
|
+
}
|
|
8
|
+
interface D1DatabaseLike {
|
|
9
|
+
prepare(query: string): D1PreparedStatement;
|
|
10
|
+
}
|
|
11
|
+
/** @internal Reset the module-scoped cache — tests only. */
|
|
12
|
+
export declare function _resetSchemaProbeCache(): void;
|
|
13
|
+
/**
|
|
14
|
+
* Probe the configured D1 binding for `gl_subscribers` AND `gl_email_sends`.
|
|
15
|
+
* Returns `{ ok: true }` on success, `{ ok: false }` on failure (either table
|
|
16
|
+
* missing). Caches the result on the first call per Worker instance.
|
|
17
|
+
*
|
|
18
|
+
* Does not throw. A missing binding is treated as `ok: false` and logged —
|
|
19
|
+
* mailer cannot function without D1, so unlike analytics this is loud rather
|
|
20
|
+
* than silent.
|
|
21
|
+
*/
|
|
22
|
+
export declare function probeMailerSchema(db: D1DatabaseLike | undefined, d1Binding: string): Promise<ProbeResult>;
|
|
23
|
+
/**
|
|
24
|
+
* Helper: build the standard 503 response mailer routes return on schema
|
|
25
|
+
* miss. The body shape is stable so observability dashboards can match on
|
|
26
|
+
* `code === 'GL_MAILER_SCHEMA_MISSING'`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function schemaMissingResponse(): Response;
|
|
29
|
+
export {};
|
|
30
|
+
//# sourceMappingURL=schema-probe.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema-probe.d.ts","sourceRoot":"","sources":["../../src/_internal/schema-probe.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,wBAAwB,6BAA6B,CAAA;AAElE,MAAM,MAAM,WAAW,GAAG;IAAE,EAAE,EAAE,OAAO,CAAA;CAAE,CAAA;AAEzC,UAAU,mBAAmB;IAC5B,KAAK,CAAC,CAAC,GAAG,OAAO,KAAK,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAA;CACvC;AAED,UAAU,cAAc;IACvB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,mBAAmB,CAAA;CAC3C;AAID,4DAA4D;AAC5D,wBAAgB,sBAAsB,IAAI,IAAI,CAE7C;AAED;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CACtC,EAAE,EAAE,cAAc,GAAG,SAAS,EAC9B,SAAS,EAAE,MAAM,GACf,OAAO,CAAC,WAAW,CAAC,CAkBtB;AAgBD;;;;GAIG;AACH,wBAAgB,qBAAqB,IAAI,QAAQ,CAQhD"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// One-shot D1 schema probe with module-scoped cache. Mailer routes that touch
|
|
2
|
+
// gl_subscribers / gl_email_sends call this before doing D1 work. On miss the
|
|
3
|
+
// route returns 503 with the GL_MAILER_SCHEMA_MISSING code instead of letting
|
|
4
|
+
// drizzle throw and falling into Astro's SSR error template.
|
|
5
|
+
//
|
|
6
|
+
// Does NOT throw. A throw here would recreate the silent-500 cascade that
|
|
7
|
+
// motivates this release. The contract: schema missing → mailer disabled
|
|
8
|
+
// for this Worker's lifetime, affected routes return 503 with a diagnostic
|
|
9
|
+
// body, observability gets one loud error per instance startup.
|
|
10
|
+
export const GL_MAILER_SCHEMA_MISSING = 'GL_MAILER_SCHEMA_MISSING';
|
|
11
|
+
let _cached = null;
|
|
12
|
+
/** @internal Reset the module-scoped cache — tests only. */
|
|
13
|
+
export function _resetSchemaProbeCache() {
|
|
14
|
+
_cached = null;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Probe the configured D1 binding for `gl_subscribers` AND `gl_email_sends`.
|
|
18
|
+
* Returns `{ ok: true }` on success, `{ ok: false }` on failure (either table
|
|
19
|
+
* missing). Caches the result on the first call per Worker instance.
|
|
20
|
+
*
|
|
21
|
+
* Does not throw. A missing binding is treated as `ok: false` and logged —
|
|
22
|
+
* mailer cannot function without D1, so unlike analytics this is loud rather
|
|
23
|
+
* than silent.
|
|
24
|
+
*/
|
|
25
|
+
export async function probeMailerSchema(db, d1Binding) {
|
|
26
|
+
if (_cached)
|
|
27
|
+
return _cached;
|
|
28
|
+
if (!db) {
|
|
29
|
+
logSchemaMissing(d1Binding, 'D1 binding is not bound');
|
|
30
|
+
_cached = { ok: false };
|
|
31
|
+
return _cached;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
await db.prepare('SELECT 1 FROM gl_subscribers LIMIT 1').first();
|
|
35
|
+
await db.prepare('SELECT 1 FROM gl_email_sends LIMIT 1').first();
|
|
36
|
+
_cached = { ok: true };
|
|
37
|
+
return _cached;
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
41
|
+
logSchemaMissing(d1Binding, message);
|
|
42
|
+
_cached = { ok: false };
|
|
43
|
+
return _cached;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function logSchemaMissing(d1Binding, underlying) {
|
|
47
|
+
console.error(`[${GL_MAILER_SCHEMA_MISSING}] @growth-labs/mailer: D1 binding "${d1Binding}" is ` +
|
|
48
|
+
'missing one or both of the gl_subscribers / gl_email_sends tables.\n' +
|
|
49
|
+
'Remediation:\n' +
|
|
50
|
+
' 1. Add to wrangler.toml under your [[d1_databases]] block:\n' +
|
|
51
|
+
' migrations_dir = "node_modules/@growth-labs/mailer/migrations"\n' +
|
|
52
|
+
` 2. Run: pnpm exec wrangler d1 migrations apply ${d1Binding} --remote\n` +
|
|
53
|
+
'See packages-docs/mailer-d1-migrations.md for the full guide.\n' +
|
|
54
|
+
'Mailer routes return 503 until the schema is present. ' +
|
|
55
|
+
`Underlying error: ${underlying}`);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Helper: build the standard 503 response mailer routes return on schema
|
|
59
|
+
* miss. The body shape is stable so observability dashboards can match on
|
|
60
|
+
* `code === 'GL_MAILER_SCHEMA_MISSING'`.
|
|
61
|
+
*/
|
|
62
|
+
export function schemaMissingResponse() {
|
|
63
|
+
return Response.json({
|
|
64
|
+
error: 'Mailer schema is not initialized',
|
|
65
|
+
code: GL_MAILER_SCHEMA_MISSING,
|
|
66
|
+
}, { status: 503 });
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=schema-probe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema-probe.js","sourceRoot":"","sources":["../../src/_internal/schema-probe.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,8EAA8E;AAC9E,8EAA8E;AAC9E,6DAA6D;AAC7D,EAAE;AACF,0EAA0E;AAC1E,yEAAyE;AACzE,2EAA2E;AAC3E,gEAAgE;AAEhE,MAAM,CAAC,MAAM,wBAAwB,GAAG,0BAA0B,CAAA;AAYlE,IAAI,OAAO,GAAuB,IAAI,CAAA;AAEtC,4DAA4D;AAC5D,MAAM,UAAU,sBAAsB;IACrC,OAAO,GAAG,IAAI,CAAA;AACf,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACtC,EAA8B,EAC9B,SAAiB;IAEjB,IAAI,OAAO;QAAE,OAAO,OAAO,CAAA;IAC3B,IAAI,CAAC,EAAE,EAAE,CAAC;QACT,gBAAgB,CAAC,SAAS,EAAE,yBAAyB,CAAC,CAAA;QACtD,OAAO,GAAG,EAAE,EAAE,EAAE,KAAK,EAAE,CAAA;QACvB,OAAO,OAAO,CAAA;IACf,CAAC;IACD,IAAI,CAAC;QACJ,MAAM,EAAE,CAAC,OAAO,CAAC,sCAAsC,CAAC,CAAC,KAAK,EAAE,CAAA;QAChE,MAAM,EAAE,CAAC,OAAO,CAAC,sCAAsC,CAAC,CAAC,KAAK,EAAE,CAAA;QAChE,OAAO,GAAG,EAAE,EAAE,EAAE,IAAI,EAAE,CAAA;QACtB,OAAO,OAAO,CAAA;IACf,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QAChE,gBAAgB,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;QACpC,OAAO,GAAG,EAAE,EAAE,EAAE,KAAK,EAAE,CAAA;QACvB,OAAO,OAAO,CAAA;IACf,CAAC;AACF,CAAC;AAED,SAAS,gBAAgB,CAAC,SAAiB,EAAE,UAAkB;IAC9D,OAAO,CAAC,KAAK,CACZ,IAAI,wBAAwB,sCAAsC,SAAS,OAAO;QACjF,sEAAsE;QACtE,gBAAgB;QAChB,gEAAgE;QAChE,yEAAyE;QACzE,oDAAoD,SAAS,aAAa;QAC1E,iEAAiE;QACjE,wDAAwD;QACxD,qBAAqB,UAAU,EAAE,CAClC,CAAA;AACF,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,qBAAqB;IACpC,OAAO,QAAQ,CAAC,IAAI,CACnB;QACC,KAAK,EAAE,kCAAkC;QACzC,IAAI,EAAE,wBAAwB;KAC9B,EACD,EAAE,MAAM,EAAE,GAAG,EAAE,CACf,CAAA;AACF,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"consumer.d.ts","sourceRoot":"","sources":["../../src/queue/consumer.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"consumer.d.ts","sourceRoot":"","sources":["../../src/queue/consumer.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAC1D,OAAO,KAAK,EAAiB,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAGnE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAGlE,wBAAsB,gBAAgB,CACrC,KAAK,EAAE,YAAY,CAAC,iBAAiB,CAAC,EACtC,GAAG,EAAE;IACJ,EAAE,EAAE,UAAU,CAAA;IACd,YAAY,CAAC,EAAE,qBAAqB,CAAA;IACpC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACtB,EACD,OAAO,EAAE,qBAAqB,GAC5B,OAAO,CAAC,IAAI,CAAC,CAuGf"}
|
package/dist/queue/consumer.js
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import { drizzle } from 'drizzle-orm/d1';
|
|
2
|
+
import { probeMailerSchema } from '../_internal/schema-probe.js';
|
|
2
3
|
import { emitMailerAnalyticsEvent } from '../utils/analytics.js';
|
|
3
4
|
import { updateSendStatus } from '../utils/bounce.js';
|
|
4
5
|
import { CloudflareEmailProvider, sleep } from '../utils/providers.js';
|
|
5
6
|
export async function handleEmailQueue(batch, env, options) {
|
|
7
|
+
// Schema probe — runs once per Worker instance. On miss the probe logs
|
|
8
|
+
// GL_MAILER_SCHEMA_MISSING and we ack every message in the batch without
|
|
9
|
+
// retry. Re-queueing without the schema would cycle indefinitely and burn
|
|
10
|
+
// Cloudflare Queue retry budget.
|
|
11
|
+
const schemaProbe = await probeMailerSchema(env.DB, options.d1Binding);
|
|
12
|
+
if (!schemaProbe.ok) {
|
|
13
|
+
for (const message of batch.messages) {
|
|
14
|
+
message.ack();
|
|
15
|
+
}
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
6
18
|
const db = drizzle(env.DB);
|
|
7
19
|
const provider = env.EMAIL_SENDER
|
|
8
20
|
? new CloudflareEmailProvider(env.EMAIL_SENDER)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"consumer.js","sourceRoot":"","sources":["../../src/queue/consumer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;
|
|
1
|
+
{"version":3,"file":"consumer.js","sourceRoot":"","sources":["../../src/queue/consumer.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"confirm.d.ts","sourceRoot":"","sources":["../../src/routes/confirm.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"confirm.d.ts","sourceRoot":"","sources":["../../src/routes/confirm.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AASrC,eAAO,MAAM,GAAG,EAAE,QAyEjB,CAAA"}
|
package/dist/routes/confirm.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { env as cloudflareEnv } from 'cloudflare:workers';
|
|
2
2
|
import { config } from 'virtual:growth-labs/mailer/config';
|
|
3
3
|
import { drizzle } from 'drizzle-orm/d1';
|
|
4
|
+
import { probeMailerSchema, schemaMissingResponse } from '../_internal/schema-probe.js';
|
|
4
5
|
import { emitMailerAnalyticsEvent } from '../utils/analytics.js';
|
|
5
6
|
import { sendTransactional } from '../utils/send.js';
|
|
6
7
|
import { confirmSubscriber, getSubscriberById } from '../utils/subscribers.js';
|
|
@@ -20,6 +21,10 @@ export const GET = async (context) => {
|
|
|
20
21
|
const bindingsEnv = cloudflareEnv;
|
|
21
22
|
const d1 = bindingsEnv[config.d1Binding];
|
|
22
23
|
const queue = bindingsEnv[config.queueBinding];
|
|
24
|
+
// Schema probe — return 503 with GL_MAILER_SCHEMA_MISSING on miss.
|
|
25
|
+
const schema = await probeMailerSchema(d1, config.d1Binding);
|
|
26
|
+
if (!schema.ok)
|
|
27
|
+
return schemaMissingResponse();
|
|
23
28
|
const db = drizzle(d1);
|
|
24
29
|
const env = { DB: d1, QUEUE: queue };
|
|
25
30
|
// Get subscriber to check status
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"confirm.js","sourceRoot":"","sources":["../../src/routes/confirm.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,IAAI,aAAa,EAAE,MAAM,oBAAoB,CAAA;AACzD,OAAO,EAAE,MAAM,EAAE,MAAM,mCAAmC,CAAA;AAE1D,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AACxC,OAAO,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAA;AAEhE,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AACpD,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAA;AAC9E,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAEhD,MAAM,CAAC,MAAM,GAAG,GAAa,KAAK,EAAE,OAAO,EAAE,EAAE;IAC9C,MAAM,EAAE,GAAG,EAAE,GAAG,OAAO,CAAA;IACvB,MAAM,KAAK,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;IAC3C,IAAI,CAAC,KAAK,EAAE,CAAC;QACZ,OAAO,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,eAAe,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IAClE,CAAC;IAED,eAAe;IACf,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,MAAM,CAAC,aAAa,EAAE,KAAK,CAAC,CAAA;IAC9D,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAC9C,OAAO,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,0BAA0B,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IAC7E,CAAC;IAED,mBAAmB;IACnB,MAAM,WAAW,GAAG,aAAwC,CAAA;IAC5D,MAAM,EAAE,GAAG,WAAW,CAAC,MAAM,CAAC,SAAS,CAAe,CAAA;IACtD,MAAM,KAAK,GAAG,WAAW,CAAC,MAAM,CAAC,YAAY,CAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"confirm.js","sourceRoot":"","sources":["../../src/routes/confirm.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subscribe.d.ts","sourceRoot":"","sources":["../../src/routes/subscribe.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"subscribe.d.ts","sourceRoot":"","sources":["../../src/routes/subscribe.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AASrC,eAAO,MAAM,IAAI,EAAE,QAsHlB,CAAA"}
|
package/dist/routes/subscribe.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { env as cloudflareEnv } from 'cloudflare:workers';
|
|
2
2
|
import { config } from 'virtual:growth-labs/mailer/config';
|
|
3
3
|
import { drizzle } from 'drizzle-orm/d1';
|
|
4
|
+
import { probeMailerSchema, schemaMissingResponse } from '../_internal/schema-probe.js';
|
|
4
5
|
import { emitMailerAnalyticsEvent } from '../utils/analytics.js';
|
|
5
6
|
import { sendTransactional } from '../utils/send.js';
|
|
6
7
|
import { createSubscriber } from '../utils/subscribers.js';
|
|
@@ -35,6 +36,11 @@ export const POST = async (context) => {
|
|
|
35
36
|
const bindingsEnv = cloudflareEnv;
|
|
36
37
|
const d1 = bindingsEnv[config.d1Binding];
|
|
37
38
|
const queue = bindingsEnv[config.queueBinding];
|
|
39
|
+
// 5a. Schema probe — runs once per Worker instance. On miss, return 503
|
|
40
|
+
// with GL_MAILER_SCHEMA_MISSING so the consumer's site doesn't 500-cascade.
|
|
41
|
+
const schema = await probeMailerSchema(d1, config.d1Binding);
|
|
42
|
+
if (!schema.ok)
|
|
43
|
+
return schemaMissingResponse();
|
|
38
44
|
const db = drizzle(d1);
|
|
39
45
|
const env = { DB: d1, QUEUE: queue };
|
|
40
46
|
// 6. Create subscriber
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subscribe.js","sourceRoot":"","sources":["../../src/routes/subscribe.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,IAAI,aAAa,EAAE,MAAM,oBAAoB,CAAA;AACzD,OAAO,EAAE,MAAM,EAAE,MAAM,mCAAmC,CAAA;AAE1D,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AACxC,OAAO,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAA;AAEhE,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AACpD,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAA;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAElD,MAAM,CAAC,MAAM,IAAI,GAAa,KAAK,EAAE,OAAO,EAAE,EAAE;IAC/C,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAA;IAC3B,wBAAwB;IACxB,MAAM,IAAI,GAAG,CAAC,MAAM,OAAO,CAAC,IAAI,EAAE,CAMjC,CAAA;IAED,8BAA8B;IAC9B,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC;QACzC,OAAO,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,yBAAyB,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IAC5E,CAAC;IAED,mCAAmC;IACnC,IAAI,CAAC,4BAA4B,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACpD,OAAO,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,sBAAsB,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IACzE,CAAC;IAED,8BAA8B;IAC9B,MAAM,iBAAiB,GAAG,MAAM,KAAK,CACpC,2DAA2D,EAC3D;QACC,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;QAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;YACpB,MAAM,EAAE,MAAM,CAAC,kBAAkB;YACjC,QAAQ,EAAE,IAAI,CAAC,cAAc;YAC7B,QAAQ,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,IAAI,SAAS;SAC9D,CAAC;KACF,CACD,CAAA;IACD,MAAM,eAAe,GAAG,CAAC,MAAM,iBAAiB,CAAC,IAAI,EAAE,CAEtD,CAAA;IACD,IAAI,CAAC,eAAe,CAAC,OAAO,EAAE,CAAC;QAC9B,OAAO,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,+BAA+B,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IAClF,CAAC;IAED,sBAAsB;IACtB,MAAM,WAAW,GAAG,aAAwC,CAAA;IAC5D,MAAM,EAAE,GAAG,WAAW,CAAC,MAAM,CAAC,SAAS,CAAe,CAAA;IACtD,MAAM,KAAK,GAAG,WAAW,CAAC,MAAM,CAAC,YAAY,CAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"subscribe.js","sourceRoot":"","sources":["../../src/routes/subscribe.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"track-click.d.ts","sourceRoot":"","sources":["../../src/routes/track-click.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"track-click.d.ts","sourceRoot":"","sources":["../../src/routes/track-click.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAOrC,eAAO,MAAM,GAAG,EAAE,QA4DjB,CAAA"}
|
|
@@ -2,6 +2,7 @@ import { env as cloudflareEnv } from 'cloudflare:workers';
|
|
|
2
2
|
import { config } from 'virtual:growth-labs/mailer/config';
|
|
3
3
|
import { and, eq, inArray } from 'drizzle-orm';
|
|
4
4
|
import { drizzle } from 'drizzle-orm/d1';
|
|
5
|
+
import { probeMailerSchema } from '../_internal/schema-probe.js';
|
|
5
6
|
import { emailSends } from '../schema.js';
|
|
6
7
|
import { emitMailerAnalyticsEvent } from '../utils/analytics.js';
|
|
7
8
|
export const GET = async (context) => {
|
|
@@ -21,19 +22,25 @@ export const GET = async (context) => {
|
|
|
21
22
|
catch {
|
|
22
23
|
return new Response('Invalid URL', { status: 400 });
|
|
23
24
|
}
|
|
24
|
-
// Update status to 'clicked' only when it hasn't already reached 'clicked'
|
|
25
|
+
// Update status to 'clicked' only when it hasn't already reached 'clicked'.
|
|
26
|
+
// On schema miss the probe logs GL_MAILER_SCHEMA_MISSING and we skip the
|
|
27
|
+
// D1 update — but always still 302 to the destination so we don't break
|
|
28
|
+
// the user's actual click intent.
|
|
25
29
|
try {
|
|
26
30
|
const bindingsEnv = cloudflareEnv;
|
|
27
31
|
if (bindingsEnv) {
|
|
28
32
|
const d1 = bindingsEnv[config.d1Binding];
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
const schema = await probeMailerSchema(d1, config.d1Binding);
|
|
34
|
+
if (schema.ok) {
|
|
35
|
+
const db = drizzle(d1);
|
|
36
|
+
await db
|
|
37
|
+
.update(emailSends)
|
|
38
|
+
.set({
|
|
39
|
+
status: 'clicked',
|
|
40
|
+
clickedAt: new Date().toISOString(),
|
|
41
|
+
})
|
|
42
|
+
.where(and(eq(emailSends.trackingId, trackingId), inArray(emailSends.status, ['sent', 'delivered', 'opened'])));
|
|
43
|
+
}
|
|
37
44
|
}
|
|
38
45
|
}
|
|
39
46
|
catch {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"track-click.js","sourceRoot":"","sources":["../../src/routes/track-click.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,IAAI,aAAa,EAAE,MAAM,oBAAoB,CAAA;AACzD,OAAO,EAAE,MAAM,EAAE,MAAM,mCAAmC,CAAA;AAE1D,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AACzC,OAAO,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAA;AAEhE,MAAM,CAAC,MAAM,GAAG,GAAa,KAAK,EAAE,OAAO,EAAE,EAAE;IAC9C,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,OAAO,CAAA;IACxC,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAA;IACpC,MAAM,WAAW,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;IAE/C,IAAI,CAAC,UAAU,IAAI,CAAC,WAAW,EAAE,CAAC;QACjC,OAAO,IAAI,QAAQ,CAAC,aAAa,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IACpD,CAAC;IAED,qEAAqE;IACrE,IAAI,CAAC;QACJ,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,CAAA;QACpC,IAAI,CAAC,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;YACrD,OAAO,IAAI,QAAQ,CAAC,kBAAkB,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;QACzD,CAAC;IACF,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,IAAI,QAAQ,CAAC,aAAa,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IACpD,CAAC;IAED,
|
|
1
|
+
{"version":3,"file":"track-click.js","sourceRoot":"","sources":["../../src/routes/track-click.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"track-open.d.ts","sourceRoot":"","sources":["../../src/routes/track-open.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"track-open.d.ts","sourceRoot":"","sources":["../../src/routes/track-open.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAQrC,eAAO,MAAM,GAAG,EAAE,QAoDjB,CAAA"}
|
|
@@ -2,6 +2,7 @@ import { env as cloudflareEnv } from 'cloudflare:workers';
|
|
|
2
2
|
import { config } from 'virtual:growth-labs/mailer/config';
|
|
3
3
|
import { and, eq, inArray } from 'drizzle-orm';
|
|
4
4
|
import { drizzle } from 'drizzle-orm/d1';
|
|
5
|
+
import { probeMailerSchema } from '../_internal/schema-probe.js';
|
|
5
6
|
import { emailSends } from '../schema.js';
|
|
6
7
|
import { emitMailerAnalyticsEvent } from '../utils/analytics.js';
|
|
7
8
|
import { TRANSPARENT_GIF } from '../utils/tracking.js';
|
|
@@ -12,19 +13,24 @@ export const GET = async (context) => {
|
|
|
12
13
|
return new Response(null, { status: 400 });
|
|
13
14
|
}
|
|
14
15
|
// Update status to 'opened' only when currently 'sent' or 'delivered'
|
|
15
|
-
// to avoid downgrading from 'clicked'.
|
|
16
|
+
// to avoid downgrading from 'clicked'. On schema miss, the probe logs
|
|
17
|
+
// GL_MAILER_SCHEMA_MISSING and we skip the D1 work — but always still
|
|
18
|
+
// return the transparent GIF so we don't break email rendering.
|
|
16
19
|
try {
|
|
17
20
|
const bindingsEnv = cloudflareEnv;
|
|
18
21
|
if (bindingsEnv) {
|
|
19
22
|
const d1 = bindingsEnv[config.d1Binding];
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
const schema = await probeMailerSchema(d1, config.d1Binding);
|
|
24
|
+
if (schema.ok) {
|
|
25
|
+
const db = drizzle(d1);
|
|
26
|
+
await db
|
|
27
|
+
.update(emailSends)
|
|
28
|
+
.set({
|
|
29
|
+
status: 'opened',
|
|
30
|
+
openedAt: new Date().toISOString(),
|
|
31
|
+
})
|
|
32
|
+
.where(and(eq(emailSends.trackingId, trackingId), inArray(emailSends.status, ['sent', 'delivered'])));
|
|
33
|
+
}
|
|
28
34
|
}
|
|
29
35
|
}
|
|
30
36
|
catch {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"track-open.js","sourceRoot":"","sources":["../../src/routes/track-open.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,IAAI,aAAa,EAAE,MAAM,oBAAoB,CAAA;AACzD,OAAO,EAAE,MAAM,EAAE,MAAM,mCAAmC,CAAA;AAE1D,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AACzC,OAAO,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAA;AAChE,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAEtD,MAAM,CAAC,MAAM,GAAG,GAAa,KAAK,EAAE,OAAO,EAAE,EAAE;IAC9C,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAA;IACnC,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAA;IACpC,IAAI,CAAC,UAAU,EAAE,CAAC;QACjB,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IAC3C,CAAC;IAED,sEAAsE;IACtE,
|
|
1
|
+
{"version":3,"file":"track-open.js","sourceRoot":"","sources":["../../src/routes/track-open.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,IAAI,aAAa,EAAE,MAAM,oBAAoB,CAAA;AACzD,OAAO,EAAE,MAAM,EAAE,MAAM,mCAAmC,CAAA;AAE1D,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AACxC,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAA;AAChE,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AACzC,OAAO,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAA;AAChE,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAEtD,MAAM,CAAC,MAAM,GAAG,GAAa,KAAK,EAAE,OAAO,EAAE,EAAE;IAC9C,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAA;IACnC,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAA;IACpC,IAAI,CAAC,UAAU,EAAE,CAAC;QACjB,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IAC3C,CAAC;IAED,sEAAsE;IACtE,sEAAsE;IACtE,sEAAsE;IACtE,gEAAgE;IAChE,IAAI,CAAC;QACJ,MAAM,WAAW,GAAG,aAAwC,CAAA;QAC5D,IAAI,WAAW,EAAE,CAAC;YACjB,MAAM,EAAE,GAAG,WAAW,CAAC,MAAM,CAAC,SAAS,CAAe,CAAA;YACtD,MAAM,MAAM,GAAG,MAAM,iBAAiB,CAAC,EAAE,EAAE,MAAM,CAAC,SAAS,CAAC,CAAA;YAC5D,IAAI,MAAM,CAAC,EAAE,EAAE,CAAC;gBACf,MAAM,EAAE,GAAG,OAAO,CAAC,EAAE,CAAC,CAAA;gBACtB,MAAM,EAAE;qBACN,MAAM,CAAC,UAAU,CAAC;qBAClB,GAAG,CAAC;oBACJ,MAAM,EAAE,QAAQ;oBAChB,QAAQ,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;iBAClC,CAAC;qBACD,KAAK,CACL,GAAG,CACF,EAAE,CAAC,UAAU,CAAC,UAAU,EAAE,UAAU,CAAC,EACrC,OAAO,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CACjD,CACD,CAAA;YACH,CAAC;QACF,CAAC;IACF,CAAC;IAAC,MAAM,CAAC;QACR,6CAA6C;IAC9C,CAAC;IAED,wBAAwB,CAAC,MAAM,EAAE,aAAwC,EAAE,mBAAmB,EAAE;QAC/F,OAAO;QACP,OAAO;QACP,WAAW,EAAE,UAAU;QACvB,KAAK,EAAE,EAAE,UAAU,EAAE;KACrB,CAAC,CAAA;IAEF,sBAAsB;IACtB,OAAO,IAAI,QAAQ,CAAC,eAAe,EAAE;QACpC,MAAM,EAAE,GAAG;QACX,OAAO,EAAE;YACR,cAAc,EAAE,WAAW;YAC3B,eAAe,EAAE,qCAAqC;YACtD,gBAAgB,EAAE,MAAM,CAAC,eAAe,CAAC,MAAM,CAAC;SAChD;KACD,CAAC,CAAA;AACH,CAAC,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"unsubscribe.d.ts","sourceRoot":"","sources":["../../src/routes/unsubscribe.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"unsubscribe.d.ts","sourceRoot":"","sources":["../../src/routes/unsubscribe.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAwErC,eAAO,MAAM,GAAG,EAAE,QA2BjB,CAAA;AAGD,eAAO,MAAM,IAAI,EAAE,QAiBlB,CAAA"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { env as cloudflareEnv } from 'cloudflare:workers';
|
|
2
2
|
import { config } from 'virtual:growth-labs/mailer/config';
|
|
3
3
|
import { drizzle } from 'drizzle-orm/d1';
|
|
4
|
+
import { probeMailerSchema, schemaMissingResponse } from '../_internal/schema-probe.js';
|
|
4
5
|
import { emitMailerAnalyticsEvent } from '../utils/analytics.js';
|
|
5
6
|
import { sendTransactional } from '../utils/send.js';
|
|
6
7
|
import { getSubscriberById, unsubscribeSubscriber } from '../utils/subscribers.js';
|
|
@@ -16,6 +17,11 @@ async function processUnsubscribe(token, context, request) {
|
|
|
16
17
|
const bindingsEnv = cloudflareEnv;
|
|
17
18
|
const d1 = bindingsEnv[config.d1Binding];
|
|
18
19
|
const queue = bindingsEnv[config.queueBinding];
|
|
20
|
+
// Schema probe — short-circuit with a Response on miss; both GET and POST
|
|
21
|
+
// handlers below forward it unchanged.
|
|
22
|
+
const schema = await probeMailerSchema(d1, config.d1Binding);
|
|
23
|
+
if (!schema.ok)
|
|
24
|
+
return { schemaMissing: true };
|
|
19
25
|
const db = drizzle(d1);
|
|
20
26
|
const env = { DB: d1, QUEUE: queue };
|
|
21
27
|
// Get subscriber
|
|
@@ -58,6 +64,8 @@ export const GET = async (context) => {
|
|
|
58
64
|
return Response.json({ error: 'Missing token' }, { status: 400 });
|
|
59
65
|
}
|
|
60
66
|
const result = await processUnsubscribe(token, context, request);
|
|
67
|
+
if ('schemaMissing' in result)
|
|
68
|
+
return schemaMissingResponse();
|
|
61
69
|
if ('error' in result) {
|
|
62
70
|
return Response.json({ error: result.error }, { status: result.status });
|
|
63
71
|
}
|
|
@@ -86,6 +94,8 @@ export const POST = async (context) => {
|
|
|
86
94
|
return new Response('Missing token', { status: 400 });
|
|
87
95
|
}
|
|
88
96
|
const result = await processUnsubscribe(token, context, request);
|
|
97
|
+
if ('schemaMissing' in result)
|
|
98
|
+
return schemaMissingResponse();
|
|
89
99
|
if ('error' in result) {
|
|
90
100
|
return new Response(result.error, { status: result.status });
|
|
91
101
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"unsubscribe.js","sourceRoot":"","sources":["../../src/routes/unsubscribe.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,IAAI,aAAa,EAAE,MAAM,oBAAoB,CAAA;AACzD,OAAO,EAAE,MAAM,EAAE,MAAM,mCAAmC,CAAA;AAE1D,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AACxC,OAAO,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAA;AAEhE,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AACpD,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAA;AAClF,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAC/D,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAE/C,KAAK,UAAU,kBAAkB,CAChC,KAAa,EACb,OAAiC,EACjC,OAAiB;IAEjB,eAAe;IACf,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,MAAM,CAAC,aAAa,EAAE,KAAK,CAAC,CAAA;IAC9D,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,MAAM,KAAK,aAAa,EAAE,CAAC;QAClD,OAAO,EAAE,KAAK,EAAE,0BAA0B,EAAE,MAAM,EAAE,GAAG,EAAE,CAAA;IAC1D,CAAC;IAED,mBAAmB;IACnB,MAAM,WAAW,GAAG,aAAwC,CAAA;IAC5D,MAAM,EAAE,GAAG,WAAW,CAAC,MAAM,CAAC,SAAS,CAAe,CAAA;IACtD,MAAM,KAAK,GAAG,WAAW,CAAC,MAAM,CAAC,YAAY,CAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"unsubscribe.js","sourceRoot":"","sources":["../../src/routes/unsubscribe.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
-- 0001_create_gl_mailer_tables.sql
|
|
2
|
+
-- @growth-labs/mailer v0.3.0+ schema.
|
|
3
|
+
-- Compatible with `wrangler d1 migrations apply`.
|
|
4
|
+
-- All statements are idempotent (IF NOT EXISTS).
|
|
5
|
+
|
|
6
|
+
CREATE TABLE IF NOT EXISTS gl_subscribers (
|
|
7
|
+
id TEXT PRIMARY KEY,
|
|
8
|
+
email TEXT NOT NULL UNIQUE,
|
|
9
|
+
name TEXT,
|
|
10
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
11
|
+
preferences TEXT NOT NULL DEFAULT '[]',
|
|
12
|
+
source TEXT NOT NULL,
|
|
13
|
+
attribution TEXT,
|
|
14
|
+
soft_bounce_count INTEGER NOT NULL DEFAULT 0,
|
|
15
|
+
subscribed_at TEXT NOT NULL,
|
|
16
|
+
confirmed_at TEXT,
|
|
17
|
+
unsubscribed_at TEXT,
|
|
18
|
+
created_at TEXT NOT NULL,
|
|
19
|
+
updated_at TEXT NOT NULL
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
CREATE TABLE IF NOT EXISTS gl_email_sends (
|
|
23
|
+
id TEXT PRIMARY KEY,
|
|
24
|
+
subscriber_id TEXT NOT NULL REFERENCES gl_subscribers(id),
|
|
25
|
+
campaign_id TEXT,
|
|
26
|
+
email TEXT NOT NULL,
|
|
27
|
+
subject TEXT NOT NULL,
|
|
28
|
+
type TEXT NOT NULL,
|
|
29
|
+
status TEXT NOT NULL DEFAULT 'queued',
|
|
30
|
+
sent_at TEXT,
|
|
31
|
+
delivered_at TEXT,
|
|
32
|
+
opened_at TEXT,
|
|
33
|
+
clicked_at TEXT,
|
|
34
|
+
bounced_at TEXT,
|
|
35
|
+
bounce_type TEXT,
|
|
36
|
+
complained_at TEXT,
|
|
37
|
+
tracking_id TEXT NOT NULL UNIQUE,
|
|
38
|
+
created_at TEXT NOT NULL
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
CREATE INDEX IF NOT EXISTS idx_subscribers_status ON gl_subscribers(status);
|
|
42
|
+
CREATE INDEX IF NOT EXISTS idx_subscribers_email ON gl_subscribers(email);
|
|
43
|
+
CREATE INDEX IF NOT EXISTS idx_subscribers_subscribed_at ON gl_subscribers(subscribed_at);
|
|
44
|
+
CREATE INDEX IF NOT EXISTS idx_sends_subscriber ON gl_email_sends(subscriber_id);
|
|
45
|
+
CREATE INDEX IF NOT EXISTS idx_sends_campaign ON gl_email_sends(campaign_id);
|
|
46
|
+
CREATE INDEX IF NOT EXISTS idx_sends_tracking ON gl_email_sends(tracking_id);
|
|
47
|
+
CREATE INDEX IF NOT EXISTS idx_sends_status ON gl_email_sends(status);
|
|
48
|
+
CREATE INDEX IF NOT EXISTS idx_sends_type_created ON gl_email_sends(type, created_at);
|
package/package.json
CHANGED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// One-shot D1 schema probe with module-scoped cache. Mailer routes that touch
|
|
2
|
+
// gl_subscribers / gl_email_sends call this before doing D1 work. On miss the
|
|
3
|
+
// route returns 503 with the GL_MAILER_SCHEMA_MISSING code instead of letting
|
|
4
|
+
// drizzle throw and falling into Astro's SSR error template.
|
|
5
|
+
//
|
|
6
|
+
// Does NOT throw. A throw here would recreate the silent-500 cascade that
|
|
7
|
+
// motivates this release. The contract: schema missing → mailer disabled
|
|
8
|
+
// for this Worker's lifetime, affected routes return 503 with a diagnostic
|
|
9
|
+
// body, observability gets one loud error per instance startup.
|
|
10
|
+
|
|
11
|
+
export const GL_MAILER_SCHEMA_MISSING = 'GL_MAILER_SCHEMA_MISSING'
|
|
12
|
+
|
|
13
|
+
export type ProbeResult = { ok: boolean }
|
|
14
|
+
|
|
15
|
+
interface D1PreparedStatement {
|
|
16
|
+
first<T = unknown>(): Promise<T | null>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface D1DatabaseLike {
|
|
20
|
+
prepare(query: string): D1PreparedStatement
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let _cached: ProbeResult | null = null
|
|
24
|
+
|
|
25
|
+
/** @internal Reset the module-scoped cache — tests only. */
|
|
26
|
+
export function _resetSchemaProbeCache(): void {
|
|
27
|
+
_cached = null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Probe the configured D1 binding for `gl_subscribers` AND `gl_email_sends`.
|
|
32
|
+
* Returns `{ ok: true }` on success, `{ ok: false }` on failure (either table
|
|
33
|
+
* missing). Caches the result on the first call per Worker instance.
|
|
34
|
+
*
|
|
35
|
+
* Does not throw. A missing binding is treated as `ok: false` and logged —
|
|
36
|
+
* mailer cannot function without D1, so unlike analytics this is loud rather
|
|
37
|
+
* than silent.
|
|
38
|
+
*/
|
|
39
|
+
export async function probeMailerSchema(
|
|
40
|
+
db: D1DatabaseLike | undefined,
|
|
41
|
+
d1Binding: string,
|
|
42
|
+
): Promise<ProbeResult> {
|
|
43
|
+
if (_cached) return _cached
|
|
44
|
+
if (!db) {
|
|
45
|
+
logSchemaMissing(d1Binding, 'D1 binding is not bound')
|
|
46
|
+
_cached = { ok: false }
|
|
47
|
+
return _cached
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
await db.prepare('SELECT 1 FROM gl_subscribers LIMIT 1').first()
|
|
51
|
+
await db.prepare('SELECT 1 FROM gl_email_sends LIMIT 1').first()
|
|
52
|
+
_cached = { ok: true }
|
|
53
|
+
return _cached
|
|
54
|
+
} catch (err) {
|
|
55
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
56
|
+
logSchemaMissing(d1Binding, message)
|
|
57
|
+
_cached = { ok: false }
|
|
58
|
+
return _cached
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function logSchemaMissing(d1Binding: string, underlying: string): void {
|
|
63
|
+
console.error(
|
|
64
|
+
`[${GL_MAILER_SCHEMA_MISSING}] @growth-labs/mailer: D1 binding "${d1Binding}" is ` +
|
|
65
|
+
'missing one or both of the gl_subscribers / gl_email_sends tables.\n' +
|
|
66
|
+
'Remediation:\n' +
|
|
67
|
+
' 1. Add to wrangler.toml under your [[d1_databases]] block:\n' +
|
|
68
|
+
' migrations_dir = "node_modules/@growth-labs/mailer/migrations"\n' +
|
|
69
|
+
` 2. Run: pnpm exec wrangler d1 migrations apply ${d1Binding} --remote\n` +
|
|
70
|
+
'See packages-docs/mailer-d1-migrations.md for the full guide.\n' +
|
|
71
|
+
'Mailer routes return 503 until the schema is present. ' +
|
|
72
|
+
`Underlying error: ${underlying}`,
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Helper: build the standard 503 response mailer routes return on schema
|
|
78
|
+
* miss. The body shape is stable so observability dashboards can match on
|
|
79
|
+
* `code === 'GL_MAILER_SCHEMA_MISSING'`.
|
|
80
|
+
*/
|
|
81
|
+
export function schemaMissingResponse(): Response {
|
|
82
|
+
return Response.json(
|
|
83
|
+
{
|
|
84
|
+
error: 'Mailer schema is not initialized',
|
|
85
|
+
code: GL_MAILER_SCHEMA_MISSING,
|
|
86
|
+
},
|
|
87
|
+
{ status: 503 },
|
|
88
|
+
)
|
|
89
|
+
}
|
package/src/queue/consumer.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { drizzle } from 'drizzle-orm/d1'
|
|
2
|
+
import { probeMailerSchema } from '../_internal/schema-probe.js'
|
|
2
3
|
import type { ResolvedMailerOptions } from '../options.js'
|
|
3
4
|
import type { EmailProvider, EmailQueueMessage } from '../types.js'
|
|
4
5
|
import { emitMailerAnalyticsEvent } from '../utils/analytics.js'
|
|
@@ -15,6 +16,18 @@ export async function handleEmailQueue(
|
|
|
15
16
|
},
|
|
16
17
|
options: ResolvedMailerOptions,
|
|
17
18
|
): Promise<void> {
|
|
19
|
+
// Schema probe — runs once per Worker instance. On miss the probe logs
|
|
20
|
+
// GL_MAILER_SCHEMA_MISSING and we ack every message in the batch without
|
|
21
|
+
// retry. Re-queueing without the schema would cycle indefinitely and burn
|
|
22
|
+
// Cloudflare Queue retry budget.
|
|
23
|
+
const schemaProbe = await probeMailerSchema(env.DB, options.d1Binding)
|
|
24
|
+
if (!schemaProbe.ok) {
|
|
25
|
+
for (const message of batch.messages) {
|
|
26
|
+
message.ack()
|
|
27
|
+
}
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
18
31
|
const db = drizzle(env.DB)
|
|
19
32
|
const provider: EmailProvider = env.EMAIL_SENDER
|
|
20
33
|
? new CloudflareEmailProvider(env.EMAIL_SENDER)
|
package/src/routes/confirm.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { env as cloudflareEnv } from 'cloudflare:workers'
|
|
|
2
2
|
import { config } from 'virtual:growth-labs/mailer/config'
|
|
3
3
|
import type { APIRoute } from 'astro'
|
|
4
4
|
import { drizzle } from 'drizzle-orm/d1'
|
|
5
|
+
import { probeMailerSchema, schemaMissingResponse } from '../_internal/schema-probe.js'
|
|
5
6
|
import { emitMailerAnalyticsEvent } from '../utils/analytics.js'
|
|
6
7
|
import type { MailerEnv } from '../utils/send.js'
|
|
7
8
|
import { sendTransactional } from '../utils/send.js'
|
|
@@ -25,6 +26,11 @@ export const GET: APIRoute = async (context) => {
|
|
|
25
26
|
const bindingsEnv = cloudflareEnv as Record<string, unknown>
|
|
26
27
|
const d1 = bindingsEnv[config.d1Binding] as D1Database
|
|
27
28
|
const queue = bindingsEnv[config.queueBinding] as Queue
|
|
29
|
+
|
|
30
|
+
// Schema probe — return 503 with GL_MAILER_SCHEMA_MISSING on miss.
|
|
31
|
+
const schema = await probeMailerSchema(d1, config.d1Binding)
|
|
32
|
+
if (!schema.ok) return schemaMissingResponse()
|
|
33
|
+
|
|
28
34
|
const db = drizzle(d1)
|
|
29
35
|
const env: MailerEnv = { DB: d1, QUEUE: queue }
|
|
30
36
|
|
package/src/routes/subscribe.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { env as cloudflareEnv } from 'cloudflare:workers'
|
|
|
2
2
|
import { config } from 'virtual:growth-labs/mailer/config'
|
|
3
3
|
import type { APIRoute } from 'astro'
|
|
4
4
|
import { drizzle } from 'drizzle-orm/d1'
|
|
5
|
+
import { probeMailerSchema, schemaMissingResponse } from '../_internal/schema-probe.js'
|
|
5
6
|
import { emitMailerAnalyticsEvent } from '../utils/analytics.js'
|
|
6
7
|
import type { MailerEnv } from '../utils/send.js'
|
|
7
8
|
import { sendTransactional } from '../utils/send.js'
|
|
@@ -53,6 +54,12 @@ export const POST: APIRoute = async (context) => {
|
|
|
53
54
|
const bindingsEnv = cloudflareEnv as Record<string, unknown>
|
|
54
55
|
const d1 = bindingsEnv[config.d1Binding] as D1Database
|
|
55
56
|
const queue = bindingsEnv[config.queueBinding] as Queue
|
|
57
|
+
|
|
58
|
+
// 5a. Schema probe — runs once per Worker instance. On miss, return 503
|
|
59
|
+
// with GL_MAILER_SCHEMA_MISSING so the consumer's site doesn't 500-cascade.
|
|
60
|
+
const schema = await probeMailerSchema(d1, config.d1Binding)
|
|
61
|
+
if (!schema.ok) return schemaMissingResponse()
|
|
62
|
+
|
|
56
63
|
const db = drizzle(d1)
|
|
57
64
|
const env: MailerEnv = { DB: d1, QUEUE: queue }
|
|
58
65
|
|
|
@@ -3,6 +3,7 @@ import { config } from 'virtual:growth-labs/mailer/config'
|
|
|
3
3
|
import type { APIRoute } from 'astro'
|
|
4
4
|
import { and, eq, inArray } from 'drizzle-orm'
|
|
5
5
|
import { drizzle } from 'drizzle-orm/d1'
|
|
6
|
+
import { probeMailerSchema } from '../_internal/schema-probe.js'
|
|
6
7
|
import { emailSends } from '../schema.js'
|
|
7
8
|
import { emitMailerAnalyticsEvent } from '../utils/analytics.js'
|
|
8
9
|
|
|
@@ -25,24 +26,30 @@ export const GET: APIRoute = async (context) => {
|
|
|
25
26
|
return new Response('Invalid URL', { status: 400 })
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
// Update status to 'clicked' only when it hasn't already reached 'clicked'
|
|
29
|
+
// Update status to 'clicked' only when it hasn't already reached 'clicked'.
|
|
30
|
+
// On schema miss the probe logs GL_MAILER_SCHEMA_MISSING and we skip the
|
|
31
|
+
// D1 update — but always still 302 to the destination so we don't break
|
|
32
|
+
// the user's actual click intent.
|
|
29
33
|
try {
|
|
30
34
|
const bindingsEnv = cloudflareEnv as Record<string, unknown>
|
|
31
35
|
if (bindingsEnv) {
|
|
32
36
|
const d1 = bindingsEnv[config.d1Binding] as D1Database
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
37
|
+
const schema = await probeMailerSchema(d1, config.d1Binding)
|
|
38
|
+
if (schema.ok) {
|
|
39
|
+
const db = drizzle(d1)
|
|
40
|
+
await db
|
|
41
|
+
.update(emailSends)
|
|
42
|
+
.set({
|
|
43
|
+
status: 'clicked',
|
|
44
|
+
clickedAt: new Date().toISOString(),
|
|
45
|
+
})
|
|
46
|
+
.where(
|
|
47
|
+
and(
|
|
48
|
+
eq(emailSends.trackingId, trackingId),
|
|
49
|
+
inArray(emailSends.status, ['sent', 'delivered', 'opened']),
|
|
50
|
+
),
|
|
51
|
+
)
|
|
52
|
+
}
|
|
46
53
|
}
|
|
47
54
|
} catch {
|
|
48
55
|
// Never fail the redirect on DB errors
|
package/src/routes/track-open.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { config } from 'virtual:growth-labs/mailer/config'
|
|
|
3
3
|
import type { APIRoute } from 'astro'
|
|
4
4
|
import { and, eq, inArray } from 'drizzle-orm'
|
|
5
5
|
import { drizzle } from 'drizzle-orm/d1'
|
|
6
|
+
import { probeMailerSchema } from '../_internal/schema-probe.js'
|
|
6
7
|
import { emailSends } from '../schema.js'
|
|
7
8
|
import { emitMailerAnalyticsEvent } from '../utils/analytics.js'
|
|
8
9
|
import { TRANSPARENT_GIF } from '../utils/tracking.js'
|
|
@@ -15,24 +16,29 @@ export const GET: APIRoute = async (context) => {
|
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
// Update status to 'opened' only when currently 'sent' or 'delivered'
|
|
18
|
-
// to avoid downgrading from 'clicked'.
|
|
19
|
+
// to avoid downgrading from 'clicked'. On schema miss, the probe logs
|
|
20
|
+
// GL_MAILER_SCHEMA_MISSING and we skip the D1 work — but always still
|
|
21
|
+
// return the transparent GIF so we don't break email rendering.
|
|
19
22
|
try {
|
|
20
23
|
const bindingsEnv = cloudflareEnv as Record<string, unknown>
|
|
21
24
|
if (bindingsEnv) {
|
|
22
25
|
const d1 = bindingsEnv[config.d1Binding] as D1Database
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
26
|
+
const schema = await probeMailerSchema(d1, config.d1Binding)
|
|
27
|
+
if (schema.ok) {
|
|
28
|
+
const db = drizzle(d1)
|
|
29
|
+
await db
|
|
30
|
+
.update(emailSends)
|
|
31
|
+
.set({
|
|
32
|
+
status: 'opened',
|
|
33
|
+
openedAt: new Date().toISOString(),
|
|
34
|
+
})
|
|
35
|
+
.where(
|
|
36
|
+
and(
|
|
37
|
+
eq(emailSends.trackingId, trackingId),
|
|
38
|
+
inArray(emailSends.status, ['sent', 'delivered']),
|
|
39
|
+
),
|
|
40
|
+
)
|
|
41
|
+
}
|
|
36
42
|
}
|
|
37
43
|
} catch {
|
|
38
44
|
// Never fail the pixel response on DB errors
|
|
@@ -2,6 +2,7 @@ import { env as cloudflareEnv } from 'cloudflare:workers'
|
|
|
2
2
|
import { config } from 'virtual:growth-labs/mailer/config'
|
|
3
3
|
import type { APIRoute } from 'astro'
|
|
4
4
|
import { drizzle } from 'drizzle-orm/d1'
|
|
5
|
+
import { probeMailerSchema, schemaMissingResponse } from '../_internal/schema-probe.js'
|
|
5
6
|
import { emitMailerAnalyticsEvent } from '../utils/analytics.js'
|
|
6
7
|
import type { MailerEnv } from '../utils/send.js'
|
|
7
8
|
import { sendTransactional } from '../utils/send.js'
|
|
@@ -24,6 +25,12 @@ async function processUnsubscribe(
|
|
|
24
25
|
const bindingsEnv = cloudflareEnv as Record<string, unknown>
|
|
25
26
|
const d1 = bindingsEnv[config.d1Binding] as D1Database
|
|
26
27
|
const queue = bindingsEnv[config.queueBinding] as Queue
|
|
28
|
+
|
|
29
|
+
// Schema probe — short-circuit with a Response on miss; both GET and POST
|
|
30
|
+
// handlers below forward it unchanged.
|
|
31
|
+
const schema = await probeMailerSchema(d1, config.d1Binding)
|
|
32
|
+
if (!schema.ok) return { schemaMissing: true as const }
|
|
33
|
+
|
|
27
34
|
const db = drizzle(d1)
|
|
28
35
|
const env: MailerEnv = { DB: d1, QUEUE: queue }
|
|
29
36
|
|
|
@@ -73,6 +80,7 @@ export const GET: APIRoute = async (context) => {
|
|
|
73
80
|
}
|
|
74
81
|
|
|
75
82
|
const result = await processUnsubscribe(token, context, request)
|
|
83
|
+
if ('schemaMissing' in result) return schemaMissingResponse()
|
|
76
84
|
if ('error' in result) {
|
|
77
85
|
return Response.json({ error: result.error }, { status: result.status })
|
|
78
86
|
}
|
|
@@ -105,6 +113,7 @@ export const POST: APIRoute = async (context) => {
|
|
|
105
113
|
}
|
|
106
114
|
|
|
107
115
|
const result = await processUnsubscribe(token, context, request)
|
|
116
|
+
if ('schemaMissing' in result) return schemaMissingResponse()
|
|
108
117
|
if ('error' in result) {
|
|
109
118
|
return new Response(result.error, { status: result.status })
|
|
110
119
|
}
|