T-HUB/apps/api/migrations/025_chatgov_p2.sql
glass beadian aa5e262f3a Tactical Media Broadcast Hub — initial release
Self-hosted multi-platform broadcast newsroom for activist collectives:
publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style
column wall with hostile-content-safe rendering, live control room with Auto
Director, bridged chat governance with polls and quorum, distributed sandboxed
transcode workers, encrypted content-addressed archive, monthly analytics.

MIT licensed. See README.md and docs/.
2026-10-10 11:56:42 +00:00

72 lines
4.2 KiB
PL/PgSQL

-- P2 competitive short-post variants. 022/023/024 exist. The runner keys
-- migrations by filename; 025_archive_search.sql belongs to another slice.
ALTER TABLE polls
ADD COLUMN kind text NOT NULL DEFAULT 'yes_no' CHECK (kind IN ('yes_no', 'variant_selection')),
ADD COLUMN winner_option_id uuid,
ADD COLUMN draft_revision bigint,
ADD COLUMN content_kind text NOT NULL DEFAULT 'short_post' CHECK (content_kind IN ('short_post', 'document')),
ADD COLUMN eligible_user_ids uuid[];
-- document is a schema seam only; P2 accepts short_post exclusively.
ALTER TABLE polls DROP CONSTRAINT polls_state_check;
ALTER TABLE polls ADD CONSTRAINT polls_state_check CHECK
(state IN ('open', 'passed', 'failed', 'vetoed', 'quorum_unreachable', 'cancelled', 'stale'));
ALTER TABLE polls DROP CONSTRAINT polls_close_reason_check;
ALTER TABLE polls ADD CONSTRAINT polls_close_reason_check CHECK
(close_reason IN ('quorum_early', 'timebox', 'veto', 'cancelled', 'quorum_unreachable', 'reset_on_change'));
CREATE TABLE poll_options (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE ON UPDATE RESTRICT,
poll_id uuid NOT NULL,
seq integer NOT NULL CHECK (seq > 0),
-- Future document revision chains can anchor here. Short posts use the
-- poll's numeric draft_revision plus this immutable text snapshot.
draft_revision_id uuid,
text text NOT NULL CHECK (char_length(text) BETWEEN 1 AND 5000),
submitted_by uuid NOT NULL REFERENCES users(id) ON DELETE NO ACTION ON UPDATE RESTRICT,
submitted_at timestamptz NOT NULL DEFAULT clock_timestamp(),
UNIQUE (poll_id, seq),
UNIQUE (id, poll_id, workspace_id),
FOREIGN KEY (poll_id, workspace_id) REFERENCES polls(id, workspace_id) ON DELETE CASCADE ON UPDATE RESTRICT
);
CREATE TRIGGER trg_poll_options_ws_immutable BEFORE UPDATE ON poll_options
FOR EACH ROW EXECUTE FUNCTION reject_workspace_change();
-- Artifacts cannot be edited or individually removed, even by SQL callers.
-- Cascading workspace/poll retention remains possible.
CREATE FUNCTION chatgov_option_immutable() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
IF TG_OP = 'UPDATE' OR pg_trigger_depth() = 1 THEN
RAISE EXCEPTION 'poll options are immutable' USING ERRCODE = '55000';
END IF;
RETURN OLD;
END $$;
CREATE TRIGGER trg_poll_options_immutable BEFORE UPDATE OR DELETE ON poll_options
FOR EACH ROW EXECUTE FUNCTION chatgov_option_immutable();
ALTER TABLE poll_options ENABLE ROW LEVEL SECURITY;
ALTER TABLE poll_options FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON poll_options
USING (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid)
WITH CHECK (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid);
GRANT SELECT, INSERT, UPDATE, DELETE ON poll_options TO sp_app;
ALTER TABLE poll_votes ADD COLUMN option_id uuid;
ALTER TABLE poll_votes ADD CONSTRAINT poll_votes_option_fk
FOREIGN KEY (option_id, poll_id, workspace_id) REFERENCES poll_options(id, poll_id, workspace_id)
ON DELETE NO ACTION ON UPDATE RESTRICT;
ALTER TABLE poll_votes ADD CONSTRAINT poll_votes_option_ballot_check CHECK (option_id IS NULL OR ballot = 'yes');
ALTER TABLE polls ADD CONSTRAINT polls_winner_option_fk
FOREIGN KEY (winner_option_id, id, workspace_id) REFERENCES poll_options(id, poll_id, workspace_id)
ON DELETE NO ACTION ON UPDATE RESTRICT;
ALTER TABLE polls ADD CONSTRAINT polls_winner_state_check CHECK
(winner_option_id IS NULL OR (kind = 'variant_selection' AND state = 'passed'));
ALTER TABLE poll_votes ADD COLUMN voter_identity_id uuid REFERENCES chat_identities(id) ON DELETE NO ACTION ON UPDATE RESTRICT;
ALTER TABLE polls ADD COLUMN final_evaluation jsonb;
-- Narrow expiry discovery follows the publishing due-schedule house pattern:
-- returns references only, never ballots/content. Mutations still use RLS.
CREATE FUNCTION chatgov_due_polls() RETURNS TABLE(workspace_id uuid, id uuid)
LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public AS $$
SELECT p.workspace_id, p.id FROM polls p WHERE p.state = 'open' AND p.closes_at <= now()
ORDER BY p.closes_at LIMIT 500
$$;
REVOKE ALL ON FUNCTION chatgov_due_polls() FROM PUBLIC;
GRANT EXECUTE ON FUNCTION chatgov_due_polls() TO sp_app;