feat(webhooks): add per-verb subscriptions and bulk-create debounce
Subscriptions now declare a `verb` (create | delete | bulk-create). POST /:did/webhooks defaults to inserting both create and delete rows when no verb is given, preserving existing all-events behavior. Update events fold into the create verb. The new bulk-create verb debounces creates per DID over 400 ms and delivers a `records` array. Migration adds the verb column with default 'create' and clones every existing row for the delete verb so legacy subscriptions keep firing on both events.
This commit is contained in:
@@ -87,26 +87,30 @@ export const saveCursor = (cursor: number) => {
|
||||
);
|
||||
};
|
||||
|
||||
export type WebhookVerb = "create" | "delete" | "bulk-create";
|
||||
|
||||
type WebhookSubscriptionRow = {
|
||||
id: number;
|
||||
did: string;
|
||||
method: string;
|
||||
url: string;
|
||||
token?: string;
|
||||
verb: WebhookVerb;
|
||||
};
|
||||
|
||||
export const addWebhookSubscription = (
|
||||
{ did, method, url, token }: Omit<WebhookSubscriptionRow, "id">,
|
||||
{ did, method, url, token, verb }: Omit<WebhookSubscriptionRow, "id">,
|
||||
): WebhookSubscriptionRow => {
|
||||
db.exec(
|
||||
"INSERT INTO webhook_subscription (did, method, url, token) VALUES (?, ?, ?, ?)",
|
||||
"INSERT INTO webhook_subscription (did, method, url, token, verb) VALUES (?, ?, ?, ?, ?)",
|
||||
did,
|
||||
method,
|
||||
url,
|
||||
token ?? null,
|
||||
verb,
|
||||
);
|
||||
return db.prepare(
|
||||
"SELECT id, did, method, url FROM webhook_subscription WHERE id = last_insert_rowid()",
|
||||
"SELECT id, did, method, url, verb FROM webhook_subscription WHERE id = last_insert_rowid()",
|
||||
).get<WebhookSubscriptionRow>()!;
|
||||
// Note: token is intentionally excluded from the SELECT (write-only)
|
||||
};
|
||||
@@ -115,10 +119,13 @@ export const deleteWebhooksByDid = (did: string): void => {
|
||||
db.exec("DELETE FROM webhook_subscription WHERE did = ?", did);
|
||||
};
|
||||
|
||||
export const getWebhooksByDid = (did: string): WebhookSubscriptionRow[] => {
|
||||
export const getWebhooksByDidAndVerb = (
|
||||
did: string,
|
||||
verb: WebhookVerb,
|
||||
): WebhookSubscriptionRow[] => {
|
||||
return db.prepare(
|
||||
"SELECT id, did, method, url, token FROM webhook_subscription WHERE did = ? ORDER BY id DESC LIMIT 10",
|
||||
).all<WebhookSubscriptionRow>(did);
|
||||
"SELECT id, did, method, url, token, verb FROM webhook_subscription WHERE did = ? AND verb = ? ORDER BY id DESC LIMIT 10",
|
||||
).all<WebhookSubscriptionRow>(did, verb);
|
||||
};
|
||||
|
||||
export const upsertNote = (note: Note) => {
|
||||
|
||||
@@ -61,4 +61,23 @@ try {
|
||||
// Column already exists — no-op
|
||||
}
|
||||
|
||||
try {
|
||||
db.exec(
|
||||
`ALTER TABLE webhook_subscription ADD COLUMN verb TEXT NOT NULL DEFAULT 'create';`,
|
||||
);
|
||||
db.exec(`
|
||||
INSERT INTO webhook_subscription (did, method, url, token, verb)
|
||||
SELECT did, method, url, token, 'delete'
|
||||
FROM webhook_subscription
|
||||
WHERE verb = 'create';
|
||||
`);
|
||||
} catch {
|
||||
// Column already exists — backfill already happened on a previous run.
|
||||
}
|
||||
|
||||
db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_webhook_subscription_did_verb
|
||||
ON webhook_subscription(did, verb);
|
||||
`);
|
||||
|
||||
db.close();
|
||||
|
||||
Reference in New Issue
Block a user