diff --git a/app/(app)/companies/[id]/deals/page.tsx b/app/(app)/companies/[id]/deals/page.tsx
index 1196738..a75cb15 100644
--- a/app/(app)/companies/[id]/deals/page.tsx
+++ b/app/(app)/companies/[id]/deals/page.tsx
@@ -12,6 +12,7 @@ export default async function DealsTab({ params }: { params: Promise<{ id: strin
name: deals.name,
value: deals.value,
stageName: stages.name,
+ stageColor: stages.color,
stageEnteredAt: deals.stageEnteredAt,
expectedCloseDate: deals.expectedCloseDate,
})
@@ -50,7 +51,7 @@ export default async function DealsTab({ params }: { params: Promise<{ id: strin
>
{d.name}
-
+
{money(d.value)}
diff --git a/app/(app)/companies/[id]/page.tsx b/app/(app)/companies/[id]/page.tsx
index d160485..b9b97b7 100644
--- a/app/(app)/companies/[id]/page.tsx
+++ b/app/(app)/companies/[id]/page.tsx
@@ -44,6 +44,7 @@ export default async function OverviewTab({ params }: { params: Promise<{ id: st
name: deals.name,
value: deals.value,
stageName: stages.name,
+ stageColor: stages.color,
})
.from(deals)
.innerJoin(stages, eq(deals.stageId, stages.id))
@@ -169,7 +170,7 @@ export default async function OverviewTab({ params }: { params: Promise<{ id: st
diff --git a/app/(app)/deals/deals-view.tsx b/app/(app)/deals/deals-view.tsx
index 441ef3f..05499a4 100644
--- a/app/(app)/deals/deals-view.tsx
+++ b/app/(app)/deals/deals-view.tsx
@@ -45,7 +45,7 @@ export function DealsView({
}: {
pipelineName: string;
view: "list" | "board";
- stages: (KanbanStage & { isWon: boolean; isLost: boolean })[];
+ stages: (KanbanStage & { color: string | null; isWon: boolean; isLost: boolean })[];
deals: DealRow[];
truncatedAt: number | null;
}) {
@@ -63,6 +63,7 @@ export function DealsView({
return {
...d,
stageName: stage?.name ?? "Unknown",
+ stageColor: stage?.color ?? null,
isWon: stage?.isWon ?? false,
isLost: stage?.isLost ?? false,
daysInStage: days,
@@ -226,7 +227,7 @@ export function DealsView({
{money(d.value)}
-
+
{FORECAST[d.stageName] ?? "Pipeline"}
@@ -256,7 +257,7 @@ export function DealsView({
) : (
({ id: s.id, name: s.name }))}
+ stages={stages.map((s) => ({ id: s.id, name: s.name, color: s.color }))}
initialDeals={deals}
/>
diff --git a/app/(app)/deals/kanban.tsx b/app/(app)/deals/kanban.tsx
index 03f7864..e2dc87f 100644
--- a/app/(app)/deals/kanban.tsx
+++ b/app/(app)/deals/kanban.tsx
@@ -3,7 +3,7 @@
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useState, type DragEvent } from "react";
-import { TemperaturePill } from "@/components/pills";
+import { StagePill, TemperaturePill } from "@/components/pills";
import { money, relativeTime } from "@/lib/format";
import { cn } from "@/lib/utils";
import { moveDealStageAction } from "./actions";
@@ -22,6 +22,7 @@ export type KanbanDeal = {
export type KanbanStage = {
id: string;
name: string;
+ color?: string | null;
};
export function KanbanBoard({
@@ -89,7 +90,7 @@ export function KanbanBoard({
onDrop={(e) => onDrop(e, stage.id)}
>
- {stage.name}
+
{list.length} · {money(total)}
diff --git a/app/(app)/deals/page.tsx b/app/(app)/deals/page.tsx
index fbd3eb9..757adf7 100644
--- a/app/(app)/deals/page.tsx
+++ b/app/(app)/deals/page.tsx
@@ -56,6 +56,7 @@ export default async function DealsPage({
stages={stageRows.map((s) => ({
id: s.id,
name: s.name,
+ color: s.color,
isWon: s.isWon,
isLost: s.isLost,
}))}
diff --git a/app/(app)/page.tsx b/app/(app)/page.tsx
index 018d5ad..502d9b9 100644
--- a/app/(app)/page.tsx
+++ b/app/(app)/page.tsx
@@ -52,6 +52,7 @@ export default async function CockpitPage() {
value: deals.value,
stageEnteredAt: deals.stageEnteredAt,
stageName: stages.name,
+ stageColor: stages.color,
companyId: deals.companyId,
companyName: companies.name,
temperature: companies.temperature,
@@ -219,7 +220,7 @@ export default async function CockpitPage() {
{money(d.value)}
-
+
(data: T): { ok: true; data: T } {
+ return { ok: true, data };
+}
+function err(code: StageOpError["code"], message: string): { ok: false; code: StageOpError["code"]; message: string } {
+ return { ok: false, code, message };
+}
+function mapError(e: unknown): { ok: false; code: StageOpError["code"]; message: string } {
+ if (e instanceof StageOpError) return err(e.code, e.message);
+ throw e;
+}
+
+function revalidateAll(): void {
+ revalidatePath("/settings/pipelines");
+ revalidatePath("/deals");
+ revalidatePath("/companies", "layout");
+}
+
+export async function addStageAction(formData: FormData) {
+ const session = await requireOrgSession();
+ const pipelineId = z.string().uuid().parse(formData.get("pipelineId"));
+ const name = NameSchema.parse(formData.get("name"));
+ const color = normalizeColor(formData.get("color"));
+ const isWon = formData.get("isWon") === "true";
+ const isLost = formData.get("isLost") === "true";
+
+ try {
+ const result = await createStage(db(), session.organizationId, {
+ pipelineId,
+ name,
+ color: color ?? null,
+ isWon,
+ isLost,
+ });
+ await recordAudit(db(), {
+ organizationId: session.organizationId,
+ actor: userActor(session.user.id, session.user.name ?? null),
+ entityType: "stage",
+ entityId: result.after.id,
+ action: "create",
+ changes: { after: result.after },
+ });
+ revalidateAll();
+ return ok(result.after);
+ } catch (e) {
+ return mapError(e);
+ }
+}
+
+export async function updateStageAction(formData: FormData) {
+ const session = await requireOrgSession();
+ const id = z.string().uuid().parse(formData.get("id"));
+ const nameRaw = formData.get("name");
+ const colorRaw = formData.get("color");
+ const isWonRaw = formData.get("isWon");
+ const isLostRaw = formData.get("isLost");
+
+ try {
+ const result = await updateStage(db(), session.organizationId, {
+ id,
+ name: nameRaw === null ? undefined : NameSchema.parse(nameRaw),
+ color: colorRaw === null ? undefined : normalizeColor(colorRaw),
+ isWon: isWonRaw === null ? undefined : isWonRaw === "true",
+ isLost: isLostRaw === null ? undefined : isLostRaw === "true",
+ });
+ if (!result) return err("not_found", "Stage not found");
+ await recordAudit(db(), {
+ organizationId: session.organizationId,
+ actor: userActor(session.user.id, session.user.name ?? null),
+ entityType: "stage",
+ entityId: result.after.id,
+ action: "update",
+ changes: diffChangedFields(result.before, result.after),
+ });
+ revalidateAll();
+ return ok(result.after);
+ } catch (e) {
+ return mapError(e);
+ }
+}
+
+const DirectionSchema = z.enum(["up", "down"]);
+
+export async function reorderStageAction(input: {
+ stageId: string;
+ direction: ReorderDirection;
+}) {
+ const session = await requireOrgSession();
+ const stageId = z.string().uuid().parse(input.stageId);
+ const direction = DirectionSchema.parse(input.direction);
+
+ try {
+ const result = await reorderStage(db(), session.organizationId, stageId, direction);
+ if (!result) return err("not_found", "Stage not found");
+ if (result.before.id === result.after.id && result.before.order === result.after.order) {
+ // boundary no-op; nothing to audit, nothing to revalidate
+ return ok(result.after);
+ }
+ await recordAudit(db(), {
+ organizationId: session.organizationId,
+ actor: userActor(session.user.id, session.user.name ?? null),
+ entityType: "stage",
+ entityId: result.after.id,
+ action: "update",
+ changes: { before: { order: result.before.order }, after: { order: result.after.order } },
+ });
+ revalidateAll();
+ return ok(result.after);
+ } catch (e) {
+ return mapError(e);
+ }
+}
+
+export async function deleteStageAction(input: {
+ stageId: string;
+ destinationStageId: string;
+}) {
+ const session = await requireOrgSession();
+ const stageId = z.string().uuid().parse(input.stageId);
+ const destinationStageId = z.string().uuid().parse(input.destinationStageId);
+
+ try {
+ const result = await deleteStage(db(), session.organizationId, stageId, destinationStageId);
+ if (!result) return err("not_found", "Stage not found");
+
+ const actor = userActor(session.user.id, session.user.name ?? null);
+ for (const dealId of result.migratedDealIds) {
+ await recordAudit(db(), {
+ organizationId: session.organizationId,
+ actor,
+ entityType: "deal",
+ entityId: dealId,
+ action: "update",
+ changes: { before: { stageId }, after: { stageId: destinationStageId } },
+ });
+ }
+ await recordAudit(db(), {
+ organizationId: session.organizationId,
+ actor,
+ entityType: "stage",
+ entityId: stageId,
+ action: "delete",
+ changes: { before: result.before },
+ });
+ revalidateAll();
+ return ok({ migratedDealCount: result.migratedDealIds.length });
+ } catch (e) {
+ return mapError(e);
+ }
+}
+
+export async function previewStageFlagToggleAction(input: { stageId: string }) {
+ const session = await requireOrgSession();
+ const stageId = z.string().uuid().parse(input.stageId);
+ const result = await previewStageFlagToggle(db(), session.organizationId, stageId);
+ if (!result) return err("not_found", "Stage not found");
+ return ok(result);
+}
+
diff --git a/app/(app)/settings/pipelines/delete-stage-dialog.tsx b/app/(app)/settings/pipelines/delete-stage-dialog.tsx
new file mode 100644
index 0000000..8ed1bda
--- /dev/null
+++ b/app/(app)/settings/pipelines/delete-stage-dialog.tsx
@@ -0,0 +1,105 @@
+"use client";
+
+import { useEffect, useState, useTransition } from "react";
+import { deleteStageAction, previewStageFlagToggleAction } from "./actions";
+import type { StageEditorRow } from "./stages-editor";
+
+export function DeleteStageDialog({
+ stage,
+ siblings,
+ onClose,
+ onError,
+}: {
+ stage: StageEditorRow;
+ siblings: StageEditorRow[];
+ onClose: () => void;
+ onError: (msg: string) => void;
+}) {
+ const others = siblings.filter((s) => s.id !== stage.id);
+ const [destinationId, setDestinationId] = useState(others[0]?.id ?? "");
+ const [dealCount, setDealCount] = useState(null);
+ const [pending, startTransition] = useTransition();
+
+ useEffect(() => {
+ let cancelled = false;
+ void previewStageFlagToggleAction({ stageId: stage.id }).then((res) => {
+ if (cancelled) return;
+ if (res.ok) setDealCount(res.data.affectedDealCount);
+ });
+ return () => {
+ cancelled = true;
+ };
+ }, [stage.id]);
+
+ function onConfirm() {
+ if (!destinationId) return;
+ startTransition(async () => {
+ const result = await deleteStageAction({ stageId: stage.id, destinationStageId: destinationId });
+ if (!result.ok) {
+ onError(result.message);
+ return;
+ }
+ onClose();
+ });
+ }
+
+ return (
+
+
+
+ Delete “{stage.name}”
+
+
+ {dealCount === null
+ ? "Counting deals on this stage…"
+ : dealCount === 0
+ ? "No deals are on this stage. Pick where future deals would land if they were."
+ : `${dealCount} deal${dealCount === 1 ? "" : "s"} will be moved to the chosen stage before this one is deleted.`}
+
+
+
+ Move deals to
+
+ setDestinationId(e.target.value)}
+ className="w-full rounded-md border border-border bg-background px-2 py-1.5 text-[13px] text-text outline-none focus:border-accent"
+ >
+ {others.length === 0 ? (
+ No other stages — cannot delete
+ ) : (
+ others.map((s) => (
+
+ {s.name}
+
+ ))
+ )}
+
+
+
+
+ Cancel
+
+
+ {pending ? "Deleting…" : "Delete"}
+
+
+
+
+ );
+}
diff --git a/app/(app)/settings/pipelines/page.tsx b/app/(app)/settings/pipelines/page.tsx
index 4e013e5..ef1f4d4 100644
--- a/app/(app)/settings/pipelines/page.tsx
+++ b/app/(app)/settings/pipelines/page.tsx
@@ -4,6 +4,8 @@ import { pipelines } from "@/db/schema/deals";
import { Badge } from "@/components/pills";
import { getStages } from "@/lib/data";
import { requireOrgSession } from "@/lib/session";
+import { isStageColor, type StageColor } from "@/lib/stage-colors";
+import { StagesEditor, type StageEditorRow } from "./stages-editor";
export default async function PipelinesSettingsPage() {
const session = await requireOrgSession();
@@ -16,22 +18,19 @@ export default async function PipelinesSettingsPage() {
const out = await Promise.all(
ps.map(async (p) => {
const sgs = await getStages(p.id, orgId);
- return {
- id: p.id,
- name: p.name,
- isDefault: p.isDefault,
- stages: sgs.map((s) => ({
- id: s.id,
- name: s.name,
- isWon: s.isWon,
- isLost: s.isLost,
- })),
- };
+ const rows: StageEditorRow[] = sgs.map((s) => ({
+ id: s.id,
+ name: s.name,
+ color: isStageColor(s.color) ? (s.color as StageColor) : null,
+ isWon: s.isWon,
+ isLost: s.isLost,
+ }));
+ return { id: p.id, name: p.name, isDefault: p.isDefault, rows };
}),
);
return (
-
+
{out.map((p) => (
-
+
{p.name}
{p.isDefault ? Default : null}
-
- {p.stages.map((s, i) => (
-
-
-
-
- {s.name}
- {s.isWon ? (
-
- is_won
-
- ) : null}
- {s.isLost ? (
-
- is_lost
-
- ) : null}
-
- ))}
-
+
))}
-
- Reorder, add, and rename stages lands in Phase 4.
-
);
}
diff --git a/app/(app)/settings/pipelines/stage-row.tsx b/app/(app)/settings/pipelines/stage-row.tsx
new file mode 100644
index 0000000..90774b6
--- /dev/null
+++ b/app/(app)/settings/pipelines/stage-row.tsx
@@ -0,0 +1,249 @@
+"use client";
+
+import { useState, useTransition, type KeyboardEvent } from "react";
+import { ChevronDown, ChevronUp, Trash2 } from "lucide-react";
+import { StagePill } from "@/components/pills";
+import {
+ STAGE_COLORS,
+ STAGE_COLOR_TOKENS,
+ type StageColor,
+} from "@/lib/stage-colors";
+import { previewStageFlagToggleAction, reorderStageAction, updateStageAction } from "./actions";
+import { DeleteStageDialog } from "./delete-stage-dialog";
+import type { StageEditorRow } from "./stages-editor";
+
+export function StageRow({
+ stage,
+ isFirst,
+ isLast,
+ siblings,
+}: {
+ stage: StageEditorRow;
+ isFirst: boolean;
+ isLast: boolean;
+ siblings: StageEditorRow[];
+}) {
+ const [editing, setEditing] = useState(false);
+ const [name, setName] = useState(stage.name);
+ const [colorPickerOpen, setColorPickerOpen] = useState(false);
+
+ function startEdit() {
+ setName(stage.name);
+ setEditing(true);
+ }
+ const [deleteOpen, setDeleteOpen] = useState(false);
+ const [error, setError] = useState
(null);
+ const [pending, startTransition] = useTransition();
+
+ function commitName() {
+ const trimmed = name.trim();
+ if (!trimmed || trimmed === stage.name) {
+ setName(stage.name);
+ setEditing(false);
+ return;
+ }
+ startTransition(async () => {
+ const fd = new FormData();
+ fd.set("id", stage.id);
+ fd.set("name", trimmed);
+ const result = await updateStageAction(fd);
+ if (!result.ok) {
+ setError(result.message);
+ setName(stage.name);
+ }
+ setEditing(false);
+ });
+ }
+
+ function pickColor(next: StageColor | null) {
+ setColorPickerOpen(false);
+ setError(null);
+ startTransition(async () => {
+ const fd = new FormData();
+ fd.set("id", stage.id);
+ fd.set("color", next ?? "");
+ const result = await updateStageAction(fd);
+ if (!result.ok) setError(result.message);
+ });
+ }
+
+ function setFlag(flag: "isWon" | "isLost", value: boolean) {
+ setError(null);
+ startTransition(async () => {
+ if (value) {
+ const preview = await previewStageFlagToggleAction({ stageId: stage.id });
+ if (preview.ok && preview.data.affectedDealCount > 0) {
+ const proceed = window.confirm(
+ `${preview.data.affectedDealCount} deal${
+ preview.data.affectedDealCount === 1 ? "" : "s"
+ } currently in this stage will be re-classified. Continue?`,
+ );
+ if (!proceed) return;
+ }
+ }
+ const fd = new FormData();
+ fd.set("id", stage.id);
+ fd.set(flag, value ? "true" : "false");
+ // Sending the other flag explicitly off when turning one on prevents the DB CHECK
+ // from rejecting a transient both-true state.
+ if (value) {
+ const other = flag === "isWon" ? "isLost" : "isWon";
+ if ((other === "isWon" && stage.isWon) || (other === "isLost" && stage.isLost)) {
+ fd.set(other, "false");
+ }
+ }
+ const result = await updateStageAction(fd);
+ if (!result.ok) setError(result.message);
+ });
+ }
+
+ function reorder(direction: "up" | "down") {
+ setError(null);
+ startTransition(async () => {
+ const result = await reorderStageAction({ stageId: stage.id, direction });
+ if (!result.ok) setError(result.message);
+ });
+ }
+
+ function onNameKey(e: KeyboardEvent) {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ commitName();
+ } else if (e.key === "Escape") {
+ e.preventDefault();
+ setName(stage.name);
+ setEditing(false);
+ }
+ }
+
+ return (
+
+
+ reorder("up")}
+ className="text-text-subtle hover:text-text disabled:opacity-25"
+ >
+
+
+ reorder("down")}
+ className="text-text-subtle hover:text-text disabled:opacity-25"
+ >
+
+
+
+
+
+
setColorPickerOpen((v) => !v)}
+ disabled={pending}
+ className="h-4 w-4 rounded-full border border-border-subtle"
+ style={{
+ background: stage.color
+ ? STAGE_COLOR_TOKENS[stage.color].text
+ : "transparent",
+ }}
+ />
+ {colorPickerOpen ? (
+
+ {STAGE_COLORS.map((c) => (
+ pickColor(c)}
+ className="h-5 w-5 rounded-full border border-border-subtle hover:scale-110"
+ style={{ background: STAGE_COLOR_TOKENS[c].text }}
+ />
+ ))}
+ pickColor(null)}
+ className="h-5 rounded-md border border-border-subtle px-1.5 text-[10px] text-text-subtle hover:text-text"
+ >
+ clear
+
+
+ ) : null}
+
+
+ {editing ? (
+ setName(e.target.value)}
+ onBlur={commitName}
+ onKeyDown={onNameKey}
+ className="flex-1 rounded border border-border bg-background px-1.5 py-0.5 text-[13px] text-text outline-none focus:border-accent"
+ disabled={pending}
+ />
+ ) : (
+
+
+
+ )}
+
+ setFlag("isWon", !stage.isWon)}
+ disabled={pending}
+ className={`rounded px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider ${
+ stage.isWon
+ ? "bg-emerald-900/40 text-emerald-300"
+ : "bg-surface-active text-text-subtle hover:text-text"
+ }`}
+ >
+ Won
+
+ setFlag("isLost", !stage.isLost)}
+ disabled={pending}
+ className={`rounded px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider ${
+ stage.isLost
+ ? "bg-red-900/40 text-red-300"
+ : "bg-surface-active text-text-subtle hover:text-text"
+ }`}
+ >
+ Lost
+
+
+ setDeleteOpen(true)}
+ disabled={pending}
+ aria-label="Delete stage"
+ className="text-text-subtle opacity-0 group-hover:opacity-100 hover:text-red-400"
+ >
+
+
+
+ {deleteOpen ? (
+ setDeleteOpen(false)}
+ onError={(msg) => setError(msg)}
+ />
+ ) : null}
+
+ {error ? (
+
+ {error}
+
+ ) : null}
+
+ );
+}
diff --git a/app/(app)/settings/pipelines/stages-editor.tsx b/app/(app)/settings/pipelines/stages-editor.tsx
new file mode 100644
index 0000000..7921f35
--- /dev/null
+++ b/app/(app)/settings/pipelines/stages-editor.tsx
@@ -0,0 +1,112 @@
+"use client";
+
+import { useState, useTransition, type FormEvent } from "react";
+import { Plus } from "lucide-react";
+import type { StageColor } from "@/lib/stage-colors";
+import { addStageAction } from "./actions";
+import { StageRow } from "./stage-row";
+
+export type StageEditorRow = {
+ id: string;
+ name: string;
+ color: StageColor | null;
+ isWon: boolean;
+ isLost: boolean;
+};
+
+export function StagesEditor({
+ pipelineId,
+ initialStages,
+}: {
+ pipelineId: string;
+ initialStages: StageEditorRow[];
+}) {
+ const [adding, setAdding] = useState(false);
+ const [newName, setNewName] = useState("");
+ const [error, setError] = useState(null);
+ const [pending, startTransition] = useTransition();
+
+ function onAdd(e: FormEvent) {
+ e.preventDefault();
+ setError(null);
+ const trimmed = newName.trim();
+ if (!trimmed) return;
+ startTransition(async () => {
+ const fd = new FormData();
+ fd.set("pipelineId", pipelineId);
+ fd.set("name", trimmed);
+ const result = await addStageAction(fd);
+ if (!result.ok) {
+ setError(result.message);
+ return;
+ }
+ setNewName("");
+ setAdding(false);
+ });
+ }
+
+ return (
+
+
+ {initialStages.map((s, i) => (
+
+ ))}
+
+
+ {adding ? (
+
+ ) : (
+
setAdding(true)}
+ className="flex items-center gap-1.5 pt-2 text-[12px] text-text-subtle hover:text-text"
+ >
+
+ Add stage
+
+ )}
+
+ {error ?
{error}
: null}
+
+ );
+}
diff --git a/components/pills.tsx b/components/pills.tsx
index 6cdd381..12295ef 100644
--- a/components/pills.tsx
+++ b/components/pills.tsx
@@ -1,3 +1,4 @@
+import { isStageColor, STAGE_COLOR_TOKENS, type StageColor } from "@/lib/stage-colors";
import { cn } from "@/lib/utils";
const TEMP_CONFIG: Record<
@@ -126,19 +127,30 @@ export function PersonaPill({ value, className }: { value: string; className?: s
export function StagePill({
value,
+ color,
className,
}: {
value: string | null | undefined;
+ color?: StageColor | string | null;
className?: string;
}) {
if (!value) {
return — ;
}
+ if (isStageColor(color)) {
+ const tokens = STAGE_COLOR_TOKENS[color];
+ return (
+
+ {value}
+
+ );
+ }
const key = value.toLowerCase().replace(/\s+/g, "_");
- const cfg = STAGE_CONFIG[key] ?? STAGE_CONFIG.prospecting!;
+ const matched = STAGE_CONFIG[key];
+ const tokens = matched ?? STAGE_CONFIG.prospecting!;
return (
-
- {cfg.label === key ? value : cfg.label}
+
+ {matched ? matched.label : value}
);
}
diff --git a/db/migrations/0006_stage-color-and-flag-check.sql b/db/migrations/0006_stage-color-and-flag-check.sql
new file mode 100644
index 0000000..7e05bbe
--- /dev/null
+++ b/db/migrations/0006_stage-color-and-flag-check.sql
@@ -0,0 +1,2 @@
+ALTER TABLE "stages" ADD COLUMN "color" text;--> statement-breakpoint
+ALTER TABLE "stages" ADD CONSTRAINT "stage_not_both_won_lost" CHECK (NOT ("stages"."is_won" AND "stages"."is_lost"));
\ No newline at end of file
diff --git a/db/migrations/meta/0006_snapshot.json b/db/migrations/meta/0006_snapshot.json
new file mode 100644
index 0000000..9ebb422
--- /dev/null
+++ b/db/migrations/meta/0006_snapshot.json
@@ -0,0 +1,3136 @@
+{
+ "id": "694b4e8e-d5dc-46ce-8fc6-9fe94609ddd3",
+ "prevId": "399ef332-19c2-49b5-8570-95513f69bcaa",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.account": {
+ "name": "account",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "id_token": {
+ "name": "id_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_token_expires_at": {
+ "name": "access_token_expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token_expires_at": {
+ "name": "refresh_token_expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "account_user_id_user_id_fk": {
+ "name": "account_user_id_user_id_fk",
+ "tableFrom": "account",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.invitation": {
+ "name": "invitation",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "inviter_id": {
+ "name": "inviter_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "invitation_organization_id_organization_id_fk": {
+ "name": "invitation_organization_id_organization_id_fk",
+ "tableFrom": "invitation",
+ "tableTo": "organization",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "invitation_inviter_id_user_id_fk": {
+ "name": "invitation_inviter_id_user_id_fk",
+ "tableFrom": "invitation",
+ "tableTo": "user",
+ "columnsFrom": [
+ "inviter_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.member": {
+ "name": "member",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'member'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "member_user_org_unique": {
+ "name": "member_user_org_unique",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "member_user_id_user_id_fk": {
+ "name": "member_user_id_user_id_fk",
+ "tableFrom": "member",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "member_organization_id_organization_id_fk": {
+ "name": "member_organization_id_organization_id_fk",
+ "tableFrom": "member",
+ "tableTo": "organization",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.organization": {
+ "name": "organization",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slug": {
+ "name": "slug",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "logo": {
+ "name": "logo",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "organization_slug_unique": {
+ "name": "organization_slug_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "slug"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.session": {
+ "name": "session",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ip_address": {
+ "name": "ip_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_agent": {
+ "name": "user_agent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "active_organization_id": {
+ "name": "active_organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "session_user_id_user_id_fk": {
+ "name": "session_user_id_user_id_fk",
+ "tableFrom": "session",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "session_token_unique": {
+ "name": "session_token_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "token"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user": {
+ "name": "user",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email_verified": {
+ "name": "email_verified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "user_email_unique": {
+ "name": "user_email_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "email"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.verification": {
+ "name": "verification",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "identifier": {
+ "name": "identifier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.companies": {
+ "name": "companies",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "domain": {
+ "name": "domain",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "industry": {
+ "name": "industry",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "employee_count": {
+ "name": "employee_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "temperature": {
+ "name": "temperature",
+ "type": "temperature",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "temperature_updated_at": {
+ "name": "temperature_updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_signal_at": {
+ "name": "last_signal_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "companies_org_domain_unique": {
+ "name": "companies_org_domain_unique",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "domain",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "companies_organization_id_organization_id_fk": {
+ "name": "companies_organization_id_organization_id_fk",
+ "tableFrom": "companies",
+ "tableTo": "organization",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.people": {
+ "name": "people",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "company_id": {
+ "name": "company_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "linkedin_url": {
+ "name": "linkedin_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "persona": {
+ "name": "persona",
+ "type": "persona",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'unknown'"
+ },
+ "engagement": {
+ "name": "engagement",
+ "type": "temperature",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_interaction_at": {
+ "name": "last_interaction_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "people_organization_id_organization_id_fk": {
+ "name": "people_organization_id_organization_id_fk",
+ "tableFrom": "people",
+ "tableTo": "organization",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "people_company_id_companies_id_fk": {
+ "name": "people_company_id_companies_id_fk",
+ "tableFrom": "people",
+ "tableTo": "companies",
+ "columnsFrom": [
+ "company_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.research": {
+ "name": "research",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "company_id": {
+ "name": "company_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "section": {
+ "name": "section",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "body": {
+ "name": "body",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_url": {
+ "name": "source_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "research_company_section_unique": {
+ "name": "research_company_section_unique",
+ "columns": [
+ {
+ "expression": "company_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "section",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "research_organization_id_organization_id_fk": {
+ "name": "research_organization_id_organization_id_fk",
+ "tableFrom": "research",
+ "tableTo": "organization",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "research_company_id_companies_id_fk": {
+ "name": "research_company_id_companies_id_fk",
+ "tableFrom": "research",
+ "tableTo": "companies",
+ "columnsFrom": [
+ "company_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.signals": {
+ "name": "signals",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "company_id": {
+ "name": "company_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "person_id": {
+ "name": "person_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source_url": {
+ "name": "source_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "occurred_at": {
+ "name": "occurred_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "signals_org_company_occurred_idx": {
+ "name": "signals_org_company_occurred_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "company_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "occurred_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "signals_organization_id_organization_id_fk": {
+ "name": "signals_organization_id_organization_id_fk",
+ "tableFrom": "signals",
+ "tableTo": "organization",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "signals_company_id_companies_id_fk": {
+ "name": "signals_company_id_companies_id_fk",
+ "tableFrom": "signals",
+ "tableTo": "companies",
+ "columnsFrom": [
+ "company_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "signals_person_id_people_id_fk": {
+ "name": "signals_person_id_people_id_fk",
+ "tableFrom": "signals",
+ "tableTo": "people",
+ "columnsFrom": [
+ "person_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.deals": {
+ "name": "deals",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "company_id": {
+ "name": "company_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "pipeline_id": {
+ "name": "pipeline_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "stage_id": {
+ "name": "stage_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "numeric(14, 2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expected_close_date": {
+ "name": "expected_close_date",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "owner_user_id": {
+ "name": "owner_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "stage_entered_at": {
+ "name": "stage_entered_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "deals_organization_id_organization_id_fk": {
+ "name": "deals_organization_id_organization_id_fk",
+ "tableFrom": "deals",
+ "tableTo": "organization",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "deals_company_id_companies_id_fk": {
+ "name": "deals_company_id_companies_id_fk",
+ "tableFrom": "deals",
+ "tableTo": "companies",
+ "columnsFrom": [
+ "company_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "deals_pipeline_id_pipelines_id_fk": {
+ "name": "deals_pipeline_id_pipelines_id_fk",
+ "tableFrom": "deals",
+ "tableTo": "pipelines",
+ "columnsFrom": [
+ "pipeline_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ },
+ "deals_stage_id_stages_id_fk": {
+ "name": "deals_stage_id_stages_id_fk",
+ "tableFrom": "deals",
+ "tableTo": "stages",
+ "columnsFrom": [
+ "stage_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ },
+ "deals_owner_user_id_user_id_fk": {
+ "name": "deals_owner_user_id_user_id_fk",
+ "tableFrom": "deals",
+ "tableTo": "user",
+ "columnsFrom": [
+ "owner_user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.pipelines": {
+ "name": "pipelines",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_default": {
+ "name": "is_default",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "pipelines_org_name_unique": {
+ "name": "pipelines_org_name_unique",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "pipelines_organization_id_organization_id_fk": {
+ "name": "pipelines_organization_id_organization_id_fk",
+ "tableFrom": "pipelines",
+ "tableTo": "organization",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.stages": {
+ "name": "stages",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "pipeline_id": {
+ "name": "pipeline_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "order": {
+ "name": "order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "color": {
+ "name": "color",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_won": {
+ "name": "is_won",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "is_lost": {
+ "name": "is_lost",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "stages_pipeline_name_unique": {
+ "name": "stages_pipeline_name_unique",
+ "columns": [
+ {
+ "expression": "pipeline_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "stages_pipeline_id_pipelines_id_fk": {
+ "name": "stages_pipeline_id_pipelines_id_fk",
+ "tableFrom": "stages",
+ "tableTo": "pipelines",
+ "columnsFrom": [
+ "pipeline_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "stage_not_both_won_lost": {
+ "name": "stage_not_both_won_lost",
+ "value": "NOT (\"stages\".\"is_won\" AND \"stages\".\"is_lost\")"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.tasks": {
+ "name": "tasks",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "company_id": {
+ "name": "company_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "person_id": {
+ "name": "person_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "deal_id": {
+ "name": "deal_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reasoning": {
+ "name": "reasoning",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "priority": {
+ "name": "priority",
+ "type": "task_priority",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'medium'"
+ },
+ "type": {
+ "name": "type",
+ "type": "task_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'other'"
+ },
+ "status": {
+ "name": "status",
+ "type": "task_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'open'"
+ },
+ "due_date": {
+ "name": "due_date",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "assignee_user_id": {
+ "name": "assignee_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "tasks_org_status_priority_idx": {
+ "name": "tasks_org_status_priority_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "priority",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "tasks_organization_id_organization_id_fk": {
+ "name": "tasks_organization_id_organization_id_fk",
+ "tableFrom": "tasks",
+ "tableTo": "organization",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "tasks_company_id_companies_id_fk": {
+ "name": "tasks_company_id_companies_id_fk",
+ "tableFrom": "tasks",
+ "tableTo": "companies",
+ "columnsFrom": [
+ "company_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "tasks_person_id_people_id_fk": {
+ "name": "tasks_person_id_people_id_fk",
+ "tableFrom": "tasks",
+ "tableTo": "people",
+ "columnsFrom": [
+ "person_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "tasks_deal_id_deals_id_fk": {
+ "name": "tasks_deal_id_deals_id_fk",
+ "tableFrom": "tasks",
+ "tableTo": "deals",
+ "columnsFrom": [
+ "deal_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "tasks_assignee_user_id_user_id_fk": {
+ "name": "tasks_assignee_user_id_user_id_fk",
+ "tableFrom": "tasks",
+ "tableTo": "user",
+ "columnsFrom": [
+ "assignee_user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.meeting_attendees": {
+ "name": "meeting_attendees",
+ "schema": "",
+ "columns": {
+ "meeting_id": {
+ "name": "meeting_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "person_id": {
+ "name": "person_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "meeting_attendees_meeting_id_meetings_id_fk": {
+ "name": "meeting_attendees_meeting_id_meetings_id_fk",
+ "tableFrom": "meeting_attendees",
+ "tableTo": "meetings",
+ "columnsFrom": [
+ "meeting_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "meeting_attendees_person_id_people_id_fk": {
+ "name": "meeting_attendees_person_id_people_id_fk",
+ "tableFrom": "meeting_attendees",
+ "tableTo": "people",
+ "columnsFrom": [
+ "person_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "meeting_attendees_meeting_id_person_id_pk": {
+ "name": "meeting_attendees_meeting_id_person_id_pk",
+ "columns": [
+ "meeting_id",
+ "person_id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.meetings": {
+ "name": "meetings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "company_id": {
+ "name": "company_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "scheduled_at": {
+ "name": "scheduled_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "duration_minutes": {
+ "name": "duration_minutes",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "summary": {
+ "name": "summary",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "recording_url": {
+ "name": "recording_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "meetings_organization_id_organization_id_fk": {
+ "name": "meetings_organization_id_organization_id_fk",
+ "tableFrom": "meetings",
+ "tableTo": "organization",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "meetings_company_id_companies_id_fk": {
+ "name": "meetings_company_id_companies_id_fk",
+ "tableFrom": "meetings",
+ "tableTo": "companies",
+ "columnsFrom": [
+ "company_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.notes": {
+ "name": "notes",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "company_id": {
+ "name": "company_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "person_id": {
+ "name": "person_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "deal_id": {
+ "name": "deal_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "author": {
+ "name": "author",
+ "type": "note_author",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'user'"
+ },
+ "author_user_id": {
+ "name": "author_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "body": {
+ "name": "body",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "notes_organization_id_organization_id_fk": {
+ "name": "notes_organization_id_organization_id_fk",
+ "tableFrom": "notes",
+ "tableTo": "organization",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "notes_company_id_companies_id_fk": {
+ "name": "notes_company_id_companies_id_fk",
+ "tableFrom": "notes",
+ "tableTo": "companies",
+ "columnsFrom": [
+ "company_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "notes_person_id_people_id_fk": {
+ "name": "notes_person_id_people_id_fk",
+ "tableFrom": "notes",
+ "tableTo": "people",
+ "columnsFrom": [
+ "person_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "notes_deal_id_deals_id_fk": {
+ "name": "notes_deal_id_deals_id_fk",
+ "tableFrom": "notes",
+ "tableTo": "deals",
+ "columnsFrom": [
+ "deal_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "notes_author_user_id_user_id_fk": {
+ "name": "notes_author_user_id_user_id_fk",
+ "tableFrom": "notes",
+ "tableTo": "user",
+ "columnsFrom": [
+ "author_user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.app_settings": {
+ "name": "app_settings",
+ "schema": "",
+ "columns": {
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "app_settings_org_key_unique": {
+ "name": "app_settings_org_key_unique",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "app_settings_organization_id_organization_id_fk": {
+ "name": "app_settings_organization_id_organization_id_fk",
+ "tableFrom": "app_settings",
+ "tableTo": "organization",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.service_tokens": {
+ "name": "service_tokens",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token_hash": {
+ "name": "token_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_by_user_id": {
+ "name": "created_by_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_used_at": {
+ "name": "last_used_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "revoked_at": {
+ "name": "revoked_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "service_tokens_organization_id_organization_id_fk": {
+ "name": "service_tokens_organization_id_organization_id_fk",
+ "tableFrom": "service_tokens",
+ "tableTo": "organization",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "service_tokens_created_by_user_id_user_id_fk": {
+ "name": "service_tokens_created_by_user_id_user_id_fk",
+ "tableFrom": "service_tokens",
+ "tableTo": "user",
+ "columnsFrom": [
+ "created_by_user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "service_tokens_token_hash_unique": {
+ "name": "service_tokens_token_hash_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "token_hash"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.jwks": {
+ "name": "jwks",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "public_key": {
+ "name": "public_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "private_key": {
+ "name": "private_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.oauth_access_token": {
+ "name": "oauth_access_token",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "client_id": {
+ "name": "client_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "session_id": {
+ "name": "session_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reference_id": {
+ "name": "reference_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_id": {
+ "name": "refresh_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "scopes": {
+ "name": "scopes",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "oauth_access_token_client_id_oauth_client_id_fk": {
+ "name": "oauth_access_token_client_id_oauth_client_id_fk",
+ "tableFrom": "oauth_access_token",
+ "tableTo": "oauth_client",
+ "columnsFrom": [
+ "client_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "oauth_access_token_session_id_session_id_fk": {
+ "name": "oauth_access_token_session_id_session_id_fk",
+ "tableFrom": "oauth_access_token",
+ "tableTo": "session",
+ "columnsFrom": [
+ "session_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "oauth_access_token_user_id_user_id_fk": {
+ "name": "oauth_access_token_user_id_user_id_fk",
+ "tableFrom": "oauth_access_token",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "oauth_access_token_refresh_id_oauth_refresh_token_id_fk": {
+ "name": "oauth_access_token_refresh_id_oauth_refresh_token_id_fk",
+ "tableFrom": "oauth_access_token",
+ "tableTo": "oauth_refresh_token",
+ "columnsFrom": [
+ "refresh_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "oauth_access_token_token_unique": {
+ "name": "oauth_access_token_token_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "token"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.oauth_client": {
+ "name": "oauth_client",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "client_id": {
+ "name": "client_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "client_secret": {
+ "name": "client_secret",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "disabled": {
+ "name": "disabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "skip_consent": {
+ "name": "skip_consent",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enable_end_session": {
+ "name": "enable_end_session",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "subject_type": {
+ "name": "subject_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scopes": {
+ "name": "scopes",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "uri": {
+ "name": "uri",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "icon": {
+ "name": "icon",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "contacts": {
+ "name": "contacts",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tos": {
+ "name": "tos",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "policy": {
+ "name": "policy",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "software_id": {
+ "name": "software_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "software_version": {
+ "name": "software_version",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "software_statement": {
+ "name": "software_statement",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "redirect_uris": {
+ "name": "redirect_uris",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "post_logout_redirect_uris": {
+ "name": "post_logout_redirect_uris",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "token_endpoint_auth_method": {
+ "name": "token_endpoint_auth_method",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "grant_types": {
+ "name": "grant_types",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "response_types": {
+ "name": "response_types",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "public": {
+ "name": "public",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "require_pkce": {
+ "name": "require_pkce",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reference_id": {
+ "name": "reference_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "oauth_client_user_id_user_id_fk": {
+ "name": "oauth_client_user_id_user_id_fk",
+ "tableFrom": "oauth_client",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "oauth_client_client_id_unique": {
+ "name": "oauth_client_client_id_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "client_id"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.oauth_consent": {
+ "name": "oauth_consent",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "client_id": {
+ "name": "client_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reference_id": {
+ "name": "reference_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scopes": {
+ "name": "scopes",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "oauth_consent_client_id_oauth_client_id_fk": {
+ "name": "oauth_consent_client_id_oauth_client_id_fk",
+ "tableFrom": "oauth_consent",
+ "tableTo": "oauth_client",
+ "columnsFrom": [
+ "client_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "oauth_consent_user_id_user_id_fk": {
+ "name": "oauth_consent_user_id_user_id_fk",
+ "tableFrom": "oauth_consent",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.oauth_refresh_token": {
+ "name": "oauth_refresh_token",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "client_id": {
+ "name": "client_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "session_id": {
+ "name": "session_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reference_id": {
+ "name": "reference_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "revoked": {
+ "name": "revoked",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "auth_time": {
+ "name": "auth_time",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scopes": {
+ "name": "scopes",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "oauth_refresh_token_client_id_oauth_client_id_fk": {
+ "name": "oauth_refresh_token_client_id_oauth_client_id_fk",
+ "tableFrom": "oauth_refresh_token",
+ "tableTo": "oauth_client",
+ "columnsFrom": [
+ "client_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "oauth_refresh_token_session_id_session_id_fk": {
+ "name": "oauth_refresh_token_session_id_session_id_fk",
+ "tableFrom": "oauth_refresh_token",
+ "tableTo": "session",
+ "columnsFrom": [
+ "session_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "oauth_refresh_token_user_id_user_id_fk": {
+ "name": "oauth_refresh_token_user_id_user_id_fk",
+ "tableFrom": "oauth_refresh_token",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.oauth_consent_scope": {
+ "name": "oauth_consent_scope",
+ "schema": "",
+ "columns": {
+ "consent_code": {
+ "name": "consent_code",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "resource": {
+ "name": "resource",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "oauth_consent_scope_user_id_user_id_fk": {
+ "name": "oauth_consent_scope_user_id_user_id_fk",
+ "tableFrom": "oauth_consent_scope",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "oauth_consent_scope_organization_id_organization_id_fk": {
+ "name": "oauth_consent_scope_organization_id_organization_id_fk",
+ "tableFrom": "oauth_consent_scope",
+ "tableTo": "organization",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.audit_log": {
+ "name": "audit_log",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "actor_type": {
+ "name": "actor_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "actor_user_id": {
+ "name": "actor_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "actor_user_name": {
+ "name": "actor_user_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "actor_token_id": {
+ "name": "actor_token_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "actor_token_name": {
+ "name": "actor_token_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "actor_client_id": {
+ "name": "actor_client_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "entity_type": {
+ "name": "entity_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entity_id": {
+ "name": "entity_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "action": {
+ "name": "action",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "changes": {
+ "name": "changes",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "audit_log_org_created_idx": {
+ "name": "audit_log_org_created_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "audit_log_org_actor_user_idx": {
+ "name": "audit_log_org_actor_user_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "actor_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "audit_log_org_entity_created_idx": {
+ "name": "audit_log_org_entity_created_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "audit_log_organization_id_organization_id_fk": {
+ "name": "audit_log_organization_id_organization_id_fk",
+ "tableFrom": "audit_log",
+ "tableTo": "organization",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "audit_log_actor_user_id_user_id_fk": {
+ "name": "audit_log_actor_user_id_user_id_fk",
+ "tableFrom": "audit_log",
+ "tableTo": "user",
+ "columnsFrom": [
+ "actor_user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "audit_log_actor_token_id_service_tokens_id_fk": {
+ "name": "audit_log_actor_token_id_service_tokens_id_fk",
+ "tableFrom": "audit_log",
+ "tableTo": "service_tokens",
+ "columnsFrom": [
+ "actor_token_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.org_invite_link": {
+ "name": "org_invite_link",
+ "schema": "",
+ "columns": {
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'member'"
+ },
+ "created_by_user_id": {
+ "name": "created_by_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "used_at": {
+ "name": "used_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "used_by_user_id": {
+ "name": "used_by_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "revoked_at": {
+ "name": "revoked_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "org_invite_link_organization_id_organization_id_fk": {
+ "name": "org_invite_link_organization_id_organization_id_fk",
+ "tableFrom": "org_invite_link",
+ "tableTo": "organization",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "org_invite_link_created_by_user_id_user_id_fk": {
+ "name": "org_invite_link_created_by_user_id_user_id_fk",
+ "tableFrom": "org_invite_link",
+ "tableTo": "user",
+ "columnsFrom": [
+ "created_by_user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "org_invite_link_used_by_user_id_user_id_fk": {
+ "name": "org_invite_link_used_by_user_id_user_id_fk",
+ "tableFrom": "org_invite_link",
+ "tableTo": "user",
+ "columnsFrom": [
+ "used_by_user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {
+ "public.note_author": {
+ "name": "note_author",
+ "schema": "public",
+ "values": [
+ "user",
+ "agent"
+ ]
+ },
+ "public.persona": {
+ "name": "persona",
+ "schema": "public",
+ "values": [
+ "champion",
+ "decision_maker",
+ "technical_evaluator",
+ "end_user",
+ "unknown"
+ ]
+ },
+ "public.task_priority": {
+ "name": "task_priority",
+ "schema": "public",
+ "values": [
+ "urgent",
+ "high",
+ "medium",
+ "low"
+ ]
+ },
+ "public.task_status": {
+ "name": "task_status",
+ "schema": "public",
+ "values": [
+ "open",
+ "done",
+ "dismissed"
+ ]
+ },
+ "public.task_type": {
+ "name": "task_type",
+ "schema": "public",
+ "values": [
+ "call",
+ "email",
+ "linkedin",
+ "research",
+ "other"
+ ]
+ },
+ "public.temperature": {
+ "name": "temperature",
+ "schema": "public",
+ "values": [
+ "cold",
+ "warm",
+ "hot",
+ "on_fire"
+ ]
+ }
+ },
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
\ No newline at end of file
diff --git a/db/migrations/meta/_journal.json b/db/migrations/meta/_journal.json
index cd33790..07d23a3 100644
--- a/db/migrations/meta/_journal.json
+++ b/db/migrations/meta/_journal.json
@@ -43,6 +43,13 @@
"when": 1778584083358,
"tag": "0005_add-member-user-org-unique",
"breakpoints": true
+ },
+ {
+ "idx": 6,
+ "version": "7",
+ "when": 1778592383831,
+ "tag": "0006_stage-color-and-flag-check",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/db/schema/deals.ts b/db/schema/deals.ts
index 465309a..166dbd8 100644
--- a/db/schema/deals.ts
+++ b/db/schema/deals.ts
@@ -1,4 +1,15 @@
-import { boolean, integer, numeric, pgTable, text, timestamp, uniqueIndex, uuid } from "drizzle-orm/pg-core";
+import { sql } from "drizzle-orm";
+import {
+ boolean,
+ check,
+ integer,
+ numeric,
+ pgTable,
+ text,
+ timestamp,
+ uniqueIndex,
+ uuid,
+} from "drizzle-orm/pg-core";
import { organization, user } from "./auth";
import { companies } from "./companies";
@@ -25,11 +36,15 @@ export const stages = pgTable(
.references(() => pipelines.id, { onDelete: "cascade" }),
name: text("name").notNull(),
order: integer("order").notNull(),
+ color: text("color"),
isWon: boolean("is_won").notNull().default(false),
isLost: boolean("is_lost").notNull().default(false),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
},
- (table) => [uniqueIndex("stages_pipeline_name_unique").on(table.pipelineId, table.name)],
+ (table) => [
+ uniqueIndex("stages_pipeline_name_unique").on(table.pipelineId, table.name),
+ check("stage_not_both_won_lost", sql`NOT (${table.isWon} AND ${table.isLost})`),
+ ],
);
export const deals = pgTable("deals", {
diff --git a/lib/audit.ts b/lib/audit.ts
index 39abcdc..a44098e 100644
--- a/lib/audit.ts
+++ b/lib/audit.ts
@@ -14,6 +14,8 @@ export const ENTITY_TYPES = [
"signal",
"research",
"service_token",
+ "stage",
+ "pipeline",
] as const;
export type EntityType = (typeof ENTITY_TYPES)[number];
diff --git a/lib/data.ts b/lib/data.ts
index c7f7a4a..18e3725 100644
--- a/lib/data.ts
+++ b/lib/data.ts
@@ -65,6 +65,7 @@ export const getStages = cache(async (pipelineId: string, organizationId: string
pipelineId: stages.pipelineId,
name: stages.name,
order: stages.order,
+ color: stages.color,
isWon: stages.isWon,
isLost: stages.isLost,
createdAt: stages.createdAt,
diff --git a/lib/mcp/server.ts b/lib/mcp/server.ts
index 5f31208..a306e6e 100644
--- a/lib/mcp/server.ts
+++ b/lib/mcp/server.ts
@@ -7,6 +7,7 @@ import { registerNoteTools } from "./tools/notes";
import { registerPeopleTools } from "./tools/people";
import { registerResearchTools } from "./tools/research";
import { registerSignalTools } from "./tools/signals";
+import { registerStageTools } from "./tools/stages";
import { registerTaskTools } from "./tools/tasks";
import type { McpContext } from "./context";
@@ -24,6 +25,7 @@ export function createMcpServer(ctx: McpContext): McpServer {
registerMeetingTools(server, ctx);
registerDealTools(server, ctx);
registerNoteTools(server, ctx);
+ registerStageTools(server, ctx);
registerAuditTools(server, ctx);
return server;
diff --git a/lib/mcp/tools/stages.ts b/lib/mcp/tools/stages.ts
new file mode 100644
index 0000000..118127e
--- /dev/null
+++ b/lib/mcp/tools/stages.ts
@@ -0,0 +1,182 @@
+import { z } from "zod";
+import { diffChangedFields, recordAudit } from "@/lib/audit";
+import { STAGE_COLORS } from "@/lib/stage-colors";
+import {
+ createStage,
+ deleteStage,
+ reorderStage,
+ StageOpError,
+ updateStage,
+} from "@/lib/stages";
+import type { McpContext } from "../context";
+import { jsonResult } from "../server";
+import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+
+const ColorEnum = z.enum(STAGE_COLORS);
+
+function toolError(e: unknown): ReturnType | null {
+ if (e instanceof StageOpError) return jsonResult({ error: e.code, message: e.message });
+ return null;
+}
+
+export function registerStageTools(server: McpServer, ctx: McpContext): void {
+ server.registerTool(
+ "create_stage",
+ {
+ title: "Create a stage",
+ description:
+ "Add a new stage to the end of a pipeline. Reorder afterwards if a different position is wanted.",
+ inputSchema: {
+ pipelineId: z.string().uuid(),
+ name: z.string().min(1).max(80),
+ color: ColorEnum.optional(),
+ isWon: z.boolean().optional(),
+ isLost: z.boolean().optional(),
+ },
+ },
+ async (args) => {
+ try {
+ const result = await createStage(ctx.db, ctx.organizationId, {
+ pipelineId: args.pipelineId,
+ name: args.name,
+ color: args.color ?? null,
+ isWon: args.isWon,
+ isLost: args.isLost,
+ });
+ await recordAudit(ctx.db, {
+ organizationId: ctx.organizationId,
+ actor: ctx.actor,
+ entityType: "stage",
+ entityId: result.after.id,
+ action: "create",
+ changes: { after: result.after },
+ });
+ return jsonResult({ stage: result.after });
+ } catch (e) {
+ const handled = toolError(e);
+ if (handled) return handled;
+ throw e;
+ }
+ },
+ );
+
+ server.registerTool(
+ "update_stage",
+ {
+ title: "Update a stage",
+ description:
+ "Patch a stage's fields. Pass `color: null` to clear the stage's colour. Omit a field to leave it unchanged.",
+ inputSchema: {
+ id: z.string().uuid(),
+ name: z.string().min(1).max(80).optional(),
+ color: ColorEnum.nullable().optional(),
+ isWon: z.boolean().optional(),
+ isLost: z.boolean().optional(),
+ },
+ },
+ async (args) => {
+ try {
+ const result = await updateStage(ctx.db, ctx.organizationId, {
+ id: args.id,
+ name: args.name,
+ color: args.color,
+ isWon: args.isWon,
+ isLost: args.isLost,
+ });
+ if (!result) return jsonResult({ error: "not_found" });
+ await recordAudit(ctx.db, {
+ organizationId: ctx.organizationId,
+ actor: ctx.actor,
+ entityType: "stage",
+ entityId: result.after.id,
+ action: "update",
+ changes: diffChangedFields(result.before, result.after),
+ });
+ return jsonResult({ stage: result.after });
+ } catch (e) {
+ const handled = toolError(e);
+ if (handled) return handled;
+ throw e;
+ }
+ },
+ );
+
+ server.registerTool(
+ "reorder_stage",
+ {
+ title: "Move a stage up or down within its pipeline",
+ description: "Swap a stage's position with the adjacent stage in the same pipeline.",
+ inputSchema: {
+ id: z.string().uuid(),
+ direction: z.enum(["up", "down"]),
+ },
+ },
+ async ({ id, direction }) => {
+ try {
+ const result = await reorderStage(ctx.db, ctx.organizationId, id, direction);
+ if (!result) return jsonResult({ error: "not_found" });
+ if (result.before.id === result.after.id && result.before.order === result.after.order) {
+ return jsonResult({ stage: result.after, moved: false });
+ }
+ await recordAudit(ctx.db, {
+ organizationId: ctx.organizationId,
+ actor: ctx.actor,
+ entityType: "stage",
+ entityId: result.after.id,
+ action: "update",
+ changes: {
+ before: { order: result.before.order },
+ after: { order: result.after.order },
+ },
+ });
+ return jsonResult({ stage: result.after, moved: true });
+ } catch (e) {
+ const handled = toolError(e);
+ if (handled) return handled;
+ throw e;
+ }
+ },
+ );
+
+ server.registerTool(
+ "delete_stage",
+ {
+ title: "Delete a stage and migrate its deals",
+ description:
+ "Delete a stage. Any deals on the deleted stage are bulk-moved to `destinationStageId` (must be a different stage in the same pipeline). Refuses to delete the only stage in a pipeline.",
+ inputSchema: {
+ id: z.string().uuid(),
+ destinationStageId: z.string().uuid(),
+ },
+ },
+ async ({ id, destinationStageId }) => {
+ try {
+ const result = await deleteStage(ctx.db, ctx.organizationId, id, destinationStageId);
+ if (!result) return jsonResult({ error: "not_found" });
+ for (const dealId of result.migratedDealIds) {
+ await recordAudit(ctx.db, {
+ organizationId: ctx.organizationId,
+ actor: ctx.actor,
+ entityType: "deal",
+ entityId: dealId,
+ action: "update",
+ changes: { before: { stageId: id }, after: { stageId: destinationStageId } },
+ });
+ }
+ await recordAudit(ctx.db, {
+ organizationId: ctx.organizationId,
+ actor: ctx.actor,
+ entityType: "stage",
+ entityId: id,
+ action: "delete",
+ changes: { before: result.before },
+ });
+ return jsonResult({ migrated_deal_count: result.migratedDealIds.length });
+ } catch (e) {
+ const handled = toolError(e);
+ if (handled) return handled;
+ throw e;
+ }
+ },
+ );
+}
diff --git a/lib/stage-colors.ts b/lib/stage-colors.ts
new file mode 100644
index 0000000..0e7ee90
--- /dev/null
+++ b/lib/stage-colors.ts
@@ -0,0 +1,33 @@
+export const STAGE_COLORS = [
+ "slate",
+ "blue",
+ "indigo",
+ "violet",
+ "pink",
+ "rose",
+ "amber",
+ "lime",
+ "emerald",
+ "teal",
+] as const;
+
+export type StageColor = (typeof STAGE_COLORS)[number];
+
+export type StageColorTokens = { bg: string; text: string };
+
+export const STAGE_COLOR_TOKENS: Record = {
+ slate: { bg: "oklch(0.19 0.01 250 / 0.6)", text: "oklch(0.55 0.04 250)" },
+ blue: { bg: "oklch(0.20 0.04 220 / 0.6)", text: "oklch(0.65 0.08 220)" },
+ indigo: { bg: "oklch(0.21 0.06 270 / 0.6)", text: "oklch(0.68 0.13 270)" },
+ violet: { bg: "oklch(0.22 0.06 295 / 0.6)", text: "oklch(0.70 0.13 295)" },
+ pink: { bg: "oklch(0.22 0.07 340 / 0.6)", text: "oklch(0.72 0.14 340)" },
+ rose: { bg: "oklch(0.22 0.07 15 / 0.6)", text: "oklch(0.70 0.14 15)" },
+ amber: { bg: "oklch(0.22 0.07 65 / 0.6)", text: "oklch(0.72 0.14 65)" },
+ lime: { bg: "oklch(0.21 0.07 125 / 0.6)", text: "oklch(0.70 0.14 125)" },
+ emerald: { bg: "oklch(0.20 0.06 155 / 0.6)", text: "oklch(0.65 0.14 155)" },
+ teal: { bg: "oklch(0.20 0.05 195 / 0.6)", text: "oklch(0.65 0.10 195)" },
+};
+
+export function isStageColor(value: unknown): value is StageColor {
+ return typeof value === "string" && (STAGE_COLORS as readonly string[]).includes(value);
+}
diff --git a/lib/stages.ts b/lib/stages.ts
new file mode 100644
index 0000000..5457fe3
--- /dev/null
+++ b/lib/stages.ts
@@ -0,0 +1,275 @@
+import { and, asc, count, eq, sql } from "drizzle-orm";
+import type { Database } from "@/db/client";
+import { deals, pipelines, stages } from "@/db/schema/deals";
+import { isStageColor, type StageColor } from "@/lib/stage-colors";
+
+export type Stage = typeof stages.$inferSelect;
+export type StageInsert = typeof stages.$inferInsert;
+
+export type StageFlagField = "isWon" | "isLost";
+
+export class StageOpError extends Error {
+ readonly code:
+ | "not_found"
+ | "wrong_pipeline"
+ | "last_stage"
+ | "name_in_use"
+ | "invalid_color"
+ | "won_lost_conflict";
+ constructor(code: StageOpError["code"], message: string) {
+ super(message);
+ this.code = code;
+ this.name = "StageOpError";
+ }
+}
+
+export async function assertStageInOrg(
+ db: Database,
+ stageId: string,
+ organizationId: string,
+): Promise {
+ const rows = await db
+ .select({ stage: stages })
+ .from(stages)
+ .innerJoin(pipelines, eq(stages.pipelineId, pipelines.id))
+ .where(and(eq(stages.id, stageId), eq(pipelines.organizationId, organizationId)))
+ .limit(1);
+ return rows[0]?.stage ?? null;
+}
+
+async function assertPipelineInOrg(
+ db: Database,
+ pipelineId: string,
+ organizationId: string,
+): Promise {
+ const rows = await db
+ .select({ id: pipelines.id })
+ .from(pipelines)
+ .where(and(eq(pipelines.id, pipelineId), eq(pipelines.organizationId, organizationId)))
+ .limit(1);
+ return Boolean(rows[0]);
+}
+
+export type CreateStageInput = {
+ pipelineId: string;
+ name: string;
+ color?: StageColor | null;
+ isWon?: boolean;
+ isLost?: boolean;
+};
+
+export async function createStage(
+ db: Database,
+ organizationId: string,
+ input: CreateStageInput,
+): Promise<{ before: null; after: Stage }> {
+ if (input.color != null && !isStageColor(input.color)) {
+ throw new StageOpError("invalid_color", "Unknown stage color");
+ }
+ if (input.isWon && input.isLost) {
+ throw new StageOpError("won_lost_conflict", "Stage cannot be both won and lost");
+ }
+ const ok = await assertPipelineInOrg(db, input.pipelineId, organizationId);
+ if (!ok) throw new StageOpError("not_found", "Pipeline not found");
+
+ const maxRows = await db
+ .select({ max: sql`MAX(${stages.order})` })
+ .from(stages)
+ .where(eq(stages.pipelineId, input.pipelineId));
+ const nextOrder = (maxRows[0]?.max ?? -1) + 1;
+
+ try {
+ const [row] = await db
+ .insert(stages)
+ .values({
+ pipelineId: input.pipelineId,
+ name: input.name,
+ order: nextOrder,
+ color: input.color ?? null,
+ isWon: input.isWon ?? false,
+ isLost: input.isLost ?? false,
+ })
+ .returning();
+ return { before: null, after: row! };
+ } catch (err) {
+ if (isUniqueViolation(err)) {
+ throw new StageOpError("name_in_use", "A stage with that name already exists");
+ }
+ throw err;
+ }
+}
+
+export type UpdateStageInput = {
+ id: string;
+ name?: string;
+ color?: StageColor | null;
+ isWon?: boolean;
+ isLost?: boolean;
+};
+
+export async function updateStage(
+ db: Database,
+ organizationId: string,
+ input: UpdateStageInput,
+): Promise<{ before: Stage; after: Stage } | null> {
+ const before = await assertStageInOrg(db, input.id, organizationId);
+ if (!before) return null;
+
+ if (input.color !== undefined && input.color !== null && !isStageColor(input.color)) {
+ throw new StageOpError("invalid_color", "Unknown stage color");
+ }
+ const nextWon = input.isWon ?? before.isWon;
+ const nextLost = input.isLost ?? before.isLost;
+ if (nextWon && nextLost) {
+ throw new StageOpError("won_lost_conflict", "Stage cannot be both won and lost");
+ }
+
+ const patch: Partial = {};
+ if (input.name !== undefined) patch.name = input.name;
+ if (input.color !== undefined) patch.color = input.color;
+ if (input.isWon !== undefined) patch.isWon = input.isWon;
+ if (input.isLost !== undefined) patch.isLost = input.isLost;
+ if (Object.keys(patch).length === 0) return { before, after: before };
+
+ try {
+ const [row] = await db
+ .update(stages)
+ .set(patch)
+ .where(eq(stages.id, input.id))
+ .returning();
+ if (!row) return null;
+ return { before, after: row };
+ } catch (err) {
+ if (isUniqueViolation(err)) {
+ throw new StageOpError("name_in_use", "A stage with that name already exists");
+ }
+ throw err;
+ }
+}
+
+export type ReorderDirection = "up" | "down";
+
+export async function reorderStage(
+ db: Database,
+ organizationId: string,
+ stageId: string,
+ direction: ReorderDirection,
+): Promise<{ before: Stage; after: Stage; partner: Stage } | null> {
+ const current = await assertStageInOrg(db, stageId, organizationId);
+ if (!current) return null;
+
+ const siblings = await db
+ .select()
+ .from(stages)
+ .where(eq(stages.pipelineId, current.pipelineId))
+ .orderBy(asc(stages.order), asc(stages.id));
+
+ const idx = siblings.findIndex((s) => s.id === stageId);
+ if (idx < 0) return null;
+ const targetIdx = direction === "up" ? idx - 1 : idx + 1;
+ if (targetIdx < 0 || targetIdx >= siblings.length) return { before: current, after: current, partner: current };
+
+ const partner = siblings[targetIdx]!;
+ const currentOrder = current.order;
+ const partnerOrder = partner.order;
+
+ const [updatedCurrent, updatedPartner] = await db.transaction(async (tx) => {
+ const [a] = await tx
+ .update(stages)
+ .set({ order: partnerOrder })
+ .where(eq(stages.id, current.id))
+ .returning();
+ const [b] = await tx
+ .update(stages)
+ .set({ order: currentOrder })
+ .where(eq(stages.id, partner.id))
+ .returning();
+ return [a!, b!];
+ });
+
+ return { before: current, after: updatedCurrent, partner: updatedPartner };
+}
+
+export async function deleteStage(
+ db: Database,
+ organizationId: string,
+ stageId: string,
+ destinationStageId: string,
+): Promise<{ before: Stage; migratedDealIds: string[] } | null> {
+ if (stageId === destinationStageId) {
+ throw new StageOpError("wrong_pipeline", "Destination must be a different stage");
+ }
+ const current = await assertStageInOrg(db, stageId, organizationId);
+ if (!current) return null;
+
+ const dest = await assertStageInOrg(db, destinationStageId, organizationId);
+ if (!dest) throw new StageOpError("not_found", "Destination stage not found");
+ if (dest.pipelineId !== current.pipelineId) {
+ throw new StageOpError("wrong_pipeline", "Destination must be in the same pipeline");
+ }
+
+ const siblingCount = await db
+ .select({ n: count() })
+ .from(stages)
+ .where(eq(stages.pipelineId, current.pipelineId));
+ if ((siblingCount[0]?.n ?? 0) <= 1) {
+ throw new StageOpError("last_stage", "Cannot delete the only stage in a pipeline");
+ }
+
+ const migratedDealIds = await db.transaction(async (tx) => {
+ const affected = await tx
+ .select({ id: deals.id })
+ .from(deals)
+ .where(eq(deals.stageId, stageId));
+ const ids = affected.map((d) => d.id);
+ if (ids.length > 0) {
+ await tx
+ .update(deals)
+ .set({ stageId: destinationStageId, stageEnteredAt: new Date(), updatedAt: new Date() })
+ .where(eq(deals.stageId, stageId));
+ }
+ await tx.delete(stages).where(eq(stages.id, stageId));
+ return ids;
+ });
+
+ return { before: current, migratedDealIds };
+}
+
+export async function previewStageFlagToggle(
+ db: Database,
+ organizationId: string,
+ stageId: string,
+): Promise<{ affectedDealCount: number } | null> {
+ const stage = await assertStageInOrg(db, stageId, organizationId);
+ if (!stage) return null;
+ const rows = await db
+ .select({ n: count() })
+ .from(deals)
+ .where(eq(deals.stageId, stageId));
+ return { affectedDealCount: rows[0]?.n ?? 0 };
+}
+
+export async function listStagesForPipeline(
+ db: Database,
+ organizationId: string,
+ pipelineId: string,
+): Promise {
+ const ok = await assertPipelineInOrg(db, pipelineId, organizationId);
+ if (!ok) return [];
+ return db
+ .select()
+ .from(stages)
+ .where(eq(stages.pipelineId, pipelineId))
+ .orderBy(asc(stages.order), asc(stages.id));
+}
+
+function isUniqueViolation(err: unknown): boolean {
+ if (typeof err !== "object" || err === null) return false;
+ const top = err as { code?: unknown; cause?: unknown };
+ if (top.code === "23505") return true;
+ if (typeof top.cause === "object" && top.cause !== null) {
+ return (top.cause as { code?: unknown }).code === "23505";
+ }
+ return false;
+}
+
diff --git a/tests/mcp-tools.test.ts b/tests/mcp-tools.test.ts
index 8a40886..9568584 100644
--- a/tests/mcp-tools.test.ts
+++ b/tests/mcp-tools.test.ts
@@ -478,6 +478,101 @@ describe("MCP tools", () => {
expect(empty.activity).toEqual([]);
});
+ test("create_stage / update_stage / reorder_stage round-trip with audit rows", async () => {
+ const orgId = await seedOrgWithPipeline();
+ const { client, close: c } = await makeClient(orgId);
+ close = c;
+ const ps = await db
+ .select({ id: pipelines.id })
+ .from(pipelines)
+ .where(eq(pipelines.organizationId, orgId))
+ .limit(1);
+ const pipelineId = ps[0]!.id;
+
+ const created = structured<{ stage: { id: string; order: number; color: string | null } }>(
+ await client.callTool({
+ name: "create_stage",
+ arguments: { pipelineId, name: "Discovery+", color: "indigo" },
+ }),
+ );
+ expect(created.stage.color).toBe("indigo");
+ expect(created.stage.order).toBe(2); // appended after the 2 seeded stages
+
+ const createAudit = await db
+ .select()
+ .from(auditLog)
+ .where(eq(auditLog.entityId, created.stage.id));
+ expect(createAudit.length).toBe(1);
+ expect(createAudit[0]!.entityType).toBe("stage");
+ expect(createAudit[0]!.action).toBe("create");
+
+ const updated = structured<{ stage: { color: string | null } }>(
+ await client.callTool({
+ name: "update_stage",
+ arguments: { id: created.stage.id, color: null },
+ }),
+ );
+ expect(updated.stage.color).toBeNull();
+
+ const reordered = structured<{ moved: boolean; stage: { order: number } }>(
+ await client.callTool({
+ name: "reorder_stage",
+ arguments: { id: created.stage.id, direction: "up" },
+ }),
+ );
+ expect(reordered.moved).toBe(true);
+ expect(reordered.stage.order).toBe(1);
+ });
+
+ test("delete_stage migrates deals and audits each one", async () => {
+ const orgId = await seedOrgWithPipeline();
+ const { client, close: c } = await makeClient(orgId);
+ close = c;
+ const allStages = await db.select().from(stages).orderBy(stages.order);
+ const sourceStage = allStages[0]!;
+ const destStage = allStages[1]!;
+
+ const company = structured<{ company: { id: string } }>(
+ await client.callTool({
+ name: "create_company",
+ arguments: { name: "Migrate Co" },
+ }),
+ );
+ const deal = structured<{ deal: { id: string } }>(
+ await client.callTool({
+ name: "create_deal",
+ arguments: { companyId: company.company.id, name: "D1", stageId: sourceStage.id },
+ }),
+ );
+
+ const result = structured<{ migrated_deal_count: number }>(
+ await client.callTool({
+ name: "delete_stage",
+ arguments: { id: sourceStage.id, destinationStageId: destStage.id },
+ }),
+ );
+ expect(result.migrated_deal_count).toBe(1);
+
+ const remaining = await db.select().from(stages).where(eq(stages.id, sourceStage.id));
+ expect(remaining.length).toBe(0);
+
+ const dealAudits = await db
+ .select()
+ .from(auditLog)
+ .where(eq(auditLog.entityId, deal.deal.id));
+ const stageMigrationRow = dealAudits.find((r) => {
+ const c = r.changes as { before?: { stageId?: string }; after?: { stageId?: string } } | null;
+ return c?.before?.stageId === sourceStage.id && c?.after?.stageId === destStage.id;
+ });
+ expect(stageMigrationRow).toBeDefined();
+
+ const stageAudits = await db
+ .select()
+ .from(auditLog)
+ .where(eq(auditLog.entityId, sourceStage.id));
+ expect(stageAudits.find((r) => r.action === "delete")).toBeDefined();
+ });
+
test("cross-org access is rejected (org-scoped queries)", async () => {
const orgA = await seedOrgWithPipeline("org_a");
const orgB = await seedOrgWithPipeline("org_b");
diff --git a/tests/stages.test.ts b/tests/stages.test.ts
new file mode 100644
index 0000000..d5f8c46
--- /dev/null
+++ b/tests/stages.test.ts
@@ -0,0 +1,232 @@
+import { eq } from "drizzle-orm";
+import { beforeEach, describe, expect, test } from "bun:test";
+import { createDb } from "@/db/client";
+import { organization } from "@/db/schema/auth";
+import { companies } from "@/db/schema/companies";
+import { deals, pipelines, stages } from "@/db/schema/deals";
+import {
+ assertStageInOrg,
+ createStage,
+ deleteStage,
+ listStagesForPipeline,
+ previewStageFlagToggle,
+ reorderStage,
+ StageOpError,
+ updateStage,
+} from "@/lib/stages";
+import { resetDb } from "./setup";
+
+const db = createDb(process.env.TEST_DATABASE_URL!);
+
+async function seedOrgWithPipeline(orgId = "org_stages"): Promise<{
+ orgId: string;
+ pipelineId: string;
+ stageIds: string[];
+}> {
+ await db.insert(organization).values({ id: orgId, name: "Stages", slug: orgId });
+ const pipelineId = crypto.randomUUID();
+ await db.insert(pipelines).values({ id: pipelineId, organizationId: orgId, name: "Sales", isDefault: true });
+ const ids = [crypto.randomUUID(), crypto.randomUUID(), crypto.randomUUID()];
+ await db.insert(stages).values([
+ { id: ids[0], pipelineId, name: "Lead", order: 0 },
+ { id: ids[1], pipelineId, name: "Discovery", order: 1 },
+ { id: ids[2], pipelineId, name: "Won", order: 2, isWon: true },
+ ]);
+ return { orgId, pipelineId, stageIds: ids };
+}
+
+async function seedCompany(orgId: string): Promise {
+ const id = crypto.randomUUID();
+ await db.insert(companies).values({ id, organizationId: orgId, name: "Acme" });
+ return id;
+}
+
+async function seedDeal(orgId: string, pipelineId: string, stageId: string): Promise {
+ const companyId = await seedCompany(orgId);
+ const id = crypto.randomUUID();
+ await db.insert(deals).values({
+ id,
+ organizationId: orgId,
+ companyId,
+ pipelineId,
+ stageId,
+ name: "Acme deal",
+ });
+ return id;
+}
+
+describe("lib/stages", () => {
+ beforeEach(async () => {
+ await resetDb();
+ });
+
+ test("assertStageInOrg returns null for cross-org stages", async () => {
+ const a = await seedOrgWithPipeline("org_a");
+ const b = await seedOrgWithPipeline("org_b");
+ expect(await assertStageInOrg(db, a.stageIds[0]!, b.orgId)).toBeNull();
+ expect(await assertStageInOrg(db, a.stageIds[0]!, a.orgId)).not.toBeNull();
+ });
+
+ test("createStage always inserts at end (max(order)+1)", async () => {
+ const { orgId, pipelineId } = await seedOrgWithPipeline();
+ const result = await createStage(db, orgId, { pipelineId, name: "Discovery+" });
+ expect(result.after.order).toBe(3);
+ });
+
+ test("createStage rejects both isWon and isLost", async () => {
+ const { orgId, pipelineId } = await seedOrgWithPipeline();
+ let caught: unknown;
+ try {
+ await createStage(db, orgId, { pipelineId, name: "Bad", isWon: true, isLost: true });
+ } catch (e) {
+ caught = e;
+ }
+ expect(caught).toBeInstanceOf(StageOpError);
+ expect((caught as StageOpError).code).toBe("won_lost_conflict");
+ });
+
+ test("createStage rejects duplicate name within a pipeline", async () => {
+ const { orgId, pipelineId } = await seedOrgWithPipeline();
+ let caught: unknown;
+ try {
+ await createStage(db, orgId, { pipelineId, name: "Lead" });
+ } catch (e) {
+ caught = e;
+ }
+ expect(caught).toBeInstanceOf(StageOpError);
+ expect((caught as StageOpError).code).toBe("name_in_use");
+ });
+
+ test("updateStage patch semantics; color: null clears", async () => {
+ const { orgId, stageIds } = await seedOrgWithPipeline();
+ const set = await updateStage(db, orgId, { id: stageIds[0]!, color: "blue" });
+ expect(set?.after.color).toBe("blue");
+
+ const cleared = await updateStage(db, orgId, { id: stageIds[0]!, color: null });
+ expect(cleared?.after.color).toBeNull();
+ expect(cleared?.after.name).toBe("Lead"); // untouched
+
+ const renamed = await updateStage(db, orgId, { id: stageIds[0]!, name: "Brand New" });
+ expect(renamed?.after.name).toBe("Brand New");
+ expect(renamed?.after.color).toBeNull(); // untouched
+ });
+
+ test("updateStage rejects setting both isWon and isLost true", async () => {
+ const { orgId, stageIds } = await seedOrgWithPipeline();
+ let caught: unknown;
+ try {
+ await updateStage(db, orgId, { id: stageIds[1]!, isWon: true, isLost: true });
+ } catch (e) {
+ caught = e;
+ }
+ expect(caught).toBeInstanceOf(StageOpError);
+ expect((caught as StageOpError).code).toBe("won_lost_conflict");
+ });
+
+ test("DB CHECK constraint blocks setting both isWon and isLost via direct write", async () => {
+ const { pipelineId } = await seedOrgWithPipeline();
+ let caught: unknown;
+ try {
+ await db
+ .insert(stages)
+ .values({ pipelineId, name: "BothBad", order: 99, isWon: true, isLost: true });
+ } catch (e) {
+ caught = e;
+ }
+ expect(caught).toBeDefined();
+ });
+
+ test("reorderStage moves down then up; boundary calls return unchanged", async () => {
+ const { orgId, pipelineId, stageIds } = await seedOrgWithPipeline();
+ const movedDown = await reorderStage(db, orgId, stageIds[0]!, "down");
+ expect(movedDown?.after.order).toBe(1);
+ expect(movedDown?.partner.order).toBe(0);
+
+ const movedUp = await reorderStage(db, orgId, stageIds[0]!, "up");
+ expect(movedUp?.after.order).toBe(0);
+
+ const sortedAfter = await listStagesForPipeline(db, orgId, pipelineId);
+ expect(sortedAfter.map((s) => s.id)).toEqual(stageIds);
+
+ // Boundary: top moving up is a no-op.
+ const noop = await reorderStage(db, orgId, stageIds[0]!, "up");
+ expect(noop?.before.order).toBe(noop?.after.order);
+ });
+
+ test("deleteStage migrates deals atomically then deletes the stage", async () => {
+ const { orgId, pipelineId, stageIds } = await seedOrgWithPipeline();
+ const dealId = await seedDeal(orgId, pipelineId, stageIds[0]!);
+
+ const result = await deleteStage(db, orgId, stageIds[0]!, stageIds[1]!);
+ expect(result?.migratedDealIds).toEqual([dealId]);
+
+ const remaining = await db.select().from(stages).where(eq(stages.id, stageIds[0]!));
+ expect(remaining.length).toBe(0);
+
+ const moved = await db.select({ stageId: deals.stageId }).from(deals).where(eq(deals.id, dealId));
+ expect(moved[0]?.stageId).toBe(stageIds[1]!);
+ });
+
+ test("deleteStage refuses self as destination", async () => {
+ const { orgId, stageIds } = await seedOrgWithPipeline();
+ let caught: unknown;
+ try {
+ await deleteStage(db, orgId, stageIds[0]!, stageIds[0]!);
+ } catch (e) {
+ caught = e;
+ }
+ expect(caught).toBeInstanceOf(StageOpError);
+ });
+
+ test("deleteStage refuses cross-pipeline destination", async () => {
+ const { orgId, stageIds } = await seedOrgWithPipeline();
+ const pipeline2 = crypto.randomUUID();
+ await db.insert(pipelines).values({ id: pipeline2, organizationId: orgId, name: "Other" });
+ const otherStageId = crypto.randomUUID();
+ await db.insert(stages).values({ id: otherStageId, pipelineId: pipeline2, name: "X", order: 0 });
+
+ let caught: unknown;
+ try {
+ await deleteStage(db, orgId, stageIds[0]!, otherStageId);
+ } catch (e) {
+ caught = e;
+ }
+ expect(caught).toBeInstanceOf(StageOpError);
+ expect((caught as StageOpError).code).toBe("wrong_pipeline");
+ });
+
+ test("deleteStage refuses the only stage in a pipeline", async () => {
+ const { orgId } = await seedOrgWithPipeline();
+ const lonely = crypto.randomUUID();
+ const onlyStage = crypto.randomUUID();
+ await db.insert(pipelines).values({ id: lonely, organizationId: orgId, name: "Lonely" });
+ await db.insert(stages).values({ id: onlyStage, pipelineId: lonely, name: "Only", order: 0 });
+ // dest must be different from src to pass the first guard, then last_stage fires.
+ const otherStageId = crypto.randomUUID();
+ await db.insert(stages).values({ id: otherStageId, pipelineId: lonely, name: "OnlyDest", order: 1 });
+ // Now delete one and try to delete the remaining lone one.
+ await deleteStage(db, orgId, onlyStage, otherStageId);
+ let caught: unknown;
+ try {
+ await deleteStage(db, orgId, otherStageId, otherStageId);
+ } catch (e) {
+ caught = e;
+ }
+ expect(caught).toBeInstanceOf(StageOpError);
+ });
+
+ test("previewStageFlagToggle counts deals on the stage", async () => {
+ const { orgId, pipelineId, stageIds } = await seedOrgWithPipeline();
+ expect((await previewStageFlagToggle(db, orgId, stageIds[0]!))?.affectedDealCount).toBe(0);
+ await seedDeal(orgId, pipelineId, stageIds[0]!);
+ await seedDeal(orgId, pipelineId, stageIds[0]!);
+ expect((await previewStageFlagToggle(db, orgId, stageIds[0]!))?.affectedDealCount).toBe(2);
+ });
+
+ test("cross-org isolation: updateStage on another org's stage returns null", async () => {
+ const a = await seedOrgWithPipeline("org_iso_a");
+ const b = await seedOrgWithPipeline("org_iso_b");
+ const result = await updateStage(db, b.orgId, { id: a.stageIds[0]!, name: "hijack" });
+ expect(result).toBeNull();
+ });
+});