improved a lot of things
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
-- Origin Graph — Initial Schema
|
||||
-- Migration: 001
|
||||
-- All tables use UUIDs as primary keys and include created_at / updated_at.
|
||||
-- Enable pgcrypto for gen_random_uuid() if not already enabled.
|
||||
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||
|
||||
-- ── Enum types ────────────────────────────────────────────────────────────────
|
||||
|
||||
CREATE TYPE origin_type AS ENUM (
|
||||
'GIT_COMMIT',
|
||||
'LINEAR_ISSUE',
|
||||
'SLACK_MESSAGE',
|
||||
'URL',
|
||||
'FORK'
|
||||
);
|
||||
|
||||
CREATE TYPE diff_status AS ENUM (
|
||||
'DRAFT',
|
||||
'EXPORTED',
|
||||
'IMPLEMENTED',
|
||||
'BLOCKED'
|
||||
);
|
||||
|
||||
CREATE TYPE team_role AS ENUM (
|
||||
'OWNER',
|
||||
'DESIGNER',
|
||||
'ENGINEER',
|
||||
'PM',
|
||||
'VIEWER'
|
||||
);
|
||||
|
||||
CREATE TYPE agent_type AS ENUM (
|
||||
'CURSOR',
|
||||
'CLAUDE_CODE',
|
||||
'GENERIC'
|
||||
);
|
||||
|
||||
CREATE TYPE workspace_plan AS ENUM (
|
||||
'FREE',
|
||||
'TEAM',
|
||||
'ENTERPRISE'
|
||||
);
|
||||
|
||||
CREATE TYPE agent_session_status AS ENUM (
|
||||
'ACTIVE',
|
||||
'COMPLETED',
|
||||
'FAILED'
|
||||
);
|
||||
|
||||
-- ── workspaces ────────────────────────────────────────────────────────────────
|
||||
|
||||
CREATE TABLE workspaces (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name TEXT NOT NULL,
|
||||
owner_id TEXT NOT NULL, -- Clerk user ID
|
||||
plan workspace_plan NOT NULL DEFAULT 'FREE',
|
||||
settings_jsonb JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- ── origins ───────────────────────────────────────────────────────────────────
|
||||
|
||||
CREATE TABLE origins (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
type origin_type NOT NULL,
|
||||
source_ref TEXT NOT NULL,
|
||||
source_metadata_jsonb JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- ── artboards ─────────────────────────────────────────────────────────────────
|
||||
|
||||
CREATE TABLE artboards (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
origin_id UUID REFERENCES origins(id) ON DELETE SET NULL,
|
||||
parent_artboard_id UUID REFERENCES artboards(id) ON DELETE SET NULL,
|
||||
metadata_jsonb JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX artboards_workspace_idx ON artboards(workspace_id);
|
||||
CREATE INDEX artboards_parent_idx ON artboards(parent_artboard_id);
|
||||
|
||||
-- ── intent_diffs ──────────────────────────────────────────────────────────────
|
||||
|
||||
CREATE TABLE intent_diffs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
artboard_id UUID NOT NULL REFERENCES artboards(id) ON DELETE CASCADE,
|
||||
author_id TEXT NOT NULL, -- Clerk user ID
|
||||
changes_jsonb JSONB NOT NULL DEFAULT '{}',
|
||||
summary TEXT NOT NULL DEFAULT '',
|
||||
status diff_status NOT NULL DEFAULT 'DRAFT',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX intent_diffs_artboard_idx ON intent_diffs(artboard_id);
|
||||
CREATE INDEX intent_diffs_status_idx ON intent_diffs(status);
|
||||
|
||||
-- ── agent_sessions ────────────────────────────────────────────────────────────
|
||||
|
||||
CREATE TABLE agent_sessions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
artboard_id UUID NOT NULL REFERENCES artboards(id) ON DELETE CASCADE,
|
||||
diff_id UUID REFERENCES intent_diffs(id) ON DELETE SET NULL,
|
||||
agent_type agent_type NOT NULL,
|
||||
messages_jsonb JSONB NOT NULL DEFAULT '[]',
|
||||
status agent_session_status NOT NULL DEFAULT 'ACTIVE',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX agent_sessions_artboard_idx ON agent_sessions(artboard_id);
|
||||
CREATE INDEX agent_sessions_diff_idx ON agent_sessions(diff_id);
|
||||
|
||||
-- ── design_language_files ─────────────────────────────────────────────────────
|
||||
|
||||
CREATE TABLE design_language_files (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
schema_jsonb JSONB NOT NULL,
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX dlf_workspace_idx ON design_language_files(workspace_id);
|
||||
|
||||
-- ── team_members ──────────────────────────────────────────────────────────────
|
||||
|
||||
CREATE TABLE team_members (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
||||
user_id TEXT NOT NULL, -- Clerk user ID
|
||||
role team_role NOT NULL DEFAULT 'VIEWER',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE (workspace_id, user_id)
|
||||
);
|
||||
|
||||
CREATE INDEX team_members_workspace_idx ON team_members(workspace_id);
|
||||
CREATE INDEX team_members_user_idx ON team_members(user_id);
|
||||
|
||||
-- ── updated_at trigger ────────────────────────────────────────────────────────
|
||||
|
||||
CREATE OR REPLACE FUNCTION set_updated_at()
|
||||
RETURNS TRIGGER LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = now();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
DECLARE t TEXT;
|
||||
BEGIN
|
||||
FOREACH t IN ARRAY ARRAY['workspaces','origins','artboards','intent_diffs',
|
||||
'agent_sessions','design_language_files','team_members']
|
||||
LOOP
|
||||
EXECUTE format(
|
||||
'CREATE TRIGGER trg_%I_updated_at
|
||||
BEFORE UPDATE ON %I
|
||||
FOR EACH ROW EXECUTE FUNCTION set_updated_at()',
|
||||
t, t
|
||||
);
|
||||
END LOOP;
|
||||
END;
|
||||
$$;
|
||||
@@ -0,0 +1,46 @@
|
||||
-- Origin Graph — Artboard Ancestry Materialized View
|
||||
-- Migration: 002
|
||||
-- Pre-computes all ancestor/descendant relationships so the app never needs
|
||||
-- recursive CTEs at query time. Updated automatically on artboards INSERT.
|
||||
|
||||
CREATE MATERIALIZED VIEW artboard_ancestry AS
|
||||
WITH RECURSIVE ancestry(artboard_id, ancestor_id, depth) AS (
|
||||
-- Base: each artboard is at depth 0 relative to itself
|
||||
SELECT id AS artboard_id, id AS ancestor_id, 0 AS depth
|
||||
FROM artboards
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- Recurse: walk up the parent chain
|
||||
SELECT a.id AS artboard_id, anc.ancestor_id, anc.depth + 1
|
||||
FROM artboards a
|
||||
JOIN ancestry anc ON a.parent_artboard_id = anc.artboard_id
|
||||
)
|
||||
SELECT artboard_id, ancestor_id, depth
|
||||
FROM ancestry
|
||||
WHERE artboard_id <> ancestor_id -- exclude self-reference
|
||||
ORDER BY artboard_id, depth;
|
||||
|
||||
CREATE UNIQUE INDEX artboard_ancestry_pk
|
||||
ON artboard_ancestry(artboard_id, ancestor_id);
|
||||
|
||||
CREATE INDEX artboard_ancestry_ancestor_idx
|
||||
ON artboard_ancestry(ancestor_id);
|
||||
|
||||
-- ── Refresh trigger ───────────────────────────────────────────────────────────
|
||||
-- Refreshes the materialized view concurrently whenever an artboard is
|
||||
-- inserted. CONCURRENTLY requires the unique index above — it allows reads
|
||||
-- to continue during refresh.
|
||||
|
||||
CREATE OR REPLACE FUNCTION refresh_artboard_ancestry()
|
||||
RETURNS TRIGGER LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
REFRESH MATERIALIZED VIEW CONCURRENTLY artboard_ancestry;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER trg_artboard_ancestry_refresh
|
||||
AFTER INSERT OR UPDATE OF parent_artboard_id ON artboards
|
||||
FOR EACH STATEMENT
|
||||
EXECUTE FUNCTION refresh_artboard_ancestry();
|
||||
@@ -0,0 +1,111 @@
|
||||
-- Origin Graph — Row-Level Security Policies
|
||||
-- Migration: 003
|
||||
-- All tables are workspace-scoped. A user may only read or write rows in
|
||||
-- workspaces where they have a team_members record. The Clerk JWT is verified
|
||||
-- server-side; auth.uid() maps to the Clerk user_id column.
|
||||
|
||||
-- Enable RLS on every table
|
||||
ALTER TABLE workspaces ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE artboards ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE origins ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE intent_diffs ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE agent_sessions ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE design_language_files ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE team_members ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- ── Helper: is the current user a member of the given workspace? ──────────────
|
||||
|
||||
CREATE OR REPLACE FUNCTION is_workspace_member(ws_id UUID)
|
||||
RETURNS BOOLEAN LANGUAGE sql SECURITY DEFINER AS $$
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM team_members
|
||||
WHERE workspace_id = ws_id
|
||||
AND user_id = auth.uid()::TEXT
|
||||
);
|
||||
$$;
|
||||
|
||||
-- ── workspaces ────────────────────────────────────────────────────────────────
|
||||
|
||||
CREATE POLICY workspaces_select ON workspaces
|
||||
FOR SELECT USING (is_workspace_member(id));
|
||||
|
||||
CREATE POLICY workspaces_insert ON workspaces
|
||||
FOR INSERT WITH CHECK (owner_id = auth.uid()::TEXT);
|
||||
|
||||
CREATE POLICY workspaces_update ON workspaces
|
||||
FOR UPDATE USING (owner_id = auth.uid()::TEXT);
|
||||
|
||||
-- ── artboards ─────────────────────────────────────────────────────────────────
|
||||
|
||||
CREATE POLICY artboards_select ON artboards
|
||||
FOR SELECT USING (is_workspace_member(workspace_id));
|
||||
|
||||
CREATE POLICY artboards_insert ON artboards
|
||||
FOR INSERT WITH CHECK (is_workspace_member(workspace_id));
|
||||
|
||||
CREATE POLICY artboards_update ON artboards
|
||||
FOR UPDATE USING (is_workspace_member(workspace_id));
|
||||
|
||||
CREATE POLICY artboards_delete ON artboards
|
||||
FOR DELETE USING (is_workspace_member(workspace_id));
|
||||
|
||||
-- ── intent_diffs ──────────────────────────────────────────────────────────────
|
||||
-- Derived from artboard's workspace membership
|
||||
CREATE POLICY intent_diffs_select ON intent_diffs
|
||||
FOR SELECT USING (
|
||||
is_workspace_member((SELECT workspace_id FROM artboards WHERE id = artboard_id))
|
||||
);
|
||||
|
||||
CREATE POLICY intent_diffs_insert ON intent_diffs
|
||||
FOR INSERT WITH CHECK (
|
||||
is_workspace_member((SELECT workspace_id FROM artboards WHERE id = artboard_id))
|
||||
);
|
||||
|
||||
CREATE POLICY intent_diffs_update ON intent_diffs
|
||||
FOR UPDATE USING (
|
||||
is_workspace_member((SELECT workspace_id FROM artboards WHERE id = artboard_id))
|
||||
);
|
||||
|
||||
-- ── agent_sessions ────────────────────────────────────────────────────────────
|
||||
|
||||
CREATE POLICY agent_sessions_select ON agent_sessions
|
||||
FOR SELECT USING (
|
||||
is_workspace_member((SELECT workspace_id FROM artboards WHERE id = artboard_id))
|
||||
);
|
||||
|
||||
CREATE POLICY agent_sessions_insert ON agent_sessions
|
||||
FOR INSERT WITH CHECK (
|
||||
is_workspace_member((SELECT workspace_id FROM artboards WHERE id = artboard_id))
|
||||
);
|
||||
|
||||
CREATE POLICY agent_sessions_update ON agent_sessions
|
||||
FOR UPDATE USING (
|
||||
is_workspace_member((SELECT workspace_id FROM artboards WHERE id = artboard_id))
|
||||
);
|
||||
|
||||
-- ── design_language_files ─────────────────────────────────────────────────────
|
||||
|
||||
CREATE POLICY dlf_select ON design_language_files
|
||||
FOR SELECT USING (is_workspace_member(workspace_id));
|
||||
|
||||
CREATE POLICY dlf_insert ON design_language_files
|
||||
FOR INSERT WITH CHECK (is_workspace_member(workspace_id));
|
||||
|
||||
CREATE POLICY dlf_update ON design_language_files
|
||||
FOR UPDATE USING (is_workspace_member(workspace_id));
|
||||
|
||||
-- ── team_members ──────────────────────────────────────────────────────────────
|
||||
|
||||
CREATE POLICY team_members_select ON team_members
|
||||
FOR SELECT USING (is_workspace_member(workspace_id));
|
||||
|
||||
-- Only workspace owners can add/remove members
|
||||
CREATE POLICY team_members_insert ON team_members
|
||||
FOR INSERT WITH CHECK (
|
||||
EXISTS (SELECT 1 FROM workspaces WHERE id = workspace_id AND owner_id = auth.uid()::TEXT)
|
||||
);
|
||||
|
||||
CREATE POLICY team_members_delete ON team_members
|
||||
FOR DELETE USING (
|
||||
EXISTS (SELECT 1 FROM workspaces WHERE id = workspace_id AND owner_id = auth.uid()::TEXT)
|
||||
);
|
||||
@@ -0,0 +1,6 @@
|
||||
-- ── Migration 004: Add notes column to intent_diffs ──────────────────────────
|
||||
-- Allows the coding agent to record why a diff was blocked or any implementation
|
||||
-- notes when updating status via the Agent Bridge MCP tool.
|
||||
|
||||
ALTER TABLE intent_diffs
|
||||
ADD COLUMN IF NOT EXISTS notes TEXT;
|
||||
@@ -1 +1,2 @@
|
||||
export {};
|
||||
export * from './types.js';
|
||||
export * from './queries.js';
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
// ── Query interface ───────────────────────────────────────────────────────────
|
||||
// These functions describe the Origin Graph query surface. They accept a
|
||||
// generic `db` client (matching Supabase's SupabaseClient shape) so the
|
||||
// package doesn't take a hard dependency on @supabase/supabase-js. Wire them
|
||||
// to TanStack Query's queryFn in the app layer.
|
||||
|
||||
import type {
|
||||
Artboard,
|
||||
Workspace,
|
||||
IntentDiff,
|
||||
DesignLanguageFile,
|
||||
AgentSession,
|
||||
TeamMember,
|
||||
InsertArtboard,
|
||||
InsertIntentDiff,
|
||||
InsertAgentSession,
|
||||
DiffStatus,
|
||||
ArtboardAncestry,
|
||||
} from './types.js';
|
||||
|
||||
// ── Minimal Supabase client interface ────────────────────────────────────────
|
||||
|
||||
export interface DbClient {
|
||||
from(table: string): {
|
||||
select(cols?: string): DbQuery;
|
||||
insert(row: unknown): DbMutation;
|
||||
update(row: unknown): DbMutation;
|
||||
delete(): DbMutation;
|
||||
};
|
||||
rpc(fn: string, args?: unknown): Promise<{ data: unknown; error: DbError | null }>;
|
||||
}
|
||||
|
||||
export interface DbQuery {
|
||||
eq(col: string, val: unknown): DbQuery;
|
||||
in(col: string, vals: unknown[]): DbQuery;
|
||||
order(col: string, opts?: { ascending?: boolean }): DbQuery;
|
||||
limit(n: number): DbQuery;
|
||||
single(): Promise<{ data: unknown; error: DbError | null }>;
|
||||
then(resolve: (result: { data: unknown[]; error: DbError | null }) => void): void;
|
||||
}
|
||||
|
||||
export interface DbMutation {
|
||||
eq(col: string, val: unknown): DbMutation;
|
||||
select(cols?: string): DbMutation;
|
||||
single(): Promise<{ data: unknown; error: DbError | null }>;
|
||||
then(resolve: (result: { data: unknown; error: DbError | null }) => void): void;
|
||||
}
|
||||
|
||||
export interface DbError {
|
||||
message: string;
|
||||
code?: string;
|
||||
}
|
||||
|
||||
// ── Artboard queries ──────────────────────────────────────────────────────────
|
||||
|
||||
export async function getArtboards(db: DbClient, workspaceId: string): Promise<Artboard[]> {
|
||||
const { data, error } = await (db
|
||||
.from('artboards')
|
||||
.select('*')
|
||||
.eq('workspace_id', workspaceId)
|
||||
.order('created_at', { ascending: false }) as unknown as Promise<{ data: Artboard[]; error: DbError | null }>);
|
||||
if (error) throw new Error(error.message);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function getArtboard(db: DbClient, id: string): Promise<Artboard> {
|
||||
const { data, error } = await (db
|
||||
.from('artboards')
|
||||
.select('*')
|
||||
.eq('id', id)
|
||||
.single() as Promise<{ data: Artboard; error: DbError | null }>);
|
||||
if (error) throw new Error(error.message);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function createArtboard(db: DbClient, row: InsertArtboard): Promise<Artboard> {
|
||||
const { data, error } = await (db
|
||||
.from('artboards')
|
||||
.insert(row)
|
||||
.select()
|
||||
.single() as Promise<{ data: Artboard; error: DbError | null }>);
|
||||
if (error) throw new Error(error.message);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function getArtboardAncestors(db: DbClient, artboardId: string): Promise<ArtboardAncestry[]> {
|
||||
const { data, error } = await (db
|
||||
.from('artboard_ancestry')
|
||||
.select('*')
|
||||
.eq('artboard_id', artboardId)
|
||||
.order('depth', { ascending: true }) as unknown as Promise<{ data: ArtboardAncestry[]; error: DbError | null }>);
|
||||
if (error) throw new Error(error.message);
|
||||
return data;
|
||||
}
|
||||
|
||||
// ── Intent diff queries ───────────────────────────────────────────────────────
|
||||
|
||||
export async function getDiffs(db: DbClient, artboardId: string): Promise<IntentDiff[]> {
|
||||
const { data, error } = await (db
|
||||
.from('intent_diffs')
|
||||
.select('*')
|
||||
.eq('artboard_id', artboardId)
|
||||
.order('created_at', { ascending: false }) as unknown as Promise<{ data: IntentDiff[]; error: DbError | null }>);
|
||||
if (error) throw new Error(error.message);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function getDiffsByStatus(db: DbClient, workspaceId: string, status: DiffStatus): Promise<IntentDiff[]> {
|
||||
// Join through artboards for workspace scoping
|
||||
const { data, error } = await (db.rpc('get_diffs_by_status', { p_workspace_id: workspaceId, p_status: status }) as Promise<{ data: IntentDiff[]; error: DbError | null }>);
|
||||
if (error) throw new Error(error.message);
|
||||
return data as IntentDiff[];
|
||||
}
|
||||
|
||||
export async function createDiff(db: DbClient, row: InsertIntentDiff): Promise<IntentDiff> {
|
||||
const { data, error } = await (db
|
||||
.from('intent_diffs')
|
||||
.insert(row)
|
||||
.select()
|
||||
.single() as Promise<{ data: IntentDiff; error: DbError | null }>);
|
||||
if (error) throw new Error(error.message);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function getDiff(db: DbClient, id: string): Promise<IntentDiff> {
|
||||
const { data, error } = await (db
|
||||
.from('intent_diffs')
|
||||
.select('*')
|
||||
.eq('id', id)
|
||||
.single() as Promise<{ data: IntentDiff; error: DbError | null }>);
|
||||
if (error) throw new Error(error.message);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function updateDiffStatus(
|
||||
db: DbClient,
|
||||
id: string,
|
||||
status: DiffStatus,
|
||||
notes?: string
|
||||
): Promise<IntentDiff> {
|
||||
const { data, error } = await (db
|
||||
.from('intent_diffs')
|
||||
.update({ status, ...(notes !== undefined ? { notes } : {}) })
|
||||
.eq('id', id)
|
||||
.select()
|
||||
.single() as Promise<{ data: IntentDiff; error: DbError | null }>);
|
||||
if (error) throw new Error(error.message);
|
||||
return data;
|
||||
}
|
||||
|
||||
// ── Design language file queries ──────────────────────────────────────────────
|
||||
|
||||
export async function getActiveDesignLanguageFile(
|
||||
db: DbClient,
|
||||
workspaceId: string
|
||||
): Promise<DesignLanguageFile | null> {
|
||||
const { data, error } = await (db
|
||||
.from('design_language_files')
|
||||
.select('*')
|
||||
.eq('workspace_id', workspaceId)
|
||||
.order('version', { ascending: false })
|
||||
.limit(1)
|
||||
.single() as Promise<{ data: DesignLanguageFile | null; error: DbError | null }>);
|
||||
if (error?.code === 'PGRST116') return null; // No rows found
|
||||
if (error) throw new Error(error.message);
|
||||
return data;
|
||||
}
|
||||
|
||||
// ── Agent session queries ─────────────────────────────────────────────────────
|
||||
|
||||
export async function createAgentSession(db: DbClient, row: InsertAgentSession): Promise<AgentSession> {
|
||||
const { data, error } = await (db
|
||||
.from('agent_sessions')
|
||||
.insert(row)
|
||||
.select()
|
||||
.single() as Promise<{ data: AgentSession; error: DbError | null }>);
|
||||
if (error) throw new Error(error.message);
|
||||
return data;
|
||||
}
|
||||
|
||||
// ── Workspace queries ─────────────────────────────────────────────────────────
|
||||
|
||||
export async function getWorkspace(db: DbClient, id: string): Promise<Workspace> {
|
||||
const { data, error } = await (db
|
||||
.from('workspaces')
|
||||
.select('*')
|
||||
.eq('id', id)
|
||||
.single() as Promise<{ data: Workspace; error: DbError | null }>);
|
||||
if (error) throw new Error(error.message);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function getTeamMembers(db: DbClient, workspaceId: string): Promise<TeamMember[]> {
|
||||
const { data, error } = await (db
|
||||
.from('team_members')
|
||||
.select('*')
|
||||
.eq('workspace_id', workspaceId) as unknown as Promise<{ data: TeamMember[]; error: DbError | null }>);
|
||||
if (error) throw new Error(error.message);
|
||||
return data;
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// ── Enums ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export const OriginTypeSchema = z.enum([
|
||||
'GIT_COMMIT',
|
||||
'LINEAR_ISSUE',
|
||||
'SLACK_MESSAGE',
|
||||
'URL',
|
||||
'FORK',
|
||||
]);
|
||||
export type OriginType = z.infer<typeof OriginTypeSchema>;
|
||||
|
||||
export const DiffStatusSchema = z.enum([
|
||||
'DRAFT',
|
||||
'EXPORTED',
|
||||
'IMPLEMENTED',
|
||||
'BLOCKED',
|
||||
]);
|
||||
export type DiffStatus = z.infer<typeof DiffStatusSchema>;
|
||||
|
||||
export const TeamRoleSchema = z.enum([
|
||||
'OWNER',
|
||||
'DESIGNER',
|
||||
'ENGINEER',
|
||||
'PM',
|
||||
'VIEWER',
|
||||
]);
|
||||
export type TeamRole = z.infer<typeof TeamRoleSchema>;
|
||||
|
||||
export const AgentTypeSchema = z.enum(['CURSOR', 'CLAUDE_CODE', 'GENERIC']);
|
||||
export type AgentType = z.infer<typeof AgentTypeSchema>;
|
||||
|
||||
export const WorkspacePlanSchema = z.enum(['FREE', 'TEAM', 'ENTERPRISE']);
|
||||
export type WorkspacePlan = z.infer<typeof WorkspacePlanSchema>;
|
||||
|
||||
// ── Row types (match PostgreSQL columns 1:1) ──────────────────────────────────
|
||||
|
||||
export const WorkspaceSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
owner_id: z.string(),
|
||||
plan: WorkspacePlanSchema,
|
||||
settings_jsonb: z.record(z.unknown()),
|
||||
created_at: z.string().datetime(),
|
||||
updated_at: z.string().datetime(),
|
||||
});
|
||||
export type Workspace = z.infer<typeof WorkspaceSchema>;
|
||||
|
||||
export const ArtboardSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
workspace_id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
origin_id: z.string().uuid().nullable(),
|
||||
parent_artboard_id: z.string().uuid().nullable(),
|
||||
metadata_jsonb: z.record(z.unknown()),
|
||||
created_at: z.string().datetime(),
|
||||
updated_at: z.string().datetime(),
|
||||
});
|
||||
export type Artboard = z.infer<typeof ArtboardSchema>;
|
||||
|
||||
export const OriginSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
type: OriginTypeSchema,
|
||||
source_ref: z.string(),
|
||||
source_metadata_jsonb: z.record(z.unknown()),
|
||||
created_at: z.string().datetime(),
|
||||
updated_at: z.string().datetime(),
|
||||
});
|
||||
export type Origin = z.infer<typeof OriginSchema>;
|
||||
|
||||
export const IntentDiffSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
artboard_id: z.string().uuid(),
|
||||
author_id: z.string(),
|
||||
changes_jsonb: z.record(z.unknown()),
|
||||
summary: z.string(),
|
||||
status: DiffStatusSchema,
|
||||
/** Free-text notes from the coding agent (e.g. why it was blocked) */
|
||||
notes: z.string().optional(),
|
||||
created_at: z.string().datetime(),
|
||||
updated_at: z.string().datetime(),
|
||||
});
|
||||
export type IntentDiff = z.infer<typeof IntentDiffSchema>;
|
||||
|
||||
export const AgentSessionSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
artboard_id: z.string().uuid(),
|
||||
diff_id: z.string().uuid().nullable(),
|
||||
agent_type: AgentTypeSchema,
|
||||
messages_jsonb: z.array(z.record(z.unknown())),
|
||||
status: z.enum(['ACTIVE', 'COMPLETED', 'FAILED']),
|
||||
created_at: z.string().datetime(),
|
||||
updated_at: z.string().datetime(),
|
||||
});
|
||||
export type AgentSession = z.infer<typeof AgentSessionSchema>;
|
||||
|
||||
export const DesignLanguageFileSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
workspace_id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
schema_jsonb: z.record(z.unknown()),
|
||||
version: z.number().int(),
|
||||
created_at: z.string().datetime(),
|
||||
updated_at: z.string().datetime(),
|
||||
});
|
||||
export type DesignLanguageFile = z.infer<typeof DesignLanguageFileSchema>;
|
||||
|
||||
export const TeamMemberSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
workspace_id: z.string().uuid(),
|
||||
user_id: z.string(),
|
||||
role: TeamRoleSchema,
|
||||
created_at: z.string().datetime(),
|
||||
updated_at: z.string().datetime(),
|
||||
});
|
||||
export type TeamMember = z.infer<typeof TeamMemberSchema>;
|
||||
|
||||
// ── Ancestry (from materialized view) ────────────────────────────────────────
|
||||
|
||||
export interface ArtboardAncestry {
|
||||
artboard_id: string;
|
||||
ancestor_id: string;
|
||||
depth: number;
|
||||
}
|
||||
|
||||
// ── Insert types (omit server-set fields) ────────────────────────────────────
|
||||
|
||||
export type InsertWorkspace = Omit<Workspace, 'id' | 'created_at' | 'updated_at'>;
|
||||
export type InsertArtboard = Omit<Artboard, 'id' | 'created_at' | 'updated_at'>;
|
||||
export type InsertOrigin = Omit<Origin, 'id' | 'created_at' | 'updated_at'>;
|
||||
export type InsertIntentDiff = Omit<IntentDiff, 'id' | 'created_at' | 'updated_at'>;
|
||||
export type InsertAgentSession = Omit<AgentSession, 'id' | 'created_at' | 'updated_at'>;
|
||||
export type InsertDesignLanguageFile = Omit<DesignLanguageFile, 'id' | 'created_at' | 'updated_at'>;
|
||||
export type InsertTeamMember = Omit<TeamMember, 'id' | 'created_at' | 'updated_at'>;
|
||||
Reference in New Issue
Block a user