import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres' /** * Migration: Community Phase 1 * * Creates all Community Management collections: * - social_platforms * - social_accounts * - community_templates (before interactions, as it's referenced) * - community_interactions * - community_rules * * Also adds communityRole to users table. */ export async function up({ db }: MigrateUpArgs): Promise { // Step 1: Add user column await db.execute(sql` ALTER TABLE "users" ADD COLUMN IF NOT EXISTS "community_role" varchar DEFAULT 'none'; `) // Step 2: Create social_platforms (no dependencies) await db.execute(sql` CREATE TABLE IF NOT EXISTS "social_platforms" ( "id" serial PRIMARY KEY NOT NULL, "name" varchar NOT NULL, "slug" varchar NOT NULL UNIQUE, "icon" varchar, "color" varchar, "is_active" boolean DEFAULT true, "api_status" varchar DEFAULT 'disconnected', "api_config_api_type" varchar, "api_config_base_url" varchar, "api_config_auth_type" varchar, "rate_limits_requests_per_minute" numeric, "rate_limits_requests_per_day" numeric, "rate_limits_quota_units_per_day" numeric, "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL ); CREATE INDEX IF NOT EXISTS "social_platforms_slug_idx" ON "social_platforms" USING btree ("slug"); CREATE INDEX IF NOT EXISTS "social_platforms_is_active_idx" ON "social_platforms" USING btree ("is_active"); CREATE INDEX IF NOT EXISTS "social_platforms_updated_at_idx" ON "social_platforms" USING btree ("updated_at"); CREATE INDEX IF NOT EXISTS "social_platforms_created_at_idx" ON "social_platforms" USING btree ("created_at"); `) // Step 3: Create social_platforms array tables await db.execute(sql` CREATE TABLE IF NOT EXISTS "social_platforms_api_config_scopes" ( "id" serial PRIMARY KEY NOT NULL, "_order" integer NOT NULL, "_parent_id" integer NOT NULL REFERENCES social_platforms(id) ON DELETE CASCADE, "scope" varchar ); CREATE INDEX IF NOT EXISTS "social_platforms_api_config_scopes_order_idx" ON "social_platforms_api_config_scopes" USING btree ("_order"); CREATE INDEX IF NOT EXISTS "social_platforms_api_config_scopes_parent_idx" ON "social_platforms_api_config_scopes" USING btree ("_parent_id"); CREATE TABLE IF NOT EXISTS "social_platforms_interaction_types" ( "id" serial PRIMARY KEY NOT NULL, "_order" integer NOT NULL, "_parent_id" integer NOT NULL REFERENCES social_platforms(id) ON DELETE CASCADE, "type" varchar NOT NULL, "label" varchar NOT NULL, "icon" varchar, "can_reply" boolean DEFAULT true ); CREATE INDEX IF NOT EXISTS "social_platforms_interaction_types_order_idx" ON "social_platforms_interaction_types" USING btree ("_order"); CREATE INDEX IF NOT EXISTS "social_platforms_interaction_types_parent_idx" ON "social_platforms_interaction_types" USING btree ("_parent_id"); `) // Step 4: Create social_accounts (depends on social_platforms, youtube_channels) await db.execute(sql` CREATE TABLE IF NOT EXISTS "social_accounts" ( "id" serial PRIMARY KEY NOT NULL, "platform_id" integer REFERENCES social_platforms(id) ON DELETE SET NULL, "linked_channel_id" integer REFERENCES youtube_channels(id) ON DELETE SET NULL, "display_name" varchar NOT NULL, "account_handle" varchar, "external_id" varchar, "account_url" varchar, "is_active" boolean DEFAULT true, "credentials_access_token" varchar, "credentials_refresh_token" varchar, "credentials_token_expires_at" timestamp(3) with time zone, "credentials_api_key" varchar, "stats_followers" numeric, "stats_total_posts" numeric, "stats_last_synced_at" timestamp(3) with time zone, "sync_settings_auto_sync_enabled" boolean DEFAULT true, "sync_settings_sync_interval_minutes" numeric DEFAULT 15, "sync_settings_sync_comments" boolean DEFAULT true, "sync_settings_sync_d_ms" boolean DEFAULT false, "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL ); CREATE INDEX IF NOT EXISTS "social_accounts_platform_idx" ON "social_accounts" USING btree ("platform_id"); CREATE INDEX IF NOT EXISTS "social_accounts_linked_channel_idx" ON "social_accounts" USING btree ("linked_channel_id"); CREATE INDEX IF NOT EXISTS "social_accounts_is_active_idx" ON "social_accounts" USING btree ("is_active"); CREATE INDEX IF NOT EXISTS "social_accounts_updated_at_idx" ON "social_accounts" USING btree ("updated_at"); CREATE INDEX IF NOT EXISTS "social_accounts_created_at_idx" ON "social_accounts" USING btree ("created_at"); `) // Step 5: Create community_templates FIRST (before interactions, as it's referenced) await db.execute(sql` CREATE TABLE IF NOT EXISTS "community_templates" ( "id" serial PRIMARY KEY NOT NULL, "category" varchar NOT NULL, "channel_id" integer REFERENCES youtube_channels(id) ON DELETE SET NULL, "requires_review" boolean DEFAULT false, "is_active" boolean DEFAULT true, "usage_count" numeric DEFAULT 0, "example_output" text, "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL ); CREATE INDEX IF NOT EXISTS "community_templates_category_idx" ON "community_templates" USING btree ("category"); CREATE INDEX IF NOT EXISTS "community_templates_channel_idx" ON "community_templates" USING btree ("channel_id"); CREATE INDEX IF NOT EXISTS "community_templates_is_active_idx" ON "community_templates" USING btree ("is_active"); CREATE INDEX IF NOT EXISTS "community_templates_updated_at_idx" ON "community_templates" USING btree ("updated_at"); CREATE INDEX IF NOT EXISTS "community_templates_created_at_idx" ON "community_templates" USING btree ("created_at"); `) // Step 6: Create community_templates related tables await db.execute(sql` CREATE TABLE IF NOT EXISTS "community_templates_locales" ( "name" varchar NOT NULL, "template" text NOT NULL, "id" serial PRIMARY KEY NOT NULL, "_locale" varchar NOT NULL, "_parent_id" integer NOT NULL REFERENCES community_templates(id) ON DELETE CASCADE, CONSTRAINT "community_templates_locales_locale_parent_id_unique" UNIQUE("_locale", "_parent_id") ); CREATE INDEX IF NOT EXISTS "community_templates_locales_locale_idx" ON "community_templates_locales" USING btree ("_locale"); CREATE INDEX IF NOT EXISTS "community_templates_locales_parent_idx" ON "community_templates_locales" USING btree ("_parent_id"); CREATE TABLE IF NOT EXISTS "community_templates_variables" ( "id" serial PRIMARY KEY NOT NULL, "_order" integer NOT NULL, "_parent_id" integer NOT NULL REFERENCES community_templates(id) ON DELETE CASCADE, "variable" varchar NOT NULL, "description" varchar, "default_value" varchar ); CREATE INDEX IF NOT EXISTS "community_templates_variables_order_idx" ON "community_templates_variables" USING btree ("_order"); CREATE INDEX IF NOT EXISTS "community_templates_variables_parent_idx" ON "community_templates_variables" USING btree ("_parent_id"); CREATE TABLE IF NOT EXISTS "community_templates_auto_suggest_keywords" ( "id" serial PRIMARY KEY NOT NULL, "_order" integer NOT NULL, "_parent_id" integer NOT NULL REFERENCES community_templates(id) ON DELETE CASCADE, "keyword" varchar NOT NULL ); CREATE INDEX IF NOT EXISTS "community_templates_auto_suggest_keywords_order_idx" ON "community_templates_auto_suggest_keywords" USING btree ("_order"); CREATE INDEX IF NOT EXISTS "community_templates_auto_suggest_keywords_parent_idx" ON "community_templates_auto_suggest_keywords" USING btree ("_parent_id"); CREATE TABLE IF NOT EXISTS "community_templates_rels" ( "id" serial PRIMARY KEY NOT NULL, "order" integer, "parent_id" integer NOT NULL REFERENCES community_templates(id) ON DELETE CASCADE, "path" varchar NOT NULL, "social_platforms_id" integer REFERENCES social_platforms(id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS "community_templates_rels_order_idx" ON "community_templates_rels" USING btree ("order"); CREATE INDEX IF NOT EXISTS "community_templates_rels_parent_idx" ON "community_templates_rels" USING btree ("parent_id"); CREATE INDEX IF NOT EXISTS "community_templates_rels_path_idx" ON "community_templates_rels" USING btree ("path"); CREATE INDEX IF NOT EXISTS "community_templates_rels_social_platforms_idx" ON "community_templates_rels" USING btree ("social_platforms_id"); `) // Step 7: Create community_interactions (now community_templates exists) await db.execute(sql` CREATE TABLE IF NOT EXISTS "community_interactions" ( "id" serial PRIMARY KEY NOT NULL, "platform_id" integer REFERENCES social_platforms(id) ON DELETE SET NULL, "social_account_id" integer REFERENCES social_accounts(id) ON DELETE SET NULL, "linked_content_id" integer REFERENCES youtube_content(id) ON DELETE SET NULL, "type" varchar NOT NULL, "external_id" varchar NOT NULL UNIQUE, "parent_interaction_id" integer, "author_name" varchar, "author_handle" varchar, "author_profile_url" varchar, "author_avatar_url" varchar, "author_is_verified" boolean DEFAULT false, "author_is_subscriber" boolean DEFAULT false, "author_is_member" boolean DEFAULT false, "author_subscriber_count" numeric, "message" text NOT NULL, "message_html" text, "published_at" timestamp(3) with time zone NOT NULL, "analysis_sentiment" varchar, "analysis_sentiment_score" numeric, "analysis_confidence" numeric, "analysis_language" varchar, "analysis_suggested_template_id" integer REFERENCES community_templates(id) ON DELETE SET NULL, "analysis_suggested_reply" text, "analysis_analyzed_at" timestamp(3) with time zone, "flags_is_medical_question" boolean DEFAULT false, "flags_requires_escalation" boolean DEFAULT false, "flags_is_spam" boolean DEFAULT false, "flags_is_from_influencer" boolean DEFAULT false, "status" varchar DEFAULT 'new' NOT NULL, "priority" varchar DEFAULT 'normal' NOT NULL, "assigned_to_id" integer REFERENCES users(id) ON DELETE SET NULL, "response_deadline" timestamp(3) with time zone, "response_text" text, "response_used_template_id" integer REFERENCES community_templates(id) ON DELETE SET NULL, "response_sent_at" timestamp(3) with time zone, "response_sent_by_id" integer REFERENCES users(id) ON DELETE SET NULL, "response_external_reply_id" varchar, "engagement_likes" numeric DEFAULT 0, "engagement_replies" numeric DEFAULT 0, "engagement_is_hearted" boolean DEFAULT false, "engagement_is_pinned" boolean DEFAULT false, "internal_notes" text, "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL ); `) // Step 8: Add self-reference for parent_interaction after table exists await db.execute(sql` ALTER TABLE "community_interactions" ADD CONSTRAINT "community_interactions_parent_fk" FOREIGN KEY ("parent_interaction_id") REFERENCES community_interactions(id) ON DELETE SET NULL; `) // Step 9: Create indexes for community_interactions await db.execute(sql` CREATE INDEX IF NOT EXISTS "community_interactions_platform_idx" ON "community_interactions" USING btree ("platform_id"); CREATE INDEX IF NOT EXISTS "community_interactions_social_account_idx" ON "community_interactions" USING btree ("social_account_id"); CREATE INDEX IF NOT EXISTS "community_interactions_linked_content_idx" ON "community_interactions" USING btree ("linked_content_id"); CREATE INDEX IF NOT EXISTS "community_interactions_external_id_idx" ON "community_interactions" USING btree ("external_id"); CREATE INDEX IF NOT EXISTS "community_interactions_parent_idx" ON "community_interactions" USING btree ("parent_interaction_id"); CREATE INDEX IF NOT EXISTS "community_interactions_status_idx" ON "community_interactions" USING btree ("status"); CREATE INDEX IF NOT EXISTS "community_interactions_priority_idx" ON "community_interactions" USING btree ("priority"); CREATE INDEX IF NOT EXISTS "community_interactions_assigned_to_idx" ON "community_interactions" USING btree ("assigned_to_id"); CREATE INDEX IF NOT EXISTS "community_interactions_updated_at_idx" ON "community_interactions" USING btree ("updated_at"); CREATE INDEX IF NOT EXISTS "community_interactions_created_at_idx" ON "community_interactions" USING btree ("created_at"); `) // Step 10: Create community_interactions array tables await db.execute(sql` CREATE TABLE IF NOT EXISTS "community_interactions_attachments" ( "id" serial PRIMARY KEY NOT NULL, "_order" integer NOT NULL, "_parent_id" integer NOT NULL REFERENCES community_interactions(id) ON DELETE CASCADE, "type" varchar, "url" varchar ); CREATE INDEX IF NOT EXISTS "community_interactions_attachments_order_idx" ON "community_interactions_attachments" USING btree ("_order"); CREATE INDEX IF NOT EXISTS "community_interactions_attachments_parent_idx" ON "community_interactions_attachments" USING btree ("_parent_id"); CREATE TABLE IF NOT EXISTS "community_interactions_analysis_topics" ( "id" serial PRIMARY KEY NOT NULL, "_order" integer NOT NULL, "_parent_id" integer NOT NULL REFERENCES community_interactions(id) ON DELETE CASCADE, "topic" varchar ); CREATE INDEX IF NOT EXISTS "community_interactions_analysis_topics_order_idx" ON "community_interactions_analysis_topics" USING btree ("_order"); CREATE INDEX IF NOT EXISTS "community_interactions_analysis_topics_parent_idx" ON "community_interactions_analysis_topics" USING btree ("_parent_id"); `) // Step 11: Create community_rules await db.execute(sql` CREATE TABLE IF NOT EXISTS "community_rules" ( "id" serial PRIMARY KEY NOT NULL, "name" varchar NOT NULL, "priority" numeric DEFAULT 100 NOT NULL, "is_active" boolean DEFAULT true, "description" text, "channel_id" integer REFERENCES youtube_channels(id) ON DELETE SET NULL, "trigger_type" varchar NOT NULL, "trigger_influencer_min_followers" numeric DEFAULT 10000, "stats_times_triggered" numeric DEFAULT 0, "stats_last_triggered_at" timestamp(3) with time zone, "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL ); CREATE INDEX IF NOT EXISTS "community_rules_name_idx" ON "community_rules" USING btree ("name"); CREATE INDEX IF NOT EXISTS "community_rules_priority_idx" ON "community_rules" USING btree ("priority"); CREATE INDEX IF NOT EXISTS "community_rules_is_active_idx" ON "community_rules" USING btree ("is_active"); CREATE INDEX IF NOT EXISTS "community_rules_channel_idx" ON "community_rules" USING btree ("channel_id"); CREATE INDEX IF NOT EXISTS "community_rules_updated_at_idx" ON "community_rules" USING btree ("updated_at"); CREATE INDEX IF NOT EXISTS "community_rules_created_at_idx" ON "community_rules" USING btree ("created_at"); `) // Step 12: Create community_rules array tables await db.execute(sql` CREATE TABLE IF NOT EXISTS "community_rules_trigger_keywords" ( "id" serial PRIMARY KEY NOT NULL, "_order" integer NOT NULL, "_parent_id" integer NOT NULL REFERENCES community_rules(id) ON DELETE CASCADE, "keyword" varchar NOT NULL, "match_type" varchar DEFAULT 'contains' ); CREATE INDEX IF NOT EXISTS "community_rules_trigger_keywords_order_idx" ON "community_rules_trigger_keywords" USING btree ("_order"); CREATE INDEX IF NOT EXISTS "community_rules_trigger_keywords_parent_idx" ON "community_rules_trigger_keywords" USING btree ("_parent_id"); CREATE TABLE IF NOT EXISTS "community_rules_actions" ( "id" serial PRIMARY KEY NOT NULL, "_order" integer NOT NULL, "_parent_id" integer NOT NULL REFERENCES community_rules(id) ON DELETE CASCADE, "action" varchar NOT NULL, "value" varchar, "target_user_id" integer REFERENCES users(id) ON DELETE SET NULL, "target_template_id" integer REFERENCES community_templates(id) ON DELETE SET NULL ); CREATE INDEX IF NOT EXISTS "community_rules_actions_order_idx" ON "community_rules_actions" USING btree ("_order"); CREATE INDEX IF NOT EXISTS "community_rules_actions_parent_idx" ON "community_rules_actions" USING btree ("_parent_id"); CREATE INDEX IF NOT EXISTS "community_rules_actions_target_user_idx" ON "community_rules_actions" USING btree ("target_user_id"); CREATE INDEX IF NOT EXISTS "community_rules_actions_target_template_idx" ON "community_rules_actions" USING btree ("target_template_id"); CREATE TABLE IF NOT EXISTS "community_rules_rels" ( "id" serial PRIMARY KEY NOT NULL, "order" integer, "parent_id" integer NOT NULL REFERENCES community_rules(id) ON DELETE CASCADE, "path" varchar NOT NULL, "social_platforms_id" integer REFERENCES social_platforms(id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS "community_rules_rels_order_idx" ON "community_rules_rels" USING btree ("order"); CREATE INDEX IF NOT EXISTS "community_rules_rels_parent_idx" ON "community_rules_rels" USING btree ("parent_id"); CREATE INDEX IF NOT EXISTS "community_rules_rels_path_idx" ON "community_rules_rels" USING btree ("path"); CREATE INDEX IF NOT EXISTS "community_rules_rels_social_platforms_idx" ON "community_rules_rels" USING btree ("social_platforms_id"); CREATE TABLE IF NOT EXISTS "community_rules_trigger_sentiment_values" ( "id" serial PRIMARY KEY NOT NULL, "_order" integer NOT NULL, "_parent_id" integer NOT NULL REFERENCES community_rules(id) ON DELETE CASCADE, "value" varchar ); CREATE INDEX IF NOT EXISTS "community_rules_trigger_sentiment_values_order_idx" ON "community_rules_trigger_sentiment_values" USING btree ("_order"); CREATE INDEX IF NOT EXISTS "community_rules_trigger_sentiment_values_parent_idx" ON "community_rules_trigger_sentiment_values" USING btree ("_parent_id"); `) // Step 13: Update system table await db.execute(sql` ALTER TABLE "payload_locked_documents_rels" ADD COLUMN IF NOT EXISTS "social_platforms_id" integer REFERENCES social_platforms(id) ON DELETE CASCADE; ALTER TABLE "payload_locked_documents_rels" ADD COLUMN IF NOT EXISTS "social_accounts_id" integer REFERENCES social_accounts(id) ON DELETE CASCADE; ALTER TABLE "payload_locked_documents_rels" ADD COLUMN IF NOT EXISTS "community_interactions_id" integer REFERENCES community_interactions(id) ON DELETE CASCADE; ALTER TABLE "payload_locked_documents_rels" ADD COLUMN IF NOT EXISTS "community_templates_id" integer REFERENCES community_templates(id) ON DELETE CASCADE; ALTER TABLE "payload_locked_documents_rels" ADD COLUMN IF NOT EXISTS "community_rules_id" integer REFERENCES community_rules(id) ON DELETE CASCADE; CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_social_platforms_idx" ON "payload_locked_documents_rels" USING btree ("social_platforms_id"); CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_social_accounts_idx" ON "payload_locked_documents_rels" USING btree ("social_accounts_id"); CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_community_interactions_idx" ON "payload_locked_documents_rels" USING btree ("community_interactions_id"); CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_community_templates_idx" ON "payload_locked_documents_rels" USING btree ("community_templates_id"); CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_community_rules_idx" ON "payload_locked_documents_rels" USING btree ("community_rules_id"); `) // Step 14: Seed YouTube Platform await db.execute(sql` INSERT INTO "social_platforms" ("name", "slug", "icon", "color", "is_active", "api_status", "api_config_api_type", "api_config_base_url", "api_config_auth_type", "rate_limits_quota_units_per_day") VALUES ('YouTube', 'youtube', '📺', '#FF0000', true, 'development', 'youtube_v3', 'https://www.googleapis.com/youtube/v3', 'oauth2', 10000) ON CONFLICT (slug) DO NOTHING; `) } export async function down({ db }: MigrateDownArgs): Promise { // Drop in reverse order of creation await db.execute(sql` DROP INDEX IF EXISTS "payload_locked_documents_rels_community_rules_idx"; DROP INDEX IF EXISTS "payload_locked_documents_rels_community_templates_idx"; DROP INDEX IF EXISTS "payload_locked_documents_rels_community_interactions_idx"; DROP INDEX IF EXISTS "payload_locked_documents_rels_social_accounts_idx"; DROP INDEX IF EXISTS "payload_locked_documents_rels_social_platforms_idx"; ALTER TABLE "payload_locked_documents_rels" DROP COLUMN IF EXISTS "community_rules_id"; ALTER TABLE "payload_locked_documents_rels" DROP COLUMN IF EXISTS "community_templates_id"; ALTER TABLE "payload_locked_documents_rels" DROP COLUMN IF EXISTS "community_interactions_id"; ALTER TABLE "payload_locked_documents_rels" DROP COLUMN IF EXISTS "social_accounts_id"; ALTER TABLE "payload_locked_documents_rels" DROP COLUMN IF EXISTS "social_platforms_id"; `) await db.execute(sql` DROP TABLE IF EXISTS "community_rules_trigger_sentiment_values"; DROP TABLE IF EXISTS "community_rules_rels"; DROP TABLE IF EXISTS "community_rules_actions"; DROP TABLE IF EXISTS "community_rules_trigger_keywords"; DROP TABLE IF EXISTS "community_rules"; `) await db.execute(sql` DROP TABLE IF EXISTS "community_interactions_analysis_topics"; DROP TABLE IF EXISTS "community_interactions_attachments"; ALTER TABLE "community_interactions" DROP CONSTRAINT IF EXISTS "community_interactions_parent_fk"; DROP TABLE IF EXISTS "community_interactions"; `) await db.execute(sql` DROP TABLE IF EXISTS "community_templates_rels"; DROP TABLE IF EXISTS "community_templates_auto_suggest_keywords"; DROP TABLE IF EXISTS "community_templates_variables"; DROP TABLE IF EXISTS "community_templates_locales"; DROP TABLE IF EXISTS "community_templates"; `) await db.execute(sql` DROP TABLE IF EXISTS "social_accounts"; `) await db.execute(sql` DROP TABLE IF EXISTS "social_platforms_interaction_types"; DROP TABLE IF EXISTS "social_platforms_api_config_scopes"; DROP TABLE IF EXISTS "social_platforms"; `) await db.execute(sql` ALTER TABLE "users" DROP COLUMN IF EXISTS "community_role"; `) }