@hogsend/db 0.7.0 → 0.9.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/0019_flippant_songbird.sql +4 -0
- package/drizzle/meta/0018_snapshot.json +3603 -0
- package/drizzle/meta/0019_snapshot.json +3616 -0
- package/drizzle/meta/_journal.json +14 -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 +55 -0
|
@@ -127,6 +127,20 @@
|
|
|
127
127
|
"when": 1780841637175,
|
|
128
128
|
"tag": "0017_long_spacker_dave",
|
|
129
129
|
"breakpoints": true
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"idx": 18,
|
|
133
|
+
"version": "7",
|
|
134
|
+
"when": 1780855021994,
|
|
135
|
+
"tag": "0018_webhooks",
|
|
136
|
+
"breakpoints": true
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"idx": 19,
|
|
140
|
+
"version": "7",
|
|
141
|
+
"when": 1780907482971,
|
|
142
|
+
"tag": "0019_flippant_songbird",
|
|
143
|
+
"breakpoints": true
|
|
130
144
|
}
|
|
131
145
|
]
|
|
132
146
|
}
|
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,55 @@
|
|
|
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
|
+
// The delivery adapter selector. "webhook" (the default) is the signed
|
|
26
|
+
// Standard-Webhooks POST that existing subscribers receive — byte-identical
|
|
27
|
+
// to before this column existed. Any other value (e.g. "posthog") selects a
|
|
28
|
+
// delivery-time TRANSFORM adapter that reuses the same durable delivery
|
|
29
|
+
// machinery but rewrites url/headers/body for a vendor destination.
|
|
30
|
+
kind: text("kind").notNull().default("webhook"),
|
|
31
|
+
// Per-destination configuration for keyed adapters (e.g. PostHog's
|
|
32
|
+
// `{ apiKey, host }`). Null for `kind="webhook"` (it reads `secret` instead).
|
|
33
|
+
// Keyed destinations keep their credentials HERE, not in a fake `whsec_`.
|
|
34
|
+
config: jsonb("config").$type<Record<string, unknown>>(),
|
|
35
|
+
// whsec_<base64url> PLAINTEXT (recoverable; re-signed every delivery).
|
|
36
|
+
// Nullable: only `kind="webhook"` carries a signing secret; keyed
|
|
37
|
+
// destinations authenticate via `config` and the webhook adapter is the only
|
|
38
|
+
// reader of this column.
|
|
39
|
+
secret: text("secret"),
|
|
40
|
+
// e.g. "whsec_AbCd" — safe to show on list/get. Nullable alongside `secret`.
|
|
41
|
+
secretPrefix: text("secret_prefix"),
|
|
42
|
+
eventTypes: jsonb("event_types")
|
|
43
|
+
.$type<WebhookEventType[]>()
|
|
44
|
+
.notNull()
|
|
45
|
+
.default([]),
|
|
46
|
+
disabled: boolean("disabled").notNull().default(false),
|
|
47
|
+
// written by the delivery task on a successful (2xx) delivery.
|
|
48
|
+
lastDeliveryAt: timestamp("last_delivery_at", { withTimezone: true }),
|
|
49
|
+
...timestamps,
|
|
50
|
+
},
|
|
51
|
+
(table) => [
|
|
52
|
+
index("webhook_endpoints_org_idx").on(table.organizationId),
|
|
53
|
+
index("webhook_endpoints_disabled_idx").on(table.disabled),
|
|
54
|
+
],
|
|
55
|
+
);
|