T-HUB/apps/api/migrations/021_stream_columns.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

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;