feat: upload audio, get a private Swedish transcript, discuss it (#33, ADR-033) #39
@@ -0,0 +1,3 @@
|
|||||||
|
-- Drops the private uploaded-transcript store and every transcript in it. There
|
||||||
|
-- is no revert of the data: the original audio was never kept (ADR-033).
|
||||||
|
DROP TABLE uploaded_transcripts;
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
-- Migration 017: uploaded_transcripts — PRIVATE transcripts of audio a user
|
||||||
|
-- uploaded themselves (ADR-033, tapir#33).
|
||||||
|
--
|
||||||
|
-- This is deliberately NOT the shared `transcripts` table (migration 015,
|
||||||
|
-- ADR-021). That table is non-RLS on the explicit grounds that it holds only
|
||||||
|
-- public caption content and nothing user-identifying. A recorded meeting is the
|
||||||
|
-- opposite: private third-party content owned by exactly one user. Reusing
|
||||||
|
-- `transcripts` — or adding a user_id to it with a partial policy — would put a
|
||||||
|
-- private row in the one table the isolation test asserts is shared, and would
|
||||||
|
-- pass CI green. A separate table keeps ADR-021's exception singular.
|
||||||
|
--
|
||||||
|
-- Row-level security follows migration 003 exactly: ENABLE + FORCE (the app
|
||||||
|
-- connects as the table owner, which bypasses RLS unless forced), and a policy
|
||||||
|
-- keyed off the per-request GUC tapir.current_user_id. With no GUC set the
|
||||||
|
-- predicate is NULL and the table is deny-all.
|
||||||
|
--
|
||||||
|
-- The original audio is never stored (ADR-033): this row is all that is kept.
|
||||||
|
-- segments holds the timestamped cues; re-transcribing 90 minutes costs ~7
|
||||||
|
-- minutes, so discarding the timestamps would be irreversible.
|
||||||
|
CREATE TABLE uploaded_transcripts (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'queued'
|
||||||
|
CHECK (status IN ('queued', 'transcribing', 'done', 'failed')),
|
||||||
|
content TEXT,
|
||||||
|
segments JSONB,
|
||||||
|
endpoint TEXT, -- which backend served it (iguana | berget); a quality fact
|
||||||
|
audio_seconds DOUBLE PRECISION,
|
||||||
|
error_kind TEXT, -- typed, never a provider's error string (ADR-033)
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
finished_at TIMESTAMPTZ,
|
||||||
|
CONSTRAINT uploaded_transcripts_done_has_content
|
||||||
|
CHECK (status <> 'done' OR content IS NOT NULL),
|
||||||
|
CONSTRAINT uploaded_transcripts_failed_has_error_kind
|
||||||
|
CHECK (status <> 'failed' OR error_kind IS NOT NULL)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_uploaded_transcripts_user ON uploaded_transcripts (user_id, created_at DESC);
|
||||||
|
|
||||||
|
ALTER TABLE uploaded_transcripts ENABLE ROW LEVEL SECURITY;
|
||||||
|
ALTER TABLE uploaded_transcripts FORCE ROW LEVEL SECURITY;
|
||||||
|
CREATE POLICY uploaded_transcripts_isolation ON uploaded_transcripts
|
||||||
|
FOR ALL
|
||||||
|
USING (user_id = current_setting('tapir.current_user_id', true)::uuid);
|
||||||
|
|
||||||
|
COMMENT ON TABLE uploaded_transcripts IS
|
||||||
|
'PRIVATE per-user transcripts of user-supplied audio. RLS-scoped (ADR-012). Never merge into the shared transcripts table (ADR-021, ADR-033).';
|
||||||
Reference in New Issue
Block a user