Files
kan/packages/db/src/schema/webhooks.ts
Nick Meinhold 33d2f23955 feat(db): add webhook schema, migration, and repository (#391)
* feat(db): add webhook schema, migration, and repository

Add the database foundation for workspace webhooks:

- Add workspace_webhooks table with migration (webhook_event enum,
  URL, secret, event subscriptions, active flag)
- Add webhook repository with CRUD operations
- Add webhooks schema definition with relations
- Extend card and list repos to return board/list names for
  webhook payload context

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(db): remove unused webhook_event enum and fix migration timestamp

- Remove dead webhook_event pgEnum from schema (events column uses text)
- Remove CREATE TYPE statement from migration SQL
- Fix migration journal timestamp to be chronologically after the
  notifications migration (idx 25)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(db): return boardPublicId from card and list repo queries

Add board publicId to getWorkspaceAndCardIdByCardPublicId and
getWorkspaceAndListIdByListPublicId return values, needed for
correct boardId in webhook payloads.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(db): add workspaceId index and document secret exposure

- Add index on workspaceId for efficient webhook lookups per workspace
- Add JSDoc comment on getActiveByWorkspaceId explaining that it
  returns secrets for server-side HMAC signing only and must never
  be exposed via client-facing endpoints

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(db): extract parseEvents helper in webhook repo

DRY up 6 repeated JSON.parse-and-cast calls into a single helper
function, addressing reviewer feedback on PR #391.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 13:23:00 +00:00

60 lines
1.7 KiB
TypeScript

import { relations } from "drizzle-orm";
import {
bigint,
bigserial,
boolean,
index,
pgTable,
text,
timestamp,
uuid,
varchar,
} from "drizzle-orm/pg-core";
import { users } from "./users";
import { workspaces } from "./workspaces";
export const webhookEvents = [
"card.created",
"card.updated",
"card.moved",
"card.deleted",
] as const;
export type WebhookEvent = (typeof webhookEvents)[number];
export const workspaceWebhooks = pgTable("workspace_webhooks", {
id: bigserial("id", { mode: "number" }).primaryKey(),
publicId: varchar("publicId", { length: 12 }).notNull().unique(),
workspaceId: bigint("workspaceId", { mode: "number" })
.notNull()
.references(() => workspaces.id, { onDelete: "cascade" }),
name: varchar("name", { length: 255 }).notNull(),
url: varchar("url", { length: 2048 }).notNull(),
secret: text("secret"),
events: text("events").notNull(), // JSON array of webhook events
active: boolean("active").notNull().default(true),
createdBy: uuid("createdBy")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt"),
}, (table) => [
index("workspace_webhooks_workspace_idx").on(table.workspaceId),
]).enableRLS();
export const workspaceWebhooksRelations = relations(
workspaceWebhooks,
({ one }) => ({
workspace: one(workspaces, {
fields: [workspaceWebhooks.workspaceId],
references: [workspaces.id],
relationName: "workspaceWebhooksWorkspace",
}),
createdByUser: one(users, {
fields: [workspaceWebhooks.createdBy],
references: [users.id],
relationName: "workspaceWebhooksCreatedByUser",
}),
}),
);