T-HUB/apps/api/migrations/019_publishing_integration.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

103 lines
5.8 KiB
PL/PgSQL

-- 018 is retained unchanged: local anp_publishing_it records its application.
-- These corrections use independently tracked resumable stages.
-- wp3:phase expand transaction
SET LOCAL lock_timeout = '5s';
ALTER TABLE publishing_attempts ADD COLUMN first_dispatch_started_at timestamptz;
CREATE OR REPLACE FUNCTION publishing_attempts_guard() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
IF (NEW.schedule_id, NEW.draft_id, NEW.connection_id, NEW.attempt_no, NEW.idempotency_key,
NEW.schedule_generation, NEW.post_snapshot, NEW.post_hash, NEW.scheduled_by_user_id,
NEW.required_permission, NEW.approved_revision, NEW.reviewer_user_id,
NEW.credential_id, NEW.credential_generation, NEW.created_at)
IS DISTINCT FROM
(OLD.schedule_id, OLD.draft_id, OLD.connection_id, OLD.attempt_no, OLD.idempotency_key,
OLD.schedule_generation, OLD.post_snapshot, OLD.post_hash, OLD.scheduled_by_user_id,
OLD.required_permission, OLD.approved_revision, OLD.reviewer_user_id,
OLD.credential_id, OLD.credential_generation, OLD.created_at) THEN
RAISE EXCEPTION 'publishing attempt snapshot is immutable' USING ERRCODE = '23514';
END IF;
IF OLD.status IN ('completed', 'abandoned') AND NEW.status IS DISTINCT FROM OLD.status THEN
RAISE EXCEPTION 'publishing attempt % is terminal', OLD.status USING ERRCODE = '23514';
END IF;
IF OLD.first_dispatch_started_at IS NOT NULL AND NEW.first_dispatch_started_at IS DISTINCT FROM OLD.first_dispatch_started_at THEN
RAISE EXCEPTION 'first dispatch time is immutable' USING ERRCODE = '23514';
END IF;
IF NEW.dispatch_count < OLD.dispatch_count THEN
RAISE EXCEPTION 'dispatch_count cannot decrease' USING ERRCODE = '23514';
END IF;
IF NEW.provider_retry_deadline IS DISTINCT FROM OLD.provider_retry_deadline
AND OLD.provider_retry_deadline IS NOT NULL THEN
RAISE EXCEPTION 'provider_retry_deadline is fixed at first dispatch' USING ERRCODE = '23514';
END IF;
IF NEW.owner_token IS NOT NULL AND NEW.owner_token IS DISTINCT FROM OLD.owner_token THEN
NEW.claim_epoch := OLD.claim_epoch + 1;
ELSE
NEW.claim_epoch := OLD.claim_epoch;
END IF;
NEW.updated_at := now();
RETURN NEW;
END $$;
CREATE OR REPLACE FUNCTION drafts_revision_fence() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
IF NEW.publication_attempt < OLD.publication_attempt THEN
RAISE EXCEPTION 'publication_attempt cannot decrease' USING ERRCODE = '23514';
END IF;
IF NEW.post IS DISTINCT FROM OLD.post THEN
IF EXISTS (SELECT 1 FROM publishing_attempts a
WHERE a.workspace_id = OLD.workspace_id AND a.draft_id = OLD.id
AND (a.status = 'unknown'
OR (a.status IN ('ready', 'in_progress') AND a.dispatch_started_at IS NOT NULL))) THEN
RAISE EXCEPTION 'draft content is locked while a dispatch is unsettled' USING ERRCODE = '55000';
END IF;
NEW.revision := OLD.revision + 1;
NEW.approved_revision := NULL;
NEW.approved_hash := NULL;
NEW.approved_at := NULL;
NEW.reviewer_user_id := NULL;
ELSE
NEW.revision := OLD.revision;
END IF;
RETURN NEW;
END $$;
-- wp3:phase dispatch_backfill batch
WITH batch AS (
SELECT id FROM publishing_attempts WHERE first_dispatch_started_at IS NULL AND dispatch_count > 0 LIMIT 1000 FOR UPDATE
)
UPDATE publishing_attempts a SET first_dispatch_started_at = CASE WHEN a.dispatch_count > 1 OR a.dispatch_started_at IS NULL THEN a.created_at ELSE a.dispatch_started_at END FROM batch WHERE a.id = batch.id;
-- wp3:phase receipt_recovery_index concurrent
CREATE INDEX CONCURRENTLY publishing_receipts_recovery ON publishing_receipts (workspace_id, attempt_id, received_at);
-- wp3:phase cutover transaction
SET LOCAL lock_timeout = '5s';
DROP FUNCTION publishing_due_schedules(integer);
CREATE FUNCTION publishing_due_schedules(p_limit integer DEFAULT 100)
RETURNS TABLE(workspace_id uuid, schedule_id uuid, idempotency_key text, generation bigint, publish_at timestamptz, evidence_only boolean)
LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public AS $$
SELECT due.workspace_id, due.schedule_id, due.idempotency_key, due.generation, due.publish_at, due.evidence_only
FROM (
SELECT sp.workspace_id, sp.id AS schedule_id, sp.idempotency_key, sp.generation,
sp.publish_at, sp.next_attempt_at AS due_at, false AS evidence_only
FROM scheduled_publications sp
WHERE sp.state = 'pending' AND sp.execution_version = 1 AND sp.next_attempt_at <= now() AND NOT EXISTS (SELECT 1 FROM publishing_attempts a JOIN publishing_receipts r ON r.attempt_id = a.id AND r.workspace_id = a.workspace_id WHERE a.idempotency_key = sp.idempotency_key AND a.workspace_id = sp.workspace_id)
UNION ALL
SELECT a.workspace_id, a.schedule_id, a.idempotency_key, a.schedule_generation,
sp.publish_at, a.lease_until AS due_at, false AS evidence_only
FROM publishing_attempts a
JOIN scheduled_publications sp ON sp.id = a.schedule_id AND sp.workspace_id = a.workspace_id
WHERE a.status = 'in_progress' AND a.lease_until < now() AND NOT EXISTS (SELECT 1 FROM publishing_receipts r WHERE r.attempt_id = a.id AND r.workspace_id = a.workspace_id)
UNION ALL
SELECT a.workspace_id, a.schedule_id, a.idempotency_key, a.schedule_generation,
sp.publish_at, r.received_at AS due_at, true AS evidence_only
FROM publishing_attempts a
JOIN publishing_receipts r ON r.attempt_id = a.id AND r.workspace_id = a.workspace_id
JOIN scheduled_publications sp ON sp.id = a.schedule_id AND sp.workspace_id = a.workspace_id
WHERE (a.status IN ('ready', 'failed', 'unknown') OR (a.status = 'in_progress' AND a.lease_until < now()))
) due
ORDER BY due.due_at, due.schedule_id
LIMIT LEAST(GREATEST(COALESCE(p_limit, 100), 1), 100)
$$;
REVOKE ALL ON FUNCTION publishing_due_schedules(integer) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION publishing_due_schedules(integer) TO sp_app;