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/.
52 lines
2.9 KiB
SQL
52 lines
2.9 KiB
SQL
-- 021: editable multi-platform streams columns — column definitions (WP3 §1.2, P0a).
|
|
-- stream_columns only: the resolved-source / ingest / fan-out / budget tables
|
|
-- (column_sources, feed_items, feed_item_columns, provider_budget_ledger) land
|
|
-- in their own forward-only migrations so each slice stays independently
|
|
-- reviewable. Additive: no existing table is touched.
|
|
--
|
|
-- `spec` and `refresh_policy` are JSONB blobs validated app-side by
|
|
-- @tactical-media-broadcast-hub/core (stream-columns.ts); invalid specs are rejected before
|
|
-- insert and never stored. The CHECKs here are only coarse backstops — the
|
|
-- authoritative typed validation lives in the app layer (017-style split).
|
|
CREATE TABLE stream_columns (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
|
type text NOT NULL CHECK (type IN ('platform_feed','keyword','account_list','saved_search')),
|
|
title text NOT NULL CHECK (char_length(title) <= 80), -- user-editable; app layer enforces too
|
|
position integer NOT NULL DEFAULT 0 CHECK (position >= 0), -- dense rank; reorder rewrites the tail
|
|
spec jsonb NOT NULL CHECK (jsonb_typeof(spec) = 'object'),
|
|
refresh_policy jsonb NOT NULL DEFAULT '{}' CHECK (jsonb_typeof(refresh_policy) = 'object'),
|
|
created_by uuid NOT NULL REFERENCES users(id),
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
deleted_at timestamptz -- soft delete keeps feed_items FK-stable (§1.2)
|
|
);
|
|
|
|
CREATE TRIGGER trg_stream_columns_ws_immutable BEFORE UPDATE ON stream_columns
|
|
FOR EACH ROW EXECUTE FUNCTION reject_workspace_change();
|
|
|
|
-- Same fail-closed tenant isolation as every tenant table (001/006/017 style):
|
|
-- no app.workspace_id => NULL => no rows. FORCE binds sp_app too.
|
|
ALTER TABLE stream_columns ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE stream_columns FORCE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY tenant_isolation ON stream_columns
|
|
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 stream_columns TO sp_app;
|
|
|
|
-- Dense column order per workspace (UI chips, seeding, reorder). Partial on
|
|
-- deleted_at IS NULL: soft-deleted columns keep their row (FK-stable items)
|
|
-- without reserving their slot, so reorder/seeding only fights live rows.
|
|
-- Named per design §1.2; lazy seeding relies on it for idempotence (§1.3).
|
|
CREATE UNIQUE INDEX stream_columns_ws_order_uq
|
|
ON stream_columns (workspace_id, position)
|
|
WHERE deleted_at IS NULL;
|
|
|
|
-- Workspace listing (GET /feeds/columns), newest-position reads served by the
|
|
-- unique index above; this covers soft-deleted lookups and count queries in
|
|
-- the partial-index style of workspace_invite_links_expiry (017).
|
|
CREATE INDEX stream_columns_ws_idx
|
|
ON stream_columns (workspace_id)
|
|
WHERE deleted_at IS NULL;
|