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/.
72 lines
4.2 KiB
PL/PgSQL
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;
|