@hogsend/db 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/drizzle/0018_webhooks.sql +41 -0
- package/drizzle/meta/0018_snapshot.json +3603 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +1 -1
- package/src/schema/enums.ts +8 -0
- package/src/schema/index.ts +2 -0
- package/src/schema/relations.ts +19 -0
- package/src/schema/webhook-deliveries.ts +58 -0
- package/src/schema/webhook-endpoints.ts +42 -0
package/package.json
CHANGED
package/src/schema/enums.ts
CHANGED
|
@@ -50,3 +50,11 @@ export const bucketMembershipStatusEnum = pgEnum("bucket_membership_status", [
|
|
|
50
50
|
"active",
|
|
51
51
|
"left",
|
|
52
52
|
]);
|
|
53
|
+
|
|
54
|
+
export const webhookDeliveryStatusEnum = pgEnum("webhook_delivery_status", [
|
|
55
|
+
"pending", // enqueued, awaiting first attempt OR a scheduled retry (nextRetryAt)
|
|
56
|
+
"sending", // a delivery run CAS'd the row and is mid-POST (orphan-recovery sentinel)
|
|
57
|
+
"delivered", // 2xx received — TERMINAL
|
|
58
|
+
"failed", // attempts exhausted — TERMINAL, mirrored to dead_letter_queue
|
|
59
|
+
"discarded", // endpoint disabled/deleted mid-flight — TERMINAL, NOT an error, NOT dead-lettered
|
|
60
|
+
]);
|
package/src/schema/index.ts
CHANGED
package/src/schema/relations.ts
CHANGED
|
@@ -25,6 +25,8 @@ import { journeyStates } from "./journey-states.js";
|
|
|
25
25
|
import { linkClicks } from "./link-clicks.js";
|
|
26
26
|
import { trackedLinks } from "./tracked-links.js";
|
|
27
27
|
import { userEvents } from "./user-events.js";
|
|
28
|
+
import { webhookDeliveries } from "./webhook-deliveries.js";
|
|
29
|
+
import { webhookEndpoints } from "./webhook-endpoints.js";
|
|
28
30
|
|
|
29
31
|
export const alertRulesRelations = relations(alertRules, ({ many }) => ({
|
|
30
32
|
history: many(alertHistory),
|
|
@@ -142,6 +144,23 @@ export const linkClicksRelations = relations(linkClicks, ({ one }) => ({
|
|
|
142
144
|
}),
|
|
143
145
|
}));
|
|
144
146
|
|
|
147
|
+
export const webhookEndpointsRelations = relations(
|
|
148
|
+
webhookEndpoints,
|
|
149
|
+
({ many }) => ({
|
|
150
|
+
deliveries: many(webhookDeliveries),
|
|
151
|
+
}),
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
export const webhookDeliveriesRelations = relations(
|
|
155
|
+
webhookDeliveries,
|
|
156
|
+
({ one }) => ({
|
|
157
|
+
endpoint: one(webhookEndpoints, {
|
|
158
|
+
fields: [webhookDeliveries.endpointId],
|
|
159
|
+
references: [webhookEndpoints.id],
|
|
160
|
+
}),
|
|
161
|
+
}),
|
|
162
|
+
);
|
|
163
|
+
|
|
145
164
|
export const userRelations = relations(user, ({ many }) => ({
|
|
146
165
|
sessions: many(session),
|
|
147
166
|
accounts: many(account),
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import {
|
|
2
|
+
index,
|
|
3
|
+
integer,
|
|
4
|
+
jsonb,
|
|
5
|
+
pgTable,
|
|
6
|
+
text,
|
|
7
|
+
timestamp,
|
|
8
|
+
uniqueIndex,
|
|
9
|
+
uuid,
|
|
10
|
+
} from "drizzle-orm/pg-core";
|
|
11
|
+
import { timestamps } from "./_shared.js";
|
|
12
|
+
import { webhookDeliveryStatusEnum } from "./enums.js";
|
|
13
|
+
import { webhookEndpoints } from "./webhook-endpoints.js";
|
|
14
|
+
|
|
15
|
+
export const webhookDeliveries = pgTable(
|
|
16
|
+
"webhook_deliveries",
|
|
17
|
+
{
|
|
18
|
+
// internal PK ONLY — NOT the Webhook-Id header.
|
|
19
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
20
|
+
endpointId: uuid("endpoint_id")
|
|
21
|
+
.notNull()
|
|
22
|
+
.references(() => webhookEndpoints.id, { onDelete: "cascade" }),
|
|
23
|
+
// denormalized, nullable (MT deferred).
|
|
24
|
+
organizationId: text("organization_id"),
|
|
25
|
+
// == Webhook-Id header; ONE per logical event, shared across endpoints +
|
|
26
|
+
// reused across retries.
|
|
27
|
+
webhookId: text("webhook_id").notNull(),
|
|
28
|
+
eventType: text("event_type").notNull(),
|
|
29
|
+
// producer-side dedup (idempotencyKey/stateId/emailSendId/...).
|
|
30
|
+
dedupeKey: text("dedupe_key"),
|
|
31
|
+
// the EXACT signed envelope { id, type, timestamp, data }.
|
|
32
|
+
payload: jsonb("payload").$type<Record<string, unknown>>().notNull(),
|
|
33
|
+
status: webhookDeliveryStatusEnum("status").notNull().default("pending"),
|
|
34
|
+
attemptCount: integer("attempt_count").notNull().default(0),
|
|
35
|
+
nextRetryAt: timestamp("next_retry_at", { withTimezone: true }),
|
|
36
|
+
lastAttemptAt: timestamp("last_attempt_at", { withTimezone: true }),
|
|
37
|
+
responseStatus: integer("response_status"),
|
|
38
|
+
// truncated to ≤1KB in app.
|
|
39
|
+
responseBodySnippet: text("response_body_snippet"),
|
|
40
|
+
deliveredAt: timestamp("delivered_at", { withTimezone: true }),
|
|
41
|
+
lastError: text("last_error"),
|
|
42
|
+
...timestamps,
|
|
43
|
+
},
|
|
44
|
+
(table) => [
|
|
45
|
+
index("webhook_deliveries_endpoint_idx").on(table.endpointId),
|
|
46
|
+
// reaper sweep: due-pending + stale-sending recovery.
|
|
47
|
+
index("webhook_deliveries_status_next_retry_idx").on(
|
|
48
|
+
table.status,
|
|
49
|
+
table.nextRetryAt,
|
|
50
|
+
),
|
|
51
|
+
// producer-side fan-out idempotency. PARTIAL-effective: Postgres treats
|
|
52
|
+
// multiple NULL dedupeKey as distinct, so undeduped events are never blocked.
|
|
53
|
+
uniqueIndex("webhook_deliveries_endpoint_dedupe_idx").on(
|
|
54
|
+
table.endpointId,
|
|
55
|
+
table.dedupeKey,
|
|
56
|
+
),
|
|
57
|
+
],
|
|
58
|
+
);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {
|
|
2
|
+
boolean,
|
|
3
|
+
index,
|
|
4
|
+
jsonb,
|
|
5
|
+
pgTable,
|
|
6
|
+
text,
|
|
7
|
+
timestamp,
|
|
8
|
+
uuid,
|
|
9
|
+
} from "drizzle-orm/pg-core";
|
|
10
|
+
import { timestamps } from "./_shared.js";
|
|
11
|
+
|
|
12
|
+
// Locally declared to avoid an engine→db dependency cycle: the engine's
|
|
13
|
+
// `webhook-signing.ts` owns the authoritative `WEBHOOK_EVENT_TYPES` tuple +
|
|
14
|
+
// `WebhookEventType` union. This schema keeps a structural string alias so the
|
|
15
|
+
// jsonb column is typed without importing the engine.
|
|
16
|
+
export type WebhookEventType = string;
|
|
17
|
+
|
|
18
|
+
export const webhookEndpoints = pgTable(
|
|
19
|
+
"webhook_endpoints",
|
|
20
|
+
{
|
|
21
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
22
|
+
organizationId: text("organization_id"),
|
|
23
|
+
url: text("url").notNull(),
|
|
24
|
+
description: text("description"),
|
|
25
|
+
// whsec_<base64url> PLAINTEXT (recoverable; re-signed every delivery).
|
|
26
|
+
secret: text("secret").notNull(),
|
|
27
|
+
// e.g. "whsec_AbCd" — safe to show on list/get.
|
|
28
|
+
secretPrefix: text("secret_prefix").notNull(),
|
|
29
|
+
eventTypes: jsonb("event_types")
|
|
30
|
+
.$type<WebhookEventType[]>()
|
|
31
|
+
.notNull()
|
|
32
|
+
.default([]),
|
|
33
|
+
disabled: boolean("disabled").notNull().default(false),
|
|
34
|
+
// written by the delivery task on a successful (2xx) delivery.
|
|
35
|
+
lastDeliveryAt: timestamp("last_delivery_at", { withTimezone: true }),
|
|
36
|
+
...timestamps,
|
|
37
|
+
},
|
|
38
|
+
(table) => [
|
|
39
|
+
index("webhook_endpoints_org_idx").on(table.organizationId),
|
|
40
|
+
index("webhook_endpoints_disabled_idx").on(table.disabled),
|
|
41
|
+
],
|
|
42
|
+
);
|