mirror of
https://github.com/willchen96/mike.git
synced 2026-06-20 21:18:07 +02:00
refactor: add table primitive and migrations by date; feat: add mcp connectors
This commit is contained in:
parent
01dfcfe0d4
commit
9a1277ba99
99 changed files with 9344 additions and 2320 deletions
32
backend/migrations/20260419_tabular_chat_jsonb.sql
Normal file
32
backend/migrations/20260419_tabular_chat_jsonb.sql
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
-- Migration date: 2026-04-19
|
||||
|
||||
-- Migration: Convert tabular_review_chat_messages.content from TEXT to JSONB
|
||||
-- and add annotations JSONB column.
|
||||
--
|
||||
-- User messages: content TEXT → JSON string (e.g. "hello" → '"hello"')
|
||||
-- Assistant messages: content TEXT → events array (e.g. "answer" → '[{"type":"content","text":"answer"}]')
|
||||
--
|
||||
-- Only convert while content is still TEXT. Re-running over jsonb content would
|
||||
-- double-wrap assistant events, so the type check makes this safe to re-run.
|
||||
DO $$
|
||||
BEGIN
|
||||
IF (
|
||||
SELECT data_type
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'tabular_review_chat_messages'
|
||||
AND column_name = 'content'
|
||||
) = 'text' THEN
|
||||
ALTER TABLE tabular_review_chat_messages
|
||||
ALTER COLUMN content TYPE jsonb
|
||||
USING CASE
|
||||
WHEN role = 'user'
|
||||
THEN to_jsonb(content)
|
||||
ELSE
|
||||
jsonb_build_array(jsonb_build_object('type', 'content', 'text', content))
|
||||
END;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
ALTER TABLE tabular_review_chat_messages
|
||||
ADD COLUMN IF NOT EXISTS annotations jsonb;
|
||||
52
backend/migrations/20260421_01_docx_editing.sql
Normal file
52
backend/migrations/20260421_01_docx_editing.sql
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
-- Migration date: 2026-04-21
|
||||
|
||||
-- Migration: DOCX editing with tracked changes.
|
||||
-- Adds per-edit Accept/Reject state and a pointer to the document's current version.
|
||||
-- Assumes document_versions table already exists (see separate migration).
|
||||
|
||||
-- 1. Broaden document_versions.source to include 'user_reject'.
|
||||
ALTER TABLE public.document_versions
|
||||
DROP CONSTRAINT IF EXISTS document_versions_source_check;
|
||||
|
||||
ALTER TABLE public.document_versions
|
||||
ADD CONSTRAINT document_versions_source_check
|
||||
CHECK (source = ANY (ARRAY[
|
||||
'upload'::text,
|
||||
'assistant_edit'::text,
|
||||
'user_accept'::text,
|
||||
'user_reject'::text,
|
||||
'generated'::text
|
||||
]));
|
||||
|
||||
-- 2. Point each document at its currently active version (null = original upload).
|
||||
ALTER TABLE public.documents
|
||||
ADD COLUMN IF NOT EXISTS current_version_id uuid
|
||||
REFERENCES public.document_versions(id) ON DELETE SET NULL;
|
||||
|
||||
-- 3. Per-edit registry. One row per tracked change proposed by the assistant.
|
||||
-- change_id is the w:id written into document.xml so Accept/Reject can
|
||||
-- locate the specific w:ins/w:del pair on the latest version.
|
||||
CREATE TABLE IF NOT EXISTS public.document_edits (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
document_id uuid NOT NULL REFERENCES public.documents(id) ON DELETE CASCADE,
|
||||
chat_message_id uuid REFERENCES public.chat_messages(id) ON DELETE SET NULL,
|
||||
version_id uuid NOT NULL REFERENCES public.document_versions(id) ON DELETE CASCADE,
|
||||
change_id text NOT NULL,
|
||||
deleted_text text NOT NULL DEFAULT '',
|
||||
inserted_text text NOT NULL DEFAULT '',
|
||||
context_before text,
|
||||
context_after text,
|
||||
status text NOT NULL DEFAULT 'pending'
|
||||
CHECK (status = ANY (ARRAY['pending'::text, 'accepted'::text, 'rejected'::text])),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
resolved_at timestamptz
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS document_edits_document_id_idx
|
||||
ON public.document_edits (document_id, created_at DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS document_edits_message_id_idx
|
||||
ON public.document_edits (chat_message_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS document_edits_version_id_idx
|
||||
ON public.document_edits (version_id);
|
||||
9
backend/migrations/20260421_02_user_api_keys.sql
Normal file
9
backend/migrations/20260421_02_user_api_keys.sql
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
-- Migration date: 2026-04-21
|
||||
|
||||
-- Migration: add optional per-user API keys for direct provider access.
|
||||
-- When set, these keys override the server-wide env keys for that user's
|
||||
-- requests; callers must fall back to env when null.
|
||||
|
||||
ALTER TABLE user_profiles
|
||||
ADD COLUMN IF NOT EXISTS claude_api_key TEXT,
|
||||
ADD COLUMN IF NOT EXISTS gemini_api_key TEXT;
|
||||
10
backend/migrations/20260423_01_docx_editing_wids.sql
Normal file
10
backend/migrations/20260423_01_docx_editing_wids.sql
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
-- Migration date: 2026-04-23
|
||||
|
||||
-- Migration: persist the actual w:ins / w:del numeric ids alongside the
|
||||
-- logical change_id. Accept/Reject needs these to locate the wrapper
|
||||
-- elements inside document.xml; change_id is our own opaque label and
|
||||
-- never lands in the file.
|
||||
|
||||
ALTER TABLE public.document_edits
|
||||
ADD COLUMN IF NOT EXISTS del_w_id text,
|
||||
ADD COLUMN IF NOT EXISTS ins_w_id text;
|
||||
32
backend/migrations/20260423_02_docx_version_number.sql
Normal file
32
backend/migrations/20260423_02_docx_version_number.sql
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
-- Migration date: 2026-04-23
|
||||
|
||||
-- Migration: give each assistant-produced version of a document a
|
||||
-- monotonic per-document version number (V1, V2, …). Only
|
||||
-- `source = 'assistant_edit'` rows carry a number; the original upload
|
||||
-- and the ephemeral user_accept/user_reject rows stay NULL. Numbers are
|
||||
-- stable once written — accept/reject now overwrite bytes in place
|
||||
-- rather than insert new rows, so the sequence never has gaps.
|
||||
|
||||
ALTER TABLE public.document_versions
|
||||
ADD COLUMN IF NOT EXISTS version_number integer;
|
||||
|
||||
-- Backfill: assign 1..N to the existing assistant_edit rows per doc,
|
||||
-- ordered by created_at ascending. Safe to re-run (only writes NULLs).
|
||||
WITH numbered AS (
|
||||
SELECT
|
||||
id,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY document_id
|
||||
ORDER BY created_at ASC
|
||||
) AS rn
|
||||
FROM public.document_versions
|
||||
WHERE source = 'assistant_edit'
|
||||
)
|
||||
UPDATE public.document_versions dv
|
||||
SET version_number = n.rn
|
||||
FROM numbered n
|
||||
WHERE dv.id = n.id
|
||||
AND dv.version_number IS NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS document_versions_doc_vnum_idx
|
||||
ON public.document_versions (document_id, version_number);
|
||||
35
backend/migrations/20260424_01_docx_version_display_name.sql
Normal file
35
backend/migrations/20260424_01_docx_version_display_name.sql
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
-- Migration date: 2026-04-24
|
||||
|
||||
-- Migration: per-version user-editable display name + user_upload source.
|
||||
-- Lets users rename individual versions (the assistant-edit default is
|
||||
-- "[Edited V{n}]") and differentiate manually-uploaded new versions from
|
||||
-- the original upload.
|
||||
|
||||
ALTER TABLE public.document_versions
|
||||
ADD COLUMN IF NOT EXISTS display_name text;
|
||||
|
||||
-- Broaden source to include 'user_upload' for versions the user uploads
|
||||
-- after the original document creation.
|
||||
ALTER TABLE public.document_versions
|
||||
DROP CONSTRAINT IF EXISTS document_versions_source_check;
|
||||
|
||||
ALTER TABLE public.document_versions
|
||||
ADD CONSTRAINT document_versions_source_check
|
||||
CHECK (source = ANY (ARRAY[
|
||||
'upload'::text,
|
||||
'user_upload'::text,
|
||||
'assistant_edit'::text,
|
||||
'user_accept'::text,
|
||||
'user_reject'::text,
|
||||
'generated'::text
|
||||
]));
|
||||
|
||||
-- Backfill: default display_name to the parent document's filename. New
|
||||
-- assistant edits inherit the prior version's display_name (see
|
||||
-- runEditDocument), so the version number is no longer baked into the
|
||||
-- default label — it's surfaced as a separate tag in the UI.
|
||||
UPDATE public.document_versions dv
|
||||
SET display_name = d.filename
|
||||
FROM public.documents d
|
||||
WHERE dv.display_name IS NULL
|
||||
AND d.id = dv.document_id;
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
-- Migration date: 2026-04-24
|
||||
|
||||
-- Migration: number the original upload as V1 so assistant edits start at V2.
|
||||
-- Before: upload rows had version_number NULL, assistant_edit rows started at 1.
|
||||
-- After: every row in document_versions has a monotonic per-document V# with
|
||||
-- the upload as V1.
|
||||
|
||||
-- Guard: this shift is not naturally idempotent (re-running would bump the
|
||||
-- numbers again). An unnumbered upload row is the signal that the migration
|
||||
-- has not run yet; once every upload row is numbered there is nothing to do.
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM public.document_versions
|
||||
WHERE source = 'upload' AND version_number IS NULL
|
||||
) THEN
|
||||
-- 1. Shift existing assistant_edit + user_upload numbers up by 1 so they no
|
||||
-- longer collide with the upload's new V1. Done first so we don't violate
|
||||
-- any uniqueness constraint while the upload row still lacks a number.
|
||||
UPDATE public.document_versions
|
||||
SET version_number = version_number + 1
|
||||
WHERE source IN ('assistant_edit', 'user_upload')
|
||||
AND version_number IS NOT NULL;
|
||||
|
||||
-- 2. Backfill every upload row's version_number to 1.
|
||||
UPDATE public.document_versions
|
||||
SET version_number = 1
|
||||
WHERE source = 'upload'
|
||||
AND version_number IS NULL;
|
||||
END IF;
|
||||
END $$;
|
||||
81
backend/migrations/20260427_01_move_storage_to_versions.sql
Normal file
81
backend/migrations/20260427_01_move_storage_to_versions.sql
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
-- Migration date: 2026-04-27
|
||||
|
||||
-- Move storage_path and pdf_storage_path from documents to document_versions.
|
||||
--
|
||||
-- Rationale: there were two sources of truth for "where the bytes live"
|
||||
-- - documents.{storage_path, pdf_storage_path} (set on initial upload)
|
||||
-- - document_versions.storage_path (set on each new version)
|
||||
-- New-version uploads only updated the latter, so /display, downloads,
|
||||
-- and assistant context all drifted to the original upload's bytes.
|
||||
--
|
||||
-- After this migration:
|
||||
-- - document_versions owns storage_path and pdf_storage_path.
|
||||
-- - documents.current_version_id is the only "which version is live" pointer.
|
||||
-- - documents.{storage_path, pdf_storage_path} are dropped.
|
||||
|
||||
-- 1. Add pdf_storage_path to document_versions.
|
||||
alter table public.document_versions
|
||||
add column if not exists pdf_storage_path text;
|
||||
|
||||
-- 2. Backfill: ensure every document has at least one document_versions row
|
||||
-- (the original upload). Older docs may predate document_versions entirely.
|
||||
insert into public.document_versions (
|
||||
document_id,
|
||||
storage_path,
|
||||
pdf_storage_path,
|
||||
source,
|
||||
version_number,
|
||||
display_name,
|
||||
created_at
|
||||
)
|
||||
select
|
||||
d.id,
|
||||
d.storage_path,
|
||||
d.pdf_storage_path,
|
||||
'upload',
|
||||
1,
|
||||
d.filename,
|
||||
d.created_at
|
||||
from public.documents d
|
||||
left join public.document_versions dv
|
||||
on dv.document_id = d.id and dv.source = 'upload'
|
||||
where dv.id is null
|
||||
and d.storage_path is not null;
|
||||
|
||||
-- 3. Backfill pdf_storage_path onto the existing 'upload' rows for docs
|
||||
-- that already had one but predate document_versions.pdf_storage_path.
|
||||
update public.document_versions dv
|
||||
set pdf_storage_path = d.pdf_storage_path
|
||||
from public.documents d
|
||||
where dv.document_id = d.id
|
||||
and dv.source = 'upload'
|
||||
and dv.pdf_storage_path is null
|
||||
and d.pdf_storage_path is not null;
|
||||
|
||||
-- 4. Backfill current_version_id for any document missing one — point it
|
||||
-- at the most recent version (assistant_edit / user_upload preferred,
|
||||
-- else the upload row).
|
||||
update public.documents d
|
||||
set current_version_id = sub.id
|
||||
from (
|
||||
select distinct on (document_id) id, document_id
|
||||
from public.document_versions
|
||||
order by document_id,
|
||||
case source
|
||||
when 'assistant_edit' then 1
|
||||
when 'user_upload' then 2
|
||||
when 'user_accept' then 3
|
||||
when 'user_reject' then 4
|
||||
when 'generated' then 5
|
||||
when 'upload' then 6
|
||||
else 7
|
||||
end,
|
||||
version_number desc nulls last,
|
||||
created_at desc
|
||||
) sub
|
||||
where d.id = sub.document_id
|
||||
and d.current_version_id is null;
|
||||
|
||||
-- 5. Drop the columns from documents.
|
||||
alter table public.documents drop column if exists storage_path;
|
||||
alter table public.documents drop column if exists pdf_storage_path;
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
-- Migration date: 2026-04-27
|
||||
|
||||
-- Migration: add shared_with to tabular_reviews so standalone reviews
|
||||
-- (project_id IS NULL) can be shared by email, mirroring projects.shared_with.
|
||||
-- Project-scoped reviews continue to inherit access from their parent project.
|
||||
|
||||
alter table public.tabular_reviews
|
||||
add column if not exists shared_with jsonb not null default '[]';
|
||||
|
||||
-- Optional but worth it: a generic GIN index speeds up the contains-query
|
||||
-- the backend uses to fan out shared-review listings.
|
||||
create index if not exists tabular_reviews_shared_with_idx
|
||||
on public.tabular_reviews using gin (shared_with);
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
-- Migration date: 2026-04-27
|
||||
|
||||
-- Migration: capture the user's organisation alongside display_name.
|
||||
-- Collected on the signup form (optional) and editable from the account
|
||||
-- page. Used for display only; no business logic depends on it yet.
|
||||
|
||||
ALTER TABLE user_profiles
|
||||
ADD COLUMN IF NOT EXISTS organisation TEXT;
|
||||
28
backend/migrations/20260428_workflow_shares_unique.sql
Normal file
28
backend/migrations/20260428_workflow_shares_unique.sql
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
-- Migration date: 2026-04-28
|
||||
|
||||
-- Migration: enforce one share row per (workflow, recipient email) so
|
||||
-- re-sharing to the same person updates the existing row instead of
|
||||
-- creating duplicates. Without this, DELETE only removes one of N copies
|
||||
-- and the recipient retains access after the owner thinks they revoked.
|
||||
|
||||
-- Collapse any existing duplicates first, keeping the most recent row.
|
||||
delete from public.workflow_shares a
|
||||
using public.workflow_shares b
|
||||
where a.workflow_id = b.workflow_id
|
||||
and a.shared_with_email = b.shared_with_email
|
||||
and a.created_at < b.created_at;
|
||||
|
||||
-- Add the unique constraint only if it is not already present (ADD CONSTRAINT
|
||||
-- has no IF NOT EXISTS form, so re-running the bare statement would error).
|
||||
do $$
|
||||
begin
|
||||
if not exists (
|
||||
select 1 from pg_constraint
|
||||
where conname = 'workflow_shares_workflow_email_unique'
|
||||
and conrelid = 'public.workflow_shares'::regclass
|
||||
) then
|
||||
alter table public.workflow_shares
|
||||
add constraint workflow_shares_workflow_email_unique
|
||||
unique (workflow_id, shared_with_email);
|
||||
end if;
|
||||
end $$;
|
||||
25
backend/migrations/20260502_secure_user_api_keys.sql
Normal file
25
backend/migrations/20260502_secure_user_api_keys.sql
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
-- Migration date: 2026-05-02
|
||||
|
||||
-- Migration: move BYO provider API keys into encrypted, server-only storage.
|
||||
-- The backend encrypts values before writing them. RLS is enabled with no
|
||||
-- client policies so browser Supabase clients cannot read key material.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.user_api_keys (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
provider text NOT NULL CHECK (provider IN ('claude', 'gemini', 'openai', 'openrouter', 'courtlistener')),
|
||||
encrypted_key text NOT NULL,
|
||||
iv text NOT NULL,
|
||||
auth_tag text NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE(user_id, provider)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_api_keys_user
|
||||
ON public.user_api_keys(user_id);
|
||||
|
||||
ALTER TABLE public.user_api_keys ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Legacy plaintext columns remain temporarily so the backend can migrate
|
||||
-- existing users on first use, then clear each migrated value.
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
-- Migration date: 2026-05-08
|
||||
|
||||
-- Migration: make application data tables backend-only.
|
||||
-- RLS remains enabled as defense in depth, but direct browser Supabase clients
|
||||
-- should not be able to query or mutate these tables with anon/authenticated.
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
table_name text;
|
||||
backend_only_tables text[] := ARRAY[
|
||||
'projects',
|
||||
'project_subfolders',
|
||||
'documents',
|
||||
'document_versions',
|
||||
'document_edits',
|
||||
'workflows',
|
||||
'hidden_workflows',
|
||||
'workflow_shares',
|
||||
'chats',
|
||||
'chat_messages',
|
||||
'tabular_reviews',
|
||||
'tabular_cells',
|
||||
'tabular_review_chats',
|
||||
'tabular_review_chat_messages',
|
||||
'user_api_keys'
|
||||
];
|
||||
BEGIN
|
||||
FOREACH table_name IN ARRAY backend_only_tables LOOP
|
||||
IF to_regclass(format('public.%I', table_name)) IS NOT NULL THEN
|
||||
EXECUTE format(
|
||||
'REVOKE ALL PRIVILEGES ON TABLE public.%I FROM anon, authenticated',
|
||||
table_name
|
||||
);
|
||||
END IF;
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
-- Migration date: 2026-05-08
|
||||
|
||||
-- Migration: move user_profiles behind the backend API.
|
||||
-- The frontend should use Supabase only for auth; profile reads and writes go
|
||||
-- through /user/profile so internal fields cannot be mutated from the browser.
|
||||
|
||||
ALTER TABLE public.user_profiles ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
DROP POLICY IF EXISTS "Users can view their own profile"
|
||||
ON public.user_profiles;
|
||||
|
||||
DROP POLICY IF EXISTS "Users can update their own profile"
|
||||
ON public.user_profiles;
|
||||
|
||||
REVOKE ALL PRIVILEGES ON TABLE public.user_profiles
|
||||
FROM anon, authenticated;
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
-- Migration date: 2026-05-09
|
||||
|
||||
-- Allow users to store an OpenAI API key alongside Claude and Gemini keys.
|
||||
do $$
|
||||
begin
|
||||
alter table public.user_api_keys
|
||||
drop constraint if exists user_api_keys_provider_check;
|
||||
|
||||
alter table public.user_api_keys
|
||||
add constraint user_api_keys_provider_check
|
||||
check (provider in ('claude', 'gemini', 'openai'));
|
||||
end $$;
|
||||
26
backend/migrations/20260511_contact_messages.sql
Normal file
26
backend/migrations/20260511_contact_messages.sql
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
-- Migration date: 2026-05-11
|
||||
|
||||
-- Store landing-page contact form submissions.
|
||||
-- The landing server route writes with the Supabase service role; browser
|
||||
-- anon/authenticated roles should not have direct table access.
|
||||
|
||||
create table if not exists public.contact_messages (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
name text,
|
||||
email text not null,
|
||||
subject text,
|
||||
message text not null,
|
||||
source text not null default 'landing',
|
||||
user_agent text,
|
||||
ip_hash text,
|
||||
created_at timestamptz not null default now(),
|
||||
responded_at timestamptz
|
||||
);
|
||||
|
||||
create index if not exists idx_contact_messages_created_at
|
||||
on public.contact_messages(created_at desc);
|
||||
|
||||
alter table public.contact_messages enable row level security;
|
||||
|
||||
revoke all privileges on table public.contact_messages
|
||||
from anon, authenticated;
|
||||
36
backend/migrations/20260513_projects_shared_with_jsonb.sql
Normal file
36
backend/migrations/20260513_projects_shared_with_jsonb.sql
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
-- Migration date: 2026-05-13
|
||||
|
||||
-- Migration: convert projects.shared_with from text[] to jsonb.
|
||||
-- tabular_reviews.shared_with is already jsonb and is intentionally untouched.
|
||||
|
||||
-- Only convert while shared_with is still text[]. Re-running the type change
|
||||
-- over an already-jsonb column is unnecessary and the guard keeps it a no-op.
|
||||
do $$
|
||||
begin
|
||||
if (
|
||||
select data_type
|
||||
from information_schema.columns
|
||||
where table_schema = 'public'
|
||||
and table_name = 'projects'
|
||||
and column_name = 'shared_with'
|
||||
) = 'ARRAY' then
|
||||
alter table public.projects
|
||||
alter column shared_with drop default;
|
||||
|
||||
alter table public.projects
|
||||
alter column shared_with type jsonb
|
||||
using case
|
||||
when shared_with is null then '[]'::jsonb
|
||||
else to_jsonb(shared_with)
|
||||
end;
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
alter table public.projects
|
||||
alter column shared_with set default '[]'::jsonb;
|
||||
|
||||
alter table public.projects
|
||||
alter column shared_with set not null;
|
||||
|
||||
create index if not exists projects_shared_with_idx
|
||||
on public.projects using gin (shared_with);
|
||||
12
backend/migrations/20260517_tabular_review_document_ids.sql
Normal file
12
backend/migrations/20260517_tabular_review_document_ids.sql
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
-- Migration date: 2026-05-17
|
||||
|
||||
-- Persist selected document rows independently from generated cells.
|
||||
-- This lets project-based tabular reviews keep an explicit document list even
|
||||
-- when the review has no columns/cells or all rows have been removed.
|
||||
|
||||
alter table public.tabular_reviews
|
||||
add column if not exists document_ids jsonb;
|
||||
|
||||
alter table public.tabular_reviews
|
||||
alter column document_ids drop not null,
|
||||
alter column document_ids drop default;
|
||||
41
backend/migrations/20260523_courtlistener_bulk_indexes.sql
Normal file
41
backend/migrations/20260523_courtlistener_bulk_indexes.sql
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
-- Migration date: 2026-05-23
|
||||
|
||||
-- CourtListener bulk-data indexes.
|
||||
--
|
||||
-- These tables hold lightweight lookup metadata imported from CourtListener
|
||||
-- CSV exports. Full opinion bodies are stored in R2 at:
|
||||
-- courtlistener/opinions/by-cluster/{cluster_id}/{opinion_id}.json
|
||||
|
||||
create table if not exists public.courtlistener_citation_index (
|
||||
id bigint primary key,
|
||||
volume text not null,
|
||||
reporter text not null,
|
||||
page text not null,
|
||||
type integer,
|
||||
cluster_id bigint not null,
|
||||
date_created timestamptz,
|
||||
date_modified timestamptz
|
||||
);
|
||||
|
||||
create index if not exists courtlistener_citation_lookup_idx
|
||||
on public.courtlistener_citation_index(volume, reporter, page);
|
||||
|
||||
create index if not exists courtlistener_citation_cluster_idx
|
||||
on public.courtlistener_citation_index(cluster_id);
|
||||
|
||||
create table if not exists public.courtlistener_opinion_cluster_index (
|
||||
id bigint primary key,
|
||||
case_name text,
|
||||
case_name_short text,
|
||||
case_name_full text,
|
||||
slug text,
|
||||
date_filed date,
|
||||
citation_count integer,
|
||||
precedential_status text,
|
||||
filepath_pdf_harvard text,
|
||||
filepath_json_harvard text,
|
||||
docket_id bigint
|
||||
);
|
||||
|
||||
revoke all on public.courtlistener_citation_index from anon, authenticated;
|
||||
revoke all on public.courtlistener_opinion_cluster_index from anon, authenticated;
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
-- Migration date: 2026-05-28
|
||||
|
||||
ALTER TABLE public.user_api_keys
|
||||
DROP CONSTRAINT IF EXISTS user_api_keys_provider_check;
|
||||
|
||||
ALTER TABLE public.user_api_keys
|
||||
ADD CONSTRAINT user_api_keys_provider_check
|
||||
CHECK (provider IN ('claude', 'gemini', 'openai', 'openrouter', 'courtlistener'));
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
-- Migration date: 2026-05-28
|
||||
|
||||
ALTER TABLE public.user_api_keys
|
||||
DROP CONSTRAINT IF EXISTS user_api_keys_provider_check;
|
||||
|
||||
ALTER TABLE public.user_api_keys
|
||||
ADD CONSTRAINT user_api_keys_provider_check
|
||||
CHECK (provider IN ('claude', 'gemini', 'openai', 'openrouter', 'courtlistener'));
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
-- Migration date: 2026-05-28
|
||||
|
||||
ALTER TABLE public.user_profiles
|
||||
ADD COLUMN IF NOT EXISTS title_model text,
|
||||
ADD COLUMN IF NOT EXISTS quote_model text;
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
-- Migration date: 2026-06-02
|
||||
|
||||
-- Add per-version file metadata.
|
||||
--
|
||||
-- documents is the stable container. document_versions owns the bytes for each
|
||||
-- version, so file metadata that describes those bytes belongs here too.
|
||||
--
|
||||
-- Safe to run before application code changes: this only adds nullable columns
|
||||
-- and backfills them from the parent document.
|
||||
|
||||
ALTER TABLE public.document_versions
|
||||
ADD COLUMN IF NOT EXISTS file_type text,
|
||||
ADD COLUMN IF NOT EXISTS size_bytes integer,
|
||||
ADD COLUMN IF NOT EXISTS page_count integer;
|
||||
|
||||
UPDATE public.document_versions dv
|
||||
SET
|
||||
file_type = COALESCE(NULLIF(btrim(dv.file_type), ''), d.file_type),
|
||||
size_bytes = COALESCE(dv.size_bytes, d.size_bytes),
|
||||
page_count = COALESCE(dv.page_count, d.page_count)
|
||||
FROM public.documents d
|
||||
WHERE dv.document_id = d.id
|
||||
AND (
|
||||
dv.file_type IS NULL
|
||||
OR btrim(dv.file_type) = ''
|
||||
OR dv.size_bytes IS NULL
|
||||
OR dv.page_count IS NULL
|
||||
);
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
-- Migration date: 2026-06-02
|
||||
|
||||
-- Temporary live-Supabase migration: add document_versions.filename without
|
||||
-- renaming or dropping document_versions.display_name yet.
|
||||
|
||||
ALTER TABLE public.document_versions
|
||||
ADD COLUMN IF NOT EXISTS filename text;
|
||||
|
||||
UPDATE public.document_versions
|
||||
SET filename = display_name
|
||||
WHERE (filename IS NULL OR btrim(filename) = '')
|
||||
AND display_name IS NOT NULL
|
||||
AND btrim(display_name) <> '';
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
-- Migration date: 2026-06-02
|
||||
|
||||
-- Destructive follow-up migration: remove legacy document-level file metadata.
|
||||
--
|
||||
-- Run this only after application code writes file_type, size_bytes,
|
||||
-- and page_count to document_versions and reads those values
|
||||
-- from the active version.
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
documents_file_metadata_count integer;
|
||||
BEGIN
|
||||
SELECT count(*)
|
||||
INTO documents_file_metadata_count
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'documents'
|
||||
AND column_name IN (
|
||||
'file_type',
|
||||
'size_bytes',
|
||||
'page_count'
|
||||
);
|
||||
|
||||
IF documents_file_metadata_count = 3 THEN
|
||||
ALTER TABLE public.document_versions
|
||||
ADD COLUMN IF NOT EXISTS file_type text,
|
||||
ADD COLUMN IF NOT EXISTS size_bytes integer,
|
||||
ADD COLUMN IF NOT EXISTS page_count integer;
|
||||
|
||||
UPDATE public.document_versions dv
|
||||
SET
|
||||
file_type = COALESCE(NULLIF(btrim(dv.file_type), ''), d.file_type),
|
||||
size_bytes = COALESCE(dv.size_bytes, d.size_bytes),
|
||||
page_count = COALESCE(dv.page_count, d.page_count)
|
||||
FROM public.documents d
|
||||
WHERE dv.document_id = d.id
|
||||
AND (
|
||||
dv.file_type IS NULL
|
||||
OR btrim(dv.file_type) = ''
|
||||
OR dv.size_bytes IS NULL
|
||||
OR dv.page_count IS NULL
|
||||
);
|
||||
END IF;
|
||||
|
||||
IF documents_file_metadata_count > 0 THEN
|
||||
ALTER TABLE public.documents
|
||||
DROP COLUMN IF EXISTS file_type,
|
||||
DROP COLUMN IF EXISTS size_bytes,
|
||||
DROP COLUMN IF EXISTS page_count;
|
||||
END IF;
|
||||
END $$;
|
||||
27
backend/migrations/20260602_04_drop_documents_filename.sql
Normal file
27
backend/migrations/20260602_04_drop_documents_filename.sql
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
-- Migration date: 2026-06-02
|
||||
|
||||
-- Migration: remove legacy document-level filename.
|
||||
--
|
||||
-- Before dropping the old column, copy any remaining legacy names onto
|
||||
-- version rows that do not yet have their own filename/display value.
|
||||
-- A later migration renames document_versions.display_name to filename.
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'documents'
|
||||
AND column_name = 'filename'
|
||||
) THEN
|
||||
UPDATE public.document_versions dv
|
||||
SET display_name = d.filename
|
||||
FROM public.documents d
|
||||
WHERE dv.document_id = d.id
|
||||
AND (dv.display_name IS NULL OR btrim(dv.display_name) = '');
|
||||
|
||||
ALTER TABLE public.documents
|
||||
DROP COLUMN filename;
|
||||
END IF;
|
||||
END $$;
|
||||
12
backend/migrations/20260603_drop_structure_tree.sql
Normal file
12
backend/migrations/20260603_drop_structure_tree.sql
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
-- Migration date: 2026-06-03
|
||||
|
||||
-- Remove unused document structure trees.
|
||||
--
|
||||
-- Safe to run before or after the document metadata migration because both
|
||||
-- columns are optional and dropped conditionally.
|
||||
|
||||
ALTER TABLE public.document_versions
|
||||
DROP COLUMN IF EXISTS structure_tree;
|
||||
|
||||
ALTER TABLE public.documents
|
||||
DROP COLUMN IF EXISTS structure_tree;
|
||||
192
backend/migrations/20260606_oss_schema_diff.sql
Normal file
192
backend/migrations/20260606_oss_schema_diff.sql
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
-- Migration date: 2026-06-06
|
||||
|
||||
-- OSS migration for the current backend/schema.sql diff.
|
||||
--
|
||||
-- This brings existing OSS Supabase databases in line with the updated fresh
|
||||
-- schema: model preference columns, BYO provider expansion, per-version
|
||||
-- document metadata, and CourtListener bulk lookup tables.
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- User profiles
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
alter table public.user_profiles
|
||||
add column if not exists title_model text,
|
||||
add column if not exists mfa_on_login boolean not null default false,
|
||||
add column if not exists quote_model text;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- User API keys
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
alter table public.user_api_keys
|
||||
drop constraint if exists user_api_keys_provider_check;
|
||||
|
||||
alter table public.user_api_keys
|
||||
add constraint user_api_keys_provider_check
|
||||
check (provider in ('claude', 'gemini', 'openai', 'openrouter', 'courtlistener'));
|
||||
|
||||
alter table public.user_api_keys enable row level security;
|
||||
|
||||
drop policy if exists user_api_keys_own on public.user_api_keys;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Document metadata now lives on document_versions
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
alter table public.document_versions
|
||||
add column if not exists filename text,
|
||||
add column if not exists file_type text,
|
||||
add column if not exists size_bytes integer,
|
||||
add column if not exists page_count integer;
|
||||
|
||||
do $$
|
||||
begin
|
||||
if exists (
|
||||
select 1
|
||||
from information_schema.columns
|
||||
where table_schema = 'public'
|
||||
and table_name = 'document_versions'
|
||||
and column_name = 'display_name'
|
||||
) then
|
||||
update public.document_versions dv
|
||||
set filename = dv.display_name
|
||||
where (dv.filename is null or btrim(dv.filename) = '')
|
||||
and dv.display_name is not null
|
||||
and btrim(dv.display_name) <> '';
|
||||
end if;
|
||||
|
||||
if exists (
|
||||
select 1
|
||||
from information_schema.columns
|
||||
where table_schema = 'public'
|
||||
and table_name = 'documents'
|
||||
and column_name = 'filename'
|
||||
) then
|
||||
update public.document_versions dv
|
||||
set filename = d.filename
|
||||
from public.documents d
|
||||
where dv.document_id = d.id
|
||||
and (dv.filename is null or btrim(dv.filename) = '')
|
||||
and d.filename is not null
|
||||
and btrim(d.filename) <> '';
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
do $$
|
||||
begin
|
||||
if exists (
|
||||
select 1
|
||||
from information_schema.columns
|
||||
where table_schema = 'public'
|
||||
and table_name = 'documents'
|
||||
and column_name = 'file_type'
|
||||
) then
|
||||
update public.document_versions dv
|
||||
set file_type = coalesce(nullif(btrim(dv.file_type), ''), d.file_type)
|
||||
from public.documents d
|
||||
where dv.document_id = d.id
|
||||
and (dv.file_type is null or btrim(dv.file_type) = '');
|
||||
end if;
|
||||
|
||||
if exists (
|
||||
select 1
|
||||
from information_schema.columns
|
||||
where table_schema = 'public'
|
||||
and table_name = 'documents'
|
||||
and column_name = 'size_bytes'
|
||||
) then
|
||||
update public.document_versions dv
|
||||
set size_bytes = d.size_bytes
|
||||
from public.documents d
|
||||
where dv.document_id = d.id
|
||||
and dv.size_bytes is null
|
||||
and d.size_bytes is not null;
|
||||
end if;
|
||||
|
||||
if exists (
|
||||
select 1
|
||||
from information_schema.columns
|
||||
where table_schema = 'public'
|
||||
and table_name = 'documents'
|
||||
and column_name = 'page_count'
|
||||
) then
|
||||
update public.document_versions dv
|
||||
set page_count = d.page_count
|
||||
from public.documents d
|
||||
where dv.document_id = d.id
|
||||
and dv.page_count is null
|
||||
and d.page_count is not null;
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
alter table public.document_versions
|
||||
drop column if exists display_name;
|
||||
|
||||
alter table public.documents
|
||||
drop column if exists filename,
|
||||
drop column if exists file_type,
|
||||
drop column if exists size_bytes,
|
||||
drop column if exists page_count,
|
||||
drop column if exists structure_tree;
|
||||
|
||||
do $$
|
||||
begin
|
||||
if not exists (
|
||||
select 1
|
||||
from pg_constraint
|
||||
where conname = 'document_versions_doc_version_unique'
|
||||
and conrelid = 'public.document_versions'::regclass
|
||||
) then
|
||||
alter table public.document_versions
|
||||
add constraint document_versions_doc_version_unique
|
||||
unique (document_id, version_number);
|
||||
end if;
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- CourtListener bulk-data indexes
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
create table if not exists public.courtlistener_citation_index (
|
||||
id bigint primary key,
|
||||
volume text not null,
|
||||
reporter text not null,
|
||||
page text not null,
|
||||
type integer,
|
||||
cluster_id bigint not null,
|
||||
date_created timestamptz,
|
||||
date_modified timestamptz
|
||||
);
|
||||
|
||||
create index if not exists courtlistener_citation_lookup_idx
|
||||
on public.courtlistener_citation_index(volume, reporter, page);
|
||||
|
||||
create index if not exists courtlistener_citation_cluster_idx
|
||||
on public.courtlistener_citation_index(cluster_id);
|
||||
|
||||
alter table public.courtlistener_citation_index enable row level security;
|
||||
|
||||
drop policy if exists cl_citation_read on public.courtlistener_citation_index;
|
||||
|
||||
create table if not exists public.courtlistener_opinion_cluster_index (
|
||||
id bigint primary key,
|
||||
case_name text,
|
||||
case_name_short text,
|
||||
case_name_full text,
|
||||
slug text,
|
||||
date_filed date,
|
||||
citation_count integer,
|
||||
precedential_status text,
|
||||
filepath_pdf_harvard text,
|
||||
filepath_json_harvard text,
|
||||
docket_id bigint
|
||||
);
|
||||
|
||||
alter table public.courtlistener_opinion_cluster_index enable row level security;
|
||||
|
||||
drop policy if exists cl_cluster_read on public.courtlistener_opinion_cluster_index;
|
||||
|
||||
revoke all on public.courtlistener_citation_index from anon, authenticated;
|
||||
revoke all on public.courtlistener_opinion_cluster_index from anon, authenticated;
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
-- Migration date: 2026-06-10
|
||||
|
||||
-- Keep document version tombstones after deleting version file bytes.
|
||||
-- Deleted versions remain visible in history but are ignored by active-file
|
||||
-- lookups and cannot be opened/downloaded/replaced.
|
||||
|
||||
alter table public.document_versions
|
||||
alter column storage_path drop not null;
|
||||
|
||||
alter table public.document_versions
|
||||
add column if not exists deleted_at timestamptz,
|
||||
add column if not exists deleted_by uuid;
|
||||
|
||||
create index if not exists document_versions_active_document_id_idx
|
||||
on public.document_versions(document_id, created_at desc)
|
||||
where deleted_at is null;
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
-- Migration date: 2026-06-10
|
||||
|
||||
ALTER TABLE public.user_profiles
|
||||
ADD COLUMN IF NOT EXISTS mfa_on_login boolean NOT NULL DEFAULT false;
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
-- Migration date: 2026-06-11
|
||||
|
||||
-- Per-user toggle for US legal research (CourtListener) tools in chat.
|
||||
--
|
||||
-- When true (the default), the CourtListener case-law tools and their system
|
||||
-- prompt are exposed to the chat assistant. When false, both the tools and the
|
||||
-- prompt are excluded from the chat. Surfaced in account settings under
|
||||
-- Features > Legal Research > Jurisdiction > US.
|
||||
--
|
||||
-- Safe to run before application code changes: this only adds a column with a
|
||||
-- default that preserves the existing (enabled) behaviour for all rows.
|
||||
|
||||
ALTER TABLE public.user_profiles
|
||||
ADD COLUMN IF NOT EXISTS legal_research_us boolean NOT NULL DEFAULT true;
|
||||
39
backend/migrations/20260613_01_chats_overview_rpc.sql
Normal file
39
backend/migrations/20260613_01_chats_overview_rpc.sql
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
-- Migration date: 2026-06-13
|
||||
|
||||
-- Global assistant chats overview read model.
|
||||
-- Returns the user's own chats plus chats under projects they own.
|
||||
|
||||
create or replace function public.get_chats_overview(
|
||||
p_user_id text,
|
||||
p_limit integer default null
|
||||
)
|
||||
returns table (
|
||||
id uuid,
|
||||
project_id uuid,
|
||||
user_id text,
|
||||
title text,
|
||||
created_at timestamptz
|
||||
)
|
||||
language sql
|
||||
stable
|
||||
as $$
|
||||
select
|
||||
c.id,
|
||||
c.project_id,
|
||||
c.user_id,
|
||||
c.title,
|
||||
c.created_at
|
||||
from public.chats c
|
||||
where c.user_id = p_user_id
|
||||
or exists (
|
||||
select 1
|
||||
from public.projects p
|
||||
where p.id = c.project_id
|
||||
and p.user_id = p_user_id
|
||||
)
|
||||
order by c.created_at desc
|
||||
limit case
|
||||
when p_limit is null then null
|
||||
else greatest(1, least(p_limit, 100))
|
||||
end;
|
||||
$$;
|
||||
81
backend/migrations/20260613_02_projects_overview_rpc.sql
Normal file
81
backend/migrations/20260613_02_projects_overview_rpc.sql
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
-- Migration date: 2026-06-13
|
||||
|
||||
-- Projects overview read model.
|
||||
-- Returns the project list, owner display name, and per-project counts in one
|
||||
-- database call for the /projects table.
|
||||
|
||||
create or replace function public.get_projects_overview(
|
||||
p_user_id text,
|
||||
p_user_email text default null
|
||||
)
|
||||
returns table (
|
||||
id uuid,
|
||||
user_id text,
|
||||
name text,
|
||||
cm_number text,
|
||||
shared_with jsonb,
|
||||
created_at timestamptz,
|
||||
updated_at timestamptz,
|
||||
is_owner boolean,
|
||||
owner_display_name text,
|
||||
owner_email text,
|
||||
document_count integer,
|
||||
chat_count integer,
|
||||
review_count integer
|
||||
)
|
||||
language sql
|
||||
stable
|
||||
as $$
|
||||
with visible_projects as (
|
||||
select p.*
|
||||
from public.projects p
|
||||
where p.user_id = p_user_id
|
||||
or (
|
||||
coalesce(p_user_email, '') <> ''
|
||||
and p.user_id <> p_user_id
|
||||
and p.shared_with @> jsonb_build_array(p_user_email)
|
||||
)
|
||||
),
|
||||
document_counts as (
|
||||
select d.project_id, count(*)::integer as document_count
|
||||
from public.documents d
|
||||
where d.project_id in (select vp.id from visible_projects vp)
|
||||
group by d.project_id
|
||||
),
|
||||
chat_counts as (
|
||||
select c.project_id, count(*)::integer as chat_count
|
||||
from public.chats c
|
||||
where c.project_id in (select vp.id from visible_projects vp)
|
||||
group by c.project_id
|
||||
),
|
||||
review_counts as (
|
||||
select tr.project_id, count(*)::integer as review_count
|
||||
from public.tabular_reviews tr
|
||||
where tr.project_id in (select vp.id from visible_projects vp)
|
||||
group by tr.project_id
|
||||
)
|
||||
select
|
||||
vp.id,
|
||||
vp.user_id,
|
||||
vp.name,
|
||||
vp.cm_number,
|
||||
vp.shared_with,
|
||||
vp.created_at,
|
||||
vp.updated_at,
|
||||
vp.user_id = p_user_id as is_owner,
|
||||
nullif(trim(up.display_name), '') as owner_display_name,
|
||||
null::text as owner_email,
|
||||
coalesce(dc.document_count, 0) as document_count,
|
||||
coalesce(cc.chat_count, 0) as chat_count,
|
||||
coalesce(rc.review_count, 0) as review_count
|
||||
from visible_projects vp
|
||||
left join public.user_profiles up
|
||||
on up.user_id::text = vp.user_id
|
||||
left join document_counts dc
|
||||
on dc.project_id = vp.id
|
||||
left join chat_counts cc
|
||||
on cc.project_id = vp.id
|
||||
left join review_counts rc
|
||||
on rc.project_id = vp.id
|
||||
order by vp.created_at desc;
|
||||
$$;
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
-- Migration date: 2026-06-13
|
||||
|
||||
-- Tabular reviews overview read model.
|
||||
-- Returns visible reviews plus document_count in one database call.
|
||||
|
||||
create or replace function public.get_tabular_reviews_overview(
|
||||
p_user_id text,
|
||||
p_user_email text default null,
|
||||
p_project_id text default null
|
||||
)
|
||||
returns table (
|
||||
id uuid,
|
||||
project_id uuid,
|
||||
user_id text,
|
||||
title text,
|
||||
columns_config jsonb,
|
||||
document_ids jsonb,
|
||||
workflow_id uuid,
|
||||
shared_with jsonb,
|
||||
created_at timestamptz,
|
||||
updated_at timestamptz,
|
||||
is_owner boolean,
|
||||
document_count integer
|
||||
)
|
||||
language sql
|
||||
stable
|
||||
as $$
|
||||
with accessible_projects as (
|
||||
select p.id
|
||||
from public.projects p
|
||||
where p.user_id = p_user_id
|
||||
or (
|
||||
coalesce(p_user_email, '') <> ''
|
||||
and p.user_id <> p_user_id
|
||||
and p.shared_with @> jsonb_build_array(p_user_email)
|
||||
)
|
||||
),
|
||||
visible_reviews as (
|
||||
select tr.*
|
||||
from public.tabular_reviews tr
|
||||
where (p_project_id is null or tr.project_id::text = p_project_id)
|
||||
and (
|
||||
p_project_id is null
|
||||
or exists (
|
||||
select 1
|
||||
from accessible_projects ap
|
||||
where ap.id::text = p_project_id
|
||||
)
|
||||
)
|
||||
and (
|
||||
tr.user_id = p_user_id
|
||||
or (
|
||||
tr.project_id in (select ap.id from accessible_projects ap)
|
||||
and tr.user_id <> p_user_id
|
||||
)
|
||||
or (
|
||||
p_project_id is null
|
||||
and coalesce(p_user_email, '') <> ''
|
||||
and tr.user_id <> p_user_id
|
||||
and tr.shared_with @> jsonb_build_array(p_user_email)
|
||||
)
|
||||
)
|
||||
),
|
||||
cell_document_counts as (
|
||||
select
|
||||
tc.review_id,
|
||||
count(distinct tc.document_id)::integer as document_count
|
||||
from public.tabular_cells tc
|
||||
where tc.review_id in (select vr.id from visible_reviews vr)
|
||||
group by tc.review_id
|
||||
)
|
||||
select
|
||||
vr.id,
|
||||
vr.project_id,
|
||||
vr.user_id,
|
||||
vr.title,
|
||||
vr.columns_config,
|
||||
vr.document_ids,
|
||||
vr.workflow_id,
|
||||
vr.shared_with,
|
||||
vr.created_at,
|
||||
vr.updated_at,
|
||||
vr.user_id = p_user_id as is_owner,
|
||||
case
|
||||
when jsonb_typeof(vr.document_ids) = 'array'
|
||||
then (
|
||||
select count(distinct doc_id.value)::integer
|
||||
from jsonb_array_elements_text(vr.document_ids) as doc_id(value)
|
||||
)
|
||||
else coalesce(cdc.document_count, 0)
|
||||
end as document_count
|
||||
from visible_reviews vr
|
||||
left join cell_document_counts cdc
|
||||
on cdc.review_id = vr.id
|
||||
order by vr.created_at desc;
|
||||
$$;
|
||||
92
backend/migrations/20260613_04_user_mcp_connectors.sql
Normal file
92
backend/migrations/20260613_04_user_mcp_connectors.sql
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
-- Server-side MCP client connector storage.
|
||||
-- Auth material is encrypted by the backend before insert. RLS is enabled with
|
||||
-- no browser policies so only the service-role backend can read connector
|
||||
-- URLs, encrypted auth config, token material, tool cache, and audit logs.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.user_mcp_connectors (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
name text NOT NULL,
|
||||
transport text NOT NULL DEFAULT 'streamable_http'
|
||||
CHECK (transport IN ('streamable_http')),
|
||||
server_url text NOT NULL,
|
||||
enabled boolean NOT NULL DEFAULT true,
|
||||
tool_policy jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
encrypted_auth_config text,
|
||||
auth_config_iv text,
|
||||
auth_config_tag text,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_mcp_connectors_user
|
||||
ON public.user_mcp_connectors(user_id);
|
||||
|
||||
ALTER TABLE public.user_mcp_connectors ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.user_mcp_oauth_tokens (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
connector_id uuid NOT NULL REFERENCES public.user_mcp_connectors(id) ON DELETE CASCADE,
|
||||
encrypted_access_token text,
|
||||
access_token_iv text,
|
||||
access_token_tag text,
|
||||
encrypted_refresh_token text,
|
||||
refresh_token_iv text,
|
||||
refresh_token_tag text,
|
||||
token_type text,
|
||||
scope text,
|
||||
expires_at timestamptz,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE(connector_id)
|
||||
);
|
||||
|
||||
ALTER TABLE public.user_mcp_oauth_tokens ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.user_mcp_connector_tools (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
connector_id uuid NOT NULL REFERENCES public.user_mcp_connectors(id) ON DELETE CASCADE,
|
||||
tool_name text NOT NULL,
|
||||
openai_tool_name text NOT NULL,
|
||||
title text,
|
||||
description text,
|
||||
input_schema jsonb NOT NULL DEFAULT '{"type":"object","properties":{}}'::jsonb,
|
||||
output_schema jsonb,
|
||||
annotations jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
enabled boolean NOT NULL DEFAULT true,
|
||||
requires_confirmation boolean NOT NULL DEFAULT false,
|
||||
last_seen_at timestamptz NOT NULL DEFAULT now(),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE(connector_id, tool_name),
|
||||
UNIQUE(openai_tool_name)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_mcp_connector_tools_connector
|
||||
ON public.user_mcp_connector_tools(connector_id);
|
||||
|
||||
ALTER TABLE public.user_mcp_connector_tools ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.user_mcp_tool_audit_logs (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
connector_id uuid NOT NULL REFERENCES public.user_mcp_connectors(id) ON DELETE CASCADE,
|
||||
tool_id uuid REFERENCES public.user_mcp_connector_tools(id) ON DELETE SET NULL,
|
||||
tool_name text NOT NULL,
|
||||
openai_tool_name text NOT NULL,
|
||||
status text NOT NULL CHECK (status IN ('ok', 'error')),
|
||||
error_message text,
|
||||
duration_ms integer NOT NULL DEFAULT 0,
|
||||
result_size_chars integer NOT NULL DEFAULT 0,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_mcp_tool_audit_logs_user_created
|
||||
ON public.user_mcp_tool_audit_logs(user_id, created_at DESC);
|
||||
|
||||
ALTER TABLE public.user_mcp_tool_audit_logs ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
REVOKE ALL ON public.user_mcp_connectors FROM anon, authenticated;
|
||||
REVOKE ALL ON public.user_mcp_oauth_tokens FROM anon, authenticated;
|
||||
REVOKE ALL ON public.user_mcp_connector_tools FROM anon, authenticated;
|
||||
REVOKE ALL ON public.user_mcp_tool_audit_logs FROM anon, authenticated;
|
||||
75
backend/migrations/20260613_05_workflows_overview_rpc.sql
Normal file
75
backend/migrations/20260613_05_workflows_overview_rpc.sql
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
-- Migration date: 2026-06-13
|
||||
|
||||
-- Workflows overview read model.
|
||||
-- Returns owned and shared workflows in one database call.
|
||||
|
||||
create or replace function public.get_workflows_overview(
|
||||
p_user_id text,
|
||||
p_user_email text default null,
|
||||
p_type text default null
|
||||
)
|
||||
returns table (
|
||||
id uuid,
|
||||
user_id text,
|
||||
title text,
|
||||
type text,
|
||||
prompt_md text,
|
||||
columns_config jsonb,
|
||||
practice text,
|
||||
is_system boolean,
|
||||
created_at timestamptz,
|
||||
allow_edit boolean,
|
||||
is_owner boolean,
|
||||
shared_by_name text
|
||||
)
|
||||
language sql
|
||||
stable
|
||||
as $$
|
||||
with owned as (
|
||||
select
|
||||
w.*,
|
||||
true as allow_edit,
|
||||
true as is_owner,
|
||||
null::text as shared_by_name,
|
||||
0 as sort_bucket
|
||||
from public.workflows w
|
||||
where w.user_id = p_user_id
|
||||
and w.is_system = false
|
||||
and (p_type is null or w.type = p_type)
|
||||
),
|
||||
shared as (
|
||||
select
|
||||
w.*,
|
||||
ws.allow_edit,
|
||||
false as is_owner,
|
||||
nullif(trim(up.display_name), '') as shared_by_name,
|
||||
1 as sort_bucket
|
||||
from public.workflow_shares ws
|
||||
join public.workflows w
|
||||
on w.id = ws.workflow_id
|
||||
left join public.user_profiles up
|
||||
on up.user_id::text = ws.shared_by_user_id
|
||||
where lower(ws.shared_with_email) = lower(coalesce(p_user_email, ''))
|
||||
and (p_type is null or w.type = p_type)
|
||||
),
|
||||
visible_workflows as (
|
||||
select * from owned
|
||||
union all
|
||||
select * from shared
|
||||
)
|
||||
select
|
||||
vw.id,
|
||||
vw.user_id,
|
||||
vw.title,
|
||||
vw.type,
|
||||
vw.prompt_md,
|
||||
vw.columns_config,
|
||||
vw.practice,
|
||||
vw.is_system,
|
||||
vw.created_at,
|
||||
vw.allow_edit,
|
||||
vw.is_owner,
|
||||
vw.shared_by_name
|
||||
from visible_workflows vw
|
||||
order by vw.sort_bucket asc, vw.created_at desc;
|
||||
$$;
|
||||
42
backend/migrations/20260615_01_mcp_connector_oauth.sql
Normal file
42
backend/migrations/20260615_01_mcp_connector_oauth.sql
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
-- Migration date: 2026-06-15
|
||||
-- Adds OAuth metadata/state needed for HTTP MCP connectors that authorize via
|
||||
-- OAuth 2.x instead of pasted bearer tokens.
|
||||
|
||||
ALTER TABLE public.user_mcp_connectors
|
||||
ADD COLUMN IF NOT EXISTS auth_type text NOT NULL DEFAULT 'none'
|
||||
CHECK (auth_type IN ('none', 'bearer', 'oauth'));
|
||||
|
||||
UPDATE public.user_mcp_connectors
|
||||
SET auth_type = CASE
|
||||
WHEN encrypted_auth_config IS NOT NULL THEN 'bearer'
|
||||
ELSE 'none'
|
||||
END
|
||||
WHERE auth_type IS NULL OR auth_type = 'none';
|
||||
|
||||
ALTER TABLE public.user_mcp_oauth_tokens
|
||||
ADD COLUMN IF NOT EXISTS authorization_server text,
|
||||
ADD COLUMN IF NOT EXISTS token_endpoint text,
|
||||
ADD COLUMN IF NOT EXISTS client_id text,
|
||||
ADD COLUMN IF NOT EXISTS encrypted_client_secret text,
|
||||
ADD COLUMN IF NOT EXISTS client_secret_iv text,
|
||||
ADD COLUMN IF NOT EXISTS client_secret_tag text,
|
||||
ADD COLUMN IF NOT EXISTS resource text;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.user_mcp_oauth_states (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
connector_id uuid NOT NULL REFERENCES public.user_mcp_connectors(id) ON DELETE CASCADE,
|
||||
state_hash text NOT NULL UNIQUE,
|
||||
encrypted_state_config text NOT NULL,
|
||||
state_config_iv text NOT NULL,
|
||||
state_config_tag text NOT NULL,
|
||||
expires_at timestamptz NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_mcp_oauth_states_expires
|
||||
ON public.user_mcp_oauth_states(expires_at);
|
||||
|
||||
ALTER TABLE public.user_mcp_oauth_states ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
REVOKE ALL ON public.user_mcp_oauth_states FROM anon, authenticated;
|
||||
Loading…
Add table
Add a link
Reference in a new issue