404 lines
19 KiB
PL/PgSQL
404 lines
19 KiB
PL/PgSQL
-- ============================================================
|
|
-- Migration: Multi-Site System (BB + AV Beat)
|
|
-- Timestamp: 20260322230000
|
|
-- Covers: site_id on articles/categories, AV Beat categories,
|
|
-- display_name_overrides, banned_terms, tracked_companies,
|
|
-- auto_story_queue, pen_names
|
|
-- ============================================================
|
|
|
|
-- ─── 1. Add site_id to native_articles ───────────────────────────────────────
|
|
ALTER TABLE public.native_articles
|
|
ADD COLUMN IF NOT EXISTS site_id INTEGER NOT NULL DEFAULT 1,
|
|
ADD COLUMN IF NOT EXISTS scope_flagged BOOLEAN NOT NULL DEFAULT false,
|
|
ADD COLUMN IF NOT EXISTS banned_term_flagged BOOLEAN NOT NULL DEFAULT false,
|
|
ADD COLUMN IF NOT EXISTS banned_term_matched TEXT,
|
|
ADD COLUMN IF NOT EXISTS editor_note TEXT,
|
|
ADD COLUMN IF NOT EXISTS scheduled_at TIMESTAMPTZ,
|
|
ADD COLUMN IF NOT EXISTS submitted_for_review_at TIMESTAMPTZ;
|
|
|
|
COMMENT ON COLUMN public.native_articles.site_id IS '1 = Broadcast Beat, 2 = AV Beat';
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_native_articles_site_id ON public.native_articles(site_id);
|
|
|
|
-- ─── 2. Add site_id to wp_imported_posts ─────────────────────────────────────
|
|
ALTER TABLE public.wp_imported_posts
|
|
ADD COLUMN IF NOT EXISTS site_id INTEGER NOT NULL DEFAULT 1;
|
|
|
|
-- ─── 3. Sites reference table ────────────────────────────────────────────────
|
|
CREATE TABLE IF NOT EXISTS public.sites (
|
|
id INTEGER PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
slug TEXT NOT NULL UNIQUE,
|
|
domain TEXT,
|
|
badge_color TEXT NOT NULL,
|
|
badge_label TEXT NOT NULL,
|
|
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
INSERT INTO public.sites (id, name, slug, domain, badge_color, badge_label) VALUES
|
|
(1, 'Broadcast Beat', 'broadcastbeat', 'broadcastbeat.com', '#0ea5e9', 'BB'),
|
|
(2, 'AV Beat', 'avbeat', 'avbeat.com', '#f97316', 'AV')
|
|
ON CONFLICT (id) DO NOTHING;
|
|
|
|
-- ─── 4. Categories table with site_id ────────────────────────────────────────
|
|
CREATE TABLE IF NOT EXISTS public.site_categories (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
site_id INTEGER NOT NULL REFERENCES public.sites(id) ON DELETE CASCADE,
|
|
name TEXT NOT NULL,
|
|
slug TEXT NOT NULL,
|
|
is_featured BOOLEAN NOT NULL DEFAULT false,
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
|
UNIQUE(site_id, slug)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_site_categories_site_id ON public.site_categories(site_id);
|
|
|
|
-- Broadcast Beat categories (site_id = 1)
|
|
INSERT INTO public.site_categories (site_id, name, slug, is_featured, sort_order) VALUES
|
|
(1, 'Featured', 'featured', true, 0),
|
|
(1, 'Audio', 'audio', false, 1),
|
|
(1, 'Broadcast', 'broadcast', false, 2),
|
|
(1, 'Business', 'business', false, 3),
|
|
(1, 'Camera', 'camera', false, 4),
|
|
(1, 'Cloud', 'cloud', false, 5),
|
|
(1, 'Content Creation', 'content-creation', false, 6),
|
|
(1, 'Distribution', 'distribution', false, 7),
|
|
(1, 'Education', 'education', false, 8),
|
|
(1, 'Events', 'events', false, 9),
|
|
(1, 'IP & Networking', 'ip-networking', false, 10),
|
|
(1, 'Live Production', 'live-production', false, 11),
|
|
(1, 'Media Management', 'media-management', false, 12),
|
|
(1, 'News', 'news', false, 13),
|
|
(1, 'OTT', 'ott', false, 14),
|
|
(1, 'Post Production', 'post-production', false, 15),
|
|
(1, 'Radio', 'radio', false, 16),
|
|
(1, 'Sports', 'sports', false, 17),
|
|
(1, 'Streaming', 'streaming', false, 18),
|
|
(1, 'Technology', 'technology', false, 19),
|
|
(1, 'Uncategorized', 'uncategorized', false, 20)
|
|
ON CONFLICT (site_id, slug) DO NOTHING;
|
|
|
|
-- AV Beat categories (site_id = 2)
|
|
INSERT INTO public.site_categories (site_id, name, slug, is_featured, sort_order) VALUES
|
|
(2, 'Featured', 'featured', true, 0),
|
|
(2, 'AV Integration', 'av-integration', false, 1),
|
|
(2, 'Digital Signage', 'digital-signage', false, 2),
|
|
(2, 'Unified Communications', 'unified-communications', false, 3),
|
|
(2, 'Streaming Technology', 'streaming-technology', false, 4),
|
|
(2, 'Conferencing & Collaboration','conferencing-collaboration', false, 5),
|
|
(2, 'Houses of Worship', 'houses-of-worship', false, 6),
|
|
(2, 'Education AV', 'education-av', false, 7),
|
|
(2, 'Control Systems', 'control-systems', false, 8),
|
|
(2, 'Display Technology', 'display-technology', false, 9),
|
|
(2, 'Sound & Audio', 'sound-audio', false, 10),
|
|
(2, 'Trade Shows & Events', 'trade-shows-events', false, 11),
|
|
(2, 'Product News', 'product-news', false, 12),
|
|
(2, 'Industry News', 'industry-news', false, 13)
|
|
ON CONFLICT (site_id, slug) DO NOTHING;
|
|
|
|
-- ─── 5. Display name overrides ───────────────────────────────────────────────
|
|
CREATE TABLE IF NOT EXISTS public.display_name_overrides (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES public.user_profiles(id) ON DELETE CASCADE,
|
|
site_id INTEGER NOT NULL REFERENCES public.sites(id) ON DELETE CASCADE,
|
|
public_display_name TEXT NOT NULL,
|
|
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
|
UNIQUE(user_id, site_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_display_name_overrides_user_site ON public.display_name_overrides(user_id, site_id);
|
|
|
|
-- Enable RLS
|
|
ALTER TABLE public.display_name_overrides ENABLE ROW LEVEL SECURITY;
|
|
|
|
DROP POLICY IF EXISTS "admin_manage_display_name_overrides" ON public.display_name_overrides;
|
|
CREATE POLICY "admin_manage_display_name_overrides"
|
|
ON public.display_name_overrides
|
|
FOR ALL
|
|
TO authenticated
|
|
USING (true)
|
|
WITH CHECK (true);
|
|
|
|
-- ─── 6. Banned terms ─────────────────────────────────────────────────────────
|
|
CREATE TABLE IF NOT EXISTS public.banned_terms (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
term TEXT NOT NULL,
|
|
ban_level TEXT NOT NULL DEFAULT 'soft' CHECK (ban_level IN ('soft', 'hard')),
|
|
site_scope TEXT NOT NULL DEFAULT 'both' CHECK (site_scope IN ('broadcastbeat', 'avbeat', 'both')),
|
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
notes TEXT,
|
|
added_by UUID REFERENCES public.user_profiles(id) ON DELETE SET NULL,
|
|
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_banned_terms_active ON public.banned_terms(is_active) WHERE is_active = true;
|
|
CREATE INDEX IF NOT EXISTS idx_banned_terms_term ON public.banned_terms(LOWER(term));
|
|
|
|
ALTER TABLE public.banned_terms ENABLE ROW LEVEL SECURITY;
|
|
|
|
DROP POLICY IF EXISTS "admin_manage_banned_terms" ON public.banned_terms;
|
|
CREATE POLICY "admin_manage_banned_terms"
|
|
ON public.banned_terms
|
|
FOR ALL
|
|
TO authenticated
|
|
USING (public.is_admin_user())
|
|
WITH CHECK (public.is_admin_user());
|
|
|
|
-- ─── 7. Tracked companies ────────────────────────────────────────────────────
|
|
CREATE TABLE IF NOT EXISTS public.tracked_companies (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
company_name TEXT NOT NULL UNIQUE,
|
|
company_website TEXT,
|
|
site_scope TEXT NOT NULL DEFAULT 'both' CHECK (site_scope IN ('broadcastbeat', 'avbeat', 'both')),
|
|
first_mentioned TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
|
last_mentioned TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
|
mention_count INTEGER NOT NULL DEFAULT 1,
|
|
auto_story_enabled BOOLEAN NOT NULL DEFAULT true,
|
|
last_crawled TIMESTAMPTZ,
|
|
next_crawl_scheduled TIMESTAMPTZ,
|
|
crawl_priority TEXT NOT NULL DEFAULT 'standard' CHECK (crawl_priority IN ('standard', 'high', 'critical')),
|
|
notes TEXT,
|
|
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_tracked_companies_name ON public.tracked_companies(LOWER(company_name));
|
|
CREATE INDEX IF NOT EXISTS idx_tracked_companies_auto_story ON public.tracked_companies(auto_story_enabled) WHERE auto_story_enabled = true;
|
|
CREATE INDEX IF NOT EXISTS idx_tracked_companies_mention_count ON public.tracked_companies(mention_count DESC);
|
|
|
|
ALTER TABLE public.tracked_companies ENABLE ROW LEVEL SECURITY;
|
|
|
|
DROP POLICY IF EXISTS "admin_manage_tracked_companies" ON public.tracked_companies;
|
|
CREATE POLICY "admin_manage_tracked_companies"
|
|
ON public.tracked_companies
|
|
FOR ALL
|
|
TO authenticated
|
|
USING (true)
|
|
WITH CHECK (true);
|
|
|
|
-- ─── 8. Auto story queue ─────────────────────────────────────────────────────
|
|
CREATE TABLE IF NOT EXISTS public.auto_story_queue (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
company_id UUID REFERENCES public.tracked_companies(id) ON DELETE CASCADE,
|
|
company_name TEXT NOT NULL,
|
|
story_type TEXT NOT NULL DEFAULT 'company_news' CHECK (story_type IN (
|
|
'product_announcement','press_release_rewrite','company_news',
|
|
'product_update','event_coverage','executive_news','thought_leadership'
|
|
)),
|
|
source_url TEXT,
|
|
source_title TEXT,
|
|
source_content_summary TEXT,
|
|
assigned_pen_name TEXT,
|
|
site_id INTEGER NOT NULL DEFAULT 1 REFERENCES public.sites(id),
|
|
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN (
|
|
'pending','generated','in_review','approved','published','rejected'
|
|
)),
|
|
priority INTEGER NOT NULL DEFAULT 5,
|
|
scheduled_publish_time TIMESTAMPTZ,
|
|
generated_title TEXT,
|
|
generated_content TEXT,
|
|
generated_excerpt TEXT,
|
|
review_status TEXT,
|
|
rejection_reason TEXT,
|
|
quality_confidence NUMERIC(3,2),
|
|
reviewed_by UUID REFERENCES public.user_profiles(id) ON DELETE SET NULL,
|
|
reviewed_at TIMESTAMPTZ,
|
|
published_article_id UUID,
|
|
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_auto_story_queue_status ON public.auto_story_queue(status);
|
|
CREATE INDEX IF NOT EXISTS idx_auto_story_queue_site_id ON public.auto_story_queue(site_id);
|
|
CREATE INDEX IF NOT EXISTS idx_auto_story_queue_company_id ON public.auto_story_queue(company_id);
|
|
CREATE INDEX IF NOT EXISTS idx_auto_story_queue_created ON public.auto_story_queue(created_at DESC);
|
|
|
|
ALTER TABLE public.auto_story_queue ENABLE ROW LEVEL SECURITY;
|
|
|
|
DROP POLICY IF EXISTS "admin_manage_auto_story_queue" ON public.auto_story_queue;
|
|
CREATE POLICY "admin_manage_auto_story_queue"
|
|
ON public.auto_story_queue
|
|
FOR ALL
|
|
TO authenticated
|
|
USING (true)
|
|
WITH CHECK (true);
|
|
|
|
-- ─── 9. Crawl log ────────────────────────────────────────────────────────────
|
|
CREATE TABLE IF NOT EXISTS public.crawl_log (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
company_id UUID REFERENCES public.tracked_companies(id) ON DELETE CASCADE,
|
|
company_name TEXT NOT NULL,
|
|
source_url TEXT,
|
|
items_found INTEGER DEFAULT 0,
|
|
stories_queued INTEGER DEFAULT 0,
|
|
status TEXT NOT NULL DEFAULT 'success' CHECK (status IN ('success','failed','partial')),
|
|
error_message TEXT,
|
|
crawled_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_crawl_log_company_id ON public.crawl_log(company_id);
|
|
CREATE INDEX IF NOT EXISTS idx_crawl_log_crawled_at ON public.crawl_log(crawled_at DESC);
|
|
|
|
ALTER TABLE public.crawl_log ENABLE ROW LEVEL SECURITY;
|
|
|
|
DROP POLICY IF EXISTS "admin_manage_crawl_log" ON public.crawl_log;
|
|
CREATE POLICY "admin_manage_crawl_log"
|
|
ON public.crawl_log
|
|
FOR ALL
|
|
TO authenticated
|
|
USING (true)
|
|
WITH CHECK (true);
|
|
|
|
-- ─── 10. Company tracker settings ────────────────────────────────────────────
|
|
CREATE TABLE IF NOT EXISTS public.company_tracker_settings (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
auto_story_global_enabled BOOLEAN NOT NULL DEFAULT true,
|
|
mention_threshold INTEGER NOT NULL DEFAULT 2,
|
|
max_stories_per_company_per_week INTEGER NOT NULL DEFAULT 3,
|
|
max_stories_per_day_per_site INTEGER NOT NULL DEFAULT 20,
|
|
auto_publish_enabled BOOLEAN NOT NULL DEFAULT false,
|
|
auto_publish_confidence_threshold NUMERIC(3,2) NOT NULL DEFAULT 0.85,
|
|
image_download_enabled BOOLEAN NOT NULL DEFAULT true,
|
|
blocked_sources TEXT[] DEFAULT ARRAY[
|
|
'tvtechnology.com','broadcastingcable.com','sportsvideo.org','ibc.org',
|
|
'digitaltveurope.com','sportspromedia.com','ravepubs.com','avnetwork.com',
|
|
'avtechnology.com','commercialintegrator.com','systemscontractor.com',
|
|
'infocommshow.org','avinteriors.com'
|
|
],
|
|
crawl_frequency_high_hours INTEGER NOT NULL DEFAULT 24,
|
|
crawl_frequency_medium_hours INTEGER NOT NULL DEFAULT 72,
|
|
crawl_frequency_standard_hours INTEGER NOT NULL DEFAULT 168,
|
|
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
INSERT INTO public.company_tracker_settings DEFAULT VALUES
|
|
ON CONFLICT DO NOTHING;
|
|
|
|
ALTER TABLE public.company_tracker_settings ENABLE ROW LEVEL SECURITY;
|
|
|
|
DROP POLICY IF EXISTS "admin_manage_tracker_settings" ON public.company_tracker_settings;
|
|
CREATE POLICY "admin_manage_tracker_settings"
|
|
ON public.company_tracker_settings
|
|
FOR ALL
|
|
TO authenticated
|
|
USING (true)
|
|
WITH CHECK (true);
|
|
|
|
-- ─── 11. Pen names ───────────────────────────────────────────────────────────
|
|
CREATE TABLE IF NOT EXISTS public.pen_names (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
name TEXT NOT NULL UNIQUE,
|
|
site_id INTEGER REFERENCES public.sites(id),
|
|
beat TEXT,
|
|
bio TEXT,
|
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
last_assigned TIMESTAMPTZ,
|
|
consecutive_count INTEGER NOT NULL DEFAULT 0,
|
|
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
-- BB pen names (site_id = 1) — from existing content engine
|
|
INSERT INTO public.pen_names (name, site_id, beat, is_active) VALUES
|
|
('Alex Morgan', 1, 'technology', true),
|
|
('Jordan Blake', 1, 'business', true),
|
|
('Sam Rivera', 1, 'live-production',true),
|
|
('Casey Quinn', 1, 'streaming', true),
|
|
('Taylor Frost', 1, 'post-production',true),
|
|
('Morgan Lee', 1, 'broadcast', true),
|
|
('Riley Stone', 1, 'events', true),
|
|
('Drew Callahan', 1, 'audio', true),
|
|
('Avery Walsh', 1, 'camera', true),
|
|
('Quinn Harmon', 1, 'ip-networking', true),
|
|
('Blake Sutton', 1, 'ott', true),
|
|
('Reese Donovan', 1, 'media-management',true)
|
|
ON CONFLICT (name) DO NOTHING;
|
|
|
|
-- AV Beat pen names (site_id = 2)
|
|
INSERT INTO public.pen_names (name, site_id, beat, is_active) VALUES
|
|
('Rex Chandler', 2, 'av-integration', true),
|
|
('Dana Flux', 2, 'digital-signage', true),
|
|
('Mitch Boardroom', 2, 'unified-communications', true),
|
|
('Sloane Rigging', 2, 'sound-audio', true),
|
|
('Chip Crosspoint', 2, 'streaming-technology', true),
|
|
('Blair Presenter', 2, 'education-av', true),
|
|
('Jordan Lumen', 2, 'display-technology', true)
|
|
ON CONFLICT (name) DO NOTHING;
|
|
|
|
-- ─── 12. Triggers for updated_at ─────────────────────────────────────────────
|
|
DROP TRIGGER IF EXISTS update_tracked_companies_updated_at ON public.tracked_companies;
|
|
CREATE TRIGGER update_tracked_companies_updated_at
|
|
BEFORE UPDATE ON public.tracked_companies
|
|
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at();
|
|
|
|
DROP TRIGGER IF EXISTS update_auto_story_queue_updated_at ON public.auto_story_queue;
|
|
CREATE TRIGGER update_auto_story_queue_updated_at
|
|
BEFORE UPDATE ON public.auto_story_queue
|
|
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at();
|
|
|
|
DROP TRIGGER IF EXISTS update_banned_terms_updated_at ON public.banned_terms;
|
|
CREATE TRIGGER update_banned_terms_updated_at
|
|
BEFORE UPDATE ON public.banned_terms
|
|
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at();
|
|
|
|
DROP TRIGGER IF EXISTS update_display_name_overrides_updated_at ON public.display_name_overrides;
|
|
CREATE TRIGGER update_display_name_overrides_updated_at
|
|
BEFORE UPDATE ON public.display_name_overrides
|
|
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at();
|
|
|
|
-- ─── 13. Helper function: get public display name ────────────────────────────
|
|
CREATE OR REPLACE FUNCTION public.get_public_display_name(p_user_id UUID, p_site_id INTEGER)
|
|
RETURNS TEXT
|
|
LANGUAGE sql
|
|
STABLE
|
|
SECURITY DEFINER
|
|
AS $$
|
|
SELECT COALESCE(
|
|
(SELECT public_display_name FROM public.display_name_overrides
|
|
WHERE user_id = p_user_id AND site_id = p_site_id LIMIT 1),
|
|
(SELECT full_name FROM public.user_profiles WHERE id = p_user_id LIMIT 1)
|
|
)
|
|
$$;
|
|
|
|
-- ─── 14. Helper function: check banned terms ─────────────────────────────────
|
|
CREATE OR REPLACE FUNCTION public.check_banned_terms(
|
|
p_text TEXT,
|
|
p_site_id INTEGER
|
|
)
|
|
RETURNS TABLE(term TEXT, ban_level TEXT)
|
|
LANGUAGE sql
|
|
STABLE
|
|
SECURITY DEFINER
|
|
AS $$
|
|
SELECT bt.term, bt.ban_level
|
|
FROM public.banned_terms bt
|
|
WHERE bt.is_active = true
|
|
AND (
|
|
bt.site_scope = 'both'
|
|
OR (bt.site_scope = 'broadcastbeat' AND p_site_id = 1)
|
|
OR (bt.site_scope = 'avbeat' AND p_site_id = 2)
|
|
)
|
|
AND LOWER(p_text) LIKE '%' || LOWER(bt.term) || '%'
|
|
LIMIT 1;
|
|
$$;
|
|
|
|
-- ─── 15. RLS for sites and site_categories (public read) ─────────────────────
|
|
ALTER TABLE public.sites ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE public.site_categories ENABLE ROW LEVEL SECURITY;
|
|
|
|
DROP POLICY IF EXISTS "public_read_sites" ON public.sites;
|
|
CREATE POLICY "public_read_sites" ON public.sites FOR SELECT TO public USING (true);
|
|
|
|
DROP POLICY IF EXISTS "admin_manage_sites" ON public.sites;
|
|
CREATE POLICY "admin_manage_sites" ON public.sites FOR ALL TO authenticated
|
|
USING (public.is_admin_user()) WITH CHECK (public.is_admin_user());
|
|
|
|
DROP POLICY IF EXISTS "public_read_site_categories" ON public.site_categories;
|
|
CREATE POLICY "public_read_site_categories" ON public.site_categories FOR SELECT TO public USING (true);
|
|
|
|
DROP POLICY IF EXISTS "admin_manage_site_categories" ON public.site_categories;
|
|
CREATE POLICY "admin_manage_site_categories" ON public.site_categories FOR ALL TO authenticated
|
|
USING (public.is_admin_user()) WITH CHECK (public.is_admin_user());
|